@shardflux/cli 0.9.0 → 0.10.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.
package/CHANGELOG.md CHANGED
@@ -1,12 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ Every command and option the README shows is available from the version named here. Breaking changes ship in minor
4
+ releases and are marked **Breaking**.
5
+
6
+ ## 0.10.0 (not yet published)
7
+
8
+ Needs `@shardflux/sdk` 0.15.0 or later.
9
+
10
+ ### One command per workspace
11
+
12
+ Additive (contracts §46), with `@shardflux/sdk` 0.15.0.
13
+
14
+ - `shard exec <key> --template <slug> -- <cmd>`: `--template` on `ws exec` and `ws exec-start` opens the key first,
15
+ so the workspace is created on first use (stderr: `created workspace <key> (template <slug>)`) and resumed if it is
16
+ suspended. Without `--template` the key must have a workspace, as before.
17
+ - `shard exec` is a shortcut for `shard workspaces exec`.
18
+ - `--template default` opens the platform default template (`python-node-browser`).
19
+
20
+ ### Computer use
21
+
22
+ - `shard ws computer status|screenshot|act|start|stop|stream <key>`: the workspace desktop (contracts §45).
23
+ `screenshot --out F` writes the image; `act <json|->` runs a batch of computer toolset actions, prints one line per
24
+ action and exits 1 when one failed (`--screenshot F` saves the screen after it; `--json` prints the full result);
25
+ `stream [--interactive] [--ttl D]` prints a private link to watch it, `stream --stop` ends every view.
26
+ - One command per mouse and keyboard action: `shard ws computer click [x y] [--button] [--count 2|3] [--mods]`,
27
+ `type <text|->`, `key <keys> [--repeat]`, `scroll <dir> [x y] [--amount]`, `move`, `drag`, `mouse-down`, `mouse-up`,
28
+ `hold-key`, `zoom <x0 y0 x1 y1> [--out F]`, `cursor`, `wait`. Every input command and `act` take `--screenshot F`,
29
+ `--settle MS` and `--format jpeg --quality N`; `screenshot` takes `--quality`.
30
+ - `shard ws computer enable|disable|inherit <key>` and `shard ws open --computer-use on|off|inherit`: the workspace's
31
+ computer use switch; `shard templates computer-use <slug> on|off`: the switch for every workspace of a template.
32
+
3
33
  ## 0.9.0 — 2026-10-04 (not yet published)
4
34
 
5
35
  - `shard ws hint <workspace>` explicitly stages a parked workspace ahead of a VM tool call, without waiting for restore or resuming a suspended workspace.
6
36
 
7
- Every command and option the README shows is available from the version named here. Breaking changes ship in minor
8
- releases and are marked **Breaking**.
9
-
10
37
  ## 0.9.0 (not yet published)
11
38
 
12
39
  Needs `@shardflux/sdk` 0.14.0 or later.
package/README.md CHANGED
@@ -42,8 +42,7 @@ password. With the Codex CLI signed in with ChatGPT on this machine, your email
42
42
 
43
43
  ```sh
44
44
  shard signup # or: shard signup --email you@example.com
45
- shard workspaces open acme/demo --template python-node-browser
46
- shard ws exec acme/demo -- python3 -c 'print(40 + 2)'
45
+ shard exec acme/demo --template default -- python3 -c 'print(40 + 2)' # created on first use
47
46
  shard files write acme/demo /home/user/notes.txt --from notes.txt
48
47
  shard files read acme/demo /home/user/notes.txt
49
48
  shard ws suspend acme/demo --wait
@@ -116,8 +115,9 @@ terminal, `--json` on every command, secrets only on standard input, and documen
116
115
  2. **Verify the email** (an `--email` signup, to start the trial): `shard verify --json` emails a
117
116
  6-digit code; ask the person for it, then `shard verify <code>`. Nothing is signed out.
118
117
  3. **Set up again any time:** `shard setup --json` is idempotent.
119
- 4. **Work:** `shard ws open <key> --template <slug> --json`, `shard ws exec <key> -- <cmd>`,
120
- `shard files ...` use the saved key.
118
+ 4. **Work:** `shard exec <key> --template default -- <cmd>` creates the workspace on first use and runs the command
119
+ (0.10.0+); later commands reuse it. `shard ws open <key> --template <slug> --json`, `shard files ...` use the saved
120
+ key too.
121
121
  5. **A paid plan:** `shard billing plans`, then `shard billing upgrade <plan> --json` prints the Stripe
122
122
  Checkout URL; give it to the person, then `shard billing upgrade <plan> --wait` (or
123
123
  `shard billing status`) until it is active. The API activates a plan only when Stripe confirms the
@@ -385,8 +385,8 @@ shows the status; `shard version --check` asks now (`--json`: `{"cli", "sdk", "u
385
385
  | `workspaces list [--state S] [--prefix P] [--lifetime persistent\|session\|any] [--purpose standard\|template_draft\|template_test\|any] [--include-deleted] [--all] [--limit N] [--cursor C]` | One page, or every page with `--all`. The next cursor is printed on stderr. By default only persistent standard workspaces are listed. A file-first workspace's state reads `running (file-first)` (0.5.0+). |
386
386
  | `workspaces get <id\|key>` | One workspace (deleted ones included), with the startup of its template's start commands and services (0.4.0+): a failed step shows its exit code, reason and output tail. Its mode, and a file-first workspace's tree revision (0.5.0+). Its memory (0.8.0+): `fixed 4096 MiB`, or `elastic: promised …, held …, plugged +… MiB`. The template version whose [immutable paths](#immutable-paths-080) it has mounted (0.8.0+): `immutable paths v3 (mounted read-only)`. |
387
387
  | `workspaces inputs <id\|key>` | The workspace's text inputs (0.4.0+). |
388
- | `workspaces exec <id\|key> [--cwd D] [--env K=V]... [--timeout T] [--stdin F\|-] [-v] [--execution-id ID] [--output-limit N] [--resource-hint auto\|light\|heavy] [--burst [--burst-vcpus N] [--burst-memory MIB]] -- <cmd> [args...]` | Runs argv with no shell. Output is streamed (in the start request itself, 0.8.0+) and resumes from byte offsets after dropped connections. `shard` exits with the command's exit code. Ctrl-C cancels the command. `-v` (0.5.0+) then prints the session id and exit status on stderr, and the memory grow the command waited for on an elastic workspace (0.8.0+; `--json`: `memory_grow`). `--cwd` is an absolute path (default `/home/user`); a relative one is refused (`invalid_cwd`, exit 2). A command that could not start (a `--cwd` that is not a directory, a program not on `PATH`) exits 6 with the workspace's reason and a hint (0.5.1+; before, it exited 1 with no output). On a file-first workspace (0.5.0+) the command runs as an execution instead: see [File-first workspaces](#file-first-workspaces). `--burst` (0.8.0+): [burst execution](#burst-execution-080). `--resource-hint` (0.9.0+): [elastic memory](#elastic-memory-080). |
389
- | `workspaces exec-start <id\|key> [--session-id ID] [--cwd D] [--timeout T] [--stdin-open] [--resource-hint auto\|light\|heavy] -- <cmd> [args...]`, `workspaces exec-read <id\|key> <session-id> [--follow]`, `workspaces exec-cancel <id\|key> <session-id> [--grace D]` | Background commands (0.8.0+): start a command and return at once, read its output and state (`--follow` streams it to its exit and exits with its code; while it runs `exec-read` exits 5), stop it. See [Background commands](#background-commands-080). |
388
+ | `workspaces exec <id\|key> [--template S] [--cwd D] [--env K=V]... [--timeout T] [--stdin F\|-] [-v] [--execution-id ID] [--output-limit N] [--resource-hint auto\|light\|heavy] [--burst [--burst-vcpus N] [--burst-memory MIB]] -- <cmd> [args...]` | Runs argv with no shell. `shard exec` is the same command (0.10.0+). `--template <slug>` (0.10.0+) opens the key first: the workspace is created on first use (stderr says so) and resumed if suspended; without it the key must have a workspace. Output is streamed (in the start request itself, 0.8.0+) and resumes from byte offsets after dropped connections. `shard` exits with the command's exit code. Ctrl-C cancels the command. `-v` (0.5.0+) then prints the session id and exit status on stderr, and the memory grow the command waited for on an elastic workspace (0.8.0+; `--json`: `memory_grow`). `--cwd` is an absolute path (default `/home/user`); a relative one is refused (`invalid_cwd`, exit 2). A command that could not start (a `--cwd` that is not a directory, a program not on `PATH`) exits 6 with the workspace's reason and a hint (0.5.1+; before, it exited 1 with no output). On a file-first workspace (0.5.0+) the command runs as an execution instead: see [File-first workspaces](#file-first-workspaces). `--burst` (0.8.0+): [burst execution](#burst-execution-080). `--resource-hint` (0.9.0+): [elastic memory](#elastic-memory-080). |
389
+ | `workspaces exec-start <id\|key> [--template S] [--session-id ID] [--cwd D] [--timeout T] [--stdin-open] [--resource-hint auto\|light\|heavy] -- <cmd> [args...]`, `workspaces exec-read <id\|key> <session-id> [--follow]`, `workspaces exec-cancel <id\|key> <session-id> [--grace D]` | Background commands (0.8.0+): start a command and return at once, read its output and state (`--follow` streams it to its exit and exits with its code; while it runs `exec-read` exits 5), stop it. See [Background commands](#background-commands-080). |
390
390
  | `executions get <id\|key> <execution-id> [--wait D]` | The result of a file-first workspace's execution (0.5.0+): its output, then what ran. Exits with the command's code, 6 if the execution failed or was lost, 5 while it still runs. |
391
391
  | `files read <id\|key> <path> [--out F]` | Raw bytes to stdout, or to a local file. |
392
392
  | `files write <id\|key> <path> [--from F\|-] [--append] [--no-parents] [--if-revision N]` | Atomic write. The default source is stdin. `--if-revision` (0.5.0+): file-first workspaces only, see [Tree revisions](#tree-revisions). |
@@ -408,6 +408,7 @@ shows the status; `shard version --check` asks now (`--json`: `{"cli", "sdk", "u
408
408
  | `workspaces secrets <id\|key> [--set NAME]... [--clear]` | Show the secret names bound to the workspace and their status, or replace them. |
409
409
  | `workspaces port expose <id\|key> <port>`, `workspaces port ls <id\|key>`, `workspaces port close <id\|key> <port>` | Serve a port of the workspace at its own private HTTPS URL, list the exposed ports, close one (0.9.0+; see [Inbound ports](#inbound-ports-090)). |
410
410
  | `workspaces port token <id\|key> <port> [--ttl D]`, `workspaces port link <id\|key> <port> [--ttl D] [--path P]`, `workspaces port callback <id\|key> <port> [--revoke]` | A port token (printed alone), a link that opens the port in a browser, the port's callback URL for webhooks and OAuth redirects (0.9.0+). |
411
+ | `workspaces computer status\|screenshot\|act\|start\|stop\|stream <id\|key>`, `workspaces computer click\|type\|key\|scroll\|move\|drag\|mouse-down\|mouse-up\|hold-key\|zoom\|cursor\|wait <id\|key> ...`, `workspaces computer enable\|disable\|inherit <id\|key>`, `templates computer-use <slug> on\|off` | The workspace desktop for computer use: its state, a screenshot (`--out F`), one command per mouse and keyboard action, a batch of actions (a JSON array), a private link to watch or use it (`stream [--interactive] [--ttl D]`, `--stop`), and the switch for a workspace or every workspace of a template (0.10.0+; see [Computer use](#computer-use-0100)). |
411
412
  | `secrets create <NAME> [--from-file F] [--description T] [--tool T]... [--allow-workspace ID]... [--org ID [--allow-project ID]... [--all-projects]]` | Create a secret. The value comes from standard input or `--from-file`, never from an argument. |
412
413
  | `secrets list [--org ID] [--include-deleted] [--all]`, `secrets get <id\|NAME>` | Secret metadata. Values are never shown. |
413
414
  | `secrets update <id\|NAME> [--description T] [--tool T]... [--allow-workspace ID]... [--any-workspace] [--allow-project ID]... [--all-projects] [--clear-projects]` | Change the description or usage permissions. |
@@ -751,6 +752,46 @@ shard ws port close acme/demo 3000
751
752
  again replaces the URL; `--revoke` removes it.
752
753
  - `close` stops the port's tokens, links and callback URL at once.
753
754
 
755
+ ## Computer use (0.10.0+)
756
+
757
+ A desktop in the workspace for agents that operate GUI software: a screen, a mouse and a keyboard. Switch it on for a
758
+ workspace (or every workspace of your template); the platform starts the desktop on the first computer command.
759
+
760
+ ```sh
761
+ shard ws open acme/demo --template python-node-browser --computer-use on
762
+ shard ws exec-start acme/demo -- chromium https://example.com # opens on the desktop
763
+ shard ws computer screenshot acme/demo --out desktop.png
764
+ shard ws computer click acme/demo 640 400
765
+ shard ws computer type acme/demo "hello" --screenshot after.png
766
+ shard ws computer key acme/demo ctrl+s
767
+ open "$(shard ws computer stream acme/demo)" # watch it live
768
+ ```
769
+
770
+ | Command | Action |
771
+ | --- | --- |
772
+ | `click <ws> [x y] [--button left\|right\|middle] [--count 2\|3] [--mods keys]` | A click, double or triple click at a point or at the pointer |
773
+ | `type <ws> <text\|->` | Types text (Unicode; `-` reads standard input) |
774
+ | `key <ws> <keys> [--repeat n]` | Keys and combinations: `Return`, `ctrl+s`, `alt+Tab`, several separated by spaces |
775
+ | `scroll <ws> up\|down\|left\|right [x y] [--amount n] [--mods keys]` | Wheel clicks (default 3) |
776
+ | `move <ws> <x> <y>`, `drag <ws> <x1> <y1> <x2> <y2>` | Moves the pointer; drags with the left button |
777
+ | `mouse-down <ws>`, `mouse-up <ws>`, `hold-key <ws> <keys> <seconds>` | Holds the left button or keys |
778
+ | `zoom <ws> <x0> <y0> <x1> <y1> [--out F]` | Saves an enlarged view of a region |
779
+ | `cursor <ws>`, `wait <ws> <seconds>` | Prints the pointer position; waits |
780
+ | `act <ws> <json\|->` | Any of these in one batch: a JSON array of actions |
781
+
782
+ Every input command takes `--screenshot F` (a screenshot after it, written to `F`), `--settle MS` (the wait before
783
+ that screenshot, default 250) and `--format jpeg --quality N` for smaller images; `screenshot` and `zoom` take
784
+ `--format` and `--quality` too.
785
+
786
+ - The actions are Claude's computer toolset members with their parameters (`screenshot`, `zoom`, `left_click`,
787
+ `right_click`, `middle_click`, `double_click`, `triple_click`, `left_click_drag`, `mouse_move`, `left_mouse_down`,
788
+ `left_mouse_up`, `cursor_position`, `scroll`, `type`, `key`, `hold_key`, `wait`). Each command above and `act` print
789
+ one line per action and exit 1 when one failed (in a batch the rest are skipped).
790
+ - `stream` prints a link to the viewer (view only unless `--interactive`; `--ttl` 1m to 7d, default 24h); anyone with
791
+ it can watch until it expires. `stream --stop` ends every view.
792
+ - `ws computer enable|disable|inherit` sets the workspace's own switch (`inherit` follows the template);
793
+ `templates computer-use <slug> on|off` sets it for every workspace of one of your templates.
794
+
754
795
  ## Start deadlines
755
796
 
756
797
  A queued open, resume or fork (`capacity_pending`) has a deadline 15 minutes after it was created.
package/dist/args.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const CLI_VERSION = "0.9.0";
1
+ export declare const CLI_VERSION = "0.10.0";
2
2
  export interface OptionSpec {
3
3
  type: 'string' | 'boolean';
4
4
  short?: string;
@@ -29,7 +29,6 @@ export interface CommandSpec {
29
29
  export declare const GLOBAL_OPTIONS: Record<string, OptionSpec>;
30
30
  /** Every command: the account plane first (sign in, set up), then the project-key commands. */
31
31
  export declare const COMMANDS: CommandSpec[];
32
- /** Canonical command words: aliases resolved, `workspaces files|operations|executions` folded to the top-level group. */
33
32
  export declare function normalizeWords(words: string[]): string[];
34
33
  export declare function findCommand(words: string[]): CommandSpec | undefined;
35
34
  /** "90s", "5m", "1.5h", "250ms"; a bare number is seconds. */
package/dist/args.js CHANGED
@@ -12,7 +12,7 @@
12
12
  import { parseArgs } from 'node:util';
13
13
  import { ACCOUNT_COMMANDS } from "./account-args.js";
14
14
  import { UsageError } from "./errors.js";
15
- export const CLI_VERSION = '0.9.0';
15
+ export const CLI_VERSION = '0.10.0';
16
16
  export const GLOBAL_OPTIONS = {
17
17
  'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
18
18
  json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
@@ -64,6 +64,13 @@ const REF = { name: 'id|key', required: true };
64
64
  /** A TCP port inside the workspace (1-65535). */
65
65
  const PORT = { name: 'port', required: true };
66
66
  const SLUG = { name: 'slug', required: true };
67
+ /** The screenshot after a computer action or batch (0.10.0+). */
68
+ const COMPUTER_LOOK = {
69
+ screenshot: { type: 'string', value: '<file>', description: 'Take a screenshot after it and write it here.' },
70
+ settle: { type: 'string', value: '<ms>', description: 'Wait this long before that screenshot when the screen may change (0-10000, default 250).' },
71
+ format: { type: 'string', value: '<png|jpeg>', description: 'Screenshot format (default png).' },
72
+ quality: { type: 'string', value: '<1-100>', description: 'JPEG quality (default 80).' },
73
+ };
67
74
  const PAGE = {
68
75
  all: { type: 'boolean', description: 'Fetch every page.' },
69
76
  limit: { type: 'string', value: '<n>', description: 'Page size.' },
@@ -218,6 +225,11 @@ const KEY_COMMANDS = [
218
225
  value: '<processful|file-first>',
219
226
  description: 'file-first: the workspace is a versioned file tree with no VM between commands; each "shard ws exec" runs in a fresh VM and keeps the files it changed under /home/user as the next tree revision. Ready at once, never suspended, persistent, needs a layered template version. Default: processful for a new key, the stored mode for an existing one (a mode never changes).',
220
227
  },
228
+ 'computer-use': {
229
+ type: 'string',
230
+ value: '<on|off|inherit>',
231
+ description: 'Computer use (0.10.0+): on gives the workspace a desktop agents drive (shard ws computer); inherit follows the template. Default: unchanged (off for a new key unless its template turns it on).',
232
+ },
221
233
  ...INPUTS,
222
234
  ...CAPS,
223
235
  ...MEMORY_ALLOCATION,
@@ -265,6 +277,11 @@ const KEY_COMMANDS = [
265
277
  description: 'Runs argv directly (no shell; use `-- bash -lc "..."` for shell syntax) and returns the session at once (JSON on stdout). Read it with "shard ws exec-read", stop it with "shard ws exec-cancel".',
266
278
  positionals: [REF, { name: 'command', required: true, variadic: true }],
267
279
  options: {
280
+ template: {
281
+ type: 'string',
282
+ value: '<slug>',
283
+ description: 'Open the key first (0.10.0+): the workspace is created from this template on first use (stderr says so) and resumed if suspended; "default" is the platform default template. Without it the key must already have a workspace.',
284
+ },
268
285
  'stdin-open': { type: 'boolean', description: 'Keep pipe stdin open for exec-input.' },
269
286
  'session-id': { type: 'string', value: '<id>', description: 'Caller-chosen id to deduplicate starts.' },
270
287
  cwd: { type: 'string', value: '<dir>', description: 'Absolute working directory.' },
@@ -305,9 +322,14 @@ const KEY_COMMANDS = [
305
322
  {
306
323
  path: ['workspaces', 'exec'],
307
324
  summary: 'Run a command in a workspace; prints its output and exits with its exit code',
308
- description: 'Runs argv directly (no shell; use `-- bash -lc "..."` for shell syntax). Processful workspaces: output is streamed as it arrives and resumes from byte offsets after dropped connections; Ctrl-C stops waiting and cancels the command. File-first workspaces: the command runs as an execution in a fresh VM on the latest tree revision; its output is printed when it ended, and the files it changed under /home/user become the next tree revision (-v lists them). An execution runs to completion: Ctrl-C stops waiting and names the command that fetches its result later. A failed or lost execution (nothing published) exits 6, as does a command that could not start (a --cwd that is not a directory, a program not on PATH), with the workspace’s reason.',
325
+ description: 'Also `shard exec` (0.10.0+). With --template the key is opened first, so one command creates the workspace on first use: `shard exec acme/demo --template default -- python3 -V`. Runs argv directly (no shell; use `-- bash -lc "..."` for shell syntax). Processful workspaces: output is streamed as it arrives and resumes from byte offsets after dropped connections; Ctrl-C stops waiting and cancels the command. File-first workspaces: the command runs as an execution in a fresh VM on the latest tree revision; its output is printed when it ended, and the files it changed under /home/user become the next tree revision (-v lists them). An execution runs to completion: Ctrl-C stops waiting and names the command that fetches its result later. A failed or lost execution (nothing published) exits 6, as does a command that could not start (a --cwd that is not a directory, a program not on PATH), with the workspace’s reason.',
309
326
  positionals: [REF, { name: 'command', required: true, variadic: true }],
310
327
  options: {
328
+ template: {
329
+ type: 'string',
330
+ value: '<slug>',
331
+ description: 'Open the key first (0.10.0+): the workspace is created from this template on first use (stderr says so) and resumed if suspended; "default" is the platform default template. Without it the key must already have a workspace.',
332
+ },
311
333
  cwd: { type: 'string', value: '<dir>', description: 'Working directory, an absolute path (default /home/user; under /home/user on a file-first workspace). A relative path is refused (invalid_cwd).' },
312
334
  env: { type: 'string', multiple: true, value: '<K=V>', description: 'Environment variable (repeatable).' },
313
335
  timeout: { type: 'string', value: '<duration>', description: 'Kill the command after this long (exit code 124).' },
@@ -484,6 +506,188 @@ const KEY_COMMANDS = [
484
506
  options: { revoke: { type: 'boolean', description: 'Revoke the port’s callback URL instead (idempotent).' } },
485
507
  examples: ['shard ws port callback acme/demo 3000', 'shard ws port callback acme/demo 3000 --revoke'],
486
508
  },
509
+ // ---- computer use (0.10.0; contracts §45) ----
510
+ {
511
+ path: ['workspaces', 'computer', 'status'],
512
+ summary: 'Show whether the workspace desktop runs, its size and its viewers (0.10.0+)',
513
+ description: 'Never starts the desktop. Needs computer use on for the workspace (shard ws computer enable). --json prints {running, display, width, height, started_at, session, streams}.',
514
+ positionals: [REF],
515
+ options: {},
516
+ },
517
+ {
518
+ path: ['workspaces', 'computer', 'screenshot'],
519
+ summary: 'Save a screenshot of the workspace desktop (0.10.0+)',
520
+ description: 'Starts the desktop when it does not run. Writes the image to --out (default screenshot.png, or screenshot.jpg with --format jpeg). --json prints {out, format, width, height, bytes}.',
521
+ positionals: [REF],
522
+ options: {
523
+ out: { type: 'string', value: '<file>', description: 'Where to write the image (default screenshot.png).' },
524
+ format: { type: 'string', value: '<png|jpeg>', description: 'Image format (default png).' },
525
+ quality: { type: 'string', value: '<1-100>', description: 'JPEG quality (default 80).' },
526
+ },
527
+ examples: ['shard ws computer screenshot acme/demo --out desktop.png', 'shard ws computer screenshot acme/demo --format jpeg --quality 60'],
528
+ },
529
+ {
530
+ path: ['workspaces', 'computer', 'act'],
531
+ summary: 'Run a batch of computer actions on the workspace desktop (0.10.0+)',
532
+ description: 'The actions are a JSON array of Claude computer toolset members with their parameters, e.g. [{"action":"left_click","coordinate":[640,400]},{"action":"type","text":"hello"}]; - reads it from standard input. They run in order; the first failure stops the batch and the rest are reported skipped. --screenshot saves a screenshot taken after the last action. --json prints the full result (images base64).',
533
+ positionals: [REF, { name: 'actions-json', required: true }],
534
+ options: { ...COMPUTER_LOOK },
535
+ examples: [
536
+ `shard ws computer act acme/demo '[{"action":"key","text":"ctrl+l"},{"action":"type","text":"example.com\\n"}]' --screenshot after.png`,
537
+ 'echo \'[{"action":"cursor_position"}]\' | shard ws computer act acme/demo -',
538
+ ],
539
+ },
540
+ {
541
+ path: ['workspaces', 'computer', 'click'],
542
+ summary: 'Click on the workspace desktop (0.10.0+)',
543
+ description: 'At <x> <y> (screen pixels from the top-left of a screenshot), or at the pointer without them. --button right|middle, --count 2|3 for a double or triple click (left button), --mods holds modifier keys (e.g. shift, ctrl+shift).',
544
+ positionals: [REF, { name: 'x' }, { name: 'y' }],
545
+ options: {
546
+ button: { type: 'string', value: '<left|right|middle>', description: 'Mouse button (default left).' },
547
+ count: { type: 'string', value: '<1|2|3>', description: 'Clicks: 2 is a double click, 3 a triple click (left button; default 1).' },
548
+ mods: { type: 'string', value: '<keys>', description: 'Modifier keys held during the click, e.g. shift or ctrl+shift.' },
549
+ ...COMPUTER_LOOK,
550
+ },
551
+ examples: ['shard ws computer click acme/demo 640 400 --screenshot after.png', 'shard ws computer click acme/demo 200 120 --count 2'],
552
+ },
553
+ {
554
+ path: ['workspaces', 'computer', 'type'],
555
+ summary: 'Type text on the workspace desktop (0.10.0+)',
556
+ description: 'Types into the focused window, Unicode included (up to 20,000 characters); - reads the text from standard input. A newline presses Return.',
557
+ positionals: [REF, { name: 'text|-', required: true }],
558
+ options: { ...COMPUTER_LOOK },
559
+ examples: ['shard ws computer type acme/demo "hello world"', 'cat notes.txt | shard ws computer type acme/demo -'],
560
+ },
561
+ {
562
+ path: ['workspaces', 'computer', 'key'],
563
+ summary: 'Press keys on the workspace desktop (0.10.0+)',
564
+ description: 'One key or combination, or several separated by spaces: Return, Escape, ctrl+s, alt+Tab, ctrl+shift+t, F5, Page_Down.',
565
+ positionals: [REF, { name: 'keys', required: true }],
566
+ options: {
567
+ repeat: { type: 'string', value: '<n>', description: 'Press it n times (1-100).' },
568
+ ...COMPUTER_LOOK,
569
+ },
570
+ examples: ['shard ws computer key acme/demo ctrl+l', 'shard ws computer key acme/demo Tab --repeat 3 --screenshot form.png'],
571
+ },
572
+ {
573
+ path: ['workspaces', 'computer', 'scroll'],
574
+ summary: 'Scroll on the workspace desktop (0.10.0+)',
575
+ description: 'Scrolls up, down, left or right by --amount wheel clicks (default 3), at <x> <y> or at the pointer.',
576
+ positionals: [REF, { name: 'up|down|left|right', required: true }, { name: 'x' }, { name: 'y' }],
577
+ options: {
578
+ amount: { type: 'string', value: '<n>', description: 'Wheel clicks (1-100, default 3).' },
579
+ mods: { type: 'string', value: '<keys>', description: 'Modifier keys held while scrolling, e.g. shift.' },
580
+ ...COMPUTER_LOOK,
581
+ },
582
+ examples: ['shard ws computer scroll acme/demo down 640 400 --amount 5'],
583
+ },
584
+ {
585
+ path: ['workspaces', 'computer', 'move'],
586
+ summary: 'Move the pointer on the workspace desktop (0.10.0+)',
587
+ positionals: [REF, { name: 'x', required: true }, { name: 'y', required: true }],
588
+ options: { ...COMPUTER_LOOK },
589
+ },
590
+ {
591
+ path: ['workspaces', 'computer', 'drag'],
592
+ summary: 'Drag with the left button on the workspace desktop (0.10.0+)',
593
+ description: 'Presses at <x1> <y1>, moves to <x2> <y2> and releases.',
594
+ positionals: [REF, { name: 'x1', required: true }, { name: 'y1', required: true }, { name: 'x2', required: true }, { name: 'y2', required: true }],
595
+ options: {
596
+ mods: { type: 'string', value: '<keys>', description: 'Modifier keys held during the drag, e.g. alt.' },
597
+ ...COMPUTER_LOOK,
598
+ },
599
+ examples: ['shard ws computer drag acme/demo 100 200 500 200'],
600
+ },
601
+ {
602
+ path: ['workspaces', 'computer', 'mouse-down'],
603
+ summary: 'Press the left mouse button and keep it down (0.10.0+)',
604
+ description: 'At the pointer (shard ws computer move first); shard ws computer mouse-up releases it.',
605
+ positionals: [REF],
606
+ options: { ...COMPUTER_LOOK },
607
+ },
608
+ {
609
+ path: ['workspaces', 'computer', 'mouse-up'],
610
+ summary: 'Release the left mouse button (0.10.0+)',
611
+ positionals: [REF],
612
+ options: { ...COMPUTER_LOOK },
613
+ },
614
+ {
615
+ path: ['workspaces', 'computer', 'hold-key'],
616
+ summary: 'Hold keys down for some seconds on the workspace desktop (0.10.0+)',
617
+ positionals: [REF, { name: 'keys', required: true }, { name: 'seconds', required: true }],
618
+ options: { ...COMPUTER_LOOK },
619
+ examples: ['shard ws computer hold-key acme/demo shift 2'],
620
+ },
621
+ {
622
+ path: ['workspaces', 'computer', 'zoom'],
623
+ summary: 'Save an enlarged view of part of the workspace desktop (0.10.0+)',
624
+ description: 'The region <x0> <y0> <x1> <y1> (screen pixels, x0 < x1, y0 < y1), scaled up to the screen width. --json prints {out, format, width, height, bytes}.',
625
+ positionals: [REF, { name: 'x0', required: true }, { name: 'y0', required: true }, { name: 'x1', required: true }, { name: 'y1', required: true }],
626
+ options: {
627
+ out: { type: 'string', value: '<file>', description: 'Where to write the image (default zoom.png).' },
628
+ format: { type: 'string', value: '<png|jpeg>', description: 'Image format (default png).' },
629
+ quality: { type: 'string', value: '<1-100>', description: 'JPEG quality (default 80).' },
630
+ },
631
+ examples: ['shard ws computer zoom acme/demo 0 0 640 200 --out toolbar.png'],
632
+ },
633
+ {
634
+ path: ['workspaces', 'computer', 'cursor'],
635
+ summary: 'Print where the pointer is on the workspace desktop (0.10.0+)',
636
+ positionals: [REF],
637
+ options: {},
638
+ },
639
+ {
640
+ path: ['workspaces', 'computer', 'wait'],
641
+ summary: 'Wait on the workspace desktop, then optionally take a screenshot (0.10.0+)',
642
+ description: 'For a page or program that is still loading: shard ws computer wait <ws> 2 --screenshot after.png.',
643
+ positionals: [REF, { name: 'seconds', required: true }],
644
+ options: { ...COMPUTER_LOOK },
645
+ },
646
+ {
647
+ path: ['workspaces', 'computer', 'start'],
648
+ summary: 'Start the workspace desktop (0.10.0+)',
649
+ description: 'A no-op while it runs. The size applies to a start: 640x480 to 2560x1600 (default 1280x800).',
650
+ positionals: [REF],
651
+ options: {
652
+ width: { type: 'string', value: '<px>', description: 'Screen width (640-2560).' },
653
+ height: { type: 'string', value: '<px>', description: 'Screen height (480-1600).' },
654
+ },
655
+ },
656
+ { path: ['workspaces', 'computer', 'stop'], summary: 'Stop the workspace desktop; its windows close (0.10.0+)', positionals: [REF], options: {} },
657
+ {
658
+ path: ['workspaces', 'computer', 'stream'],
659
+ summary: 'Print a private link to watch the workspace desktop live (0.10.0+)',
660
+ description: 'Starts the viewer in the workspace, exposes its port and signs a link to it (inbound ports). View only unless --interactive. Anyone with the link can open it until it expires: share it like a password. --stop ends every view instead. --json prints {url, expires_at, port, interactive}.',
661
+ positionals: [REF],
662
+ options: {
663
+ interactive: { type: 'boolean', description: 'Let the viewer use the mouse and keyboard.' },
664
+ ttl: { type: 'string', value: '<duration>', description: 'Lifetime of the link, 1m to 7d (default 24h).' },
665
+ stop: { type: 'boolean', description: 'End every view and close the viewer ports.' },
666
+ },
667
+ examples: ['open "$(shard ws computer stream acme/demo)"', 'shard ws computer stream acme/demo --stop'],
668
+ },
669
+ {
670
+ path: ['workspaces', 'computer', 'enable'],
671
+ summary: 'Switch computer use on for a workspace (0.10.0+)',
672
+ description: 'Tool tokens then carry the computer tool; the platform starts the desktop on the first computer call. --json prints {enabled, workspace, template, available}.',
673
+ positionals: [REF],
674
+ options: {},
675
+ },
676
+ { path: ['workspaces', 'computer', 'disable'], summary: 'Switch computer use off for a workspace (0.10.0+)', positionals: [REF], options: {} },
677
+ {
678
+ path: ['workspaces', 'computer', 'inherit'],
679
+ summary: 'Let a workspace follow its template’s computer use switch (0.10.0+)',
680
+ positionals: [REF],
681
+ options: {},
682
+ },
683
+ {
684
+ path: ['templates', 'computer-use'],
685
+ summary: 'Switch computer use on or off for every workspace of one of your templates (0.10.0+)',
686
+ description: 'A workspace may override it (shard ws computer enable|disable). Platform templates keep it off: switch a workspace on instead.',
687
+ positionals: [SLUG, { name: 'on|off', required: true }],
688
+ options: {},
689
+ examples: ['shard templates computer-use my-desktop-agent on'],
690
+ },
487
691
  {
488
692
  path: ['files', 'read'],
489
693
  summary: 'Print (or save) a file from a workspace',
@@ -891,8 +1095,10 @@ const ALIASES = {
891
1095
  /** Groups also reachable under `workspaces` (`shard workspaces files read ...` == `shard files read ...`). */
892
1096
  const FOLDED = new Set(['files', 'operations', 'executions']);
893
1097
  /** Canonical command words: aliases resolved, `workspaces files|operations|executions` folded to the top-level group. */
1098
+ /** Top-level shortcuts for a whole command path (0.10.0+): `shard exec` is `shard workspaces exec`. */
1099
+ const SHORTCUTS = { exec: ['workspaces', 'exec'] };
894
1100
  export function normalizeWords(words) {
895
- const w = words.map((x) => ALIASES[x] ?? x);
1101
+ const w = words.flatMap((x, i) => (i === 0 ? SHORTCUTS[x] : undefined) ?? [ALIASES[x] ?? x]);
896
1102
  if (w[0] === 'workspaces' && w[1] !== undefined && FOLDED.has(w[1]))
897
1103
  return w.slice(1);
898
1104
  return w;
@@ -96,7 +96,7 @@ export declare function principal(ctx: Ctx): Promise<{
96
96
  id: string;
97
97
  key_id: string;
98
98
  };
99
- tool_permissions: ("git" | "exec" | "files" | "pty" | "process" | "browser")[];
99
+ tool_permissions: ("git" | "exec" | "files" | "pty" | "process" | "browser" | "computer")[];
100
100
  }>;
101
101
  /** The exit code `shard exec` returns for a finished command. */
102
102
  export declare function execExitCode(r: Pick<RunResult, 'exitCode' | 'termSignal' | 'timedOut'>): number;
package/dist/commands.js CHANGED
@@ -346,6 +346,22 @@ async function waitOperation(ctx, operationId, timeoutMs) {
346
346
  * Runs an SDK call that waits itself (a waited open or lifecycle call). Ctrl-C reports the operation it was waiting
347
347
  * for, as the SDK observed it; a held open interrupted before the server answered has none yet (`unnamed`).
348
348
  */
349
+ /**
350
+ * The workspace of `ws exec` / `exec-start`: with `--template <slug>` (0.10.0+) the key is opened first (created on first
351
+ * use, resumed if suspended; stderr says when it was created), else `<id|key>` must name an existing workspace.
352
+ */
353
+ async function execTarget(ctx, values, ref) {
354
+ const template = str(values.template);
355
+ if (template === undefined)
356
+ return resolveWorkspace(ctx, ref);
357
+ const ws = await waited(ctx, 'the open continues server side (run the command again to keep waiting).', () => ctx
358
+ .cloud()
359
+ .workspace(ref, { template, agentLabel: ctx.agentLabel, wait: { timeoutMs: DEFAULT_WAIT_MS, signal: ctx.signal } })
360
+ .open());
361
+ if (ws.created === true && !ctx.json)
362
+ ctx.io.stderr.write(`created workspace ${ws.key} (template ${template})\n`);
363
+ return ws;
364
+ }
349
365
  async function waited(ctx, unnamed, call) {
350
366
  try {
351
367
  return await call();
@@ -623,6 +639,71 @@ async function portWorkspace(ctx, ref) {
623
639
  requireProcessful(ws, 'inbound_ports', 'Serving a port');
624
640
  return ws;
625
641
  }
642
+ /** The workspace of a computer command; a file-first workspace has no desktop (refused before any request). */
643
+ /** --format/--quality of a desktop image (0.10.0+). */
644
+ function imageOpts(values) {
645
+ const format = str(values.format);
646
+ if (format !== undefined && format !== 'png' && format !== 'jpeg')
647
+ throw new UsageError(`--format must be png or jpeg (got "${format}")`);
648
+ const quality = str(values.quality);
649
+ return { ...(format !== undefined ? { format } : {}), ...(quality !== undefined ? { quality: parsePositiveInt(quality, 'quality', 100) } : {}) };
650
+ }
651
+ /** --screenshot <file>, --settle and the image options: the screenshot after a computer action or batch. */
652
+ function lookOpts(values) {
653
+ const shotTo = str(values.screenshot);
654
+ const settle = str(values.settle);
655
+ if (settle !== undefined && (!/^\d+$/.test(settle.trim()) || Number(settle) > 10_000))
656
+ throw new UsageError(`--settle must be 0 to 10000 milliseconds (got "${settle}")`);
657
+ return { shotTo, req: { screenshot: shotTo !== undefined, ...(settle !== undefined ? { settle_ms: Number(settle) } : {}), ...imageOpts(values) } };
658
+ }
659
+ /** A screen coordinate argument (pixels from the top-left). */
660
+ function pixel(text, name) {
661
+ if (!/^\d+$/.test(text.trim()))
662
+ throw new UsageError(`<${name}> must be a whole number of pixels (got "${text}")`);
663
+ return Number(text);
664
+ }
665
+ /** <x> <y> as a coordinate; neither means at the pointer. */
666
+ function pointAt(x, y) {
667
+ if (x === undefined)
668
+ return {};
669
+ if (y === undefined)
670
+ throw new UsageError('pass both <x> and <y>, or neither to act at the pointer');
671
+ return { coordinate: [pixel(x, 'x'), pixel(y, 'y')] };
672
+ }
673
+ const modsOf = (values) => (str(values.mods) !== undefined ? { text: str(values.mods) } : {});
674
+ function secondsOf(text, name) {
675
+ if (!/^\d+(\.\d+)?$/.test(text.trim()) || Number(text) > 300)
676
+ throw new UsageError(`${name} must be 0 to 300 seconds (got "${text}")`);
677
+ return Number(text);
678
+ }
679
+ /** Runs computer actions as one batch and prints one line per action; exit 1 when one failed. */
680
+ async function runComputer(ctx, ref, actions, look) {
681
+ const ws = await computerWorkspace(ctx, ref);
682
+ const r = await cellOf(ctx, ws).computer.act({ actions, ...look.req });
683
+ if (look.shotTo !== undefined && r.screenshot)
684
+ await writeFile(look.shotTo, Buffer.from(r.screenshot.data, 'base64'));
685
+ const lines = r.results.map((x, i) => {
686
+ const state = x.skipped ? 'skipped' : x.ok ? 'ok' : `failed: ${x.error?.message ?? ''}`;
687
+ return `${String(i + 1).padStart(3)} ${x.action.padEnd(16)} ${state}${x.output ? ` ${x.output}` : ''}${x.image ? ' (image)' : ''} ${x.took_ms} ms`;
688
+ });
689
+ const tail = `cursor ${r.cursor[0]},${r.cursor[1]} on a ${r.display.width}x${r.display.height} screen${look.shotTo !== undefined && r.screenshot ? `; screenshot written to ${look.shotTo}` : ''}`;
690
+ out(ctx, `${lines.join('\n')}${lines.length ? '\n' : ''}${tail}\n`, r);
691
+ return r.results.every((x) => x.ok) ? 0 : 1;
692
+ }
693
+ async function computerWorkspace(ctx, ref) {
694
+ const ws = await resolveWorkspace(ctx, ref);
695
+ requireProcessful(ws, 'computer_use', 'Computer use');
696
+ return ws;
697
+ }
698
+ async function setComputerUse(ctx, ref, enabled) {
699
+ const ws = await computerWorkspace(ctx, ref);
700
+ await ws.setComputerUse(enabled);
701
+ const cu = ws.computerUse;
702
+ const why = cu.workspace === null ? `following its template (${cu.template ? 'on' : 'off'})` : 'set on the workspace';
703
+ const note = !cu.available ? ' Its template version cannot run a desktop.' : '';
704
+ out(ctx, `Computer use is ${cu.enabled ? 'on' : 'off'} for ${ws.key}, ${why}.${note}\n`, cu);
705
+ return 0;
706
+ }
626
707
  /** A port as the API sends it (`--json`). */
627
708
  const portJson = (p) => ({ port: p.port, url: p.url, created_at: p.createdAt, callback: p.callback ? { created_at: p.callback.createdAt } : null });
628
709
  function requireProcessful(ws, operation, what) {
@@ -768,7 +849,7 @@ export const KEY_HANDLERS = {
768
849
  const timeout = str(values.timeout);
769
850
  const timeoutMs = timeout === undefined ? undefined : parseDuration(timeout, 'timeout');
770
851
  const resourceHint = oneOf(values, 'resource-hint', RESOURCE_HINTS);
771
- const ws = await resolveWorkspace(ctx, ref);
852
+ const ws = await execTarget(ctx, values, ref);
772
853
  const session = await cellOf(ctx, ws).exec.start({ argv, stdin_open: values['stdin-open'] === true,
773
854
  ...(resourceHint !== undefined ? { resource_hint: resourceHint } : {}),
774
855
  ...(str(values['session-id']) ? { session_id: str(values['session-id']) } : {}), ...(str(values.cwd) ? { cwd: str(values.cwd) } : {}),
@@ -858,6 +939,10 @@ export const KEY_HANDLERS = {
858
939
  const bind = secretNames(strs(values.secret), '--secret');
859
940
  const lifetime = oneOf(values, 'lifetime', LIFETIMES);
860
941
  const mode = modeFlag(values);
942
+ const cuText = str(values['computer-use']);
943
+ if (cuText !== undefined && !['on', 'off', 'inherit'].includes(cuText))
944
+ throw new UsageError(`--computer-use must be on, off or inherit (got "${cuText}")`);
945
+ const computerUse = cuText === undefined ? undefined : cuText === 'inherit' ? null : cuText === 'on';
861
946
  if (mode === 'file_first' && lifetime === 'session')
862
947
  throw new UsageError('a file-first workspace is persistent: --lifetime session is for processful workspaces');
863
948
  const inputs = await inputsFrom(ctx, values);
@@ -875,6 +960,7 @@ export const KEY_HANDLERS = {
875
960
  ...(inputs ? { inputs } : {}),
876
961
  ...(lifetime ? { lifetime } : {}),
877
962
  ...(mode ? { mode } : {}),
963
+ ...(computerUse !== undefined ? { computerUse } : {}),
878
964
  agentLabel: ctx.agentLabel,
879
965
  wait: values.wait === false ? false : { timeoutMs, signal: ctx.signal },
880
966
  }));
@@ -947,7 +1033,7 @@ export const KEY_HANDLERS = {
947
1033
  const burst = burstOptions(values, stdinSource !== undefined);
948
1034
  const resourceHint = oneOf(values, 'resource-hint', RESOURCE_HINTS);
949
1035
  const stdin = stdinSource === undefined ? undefined : await readInput(ctx, stdinSource, MAX_STDIN, '--stdin');
950
- const ws = await resolveWorkspace(ctx, ref);
1036
+ const ws = await execTarget(ctx, values, ref);
951
1037
  const cell = cellOf(ctx, ws);
952
1038
  const cwd = str(values.cwd);
953
1039
  if (ws.mode === 'file_first') {
@@ -1795,6 +1881,158 @@ export const KEY_HANDLERS = {
1795
1881
  }
1796
1882
  return 0;
1797
1883
  },
1884
+ // ---- computer use (0.10.0; contracts §45) ----
1885
+ async 'workspaces computer status'(ctx, _values, [ref]) {
1886
+ const ws = await computerWorkspace(ctx, ref);
1887
+ const st = await cellOf(ctx, ws).computer.status();
1888
+ const views = [st.streams.view ? 'view only' : null, st.streams.interactive ? 'interactive' : null].filter(Boolean);
1889
+ out(ctx, st.running
1890
+ ? `Desktop of ${ws.key}: running ${st.width}x${st.height} since ${time(st.started_at ?? '')}; viewers: ${views.length ? views.join(', ') : 'none'}.\n`
1891
+ : `Desktop of ${ws.key}: not running (the first computer call starts it).\n`, st);
1892
+ return 0;
1893
+ },
1894
+ async 'workspaces computer screenshot'(ctx, values, [ref]) {
1895
+ const img = imageOpts(values);
1896
+ const dest = str(values.out) ?? (img.format === 'jpeg' ? 'screenshot.jpg' : 'screenshot.png');
1897
+ const ws = await computerWorkspace(ctx, ref);
1898
+ const r = await cellOf(ctx, ws).computer.act({ actions: [], screenshot: true, ...img });
1899
+ if (!r.screenshot)
1900
+ throw new ShardfluxProtocolError('the desktop returned no screenshot', 200, 'cell');
1901
+ const bytes = Buffer.from(r.screenshot.data, 'base64');
1902
+ await writeFile(dest, bytes);
1903
+ out(ctx, `Wrote a ${r.screenshot.width}x${r.screenshot.height} screenshot of ${ws.key} to ${dest}.\n`, {
1904
+ out: dest, format: r.screenshot.format, width: r.screenshot.width, height: r.screenshot.height, bytes: bytes.length,
1905
+ });
1906
+ return 0;
1907
+ },
1908
+ async 'workspaces computer act'(ctx, values, [ref, source]) {
1909
+ const text = source === '-' ? Buffer.from(await readInput(ctx, '-', 1 << 20, 'actions')).toString('utf8') : source;
1910
+ let actions;
1911
+ try {
1912
+ actions = JSON.parse(text);
1913
+ }
1914
+ catch {
1915
+ throw new UsageError('<actions-json> must be a JSON array of actions, e.g. [{"action":"screenshot"}]');
1916
+ }
1917
+ if (!Array.isArray(actions))
1918
+ throw new UsageError('<actions-json> must be a JSON array of actions');
1919
+ return runComputer(ctx, ref, actions, lookOpts(values));
1920
+ },
1921
+ async 'workspaces computer click'(ctx, values, [ref, x, y]) {
1922
+ const button = str(values.button) ?? 'left';
1923
+ if (button !== 'left' && button !== 'right' && button !== 'middle')
1924
+ throw new UsageError(`--button must be left, right or middle (got "${button}")`);
1925
+ const count = str(values.count) !== undefined ? parsePositiveInt(str(values.count), 'count', 3) : 1;
1926
+ if (count > 1 && button !== 'left')
1927
+ throw new UsageError('--count 2 or 3 is a double or triple click of the left button');
1928
+ const action = button === 'right' ? 'right_click' : button === 'middle' ? 'middle_click' : ['left_click', 'double_click', 'triple_click'][count - 1];
1929
+ return runComputer(ctx, ref, [{ action, ...pointAt(x, y), ...modsOf(values) }], lookOpts(values));
1930
+ },
1931
+ async 'workspaces computer type'(ctx, values, [ref, source]) {
1932
+ const text = source === '-' ? Buffer.from(await readInput(ctx, '-', 256 << 10, 'text')).toString('utf8') : source;
1933
+ if (text === '')
1934
+ throw new UsageError('<text> must not be empty');
1935
+ return runComputer(ctx, ref, [{ action: 'type', text }], lookOpts(values));
1936
+ },
1937
+ async 'workspaces computer key'(ctx, values, [ref, keys]) {
1938
+ const repeat = str(values.repeat) !== undefined ? parsePositiveInt(str(values.repeat), 'repeat', 100) : undefined;
1939
+ return runComputer(ctx, ref, [{ action: 'key', text: keys, ...(repeat !== undefined ? { repeat } : {}) }], lookOpts(values));
1940
+ },
1941
+ async 'workspaces computer scroll'(ctx, values, [ref, direction, x, y]) {
1942
+ if (direction !== 'up' && direction !== 'down' && direction !== 'left' && direction !== 'right') {
1943
+ throw new UsageError(`expected up, down, left or right (got "${direction}")`);
1944
+ }
1945
+ const amount = str(values.amount) !== undefined ? parsePositiveInt(str(values.amount), 'amount', 100) : 3;
1946
+ return runComputer(ctx, ref, [{ action: 'scroll', scroll_direction: direction, scroll_amount: amount, ...pointAt(x, y), ...modsOf(values) }], lookOpts(values));
1947
+ },
1948
+ async 'workspaces computer move'(ctx, values, [ref, x, y]) {
1949
+ return runComputer(ctx, ref, [{ action: 'mouse_move', ...pointAt(x, y) }], lookOpts(values));
1950
+ },
1951
+ async 'workspaces computer drag'(ctx, values, [ref, x1, y1, x2, y2]) {
1952
+ const start = pointAt(x1, y1).coordinate;
1953
+ return runComputer(ctx, ref, [{ action: 'left_click_drag', start_coordinate: start, ...pointAt(x2, y2), ...modsOf(values) }], lookOpts(values));
1954
+ },
1955
+ async 'workspaces computer mouse-down'(ctx, values, [ref]) {
1956
+ return runComputer(ctx, ref, [{ action: 'left_mouse_down' }], lookOpts(values));
1957
+ },
1958
+ async 'workspaces computer mouse-up'(ctx, values, [ref]) {
1959
+ return runComputer(ctx, ref, [{ action: 'left_mouse_up' }], lookOpts(values));
1960
+ },
1961
+ async 'workspaces computer hold-key'(ctx, values, [ref, keys, seconds]) {
1962
+ return runComputer(ctx, ref, [{ action: 'hold_key', text: keys, duration: secondsOf(seconds, '<seconds>') }], lookOpts(values));
1963
+ },
1964
+ async 'workspaces computer wait'(ctx, values, [ref, seconds]) {
1965
+ return runComputer(ctx, ref, [{ action: 'wait', duration: secondsOf(seconds, '<seconds>') }], lookOpts(values));
1966
+ },
1967
+ async 'workspaces computer zoom'(ctx, values, [ref, x0, y0, x1, y1]) {
1968
+ const img = imageOpts(values);
1969
+ const region = [pixel(x0, 'x0'), pixel(y0, 'y0'), pixel(x1, 'x1'), pixel(y1, 'y1')];
1970
+ const dest = str(values.out) ?? (img.format === 'jpeg' ? 'zoom.jpg' : 'zoom.png');
1971
+ const ws = await computerWorkspace(ctx, ref);
1972
+ const r = await cellOf(ctx, ws).computer.act({ actions: [{ action: 'zoom', region }], ...img });
1973
+ const res = r.results[0];
1974
+ if (!res?.ok || !res.image) {
1975
+ out(ctx, `zoom failed: ${res?.error?.message ?? 'no image'}\n`, r);
1976
+ return 1;
1977
+ }
1978
+ const bytes = Buffer.from(res.image.data, 'base64');
1979
+ await writeFile(dest, bytes);
1980
+ out(ctx, `Wrote a ${res.image.width}x${res.image.height} view of [${region.join(', ')}] on ${ws.key} to ${dest}.\n`, {
1981
+ out: dest, format: res.image.format, width: res.image.width, height: res.image.height, bytes: bytes.length,
1982
+ });
1983
+ return 0;
1984
+ },
1985
+ async 'workspaces computer cursor'(ctx, _values, [ref]) {
1986
+ const ws = await computerWorkspace(ctx, ref);
1987
+ const r = await cellOf(ctx, ws).computer.act({ actions: [{ action: 'cursor_position' }] });
1988
+ out(ctx, `${r.cursor[0]} ${r.cursor[1]}\n`, { x: r.cursor[0], y: r.cursor[1], display: r.display });
1989
+ return 0;
1990
+ },
1991
+ async 'workspaces computer start'(ctx, values, [ref]) {
1992
+ const width = str(values.width) !== undefined ? parsePositiveInt(str(values.width), '--width') : undefined;
1993
+ const height = str(values.height) !== undefined ? parsePositiveInt(str(values.height), '--height') : undefined;
1994
+ const ws = await computerWorkspace(ctx, ref);
1995
+ const st = await cellOf(ctx, ws).computer.start({ ...(width !== undefined ? { width } : {}), ...(height !== undefined ? { height } : {}) });
1996
+ out(ctx, `Desktop of ${ws.key}: running ${st.width}x${st.height}.\n`, st);
1997
+ return 0;
1998
+ },
1999
+ async 'workspaces computer stop'(ctx, _values, [ref]) {
2000
+ const ws = await computerWorkspace(ctx, ref);
2001
+ await cellOf(ctx, ws).computer.stop();
2002
+ out(ctx, `Stopped the desktop of ${ws.key}.\n`, { workspace_id: ws.id, stopped: true });
2003
+ return 0;
2004
+ },
2005
+ async 'workspaces computer stream'(ctx, values, [ref]) {
2006
+ const ws = await computerWorkspace(ctx, ref);
2007
+ if (values.stop === true) {
2008
+ await ws.computer.stopStream();
2009
+ out(ctx, `Ended every view of the desktop of ${ws.key}.\n`, { workspace_id: ws.id, stopped: true });
2010
+ return 0;
2011
+ }
2012
+ const ttl = ttlSeconds(values, PORT_LINK_TTL);
2013
+ const s = await ws.computer.stream({ interactive: values.interactive === true, ...(ttl !== undefined ? { ttlSeconds: ttl } : {}) });
2014
+ out(ctx, `${s.url}\n`, { url: s.url, expires_at: s.expiresAt, port: s.port, interactive: s.interactive });
2015
+ if (!ctx.json) {
2016
+ ctx.io.stderr.write(`${s.interactive ? 'Uses' : 'Watches'} the desktop of ${ws.key} until ${time(s.expiresAt)}; anyone with the link can open it until then.\n`);
2017
+ }
2018
+ return 0;
2019
+ },
2020
+ async 'workspaces computer enable'(ctx, _values, [ref]) {
2021
+ return setComputerUse(ctx, ref, true);
2022
+ },
2023
+ async 'workspaces computer disable'(ctx, _values, [ref]) {
2024
+ return setComputerUse(ctx, ref, false);
2025
+ },
2026
+ async 'workspaces computer inherit'(ctx, _values, [ref]) {
2027
+ return setComputerUse(ctx, ref, null);
2028
+ },
2029
+ async 'templates computer-use'(ctx, _values, [slug, state]) {
2030
+ if (state !== 'on' && state !== 'off')
2031
+ throw new UsageError(`expected on or off (got "${state}")`);
2032
+ const r = await ctx.cloud().templates.setComputerUse(slug, state === 'on');
2033
+ out(ctx, `Computer use is ${r.enabled ? 'on' : 'off'} for the workspaces of ${slug} (a workspace's own setting wins).\n`, r);
2034
+ return 0;
2035
+ },
1798
2036
  async 'workspaces secrets'(ctx, values, [ref]) {
1799
2037
  const set = secretNames(strs(values.set), '--set');
1800
2038
  const clear = values.clear === true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shardflux/cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "shard: the Shardflux command line. Create an account, sign in, set up keys and billing, then open, run commands in, move files to, suspend, resume and fork persistent agent workspaces.",
6
6
  "license": "Apache-2.0",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "yaml": "2.9.1",
47
- "@shardflux/sdk": "^0.14.0"
47
+ "@shardflux/sdk": "^0.15.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@eslint/js": "10.0.1",