@autifyhq/aximo-cli 0.1.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.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +379 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +21 -0
  5. package/dist/client.d.ts +18 -0
  6. package/dist/client.js +90 -0
  7. package/dist/client.test.d.ts +1 -0
  8. package/dist/client.test.js +127 -0
  9. package/dist/commands/configure.d.ts +24 -0
  10. package/dist/commands/configure.js +103 -0
  11. package/dist/commands/configure.test.d.ts +1 -0
  12. package/dist/commands/configure.test.js +96 -0
  13. package/dist/commands/device/connect.d.ts +2 -0
  14. package/dist/commands/device/connect.js +84 -0
  15. package/dist/commands/device/doctor.d.ts +34 -0
  16. package/dist/commands/device/doctor.js +421 -0
  17. package/dist/commands/device/doctor.test.d.ts +1 -0
  18. package/dist/commands/device/doctor.test.js +89 -0
  19. package/dist/commands/device/index.d.ts +2 -0
  20. package/dist/commands/device/index.js +16 -0
  21. package/dist/commands/device/list.d.ts +20 -0
  22. package/dist/commands/device/list.js +40 -0
  23. package/dist/commands/device/list.test.d.ts +1 -0
  24. package/dist/commands/device/list.test.js +38 -0
  25. package/dist/commands/device/logs.d.ts +34 -0
  26. package/dist/commands/device/logs.js +160 -0
  27. package/dist/commands/device/logs.test.d.ts +1 -0
  28. package/dist/commands/device/logs.test.js +70 -0
  29. package/dist/commands/device/paths.d.ts +12 -0
  30. package/dist/commands/device/paths.js +40 -0
  31. package/dist/commands/device/ping.d.ts +21 -0
  32. package/dist/commands/device/ping.js +66 -0
  33. package/dist/commands/device/ping.test.d.ts +1 -0
  34. package/dist/commands/device/ping.test.js +68 -0
  35. package/dist/commands/device/register.d.ts +23 -0
  36. package/dist/commands/device/register.js +83 -0
  37. package/dist/commands/device/reset.d.ts +30 -0
  38. package/dist/commands/device/reset.js +67 -0
  39. package/dist/commands/device/reset.test.d.ts +1 -0
  40. package/dist/commands/device/reset.test.js +69 -0
  41. package/dist/commands/device/setup.d.ts +25 -0
  42. package/dist/commands/device/setup.js +105 -0
  43. package/dist/commands/device/setup.test.d.ts +1 -0
  44. package/dist/commands/device/setup.test.js +64 -0
  45. package/dist/commands/device/status.d.ts +16 -0
  46. package/dist/commands/device/status.js +20 -0
  47. package/dist/commands/device/unregister.d.ts +14 -0
  48. package/dist/commands/device/unregister.js +60 -0
  49. package/dist/commands/health/check.d.ts +5 -0
  50. package/dist/commands/health/check.js +20 -0
  51. package/dist/commands/health/check.test.d.ts +1 -0
  52. package/dist/commands/health/check.test.js +26 -0
  53. package/dist/commands/health/index.d.ts +2 -0
  54. package/dist/commands/health/index.js +7 -0
  55. package/dist/commands/project/index.d.ts +2 -0
  56. package/dist/commands/project/index.js +7 -0
  57. package/dist/commands/project/list.d.ts +7 -0
  58. package/dist/commands/project/list.js +20 -0
  59. package/dist/commands/project/list.test.d.ts +1 -0
  60. package/dist/commands/project/list.test.js +28 -0
  61. package/dist/commands/schema.d.ts +27 -0
  62. package/dist/commands/schema.js +146 -0
  63. package/dist/commands/schema.test.d.ts +1 -0
  64. package/dist/commands/schema.test.js +38 -0
  65. package/dist/commands/session/create.d.ts +78 -0
  66. package/dist/commands/session/create.js +89 -0
  67. package/dist/commands/session/get.d.ts +11 -0
  68. package/dist/commands/session/get.js +30 -0
  69. package/dist/commands/session/index.d.ts +2 -0
  70. package/dist/commands/session/index.js +8 -0
  71. package/dist/commands/session/session.test.d.ts +1 -0
  72. package/dist/commands/session/session.test.js +128 -0
  73. package/dist/commands/trigger/case/get.d.ts +14 -0
  74. package/dist/commands/trigger/case/get.js +33 -0
  75. package/dist/commands/trigger/case/index.d.ts +2 -0
  76. package/dist/commands/trigger/case/index.js +8 -0
  77. package/dist/commands/trigger/case/run.d.ts +58 -0
  78. package/dist/commands/trigger/case/run.js +81 -0
  79. package/dist/commands/trigger/index.d.ts +2 -0
  80. package/dist/commands/trigger/index.js +8 -0
  81. package/dist/commands/trigger/plan/get.d.ts +14 -0
  82. package/dist/commands/trigger/plan/get.js +33 -0
  83. package/dist/commands/trigger/plan/index.d.ts +2 -0
  84. package/dist/commands/trigger/plan/index.js +8 -0
  85. package/dist/commands/trigger/plan/run.d.ts +33 -0
  86. package/dist/commands/trigger/plan/run.js +63 -0
  87. package/dist/commands/trigger/trigger.test.d.ts +1 -0
  88. package/dist/commands/trigger/trigger.test.js +107 -0
  89. package/dist/commands/upload/create.d.ts +32 -0
  90. package/dist/commands/upload/create.js +65 -0
  91. package/dist/commands/upload/get.d.ts +10 -0
  92. package/dist/commands/upload/get.js +30 -0
  93. package/dist/commands/upload/index.d.ts +2 -0
  94. package/dist/commands/upload/index.js +11 -0
  95. package/dist/commands/upload/list.d.ts +15 -0
  96. package/dist/commands/upload/list.js +31 -0
  97. package/dist/commands/upload/put.d.ts +24 -0
  98. package/dist/commands/upload/put.js +62 -0
  99. package/dist/commands/upload/submit.d.ts +19 -0
  100. package/dist/commands/upload/submit.js +36 -0
  101. package/dist/commands/upload/upload.test.d.ts +1 -0
  102. package/dist/commands/upload/upload.test.js +133 -0
  103. package/dist/config.d.ts +7 -0
  104. package/dist/config.js +36 -0
  105. package/dist/desktop-server/agent-sync.d.ts +19 -0
  106. package/dist/desktop-server/agent-sync.js +141 -0
  107. package/dist/desktop-server/agent-sync.test.d.ts +1 -0
  108. package/dist/desktop-server/agent-sync.test.js +130 -0
  109. package/dist/desktop-server/setup.d.ts +19 -0
  110. package/dist/desktop-server/setup.js +200 -0
  111. package/dist/errors.d.ts +2 -0
  112. package/dist/errors.js +71 -0
  113. package/dist/errors.test.d.ts +1 -0
  114. package/dist/errors.test.js +88 -0
  115. package/dist/index.d.ts +19 -0
  116. package/dist/index.js +17 -0
  117. package/dist/mutate.d.ts +13 -0
  118. package/dist/mutate.js +25 -0
  119. package/dist/mutate.test.d.ts +1 -0
  120. package/dist/mutate.test.js +49 -0
  121. package/dist/output.d.ts +5 -0
  122. package/dist/output.js +23 -0
  123. package/dist/output.test.d.ts +1 -0
  124. package/dist/output.test.js +42 -0
  125. package/dist/test-setup.d.ts +1 -0
  126. package/dist/test-setup.js +13 -0
  127. package/dist/test-utils.d.ts +4 -0
  128. package/dist/test-utils.js +5 -0
  129. package/dist/types.d.ts +121 -0
  130. package/dist/types.js +2 -0
  131. package/dist/validate.d.ts +14 -0
  132. package/dist/validate.js +103 -0
  133. package/dist/validate.test.d.ts +1 -0
  134. package/dist/validate.test.js +96 -0
  135. package/package.json +72 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Autify Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,379 @@
1
+ # @autifyhq/aximo-cli
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@autifyhq/aximo-cli)](https://www.npmjs.com/package/@autifyhq/aximo-cli)
4
+ [![CI](https://github.com/autifyhq/aximo-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/autifyhq/aximo-cli/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
6
+
7
+ CLI for the [Autify Aximo API](https://aximo.autify.com/openapi.yaml), built for coding agents. Every command returns a JSON envelope (`{ok, data}` or `{ok: false, error}`) so output pipes cleanly into scripts and other tools.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm i -g @autifyhq/aximo-cli
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # 1. Configure credentials (interactive) — or set env vars, see Configuration below
19
+ aximo configure
20
+
21
+ # 2. Verify the connection
22
+ aximo health check
23
+
24
+ # 3. Run a desktop test session
25
+ aximo project list
26
+ aximo session create \
27
+ --project-id PROJECT_ID \
28
+ --title "Login test" \
29
+ --prompt "Go to https://example.com and verify the page loads."
30
+
31
+ # 4. Poll session status
32
+ aximo session get SESSION_ID
33
+ ```
34
+
35
+ Get your API key from **Settings → API Keys** in the [Aximo dashboard](https://aximo.autify.com).
36
+
37
+ ## Configuration
38
+
39
+ The CLI resolves each credential independently in this order:
40
+
41
+ 1. Value in `$AXIMO_HOME/config.json` (written by `aximo configure`)
42
+ 2. Environment variable
43
+ 3. Default (for base URL only: `https://aximo.autify.com`)
44
+
45
+ | Variable | Required | Default | Notes |
46
+ | ---------------- | -------- | -------------------------- | ------------------------------------------------------------ |
47
+ | `AXIMO_API_KEY` | Yes | — | Organization API key |
48
+ | `AXIMO_BASE_URL` | No | `https://aximo.autify.com` | Override for sandbox / self-hosted |
49
+ | `AXIMO_HOME` | No | `~/.aximo` | Data directory (config, device credentials, embedded Python) |
50
+
51
+ ### Using `aximo configure`
52
+
53
+ ```bash
54
+ aximo configure # interactive prompts
55
+ aximo configure --base-url URL --api-key KEY # non-interactive
56
+ ```
57
+
58
+ The interactive flow prompts first for the base URL (empty → keep existing, else falls back to the Aximo default), then for the API key (empty → keep existing; errors if nothing is saved). Config is written to `$AXIMO_HOME/config.json` with mode `0600`. To change sandbox/prod, re-run `aximo configure` and enter a new URL.
59
+
60
+ ### Using environment variables
61
+
62
+ If you'd rather keep credentials out of a file (CI, ephemeral shells), set env vars:
63
+
64
+ ```bash
65
+ # macOS / Linux
66
+ export AXIMO_API_KEY=your-api-key
67
+ export AXIMO_BASE_URL=https://aximo-sbx.autify.dev # optional, prod is the default
68
+
69
+ # Windows (PowerShell, persistent)
70
+ [Environment]::SetEnvironmentVariable("AXIMO_API_KEY", "your-api-key", "User")
71
+ [Environment]::SetEnvironmentVariable("AXIMO_BASE_URL", "https://aximo-sbx.autify.dev", "User")
72
+ ```
73
+
74
+ ## Commands
75
+
76
+ Run `aximo <command> --help` for detailed usage. `aximo schema [command.path]` returns machine-readable argument introspection (e.g. `aximo schema session.create`).
77
+
78
+ | Command | Description |
79
+ | ------------------------- | ---------------------------------------------------- |
80
+ | `aximo configure` | Save credentials to `$AXIMO_HOME/config.json` |
81
+ | `aximo health check` | Check API reachability |
82
+ | `aximo project list` | List organization projects |
83
+ | `aximo session create` | Create and queue a test session |
84
+ | `aximo session get` | Get session status |
85
+ | `aximo trigger case run` | Trigger a dashboard-created test case |
86
+ | `aximo trigger case get` | Get triggered case status |
87
+ | `aximo trigger plan run` | Trigger a test plan |
88
+ | `aximo trigger plan get` | Get plan run status |
89
+ | `aximo upload create` | Create an upload slot for an APK/IPA |
90
+ | `aximo upload list` | List previously uploaded apps |
91
+ | `aximo upload get` | Poll upload processing status |
92
+ | `aximo upload put` | Upload binary file to the slot |
93
+ | `aximo upload submit` | Finalize upload and trigger processing |
94
+ | `aximo device list` | List mobile devices from Device Farm |
95
+ | `aximo device setup` | First-time BYOD onboarding (API + Python + register) |
96
+ | `aximo device register` | Register this machine as a BYOD device |
97
+ | `aximo device connect` | Start the BYOD agent and connect to the relay |
98
+ | `aximo device status` | List your organization's BYOD devices |
99
+ | `aximo device ping` | Check your BYOD device's server-side status |
100
+ | `aximo device doctor` | Diagnose BYOD setup (env, Python, relay, status) |
101
+ | `aximo device logs` | Show the BYOD agent log |
102
+ | `aximo device unregister` | Remove a BYOD device server-side + clear local creds |
103
+ | `aximo device reset` | Remove BYOD local state (Python, deps, credentials) |
104
+ | `aximo schema` | Show CLI surface as JSON |
105
+
106
+ Mutating commands support `--json '<raw body>'` for direct OpenAPI payloads and `--dry-run` to preview requests without executing.
107
+
108
+ ### Health
109
+
110
+ ```bash
111
+ aximo health check
112
+ ```
113
+
114
+ Returns `{ok: true, data: {status: "ok"}}` when the API is reachable and your API key is valid.
115
+
116
+ ### Projects
117
+
118
+ ```bash
119
+ aximo project list
120
+ ```
121
+
122
+ Lists active workspaces in your organization. Grab the `id` to pass as `--project-id` for session creation.
123
+
124
+ ### Sessions
125
+
126
+ ```bash
127
+ aximo session create --title <t> --prompt <p> --project-id <id> \
128
+ [--platform ANDROID|IOS] [--model <m>] [--start-url <url>] \
129
+ [--case-id <id>] [--tags <comma-separated>] \
130
+ [--device-arn <arn>] [--device-name <name>] \
131
+ [--upload-arn <arn>] [--upload-type ANDROID_APP|IOS_APP] \
132
+ [--app-name <name>] [--bundle-id <id>] \
133
+ [--json '<body>'] [--dry-run]
134
+
135
+ aximo session get <sessionId>
136
+ ```
137
+
138
+ Desktop sessions default to `platform=desktop`, `model=sonnet`. Mobile sessions require both `--device-arn` and `--upload-arn`.
139
+
140
+ ### Triggers (test cases and plans)
141
+
142
+ ```bash
143
+ aximo trigger case run <caseId> [...]
144
+ aximo trigger case get <caseId> <sessionId>
145
+ aximo trigger plan run <planId> [...]
146
+ aximo trigger plan get <planId> <planRunId>
147
+ ```
148
+
149
+ Triggers dashboard-created test cases or plans. API access must be enabled in the Aximo dashboard settings. `trigger case run` returns a `sessionId` you can poll; `trigger plan run` returns per-case breakdown.
150
+
151
+ ### Uploads
152
+
153
+ ```bash
154
+ aximo upload create --file-name <name> [--file-size <bytes>] [--platform ANDROID|IOS]
155
+ aximo upload put <uploadId> --file <path>
156
+ aximo upload submit <uploadId>
157
+ aximo upload get <uploadId>
158
+ aximo upload list [--file-type ANDROID_APP|IOS_APP]
159
+ ```
160
+
161
+ Typical flow: `create` → `put` (sends binary to the presigned URL) → `submit` → poll `get` until `status=succeeded` → extract `uploadArn` for mobile sessions. `upload list` auto-re-uploads expired Device Farm entries.
162
+
163
+ ### Schema
164
+
165
+ ```bash
166
+ aximo schema # all commands
167
+ aximo schema session.create # one command's arg schema
168
+ ```
169
+
170
+ Machine-readable JSON view of the CLI surface — used by agent skills for discovery.
171
+
172
+ ## Device (BYOD — Bring Your Own Device)
173
+
174
+ The `device` commands let you register your own desktop as a test execution target. Aximo's agent controls it remotely over WebSocket to run tests against apps on your machine (useful when tests need access to your LAN, VPN, or a specific desktop environment).
175
+
176
+ Terminology note: **`aximo device list` returns mobile devices** from Device Farm; **`aximo device status` returns BYOD devices** you've registered. Don't confuse the two.
177
+
178
+ ### Prerequisites
179
+
180
+ - **Node.js 20+**, plus `aximo-cli` installed globally (see [Install](#install)).
181
+ - **An Aximo API key**, configured via `aximo configure` or env vars.
182
+ - **Active desktop session** — the agent uses `pyautogui` for screenshots and input, so the screen must be unlocked and awake while tests run.
183
+ - **Outbound WebSocket to port 443** — most corporate firewalls allow this; if yours doesn't, see [Troubleshooting](#byod-troubleshooting).
184
+
185
+ > Python is **not** a prerequisite. On first `connect` (or via `device setup`), the CLI downloads a self-contained CPython build into `$AXIMO_HOME/python/` (~35 MB download, ~130 MB installed) and installs the required packages. Your system Python is untouched.
186
+
187
+ ### First-time setup
188
+
189
+ One-command onboarding:
190
+
191
+ ```bash
192
+ aximo device setup --name "My Windows PC"
193
+ ```
194
+
195
+ This verifies API reachability, installs the embedded Python runtime, installs Python dependencies, and registers this machine. If the device is already registered locally, it skips registration and reports `alreadyRegistered: true`.
196
+
197
+ Or step-by-step, if you'd rather see each stage:
198
+
199
+ ```bash
200
+ aximo health check # confirm credentials + connectivity
201
+ aximo device register --name "My Windows PC" # creates server-side device, writes $AXIMO_HOME/device.json (0600)
202
+ aximo device connect # installs embedded Python on first run, then connects
203
+ ```
204
+
205
+ On success, `device connect` prints something like:
206
+
207
+ ```
208
+ Preparing Python environment...
209
+ Fetching embedded Python 3.12.13 (x86_64-pc-windows-msvc)...
210
+ Extracting...
211
+ Installing Python dependencies...
212
+ Python ready: C:\Users\<you>\.aximo\python\python.exe
213
+ Agent version: 8f3a1c04
214
+ Starting desktop agent, relay: wss://aximo.autify.com/devices/tunnel
215
+ Device "My Windows PC" is online. Press Ctrl+C to disconnect.
216
+ ```
217
+
218
+ Subsequent `connect` runs skip the Python download and dep install — they re-run only when the server-hosted agent script changes.
219
+
220
+ ### Daily use
221
+
222
+ ```bash
223
+ aximo device connect # start the agent; keep the terminal open while tests run
224
+ # Ctrl+C to disconnect
225
+ ```
226
+
227
+ Your device shows up in the Aximo UI under **Settings → Devices** with status `online` while connected. In the session form, pick your device as the execution target; Aximo routes the agent's actions through the tunnel to your machine.
228
+
229
+ ### Managing devices
230
+
231
+ ```bash
232
+ aximo device status # list YOUR BYOD devices, with online/offline + lastSeenAt
233
+ aximo device ping # server-side status for the locally registered device
234
+ aximo device ping --device-id DEVICE_ID # same, for a specific device
235
+ aximo device unregister # delete server-side + clear local credentials
236
+ aximo device unregister --device-id DEVICE_ID # unregister an orphaned device (no local creds)
237
+ ```
238
+
239
+ `device ping` returns what the server knows, including `lastSeenAgoSeconds` and the HTTP round-trip `latencyMs`. It does **not** round-trip through the relay, so a passing ping tells you "the server thinks this device is online," not "the local agent is responding right now."
240
+
241
+ ### Re-registering
242
+
243
+ `aximo device register` refuses to overwrite an existing local registration. To move a registration to a different device name:
244
+
245
+ ```bash
246
+ aximo device unregister # clean up the old one server-side
247
+ aximo device register --name "New Name"
248
+ ```
249
+
250
+ If you've lost the original local credentials and need to delete the orphaned server-side device:
251
+
252
+ ```bash
253
+ aximo device status # find its deviceId
254
+ aximo device unregister --device-id DEVICE_ID
255
+ ```
256
+
257
+ Or use `aximo device register --force` — but that silently orphans the previous server-side device, so the explicit `unregister` + `register` path is safer.
258
+
259
+ ### Inspecting and resetting local state
260
+
261
+ ```bash
262
+ aximo device logs # tail the last 50 lines of the agent log
263
+ aximo device logs --follow # stream new lines (not JSON-enveloped; human use)
264
+ aximo device logs --path # print the log file path and exit
265
+ aximo device logs --clear # truncate the log
266
+
267
+ aximo device doctor # run all checks: env, API, Python, relay, status
268
+ aximo device doctor --skip-relay # skip WebSocket reachability probe
269
+ aximo device doctor --skip-api # offline checks only
270
+
271
+ aximo device reset --python # remove embedded Python + its cache
272
+ aximo device reset --deps # reinstall deps on next setup
273
+ aximo device reset --device # delete local credentials (server-side stays; run `unregister` first to also delete there)
274
+ aximo device reset --all # wipe $AXIMO_HOME entirely
275
+ ```
276
+
277
+ `doctor` is the first thing to run when anything's off — it covers the whole stack in one shot.
278
+
279
+ ### BYOD troubleshooting
280
+
281
+ **"No API key configured."**
282
+ Either run `aximo configure`, or set `AXIMO_API_KEY` in your environment. Verify with `echo $AXIMO_API_KEY` (or `echo $env:AXIMO_API_KEY` on PowerShell).
283
+
284
+ **"Authentication failed."**
285
+ Your device token is invalid (deleted server-side, or the server URL changed). Clean up and re-register:
286
+
287
+ ```bash
288
+ aximo device unregister
289
+ aximo device register --name "My PC"
290
+ aximo device connect
291
+ ```
292
+
293
+ **"Disconnected. Reconnecting in Xs..."**
294
+ Normal for brief network blips — the agent retries with exponential backoff (1 s → 30 s max). If it keeps failing:
295
+
296
+ - Check your internet connection.
297
+ - Confirm `AXIMO_BASE_URL` is correct and reachable: `aximo health check`.
298
+ - Confirm outbound WebSocket on 443 isn't blocked: `aximo device doctor` runs a handshake probe.
299
+
300
+ **Python download or pip install fails (corporate proxy).**
301
+ The embedded Python is fetched from `github.com/astral-sh/python-build-standalone/releases`; packages come from PyPI. Behind a proxy:
302
+
303
+ ```bash
304
+ # macOS / Linux
305
+ ~/.aximo/python/bin/python3 -m pip config set global.proxy http://your-proxy:port
306
+
307
+ # Windows
308
+ %USERPROFILE%\.aximo\python\python.exe -m pip config set global.proxy http://your-proxy:port
309
+ ```
310
+
311
+ To retry from a clean slate: `aximo device reset --python && aximo device setup`.
312
+
313
+ **Device shows online, but tests fail on screenshot / click.**
314
+ Your screen is locked or the session is inactive. `pyautogui` needs an active desktop session.
315
+
316
+ **macOS: actions seem to do nothing.**
317
+ System Settings → Privacy & Security → Accessibility and Screen Recording — both need to be granted to the terminal (or the Python binary that spawned).
318
+
319
+ **Linux: "missing: scrot, xclip".**
320
+ `aximo device doctor` flags this when the tools aren't on PATH. Install via your package manager (`apt install scrot xclip` on Debian/Ubuntu).
321
+
322
+ **Windows: coordinates off / scrolling too fast or too slow.**
323
+ The agent disables DPI-awareness and uses a calibrated scroll amount — if you still see mismatches, run `aximo device doctor` and share the output.
324
+
325
+ ### Environment & file layout
326
+
327
+ | Path | Mode | Purpose |
328
+ | --------------------------------- | ---- | -------------------------------------------------------- |
329
+ | `$AXIMO_HOME/config.json` | 0600 | CLI credentials (from `aximo configure`) |
330
+ | `$AXIMO_HOME/device.json` | 0600 | BYOD registration + device token |
331
+ | `$AXIMO_HOME/agent/remote.py` | 0644 | Python agent (server-synced) |
332
+ | `$AXIMO_HOME/agent/manifest.json` | 0644 | Version + sha256 of cached agent files |
333
+ | `$AXIMO_HOME/python/` | — | Embedded CPython runtime |
334
+ | `$AXIMO_HOME/remote.log` | — | Agent log (rotate/clear via `aximo device logs --clear`) |
335
+
336
+ Default `$AXIMO_HOME` is `~/.aximo` on macOS/Linux, `%USERPROFILE%\.aximo` on Windows. Override via the `AXIMO_HOME` env var.
337
+
338
+ ### Sandbox vs production
339
+
340
+ ```bash
341
+ # Sandbox
342
+ aximo configure --base-url https://aximo-sbx.autify.dev --api-key <sbx-key>
343
+
344
+ # Production (default)
345
+ aximo configure --base-url https://aximo.autify.com --api-key <prod-key>
346
+ ```
347
+
348
+ Registrations are per-environment — a device registered against sandbox can't connect to production and vice versa.
349
+
350
+ ## Agent Skills
351
+
352
+ Ships with an [agent skill](https://skills.sh/). Once installed, the agent discovers commands via `aximo schema` and follows workflows in `skills/aximo-cli/`.
353
+
354
+ ```bash
355
+ npx skills add autifyhq/aximo-cli
356
+ ```
357
+
358
+ See [commands reference](./skills/aximo-cli/references/commands.md) and [workflows](./skills/aximo-cli/references/workflows.md) for the agent-oriented docs.
359
+
360
+ ## Contributing
361
+
362
+ ### Requirements
363
+
364
+ - [mise](https://mise.jdx.dev/)
365
+
366
+ ### Preparation
367
+
368
+ ```bash
369
+ pnpm install
370
+ pnpm run cli -- health check
371
+ ```
372
+
373
+ ### Release Process
374
+
375
+ Automated via [release-please](https://github.com/googleapis/release-please):
376
+
377
+ 1. Merge commits to `main` with Conventional Commit messages
378
+ 2. release-please creates/updates a Release PR with CHANGELOG.md
379
+ 3. Merge the Release PR to publish a GitHub Release and npm package
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { defineCommand, runMain } from "citty";
3
+ import pkg from "../package.json" with { type: "json" };
4
+ const main = defineCommand({
5
+ meta: {
6
+ name: "aximo",
7
+ version: pkg.version,
8
+ description: "Autify Aximo CLI for coding agents",
9
+ },
10
+ subCommands: {
11
+ configure: () => import("./commands/configure.js").then((m) => m.default),
12
+ health: () => import("./commands/health/index.js").then((m) => m.default),
13
+ session: () => import("./commands/session/index.js").then((m) => m.default),
14
+ trigger: () => import("./commands/trigger/index.js").then((m) => m.default),
15
+ device: () => import("./commands/device/index.js").then((m) => m.default),
16
+ upload: () => import("./commands/upload/index.js").then((m) => m.default),
17
+ project: () => import("./commands/project/index.js").then((m) => m.default),
18
+ schema: () => import("./commands/schema.js").then((m) => m.default),
19
+ },
20
+ });
21
+ runMain(main);
@@ -0,0 +1,18 @@
1
+ export declare const DEFAULT_BASE_URL = "https://aximo.autify.com";
2
+ export declare function getApiKey(): string;
3
+ export declare function getBaseUrl(): string;
4
+ export interface RequestOptions {
5
+ method?: "GET" | "POST" | "PUT" | "DELETE";
6
+ query?: Record<string, string | number | boolean | undefined>;
7
+ body?: unknown;
8
+ rawBody?: BodyInit | undefined;
9
+ contentType?: string | undefined;
10
+ expectJson?: boolean;
11
+ }
12
+ export interface ClientDeps {
13
+ apiKey?: string;
14
+ baseUrl?: string;
15
+ fetchImpl?: typeof fetch;
16
+ }
17
+ export declare function request<T>(path: string, options?: RequestOptions, deps?: ClientDeps): Promise<T>;
18
+ export declare function requestBuffer(path: string, deps?: ClientDeps): Promise<Buffer>;
package/dist/client.js ADDED
@@ -0,0 +1,90 @@
1
+ import { loadConfig } from "./config.js";
2
+ export const DEFAULT_BASE_URL = "https://aximo.autify.com";
3
+ export function getApiKey() {
4
+ const config = loadConfig();
5
+ const key = config.apiKey ?? process.env["AXIMO_API_KEY"];
6
+ if (!key) {
7
+ throw new Error("No API key configured. Run `aximo configure`, set AXIMO_API_KEY in your environment, or use `node --env-file=.env`.");
8
+ }
9
+ return key;
10
+ }
11
+ export function getBaseUrl() {
12
+ const config = loadConfig();
13
+ const raw = config.baseUrl ?? process.env["AXIMO_BASE_URL"] ?? DEFAULT_BASE_URL;
14
+ return raw.replace(/\/+$/, "");
15
+ }
16
+ function httpError(status, message, retryAfter) {
17
+ const err = Object.assign(new Error(message), { status });
18
+ if (retryAfter !== undefined)
19
+ err.retryAfter = retryAfter;
20
+ return err;
21
+ }
22
+ async function errorMessage(res) {
23
+ const text = await res.text().catch(() => "");
24
+ if (!text)
25
+ return res.statusText || `HTTP ${res.status}`;
26
+ try {
27
+ const parsed = JSON.parse(text);
28
+ if (typeof parsed.error === "string")
29
+ return parsed.error;
30
+ if (typeof parsed.message === "string")
31
+ return parsed.message;
32
+ }
33
+ catch {
34
+ // fall through
35
+ }
36
+ return text;
37
+ }
38
+ function buildUrl(baseUrl, path, query) {
39
+ const url = new URL(path.startsWith("/") ? path : `/${path}`, `${baseUrl}/`);
40
+ if (query) {
41
+ for (const [k, v] of Object.entries(query)) {
42
+ if (v === undefined)
43
+ continue;
44
+ url.searchParams.set(k, String(v));
45
+ }
46
+ }
47
+ return url.toString();
48
+ }
49
+ export async function request(path, options = {}, deps = {}) {
50
+ const apiKey = deps.apiKey ?? getApiKey();
51
+ const baseUrl = deps.baseUrl ?? getBaseUrl();
52
+ const fetchImpl = deps.fetchImpl ?? fetch;
53
+ const method = options.method ?? "GET";
54
+ const url = buildUrl(baseUrl, path, options.query);
55
+ const headers = { "x-api-key": apiKey };
56
+ let body;
57
+ if (options.rawBody !== undefined) {
58
+ body = options.rawBody;
59
+ if (options.contentType)
60
+ headers["Content-Type"] = options.contentType;
61
+ }
62
+ else if (options.body !== undefined) {
63
+ headers["Content-Type"] = "application/json";
64
+ body = JSON.stringify(options.body);
65
+ }
66
+ const res = await fetchImpl(url, { method, headers, ...(body !== undefined ? { body } : {}) });
67
+ if (!res.ok) {
68
+ const retryAfter = res.headers.get("retry-after") ?? undefined;
69
+ throw httpError(res.status, await errorMessage(res), retryAfter);
70
+ }
71
+ if (options.expectJson === false) {
72
+ return undefined;
73
+ }
74
+ const text = await res.text();
75
+ if (!text)
76
+ return undefined;
77
+ return JSON.parse(text);
78
+ }
79
+ export async function requestBuffer(path, deps = {}) {
80
+ const apiKey = deps.apiKey ?? getApiKey();
81
+ const baseUrl = deps.baseUrl ?? getBaseUrl();
82
+ const fetchImpl = deps.fetchImpl ?? fetch;
83
+ const url = buildUrl(baseUrl, path);
84
+ const res = await fetchImpl(url, { method: "GET", headers: { "x-api-key": apiKey } });
85
+ if (!res.ok) {
86
+ const retryAfter = res.headers.get("retry-after") ?? undefined;
87
+ throw httpError(res.status, await errorMessage(res), retryAfter);
88
+ }
89
+ return Buffer.from(await res.arrayBuffer());
90
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,127 @@
1
+ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
2
+ import { server, http, HttpResponse, TEST_BASE_URL } from "./test-utils.js";
3
+ import { getApiKey, getBaseUrl, request } from "./client.js";
4
+ const ORIGINAL_KEY = process.env["AXIMO_API_KEY"];
5
+ const ORIGINAL_URL = process.env["AXIMO_BASE_URL"];
6
+ beforeAll(() => {
7
+ process.env["AXIMO_API_KEY"] = "test-key";
8
+ process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
9
+ server.listen();
10
+ });
11
+ afterEach(() => server.resetHandlers());
12
+ afterAll(() => {
13
+ server.close();
14
+ if (ORIGINAL_KEY !== undefined)
15
+ process.env["AXIMO_API_KEY"] = ORIGINAL_KEY;
16
+ else
17
+ delete process.env["AXIMO_API_KEY"];
18
+ if (ORIGINAL_URL !== undefined)
19
+ process.env["AXIMO_BASE_URL"] = ORIGINAL_URL;
20
+ else
21
+ delete process.env["AXIMO_BASE_URL"];
22
+ });
23
+ describe("getApiKey", () => {
24
+ it("returns AXIMO_API_KEY when set", () => {
25
+ expect(getApiKey()).toBe("test-key");
26
+ });
27
+ it("throws when unset", () => {
28
+ const prev = process.env["AXIMO_API_KEY"];
29
+ delete process.env["AXIMO_API_KEY"];
30
+ try {
31
+ expect(() => getApiKey()).toThrow("AXIMO_API_KEY");
32
+ }
33
+ finally {
34
+ if (prev !== undefined)
35
+ process.env["AXIMO_API_KEY"] = prev;
36
+ }
37
+ });
38
+ });
39
+ describe("getBaseUrl", () => {
40
+ it("returns override without trailing slash", () => {
41
+ process.env["AXIMO_BASE_URL"] = "https://example.test/";
42
+ expect(getBaseUrl()).toBe("https://example.test");
43
+ process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
44
+ });
45
+ it("defaults to https://aximo.autify.com", () => {
46
+ delete process.env["AXIMO_BASE_URL"];
47
+ expect(getBaseUrl()).toBe("https://aximo.autify.com");
48
+ process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
49
+ });
50
+ });
51
+ describe("request", () => {
52
+ it("sends x-api-key header and parses JSON", async () => {
53
+ let seenKey = null;
54
+ server.use(http.get(`${TEST_BASE_URL}/api/health`, ({ request: req }) => {
55
+ seenKey = req.headers.get("x-api-key");
56
+ return HttpResponse.json({ ok: "yes" });
57
+ }));
58
+ const result = await request("/api/health");
59
+ expect(result).toEqual({ ok: "yes" });
60
+ expect(seenKey).toBe("test-key");
61
+ });
62
+ it("encodes query string", async () => {
63
+ let seenUrl = "";
64
+ server.use(http.get(`${TEST_BASE_URL}/api/devices`, ({ request: req }) => {
65
+ seenUrl = req.url;
66
+ return HttpResponse.json({ devices: [] });
67
+ }));
68
+ await request("/api/devices", { query: { platform: "IOS", availableOnly: true } });
69
+ expect(seenUrl).toContain("platform=IOS");
70
+ expect(seenUrl).toContain("availableOnly=true");
71
+ });
72
+ it("POSTs JSON body with content-type", async () => {
73
+ let seenBody;
74
+ let seenCT = "";
75
+ server.use(http.post(`${TEST_BASE_URL}/api/sessions`, async ({ request: req }) => {
76
+ seenCT = req.headers.get("content-type") ?? "";
77
+ seenBody = await req.json();
78
+ return HttpResponse.json({ success: true, sessionId: "s1" }, { status: 201 });
79
+ }));
80
+ const result = await request("/api/sessions", {
81
+ method: "POST",
82
+ body: { title: "t" },
83
+ });
84
+ expect(result.sessionId).toBe("s1");
85
+ expect(seenCT).toContain("application/json");
86
+ expect(seenBody).toEqual({ title: "t" });
87
+ });
88
+ it("throws HttpError with status on non-2xx", async () => {
89
+ server.use(http.get(`${TEST_BASE_URL}/api/boom`, () => HttpResponse.json({ error: "Unauthorized" }, { status: 401 })));
90
+ await expect(request("/api/boom")).rejects.toMatchObject({
91
+ status: 401,
92
+ message: "Unauthorized",
93
+ });
94
+ });
95
+ it("extracts error from {error} body", async () => {
96
+ server.use(http.get(`${TEST_BASE_URL}/api/boom`, () => HttpResponse.json({ error: "Rate limit exceeded" }, { status: 429 })));
97
+ await expect(request("/api/boom")).rejects.toMatchObject({
98
+ status: 429,
99
+ message: "Rate limit exceeded",
100
+ });
101
+ });
102
+ it("attaches Retry-After header on 429", async () => {
103
+ server.use(http.get(`${TEST_BASE_URL}/api/limited`, () => HttpResponse.json({ error: "slow down" }, { status: 429, headers: { "Retry-After": "60" } })));
104
+ await expect(request("/api/limited")).rejects.toMatchObject({
105
+ status: 429,
106
+ retryAfter: "60",
107
+ });
108
+ });
109
+ it("supports rawBody PUT (binary)", async () => {
110
+ let seenCT = "";
111
+ let seenMethod = "";
112
+ server.use(http.put(`${TEST_BASE_URL}/api/uploads/u1`, ({ request: req }) => {
113
+ seenCT = req.headers.get("content-type") ?? "";
114
+ seenMethod = req.method;
115
+ return HttpResponse.json({ success: true });
116
+ }));
117
+ const bytes = new Uint8Array([1, 2, 3]);
118
+ const result = await request("/api/uploads/u1", {
119
+ method: "PUT",
120
+ rawBody: bytes,
121
+ contentType: "application/octet-stream",
122
+ });
123
+ expect(result.success).toBe(true);
124
+ expect(seenCT).toContain("application/octet-stream");
125
+ expect(seenMethod).toBe("PUT");
126
+ });
127
+ });