@nothumanwork/sauron 0.2.1
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 +254 -0
- package/bin/sauron.js +55 -0
- package/distribution/targets.json +23 -0
- package/npm/bin/.gitkeep +1 -0
- package/npm/bin/aarch64-apple-darwin/sauron +0 -0
- package/npm/bin/x86_64-apple-darwin/sauron +0 -0
- package/npm/bin/x86_64-unknown-linux-gnu/sauron +0 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# sauron (Rust)
|
|
2
|
+
|
|
3
|
+
A fully Rust-native CLI for AI agents to control Chrome via the **Chrome DevTools Protocol (CDP)**.
|
|
4
|
+
|
|
5
|
+
This is a rewrite of the attached Bun/TypeScript `sauron` project as a compiled Rust binary.
|
|
6
|
+
|
|
7
|
+
## Goals
|
|
8
|
+
|
|
9
|
+
- **Agent-friendly** JSON output for all commands (v2 envelope)
|
|
10
|
+
- **Fast** startup and execution (single static-ish binary)
|
|
11
|
+
- **Process-safe concurrency** with mandatory runtime sessions (`start` before browser commands)
|
|
12
|
+
- **Per-session isolation** with generated `session_id`, `instance`, and `client` IDs by default
|
|
13
|
+
- Filesystem-only runtime state storage under `~/.sauron/runtime/`
|
|
14
|
+
- Uses Chrome **`--headless=new`** only (opinionated headless runtime)
|
|
15
|
+
- Default viewport is **1440x900** (override with `--viewport WIDTHxHEIGHT`)
|
|
16
|
+
|
|
17
|
+
## V2 Interface
|
|
18
|
+
|
|
19
|
+
- `v2` is the only supported interface.
|
|
20
|
+
- No backward compatibility layer for v1 command names or output shape.
|
|
21
|
+
- Migration and implementation notes: [specs/v2-integration.md](/Users/mish/projects/sauron/specs/v2-integration.md)
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
./install.sh
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
By default, `install.sh`:
|
|
30
|
+
- builds and installs for your host target to `/usr/local/bin/sauron`,
|
|
31
|
+
- attempts to build the release matrix for:
|
|
32
|
+
- `aarch64-apple-darwin`
|
|
33
|
+
- `x86_64-apple-darwin`
|
|
34
|
+
- `x86_64-unknown-linux-gnu`
|
|
35
|
+
|
|
36
|
+
Notes:
|
|
37
|
+
- Linux cross-builds from non-Linux hosts require `zig` and `cargo-zigbuild`.
|
|
38
|
+
- Missing cross-toolchain prerequisites are reported as warnings and skipped; host install still succeeds.
|
|
39
|
+
|
|
40
|
+
Useful options:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
./install.sh --no-matrix # host-only build + install
|
|
44
|
+
./install.sh --prefix "$HOME/.local" # install under custom prefix
|
|
45
|
+
./install.sh --windows # best-effort Windows build too
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If you prefer the old local cargo install workflow:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cargo install --path .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or run in place:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cargo run -- --help
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or install the published npm package:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install -g @nothumanwork/sauron
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The npm package bundles prebuilt binaries for:
|
|
67
|
+
- `x86_64-unknown-linux-gnu`
|
|
68
|
+
- `x86_64-apple-darwin`
|
|
69
|
+
- `aarch64-apple-darwin`
|
|
70
|
+
|
|
71
|
+
The `sauron` launcher inside the package selects the matching binary for the current host OS and architecture at runtime.
|
|
72
|
+
|
|
73
|
+
## Quick start
|
|
74
|
+
|
|
75
|
+
Each shell/process must start a runtime session first:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
sauron runtime start
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
macOS defaults to GPU + WebGL on `runtime start` (opt out with `--no-webgl --no-gpu`).
|
|
82
|
+
|
|
83
|
+
Then run browser commands from the same project directory:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
sauron page goto https://example.com
|
|
87
|
+
sauron page snapshot --format json
|
|
88
|
+
sauron input click --ref @e1
|
|
89
|
+
sauron page screenshot --responsive --quality medium
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Clean up with:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
sauron runtime stop
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Mandatory session lifecycle
|
|
99
|
+
|
|
100
|
+
- Non-`runtime start` commands require an active runtime session.
|
|
101
|
+
- Session resolution order is:
|
|
102
|
+
- explicit `--session-id`
|
|
103
|
+
- current process binding
|
|
104
|
+
- current project binding
|
|
105
|
+
- `SAURON_SESSION_ID` fallback
|
|
106
|
+
- If none resolve to an active session, commands fail with `SESSION_REQUIRED`.
|
|
107
|
+
- `start` auto-generates:
|
|
108
|
+
- `session_id` (`sess-...`)
|
|
109
|
+
- `instance` (`inst-...`)
|
|
110
|
+
- `client` (`client-...`)
|
|
111
|
+
- You can still override IDs:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
sauron --session-id mysession --instance work --client alice runtime start
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Interaction Flow
|
|
118
|
+
|
|
119
|
+
```mermaid
|
|
120
|
+
flowchart TD
|
|
121
|
+
U["User or AI Agent"] --> CLI["sauron CLI"]
|
|
122
|
+
CLI --> L["Lifecycle Command"]
|
|
123
|
+
CLI --> B["Browser Command"]
|
|
124
|
+
L --> R["Runtime Store"]
|
|
125
|
+
L --> D["Chrome Daemon"]
|
|
126
|
+
B --> RS["Resolve Active Session"]
|
|
127
|
+
RS --> R
|
|
128
|
+
B --> P["Page and Browser Client"]
|
|
129
|
+
P --> C["CDP Transport"]
|
|
130
|
+
C --> CH["Chrome DevTools Endpoint"]
|
|
131
|
+
B --> O["JSON Result Envelope"]
|
|
132
|
+
O --> U
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Component Data Flow
|
|
136
|
+
|
|
137
|
+
```mermaid
|
|
138
|
+
flowchart LR
|
|
139
|
+
M["CLI Router (main.rs)"] --> RT["Runtime and Session Resolution (runtime.rs)"]
|
|
140
|
+
M --> DM["Daemon Control (daemon.rs)"]
|
|
141
|
+
M --> BR["Browser Actions (browser.rs)"]
|
|
142
|
+
BR --> CDP["CDP Client (cdp.rs)"]
|
|
143
|
+
CDP --> CH["Chrome"]
|
|
144
|
+
RT --> FS["Runtime Filesystem Store"]
|
|
145
|
+
BR --> SNAP["Snapshot and Ref State"]
|
|
146
|
+
BR --> SES["Saved Browser State Sessions"]
|
|
147
|
+
BR --> LOG["Command Logs"]
|
|
148
|
+
SNAP --> FS
|
|
149
|
+
SES --> FS
|
|
150
|
+
LOG --> FS
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Concurrent session workflow
|
|
154
|
+
|
|
155
|
+
Terminal A:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
sauron runtime start
|
|
159
|
+
sauron page goto https://example.com
|
|
160
|
+
sauron state save logged-in
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Terminal B (independent shell/process):
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
sauron runtime start
|
|
167
|
+
sauron page goto https://news.ycombinator.com
|
|
168
|
+
sauron state save baseline
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Both sessions are isolated and can run concurrently without conflicts.
|
|
172
|
+
|
|
173
|
+
If you previously exported `SAURON_SESSION_ID`, clear it to avoid overriding project-aware routing:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
unset SAURON_SESSION_ID
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Runtime state
|
|
180
|
+
|
|
181
|
+
Runtime session state is stored on the local filesystem under `~/.sauron/runtime/`.
|
|
182
|
+
|
|
183
|
+
## Session logs
|
|
184
|
+
|
|
185
|
+
Each session writes NDJSON logs to:
|
|
186
|
+
|
|
187
|
+
`~/.sauron/runtime/logs/<session_id>.ndjson`
|
|
188
|
+
|
|
189
|
+
Each line includes timestamp, session metadata, command name, status, and error details when present.
|
|
190
|
+
|
|
191
|
+
## CLI flag placement
|
|
192
|
+
|
|
193
|
+
Global flags (`--session-id`, `--port`, etc.) must be placed before the subcommand:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
sauron --session-id mysession page goto https://example.com
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`--viewport` is global and applies to `start` and browser commands:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
sauron --viewport 1440x900 runtime start
|
|
203
|
+
sauron --viewport 390x844 page screenshot
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Output contract
|
|
207
|
+
|
|
208
|
+
All commands return exactly one JSON object in a unified v2 envelope:
|
|
209
|
+
|
|
210
|
+
- Success:
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"meta": { "requestId": "...", "timestamp": "...", "durationMs": 12 },
|
|
215
|
+
"result": { "ok": true, "command": "page.snapshot", "data": { /* ... */ } }
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- Error:
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"meta": { "requestId": "...", "timestamp": "...", "durationMs": 9 },
|
|
224
|
+
"result": {
|
|
225
|
+
"ok": false,
|
|
226
|
+
"command": "input.click",
|
|
227
|
+
"error": {
|
|
228
|
+
"code": "ELEMENT_NOT_FOUND",
|
|
229
|
+
"message": "...",
|
|
230
|
+
"hint": "...",
|
|
231
|
+
"recoverable": true,
|
|
232
|
+
"exitCode": 1,
|
|
233
|
+
"category": "state"
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Notes
|
|
240
|
+
|
|
241
|
+
- You need a local Chrome/Chromium install.
|
|
242
|
+
- The daemon uses `--remote-debugging-port=<port>`.
|
|
243
|
+
|
|
244
|
+
## Automated Releases
|
|
245
|
+
|
|
246
|
+
Pushes to `main` cut the next patch version automatically. GitHub Actions then:
|
|
247
|
+
|
|
248
|
+
- builds the release binaries on GitHub-hosted Linux and macOS runners,
|
|
249
|
+
- stages them into the scoped npm package `@nothumanwork/sauron`,
|
|
250
|
+
- publishes the npm package,
|
|
251
|
+
- tags the repository with `v<version>`, and
|
|
252
|
+
- creates the matching GitHub release with per-target tarballs.
|
|
253
|
+
|
|
254
|
+
The release version is kept in sync across `Cargo.toml`, `Cargo.lock`, and `package.json`.
|
package/bin/sauron.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
const { existsSync, readFileSync } = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.resolve(__dirname, '..');
|
|
10
|
+
const targetConfig = JSON.parse(
|
|
11
|
+
readFileSync(path.join(repoRoot, 'distribution', 'targets.json'), 'utf8')
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
function resolveTarget() {
|
|
15
|
+
return targetConfig.targets.find(
|
|
16
|
+
(target) => target.platform === process.platform && target.arch === process.arch
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function supportedTargetList() {
|
|
21
|
+
return targetConfig.targets
|
|
22
|
+
.map((target) => `${target.platform}/${target.arch} -> ${target.triple}`)
|
|
23
|
+
.join(', ');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const target = resolveTarget();
|
|
27
|
+
|
|
28
|
+
if (!target) {
|
|
29
|
+
console.error(
|
|
30
|
+
`Unsupported host ${process.platform}/${process.arch}. Supported targets: ${supportedTargetList()}`
|
|
31
|
+
);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const binaryPath = path.join(repoRoot, 'npm', 'bin', target.triple, targetConfig.binaryName);
|
|
36
|
+
|
|
37
|
+
if (!existsSync(binaryPath)) {
|
|
38
|
+
console.error(
|
|
39
|
+
`Missing bundled binary for ${target.triple} at ${binaryPath}. Reinstall @nothumanwork/sauron or fetch a release asset for this target.`
|
|
40
|
+
);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = spawnSync(binaryPath, process.argv.slice(2), { stdio: 'inherit' });
|
|
45
|
+
|
|
46
|
+
if (result.error) {
|
|
47
|
+
console.error(`Failed to launch ${binaryPath}: ${result.error.message}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (result.signal) {
|
|
52
|
+
process.kill(process.pid, result.signal);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"binaryName": "sauron",
|
|
3
|
+
"targets": [
|
|
4
|
+
{
|
|
5
|
+
"triple": "x86_64-unknown-linux-gnu",
|
|
6
|
+
"runner": "ubuntu-latest",
|
|
7
|
+
"platform": "linux",
|
|
8
|
+
"arch": "x64"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"triple": "x86_64-apple-darwin",
|
|
12
|
+
"runner": "macos-latest",
|
|
13
|
+
"platform": "darwin",
|
|
14
|
+
"arch": "x64"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"triple": "aarch64-apple-darwin",
|
|
18
|
+
"runner": "macos-latest",
|
|
19
|
+
"platform": "darwin",
|
|
20
|
+
"arch": "arm64"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
package/npm/bin/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nothumanwork/sauron",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Rust-native CLI for controlling Chrome via CDP",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/thehumanworks/sauron#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/thehumanworks/sauron.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/thehumanworks/sauron/issues"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"sauron": "./bin/sauron.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin/",
|
|
19
|
+
"distribution/targets.json",
|
|
20
|
+
"npm/bin/",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"release:current": "node ./scripts/release-version.mjs current",
|
|
31
|
+
"release:next-patch": "node ./scripts/release-version.mjs next-patch",
|
|
32
|
+
"release:set-version": "node ./scripts/release-version.mjs set",
|
|
33
|
+
"stage:binaries": "node ./scripts/stage-npm-binaries.mjs",
|
|
34
|
+
"pack:dry-run": "npm pack --dry-run"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"chrome",
|
|
38
|
+
"cdp",
|
|
39
|
+
"cli",
|
|
40
|
+
"automation",
|
|
41
|
+
"rust"
|
|
42
|
+
]
|
|
43
|
+
}
|