@lizard-build/sdk 0.1.47 → 0.1.48

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 CHANGED
@@ -59,6 +59,24 @@ const result = await sandbox.process.exec('printf "hello\\n"', {
59
59
  if (result.exitCode !== 0) throw new Error(result.stderr)
60
60
  ```
61
61
 
62
+ ## Desktop (computer use)
63
+
64
+ The `desktop` template runs XFCE and Chromium you can watch in a browser and an agent can drive:
65
+
66
+ ```ts
67
+ import { Sandbox } from '@lizard-build/sdk'
68
+
69
+ const sandbox = await Sandbox.create('desktop', { project: 'my-project' })
70
+ const { url, viewOnlyUrl } = await sandbox.desktop.start() // open `url` in a browser
71
+ await sandbox.desktop.openUrl('https://example.com')
72
+ await sandbox.desktop.click(640, 400)
73
+ await sandbox.desktop.type('hello')
74
+ await sandbox.desktop.press('Return')
75
+ const png: Uint8Array = await sandbox.desktop.screenshot()
76
+ ```
77
+
78
+ Treat `url` like a credential: anyone with it can see and control the desktop. Share `viewOnlyUrl` when someone only needs to watch. See the [desktop guide](../../docs/sandboxes.md#desktop-computer-use).
79
+
62
80
  ## Run Python
63
81
 
64
82
  Run Python with the `interpreter` template. Each process command starts a separate Python process, so Python variables do not survive between calls. Save intermediate results to files. The API returns stdout, stderr, and an exit code; it does not return typed notebook results or chart objects.
package/dist/index.d.mts CHANGED
@@ -401,6 +401,8 @@ interface ProcessResult {
401
401
  stdout: string;
402
402
  stderr: string;
403
403
  exitCode: number;
404
+ /** Set when the platform cut the output off at its per-stream limit (512 KiB). */
405
+ truncated?: boolean;
404
406
  }
405
407
  interface ProcessOpts {
406
408
  timeoutMs?: number;
@@ -432,7 +434,9 @@ declare class Process {
432
434
  * The command runs in a shell inside the Lizard sandbox and returns
433
435
  * stdout, stderr, and the exit code when it finishes.
434
436
  *
435
- * @param cmd Shell command to run inside the sandbox.
437
+ * @param cmd Shell command to run inside the sandbox, or an argv array that is
438
+ * executed directly with no shell — nothing in it is interpreted, so it is the
439
+ * safe way to pass untrusted text as an argument.
436
440
  * @param opts Optional execution options — environment variables, working
437
441
  * directory, user, and timeout.
438
442
  *
@@ -442,6 +446,11 @@ declare class Process {
442
446
  * console.log(result.stdout)
443
447
  * ```
444
448
  *
449
+ * @example Pass untrusted text as an argument, not through a shell:
450
+ * ```ts
451
+ * await sandbox.process.exec(['grep', '-rn', '--', userQuery, '/workspace'])
452
+ * ```
453
+ *
445
454
  * @example Run with a custom working directory and env vars:
446
455
  * ```ts
447
456
  * const result = await sandbox.process.exec('npm test', {
@@ -450,7 +459,7 @@ declare class Process {
450
459
  * })
451
460
  * ```
452
461
  */
453
- exec(cmd: string, opts?: ProcessOpts): Promise<ProcessResult>;
462
+ exec(cmd: string | string[], opts?: ProcessOpts): Promise<ProcessResult>;
454
463
  /**
455
464
  * Read an SSE exec stream, handing each line to the caller as it arrives while
456
465
  * still accumulating the full result. Events are `{stream, line}` for output and
@@ -642,6 +651,119 @@ declare class Watcher {
642
651
  close(): Promise<void>;
643
652
  }
644
653
 
654
+ /**
655
+ * State of a sandbox's desktop, and the URLs that stream it.
656
+ *
657
+ * **Both URLs are credentials.** The port is published on a public hostname, and each
658
+ * URL carries the stream token and a VNC password. Anyone holding `url` can see and
659
+ * control the desktop; anyone holding `viewOnlyUrl` can watch it. Do not log them or
660
+ * put them anywhere you would not put an API key.
661
+ */
662
+ interface DesktopInfo {
663
+ running: boolean;
664
+ /** Screen width in pixels, or `null` when the desktop has never been started. */
665
+ width: number | null;
666
+ height: number | null;
667
+ /** noVNC page with full control (mouse and keyboard). Treat like a credential. */
668
+ url: string;
669
+ /** noVNC page that can only watch; enforced by the VNC server, not the page. */
670
+ viewOnlyUrl: string;
671
+ }
672
+ interface DesktopStartOpts {
673
+ /** Screen width, 640–3840. Pass together with `height`; default 1280x800. */
674
+ width?: number;
675
+ /** Screen height, 480–2160. */
676
+ height?: number;
677
+ }
678
+ type MouseButton = 'left' | 'right' | 'middle';
679
+ type ScrollDirection = 'up' | 'down' | 'left' | 'right';
680
+ interface ClickOpts {
681
+ /** Default `'left'`. */
682
+ button?: MouseButton;
683
+ /** Double-click instead of a single click. */
684
+ double?: boolean;
685
+ }
686
+ /**
687
+ * A graphical desktop in the sandbox, for computer-use agents and for people to watch.
688
+ *
689
+ * Only the `desktop` template has one (Xvfb + XFCE + Chromium on `DISPLAY=:1`); on
690
+ * any other template the calls fail with `code === 'DESKTOP_NOT_SUPPORTED'`.
691
+ *
692
+ * Input and screenshots run inside the sandbox with `xdotool` and `scrot`. Every
693
+ * argument goes to the sandbox as an argv array and is never parsed by a shell, so
694
+ * text typed on behalf of a model or a user cannot run commands.
695
+ *
696
+ * Access via `sandbox.desktop`.
697
+ *
698
+ * @example
699
+ * ```ts
700
+ * const sandbox = await Sandbox.create('desktop', { project: 'my-project' })
701
+ * const { url } = await sandbox.desktop.start() // open in a browser to watch/drive
702
+ * await sandbox.desktop.openUrl('https://example.com')
703
+ * await sandbox.desktop.click(640, 400)
704
+ * await sandbox.desktop.type('hello')
705
+ * await sandbox.desktop.press('Return')
706
+ * const png = await sandbox.desktop.screenshot()
707
+ * ```
708
+ */
709
+ declare class Desktop {
710
+ private readonly sandboxId;
711
+ private readonly config;
712
+ private readonly process;
713
+ constructor(sandboxId: string, config: ConnectionConfig, process: Process);
714
+ private get client();
715
+ private get path();
716
+ /**
717
+ * Start the desktop, publish its stream and return the URLs. Idempotent: calling
718
+ * it on a running desktop returns the same URLs (the size of a running desktop is
719
+ * not changed).
720
+ */
721
+ start(opts?: DesktopStartOpts): Promise<DesktopInfo>;
722
+ /** The desktop's state and URLs, without starting it. */
723
+ info(): Promise<DesktopInfo>;
724
+ /** Stop the desktop and unpublish its stream. Its URLs stay the same for the next start. */
725
+ stop(): Promise<void>;
726
+ /**
727
+ * A PNG of the whole screen. The desktop must be running.
728
+ *
729
+ * @example
730
+ * ```ts
731
+ * import { writeFileSync } from 'node:fs'
732
+ * writeFileSync('screen.png', await sandbox.desktop.screenshot())
733
+ * ```
734
+ */
735
+ screenshot(): Promise<Uint8Array>;
736
+ /** Click at (x, y). */
737
+ click(x: number, y: number, opts?: ClickOpts): Promise<void>;
738
+ /** Move the pointer to (x, y) without clicking. */
739
+ moveMouse(x: number, y: number): Promise<void>;
740
+ /** Press the left button at (fromX, fromY), move to (toX, toY) and release. */
741
+ drag(fromX: number, fromY: number, toX: number, toY: number): Promise<void>;
742
+ /**
743
+ * Type text into the focused window, as keystrokes. The text is passed as a
744
+ * single argument after `--`, never through a shell.
745
+ */
746
+ type(text: string): Promise<void>;
747
+ /**
748
+ * Press a key or a chord, in xdotool keysym syntax: `'Return'`, `'ctrl+l'`,
749
+ * `'alt+Tab'`. An array presses them one after another.
750
+ */
751
+ press(keys: string | string[]): Promise<void>;
752
+ /** Scroll with the mouse wheel at the pointer's current position. `amount` is wheel clicks. */
753
+ scroll(direction: ScrollDirection, amount?: number): Promise<void>;
754
+ /** Where the pointer is now. */
755
+ cursorPosition(): Promise<{
756
+ x: number;
757
+ y: number;
758
+ }>;
759
+ /**
760
+ * Open a URL in a new Chromium window on the desktop. Returns once Chromium has
761
+ * been launched, not when the page has loaded — take a screenshot to see it.
762
+ */
763
+ openUrl(url: string): Promise<void>;
764
+ private run;
765
+ }
766
+
645
767
  interface SandboxInfo {
646
768
  sandboxId: string;
647
769
  template: string;
@@ -837,6 +959,19 @@ declare class Sandbox extends SandboxClient {
837
959
  * ```
838
960
  */
839
961
  readonly process: Process;
962
+ /**
963
+ * Drive the graphical desktop of a `desktop`-template sandbox: stream it to a
964
+ * browser, take screenshots, click and type.
965
+ *
966
+ * @example
967
+ * ```ts
968
+ * const sandbox = await Sandbox.create('desktop', { project: 'my-project' })
969
+ * const { url } = await sandbox.desktop.start() // a credential: anyone with it controls the desktop
970
+ * await sandbox.desktop.click(100, 200)
971
+ * const png = await sandbox.desktop.screenshot()
972
+ * ```
973
+ */
974
+ readonly desktop: Desktop;
840
975
  protected readonly connectionConfig: ConnectionConfig;
841
976
  constructor(opts: {
842
977
  sandboxId: string;
@@ -1813,4 +1948,4 @@ declare class LizardCLI {
1813
1948
  }): Promise<CLIResult>;
1814
1949
  }
1815
1950
 
1816
- export { type Account, type Addon, type AddonType, AddonsAPI, type ApiKey, type ApiKeyScope, ApiKeysAPI, AuthenticationError, type Balance, BillingAPI, type CLIOptions, type CLIResult, type CodeContext, CodeSandbox, ConfigApplyError, type ConfigResult, ConflictError, type ConnectionOpts, type CostMetrics, type CreateAddonOpts, type CreateApiKeyOpts, type CreateContextOpts, type CreateProjectOpts, type CreateServiceOpts, type CreateVolumeOpts, type CreateWorkspaceOpts, type CreatedApiKey, type DeployEvent, DeployHandle, type DomainInfo, type DomainStatus, DomainsAPI, Execution, ExecutionError, type FileInfo, type FsOpts, GitHubAPI, type ListTransactionsOpts, Lizard, LizardCLI, LizardError, type LizardOpts, type LogLine, type MetricPoint, type MetricRange, MetricsAPI, NotFoundError, PlatformClient, type ProcessOpts, type ProcessResult, type Project, type ProjectConfig, type ProjectCost, ProjectsAPI, type RawMetrics, type Region, RegionsAPI, type RunCodeLanguage, type RunCodeOpts, Sandbox, type SandboxInfo, type SandboxOpts, type SandboxSnapshot, type ScaleOpts, type Secret, SecretsAPI, type Service, type ServiceMetrics, type ServiceUpdate, ServicesAPI, type SetSecretOpts, type SnapshotOpts, type SnapshotWaitOpts, StorageAPI, type StreamEvent, TimeoutError, type Transaction, type TransactionPage, Volume, type VolumeInfo, type Workspace, WorkspacesAPI, resolveProjectId };
1951
+ export { type Account, type Addon, type AddonType, AddonsAPI, type ApiKey, type ApiKeyScope, ApiKeysAPI, AuthenticationError, type Balance, BillingAPI, type CLIOptions, type CLIResult, type ClickOpts, type CodeContext, CodeSandbox, ConfigApplyError, type ConfigResult, ConflictError, type ConnectionOpts, type CostMetrics, type CreateAddonOpts, type CreateApiKeyOpts, type CreateContextOpts, type CreateProjectOpts, type CreateServiceOpts, type CreateVolumeOpts, type CreateWorkspaceOpts, type CreatedApiKey, type DeployEvent, DeployHandle, Desktop, type DesktopInfo, type DesktopStartOpts, type DomainInfo, type DomainStatus, DomainsAPI, Execution, ExecutionError, type FileInfo, type FsOpts, GitHubAPI, type ListTransactionsOpts, Lizard, LizardCLI, LizardError, type LizardOpts, type LogLine, type MetricPoint, type MetricRange, MetricsAPI, type MouseButton, NotFoundError, PlatformClient, type ProcessOpts, type ProcessResult, type Project, type ProjectConfig, type ProjectCost, ProjectsAPI, type RawMetrics, type Region, RegionsAPI, type RunCodeLanguage, type RunCodeOpts, Sandbox, type SandboxInfo, type SandboxOpts, type SandboxSnapshot, type ScaleOpts, type ScrollDirection, type Secret, SecretsAPI, type Service, type ServiceMetrics, type ServiceUpdate, ServicesAPI, type SetSecretOpts, type SnapshotOpts, type SnapshotWaitOpts, StorageAPI, type StreamEvent, TimeoutError, type Transaction, type TransactionPage, Volume, type VolumeInfo, type Workspace, WorkspacesAPI, resolveProjectId };
package/dist/index.d.ts CHANGED
@@ -401,6 +401,8 @@ interface ProcessResult {
401
401
  stdout: string;
402
402
  stderr: string;
403
403
  exitCode: number;
404
+ /** Set when the platform cut the output off at its per-stream limit (512 KiB). */
405
+ truncated?: boolean;
404
406
  }
405
407
  interface ProcessOpts {
406
408
  timeoutMs?: number;
@@ -432,7 +434,9 @@ declare class Process {
432
434
  * The command runs in a shell inside the Lizard sandbox and returns
433
435
  * stdout, stderr, and the exit code when it finishes.
434
436
  *
435
- * @param cmd Shell command to run inside the sandbox.
437
+ * @param cmd Shell command to run inside the sandbox, or an argv array that is
438
+ * executed directly with no shell — nothing in it is interpreted, so it is the
439
+ * safe way to pass untrusted text as an argument.
436
440
  * @param opts Optional execution options — environment variables, working
437
441
  * directory, user, and timeout.
438
442
  *
@@ -442,6 +446,11 @@ declare class Process {
442
446
  * console.log(result.stdout)
443
447
  * ```
444
448
  *
449
+ * @example Pass untrusted text as an argument, not through a shell:
450
+ * ```ts
451
+ * await sandbox.process.exec(['grep', '-rn', '--', userQuery, '/workspace'])
452
+ * ```
453
+ *
445
454
  * @example Run with a custom working directory and env vars:
446
455
  * ```ts
447
456
  * const result = await sandbox.process.exec('npm test', {
@@ -450,7 +459,7 @@ declare class Process {
450
459
  * })
451
460
  * ```
452
461
  */
453
- exec(cmd: string, opts?: ProcessOpts): Promise<ProcessResult>;
462
+ exec(cmd: string | string[], opts?: ProcessOpts): Promise<ProcessResult>;
454
463
  /**
455
464
  * Read an SSE exec stream, handing each line to the caller as it arrives while
456
465
  * still accumulating the full result. Events are `{stream, line}` for output and
@@ -642,6 +651,119 @@ declare class Watcher {
642
651
  close(): Promise<void>;
643
652
  }
644
653
 
654
+ /**
655
+ * State of a sandbox's desktop, and the URLs that stream it.
656
+ *
657
+ * **Both URLs are credentials.** The port is published on a public hostname, and each
658
+ * URL carries the stream token and a VNC password. Anyone holding `url` can see and
659
+ * control the desktop; anyone holding `viewOnlyUrl` can watch it. Do not log them or
660
+ * put them anywhere you would not put an API key.
661
+ */
662
+ interface DesktopInfo {
663
+ running: boolean;
664
+ /** Screen width in pixels, or `null` when the desktop has never been started. */
665
+ width: number | null;
666
+ height: number | null;
667
+ /** noVNC page with full control (mouse and keyboard). Treat like a credential. */
668
+ url: string;
669
+ /** noVNC page that can only watch; enforced by the VNC server, not the page. */
670
+ viewOnlyUrl: string;
671
+ }
672
+ interface DesktopStartOpts {
673
+ /** Screen width, 640–3840. Pass together with `height`; default 1280x800. */
674
+ width?: number;
675
+ /** Screen height, 480–2160. */
676
+ height?: number;
677
+ }
678
+ type MouseButton = 'left' | 'right' | 'middle';
679
+ type ScrollDirection = 'up' | 'down' | 'left' | 'right';
680
+ interface ClickOpts {
681
+ /** Default `'left'`. */
682
+ button?: MouseButton;
683
+ /** Double-click instead of a single click. */
684
+ double?: boolean;
685
+ }
686
+ /**
687
+ * A graphical desktop in the sandbox, for computer-use agents and for people to watch.
688
+ *
689
+ * Only the `desktop` template has one (Xvfb + XFCE + Chromium on `DISPLAY=:1`); on
690
+ * any other template the calls fail with `code === 'DESKTOP_NOT_SUPPORTED'`.
691
+ *
692
+ * Input and screenshots run inside the sandbox with `xdotool` and `scrot`. Every
693
+ * argument goes to the sandbox as an argv array and is never parsed by a shell, so
694
+ * text typed on behalf of a model or a user cannot run commands.
695
+ *
696
+ * Access via `sandbox.desktop`.
697
+ *
698
+ * @example
699
+ * ```ts
700
+ * const sandbox = await Sandbox.create('desktop', { project: 'my-project' })
701
+ * const { url } = await sandbox.desktop.start() // open in a browser to watch/drive
702
+ * await sandbox.desktop.openUrl('https://example.com')
703
+ * await sandbox.desktop.click(640, 400)
704
+ * await sandbox.desktop.type('hello')
705
+ * await sandbox.desktop.press('Return')
706
+ * const png = await sandbox.desktop.screenshot()
707
+ * ```
708
+ */
709
+ declare class Desktop {
710
+ private readonly sandboxId;
711
+ private readonly config;
712
+ private readonly process;
713
+ constructor(sandboxId: string, config: ConnectionConfig, process: Process);
714
+ private get client();
715
+ private get path();
716
+ /**
717
+ * Start the desktop, publish its stream and return the URLs. Idempotent: calling
718
+ * it on a running desktop returns the same URLs (the size of a running desktop is
719
+ * not changed).
720
+ */
721
+ start(opts?: DesktopStartOpts): Promise<DesktopInfo>;
722
+ /** The desktop's state and URLs, without starting it. */
723
+ info(): Promise<DesktopInfo>;
724
+ /** Stop the desktop and unpublish its stream. Its URLs stay the same for the next start. */
725
+ stop(): Promise<void>;
726
+ /**
727
+ * A PNG of the whole screen. The desktop must be running.
728
+ *
729
+ * @example
730
+ * ```ts
731
+ * import { writeFileSync } from 'node:fs'
732
+ * writeFileSync('screen.png', await sandbox.desktop.screenshot())
733
+ * ```
734
+ */
735
+ screenshot(): Promise<Uint8Array>;
736
+ /** Click at (x, y). */
737
+ click(x: number, y: number, opts?: ClickOpts): Promise<void>;
738
+ /** Move the pointer to (x, y) without clicking. */
739
+ moveMouse(x: number, y: number): Promise<void>;
740
+ /** Press the left button at (fromX, fromY), move to (toX, toY) and release. */
741
+ drag(fromX: number, fromY: number, toX: number, toY: number): Promise<void>;
742
+ /**
743
+ * Type text into the focused window, as keystrokes. The text is passed as a
744
+ * single argument after `--`, never through a shell.
745
+ */
746
+ type(text: string): Promise<void>;
747
+ /**
748
+ * Press a key or a chord, in xdotool keysym syntax: `'Return'`, `'ctrl+l'`,
749
+ * `'alt+Tab'`. An array presses them one after another.
750
+ */
751
+ press(keys: string | string[]): Promise<void>;
752
+ /** Scroll with the mouse wheel at the pointer's current position. `amount` is wheel clicks. */
753
+ scroll(direction: ScrollDirection, amount?: number): Promise<void>;
754
+ /** Where the pointer is now. */
755
+ cursorPosition(): Promise<{
756
+ x: number;
757
+ y: number;
758
+ }>;
759
+ /**
760
+ * Open a URL in a new Chromium window on the desktop. Returns once Chromium has
761
+ * been launched, not when the page has loaded — take a screenshot to see it.
762
+ */
763
+ openUrl(url: string): Promise<void>;
764
+ private run;
765
+ }
766
+
645
767
  interface SandboxInfo {
646
768
  sandboxId: string;
647
769
  template: string;
@@ -837,6 +959,19 @@ declare class Sandbox extends SandboxClient {
837
959
  * ```
838
960
  */
839
961
  readonly process: Process;
962
+ /**
963
+ * Drive the graphical desktop of a `desktop`-template sandbox: stream it to a
964
+ * browser, take screenshots, click and type.
965
+ *
966
+ * @example
967
+ * ```ts
968
+ * const sandbox = await Sandbox.create('desktop', { project: 'my-project' })
969
+ * const { url } = await sandbox.desktop.start() // a credential: anyone with it controls the desktop
970
+ * await sandbox.desktop.click(100, 200)
971
+ * const png = await sandbox.desktop.screenshot()
972
+ * ```
973
+ */
974
+ readonly desktop: Desktop;
840
975
  protected readonly connectionConfig: ConnectionConfig;
841
976
  constructor(opts: {
842
977
  sandboxId: string;
@@ -1813,4 +1948,4 @@ declare class LizardCLI {
1813
1948
  }): Promise<CLIResult>;
1814
1949
  }
1815
1950
 
1816
- export { type Account, type Addon, type AddonType, AddonsAPI, type ApiKey, type ApiKeyScope, ApiKeysAPI, AuthenticationError, type Balance, BillingAPI, type CLIOptions, type CLIResult, type CodeContext, CodeSandbox, ConfigApplyError, type ConfigResult, ConflictError, type ConnectionOpts, type CostMetrics, type CreateAddonOpts, type CreateApiKeyOpts, type CreateContextOpts, type CreateProjectOpts, type CreateServiceOpts, type CreateVolumeOpts, type CreateWorkspaceOpts, type CreatedApiKey, type DeployEvent, DeployHandle, type DomainInfo, type DomainStatus, DomainsAPI, Execution, ExecutionError, type FileInfo, type FsOpts, GitHubAPI, type ListTransactionsOpts, Lizard, LizardCLI, LizardError, type LizardOpts, type LogLine, type MetricPoint, type MetricRange, MetricsAPI, NotFoundError, PlatformClient, type ProcessOpts, type ProcessResult, type Project, type ProjectConfig, type ProjectCost, ProjectsAPI, type RawMetrics, type Region, RegionsAPI, type RunCodeLanguage, type RunCodeOpts, Sandbox, type SandboxInfo, type SandboxOpts, type SandboxSnapshot, type ScaleOpts, type Secret, SecretsAPI, type Service, type ServiceMetrics, type ServiceUpdate, ServicesAPI, type SetSecretOpts, type SnapshotOpts, type SnapshotWaitOpts, StorageAPI, type StreamEvent, TimeoutError, type Transaction, type TransactionPage, Volume, type VolumeInfo, type Workspace, WorkspacesAPI, resolveProjectId };
1951
+ export { type Account, type Addon, type AddonType, AddonsAPI, type ApiKey, type ApiKeyScope, ApiKeysAPI, AuthenticationError, type Balance, BillingAPI, type CLIOptions, type CLIResult, type ClickOpts, type CodeContext, CodeSandbox, ConfigApplyError, type ConfigResult, ConflictError, type ConnectionOpts, type CostMetrics, type CreateAddonOpts, type CreateApiKeyOpts, type CreateContextOpts, type CreateProjectOpts, type CreateServiceOpts, type CreateVolumeOpts, type CreateWorkspaceOpts, type CreatedApiKey, type DeployEvent, DeployHandle, Desktop, type DesktopInfo, type DesktopStartOpts, type DomainInfo, type DomainStatus, DomainsAPI, Execution, ExecutionError, type FileInfo, type FsOpts, GitHubAPI, type ListTransactionsOpts, Lizard, LizardCLI, LizardError, type LizardOpts, type LogLine, type MetricPoint, type MetricRange, MetricsAPI, type MouseButton, NotFoundError, PlatformClient, type ProcessOpts, type ProcessResult, type Project, type ProjectConfig, type ProjectCost, ProjectsAPI, type RawMetrics, type Region, RegionsAPI, type RunCodeLanguage, type RunCodeOpts, Sandbox, type SandboxInfo, type SandboxOpts, type SandboxSnapshot, type ScaleOpts, type ScrollDirection, type Secret, SecretsAPI, type Service, type ServiceMetrics, type ServiceUpdate, ServicesAPI, type SetSecretOpts, type SnapshotOpts, type SnapshotWaitOpts, StorageAPI, type StreamEvent, TimeoutError, type Transaction, type TransactionPage, Volume, type VolumeInfo, type Workspace, WorkspacesAPI, resolveProjectId };
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ __export(index_exports, {
28
28
  ConfigApplyError: () => ConfigApplyError,
29
29
  ConflictError: () => ConflictError,
30
30
  DeployHandle: () => DeployHandle,
31
+ Desktop: () => Desktop,
31
32
  DomainsAPI: () => DomainsAPI,
32
33
  GitHubAPI: () => GitHubAPI,
33
34
  Lizard: () => Lizard,
@@ -512,7 +513,9 @@ var Process = class {
512
513
  * The command runs in a shell inside the Lizard sandbox and returns
513
514
  * stdout, stderr, and the exit code when it finishes.
514
515
  *
515
- * @param cmd Shell command to run inside the sandbox.
516
+ * @param cmd Shell command to run inside the sandbox, or an argv array that is
517
+ * executed directly with no shell — nothing in it is interpreted, so it is the
518
+ * safe way to pass untrusted text as an argument.
516
519
  * @param opts Optional execution options — environment variables, working
517
520
  * directory, user, and timeout.
518
521
  *
@@ -522,6 +525,11 @@ var Process = class {
522
525
  * console.log(result.stdout)
523
526
  * ```
524
527
  *
528
+ * @example Pass untrusted text as an argument, not through a shell:
529
+ * ```ts
530
+ * await sandbox.process.exec(['grep', '-rn', '--', userQuery, '/workspace'])
531
+ * ```
532
+ *
525
533
  * @example Run with a custom working directory and env vars:
526
534
  * ```ts
527
535
  * const result = await sandbox.process.exec('npm test', {
@@ -838,6 +846,157 @@ var Watcher = class {
838
846
  }
839
847
  };
840
848
 
849
+ // src/sandbox/desktop.ts
850
+ var BUTTONS = { left: "1", middle: "2", right: "3" };
851
+ var SCROLL_BUTTONS = { up: "4", down: "5", left: "6", right: "7" };
852
+ var TYPE_DELAY_MS = 12;
853
+ var Desktop = class {
854
+ constructor(sandboxId, config, process2) {
855
+ this.sandboxId = sandboxId;
856
+ this.config = config;
857
+ this.process = process2;
858
+ }
859
+ get client() {
860
+ return new PlatformClient(this.config);
861
+ }
862
+ get path() {
863
+ return `/api/sandboxes/${encodeURIComponent(this.sandboxId)}/desktop`;
864
+ }
865
+ /**
866
+ * Start the desktop, publish its stream and return the URLs. Idempotent: calling
867
+ * it on a running desktop returns the same URLs (the size of a running desktop is
868
+ * not changed).
869
+ */
870
+ start(opts = {}) {
871
+ const body = {};
872
+ if (opts.width !== void 0) body.width = opts.width;
873
+ if (opts.height !== void 0) body.height = opts.height;
874
+ return this.client.post(this.path, body);
875
+ }
876
+ /** The desktop's state and URLs, without starting it. */
877
+ info() {
878
+ return this.client.get(this.path);
879
+ }
880
+ /** Stop the desktop and unpublish its stream. Its URLs stay the same for the next start. */
881
+ async stop() {
882
+ await this.client.delete(this.path);
883
+ }
884
+ /**
885
+ * A PNG of the whole screen. The desktop must be running.
886
+ *
887
+ * @example
888
+ * ```ts
889
+ * import { writeFileSync } from 'node:fs'
890
+ * writeFileSync('screen.png', await sandbox.desktop.screenshot())
891
+ * ```
892
+ */
893
+ async screenshot() {
894
+ const r = await this.run(["lizard-desktop", "screenshot"]);
895
+ if (r.truncated) {
896
+ throw new LizardError("Screenshot is larger than the exec output limit; start the desktop at a smaller size");
897
+ }
898
+ return decodeBase64(r.stdout.trim());
899
+ }
900
+ /** Click at (x, y). */
901
+ async click(x, y, opts = {}) {
902
+ const button = BUTTONS[opts.button ?? "left"];
903
+ if (!button) throw new LizardError(`Unknown mouse button: ${String(opts.button)}`);
904
+ const repeat = opts.double ? ["--repeat", "2", "--delay", "100"] : [];
905
+ await this.run(["xdotool", "mousemove", coord(x), coord(y), "click", ...repeat, button]);
906
+ }
907
+ /** Move the pointer to (x, y) without clicking. */
908
+ async moveMouse(x, y) {
909
+ await this.run(["xdotool", "mousemove", coord(x), coord(y)]);
910
+ }
911
+ /** Press the left button at (fromX, fromY), move to (toX, toY) and release. */
912
+ async drag(fromX, fromY, toX, toY) {
913
+ await this.run([
914
+ "xdotool",
915
+ "mousemove",
916
+ coord(fromX),
917
+ coord(fromY),
918
+ "mousedown",
919
+ "1",
920
+ "sleep",
921
+ "0.1",
922
+ "mousemove",
923
+ coord(toX),
924
+ coord(toY),
925
+ "sleep",
926
+ "0.1",
927
+ "mouseup",
928
+ "1"
929
+ ]);
930
+ }
931
+ /**
932
+ * Type text into the focused window, as keystrokes. The text is passed as a
933
+ * single argument after `--`, never through a shell.
934
+ */
935
+ async type(text) {
936
+ if (!text) return;
937
+ const timeoutMs = Math.min(6e5, Math.max(6e4, text.length * TYPE_DELAY_MS * 3));
938
+ await this.run(["xdotool", "type", "--delay", String(TYPE_DELAY_MS), "--", text], timeoutMs);
939
+ }
940
+ /**
941
+ * Press a key or a chord, in xdotool keysym syntax: `'Return'`, `'ctrl+l'`,
942
+ * `'alt+Tab'`. An array presses them one after another.
943
+ */
944
+ async press(keys) {
945
+ const list = Array.isArray(keys) ? keys : [keys];
946
+ if (list.length === 0 || list.some((k) => typeof k !== "string" || !k)) {
947
+ throw new LizardError("press() needs at least one non-empty key");
948
+ }
949
+ await this.run(["xdotool", "key", "--", ...list]);
950
+ }
951
+ /** Scroll with the mouse wheel at the pointer's current position. `amount` is wheel clicks. */
952
+ async scroll(direction, amount = 3) {
953
+ const button = SCROLL_BUTTONS[direction];
954
+ if (!button) throw new LizardError(`Unknown scroll direction: ${String(direction)}`);
955
+ if (!Number.isInteger(amount) || amount < 1) throw new LizardError("scroll amount must be a positive integer");
956
+ await this.run(["xdotool", "click", "--repeat", String(amount), "--delay", "30", button]);
957
+ }
958
+ /** Where the pointer is now. */
959
+ async cursorPosition() {
960
+ const r = await this.run(["xdotool", "getmouselocation", "--shell"]);
961
+ const x = /^X=(\d+)$/m.exec(r.stdout);
962
+ const y = /^Y=(\d+)$/m.exec(r.stdout);
963
+ if (!x || !y) throw new LizardError(`Unexpected xdotool output: ${r.stdout.trim()}`);
964
+ return { x: Number(x[1]), y: Number(y[1]) };
965
+ }
966
+ /**
967
+ * Open a URL in a new Chromium window on the desktop. Returns once Chromium has
968
+ * been launched, not when the page has loaded — take a screenshot to see it.
969
+ */
970
+ async openUrl(url) {
971
+ await this.run([
972
+ "sh",
973
+ "-c",
974
+ 'setsid chromium --new-window "$1" >/dev/null 2>&1 < /dev/null &',
975
+ "sh",
976
+ url
977
+ ]);
978
+ }
979
+ async run(argv, timeoutMs) {
980
+ const r = await this.process.exec(argv, timeoutMs ? { timeoutMs } : void 0);
981
+ if (r.exitCode !== 0) {
982
+ const err = new LizardError(`${argv[0]} ${argv[1] ?? ""} failed (exit ${r.exitCode}): ${(r.stderr || r.stdout).trim()}`);
983
+ if (r.exitCode === 127) err.code = "DESKTOP_NOT_SUPPORTED";
984
+ throw err;
985
+ }
986
+ return r;
987
+ }
988
+ };
989
+ function coord(n) {
990
+ if (!Number.isFinite(n) || n < 0) throw new LizardError(`Invalid screen coordinate: ${n}`);
991
+ return String(Math.round(n));
992
+ }
993
+ function decodeBase64(b64) {
994
+ if (!b64 || !/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) {
995
+ throw new LizardError("Screenshot returned no image");
996
+ }
997
+ return new Uint8Array(Buffer.from(b64, "base64"));
998
+ }
999
+
841
1000
  // src/sandbox/client.ts
842
1001
  var SandboxClient = class {
843
1002
  static async createSandbox(template, timeoutMs, opts) {
@@ -939,6 +1098,7 @@ var Sandbox = class extends SandboxClient {
939
1098
  this.connectionConfig = new ConnectionConfig(opts);
940
1099
  this.fs = new Fs(this.sandboxId, this.connectionConfig);
941
1100
  this.process = new Process(this.sandboxId, this.connectionConfig);
1101
+ this.desktop = new Desktop(this.sandboxId, this.connectionConfig, this.process);
942
1102
  }
943
1103
  static async create(templateOrOpts, opts) {
944
1104
  const template = typeof templateOrOpts === "string" ? templateOrOpts : templateOrOpts?.template ?? this.defaultTemplate;
@@ -1988,6 +2148,7 @@ async function* readLines(body) {
1988
2148
  ConfigApplyError,
1989
2149
  ConflictError,
1990
2150
  DeployHandle,
2151
+ Desktop,
1991
2152
  DomainsAPI,
1992
2153
  GitHubAPI,
1993
2154
  Lizard,
package/dist/index.mjs CHANGED
@@ -461,7 +461,9 @@ var Process = class {
461
461
  * The command runs in a shell inside the Lizard sandbox and returns
462
462
  * stdout, stderr, and the exit code when it finishes.
463
463
  *
464
- * @param cmd Shell command to run inside the sandbox.
464
+ * @param cmd Shell command to run inside the sandbox, or an argv array that is
465
+ * executed directly with no shell — nothing in it is interpreted, so it is the
466
+ * safe way to pass untrusted text as an argument.
465
467
  * @param opts Optional execution options — environment variables, working
466
468
  * directory, user, and timeout.
467
469
  *
@@ -471,6 +473,11 @@ var Process = class {
471
473
  * console.log(result.stdout)
472
474
  * ```
473
475
  *
476
+ * @example Pass untrusted text as an argument, not through a shell:
477
+ * ```ts
478
+ * await sandbox.process.exec(['grep', '-rn', '--', userQuery, '/workspace'])
479
+ * ```
480
+ *
474
481
  * @example Run with a custom working directory and env vars:
475
482
  * ```ts
476
483
  * const result = await sandbox.process.exec('npm test', {
@@ -787,6 +794,157 @@ var Watcher = class {
787
794
  }
788
795
  };
789
796
 
797
+ // src/sandbox/desktop.ts
798
+ var BUTTONS = { left: "1", middle: "2", right: "3" };
799
+ var SCROLL_BUTTONS = { up: "4", down: "5", left: "6", right: "7" };
800
+ var TYPE_DELAY_MS = 12;
801
+ var Desktop = class {
802
+ constructor(sandboxId, config, process2) {
803
+ this.sandboxId = sandboxId;
804
+ this.config = config;
805
+ this.process = process2;
806
+ }
807
+ get client() {
808
+ return new PlatformClient(this.config);
809
+ }
810
+ get path() {
811
+ return `/api/sandboxes/${encodeURIComponent(this.sandboxId)}/desktop`;
812
+ }
813
+ /**
814
+ * Start the desktop, publish its stream and return the URLs. Idempotent: calling
815
+ * it on a running desktop returns the same URLs (the size of a running desktop is
816
+ * not changed).
817
+ */
818
+ start(opts = {}) {
819
+ const body = {};
820
+ if (opts.width !== void 0) body.width = opts.width;
821
+ if (opts.height !== void 0) body.height = opts.height;
822
+ return this.client.post(this.path, body);
823
+ }
824
+ /** The desktop's state and URLs, without starting it. */
825
+ info() {
826
+ return this.client.get(this.path);
827
+ }
828
+ /** Stop the desktop and unpublish its stream. Its URLs stay the same for the next start. */
829
+ async stop() {
830
+ await this.client.delete(this.path);
831
+ }
832
+ /**
833
+ * A PNG of the whole screen. The desktop must be running.
834
+ *
835
+ * @example
836
+ * ```ts
837
+ * import { writeFileSync } from 'node:fs'
838
+ * writeFileSync('screen.png', await sandbox.desktop.screenshot())
839
+ * ```
840
+ */
841
+ async screenshot() {
842
+ const r = await this.run(["lizard-desktop", "screenshot"]);
843
+ if (r.truncated) {
844
+ throw new LizardError("Screenshot is larger than the exec output limit; start the desktop at a smaller size");
845
+ }
846
+ return decodeBase64(r.stdout.trim());
847
+ }
848
+ /** Click at (x, y). */
849
+ async click(x, y, opts = {}) {
850
+ const button = BUTTONS[opts.button ?? "left"];
851
+ if (!button) throw new LizardError(`Unknown mouse button: ${String(opts.button)}`);
852
+ const repeat = opts.double ? ["--repeat", "2", "--delay", "100"] : [];
853
+ await this.run(["xdotool", "mousemove", coord(x), coord(y), "click", ...repeat, button]);
854
+ }
855
+ /** Move the pointer to (x, y) without clicking. */
856
+ async moveMouse(x, y) {
857
+ await this.run(["xdotool", "mousemove", coord(x), coord(y)]);
858
+ }
859
+ /** Press the left button at (fromX, fromY), move to (toX, toY) and release. */
860
+ async drag(fromX, fromY, toX, toY) {
861
+ await this.run([
862
+ "xdotool",
863
+ "mousemove",
864
+ coord(fromX),
865
+ coord(fromY),
866
+ "mousedown",
867
+ "1",
868
+ "sleep",
869
+ "0.1",
870
+ "mousemove",
871
+ coord(toX),
872
+ coord(toY),
873
+ "sleep",
874
+ "0.1",
875
+ "mouseup",
876
+ "1"
877
+ ]);
878
+ }
879
+ /**
880
+ * Type text into the focused window, as keystrokes. The text is passed as a
881
+ * single argument after `--`, never through a shell.
882
+ */
883
+ async type(text) {
884
+ if (!text) return;
885
+ const timeoutMs = Math.min(6e5, Math.max(6e4, text.length * TYPE_DELAY_MS * 3));
886
+ await this.run(["xdotool", "type", "--delay", String(TYPE_DELAY_MS), "--", text], timeoutMs);
887
+ }
888
+ /**
889
+ * Press a key or a chord, in xdotool keysym syntax: `'Return'`, `'ctrl+l'`,
890
+ * `'alt+Tab'`. An array presses them one after another.
891
+ */
892
+ async press(keys) {
893
+ const list = Array.isArray(keys) ? keys : [keys];
894
+ if (list.length === 0 || list.some((k) => typeof k !== "string" || !k)) {
895
+ throw new LizardError("press() needs at least one non-empty key");
896
+ }
897
+ await this.run(["xdotool", "key", "--", ...list]);
898
+ }
899
+ /** Scroll with the mouse wheel at the pointer's current position. `amount` is wheel clicks. */
900
+ async scroll(direction, amount = 3) {
901
+ const button = SCROLL_BUTTONS[direction];
902
+ if (!button) throw new LizardError(`Unknown scroll direction: ${String(direction)}`);
903
+ if (!Number.isInteger(amount) || amount < 1) throw new LizardError("scroll amount must be a positive integer");
904
+ await this.run(["xdotool", "click", "--repeat", String(amount), "--delay", "30", button]);
905
+ }
906
+ /** Where the pointer is now. */
907
+ async cursorPosition() {
908
+ const r = await this.run(["xdotool", "getmouselocation", "--shell"]);
909
+ const x = /^X=(\d+)$/m.exec(r.stdout);
910
+ const y = /^Y=(\d+)$/m.exec(r.stdout);
911
+ if (!x || !y) throw new LizardError(`Unexpected xdotool output: ${r.stdout.trim()}`);
912
+ return { x: Number(x[1]), y: Number(y[1]) };
913
+ }
914
+ /**
915
+ * Open a URL in a new Chromium window on the desktop. Returns once Chromium has
916
+ * been launched, not when the page has loaded — take a screenshot to see it.
917
+ */
918
+ async openUrl(url) {
919
+ await this.run([
920
+ "sh",
921
+ "-c",
922
+ 'setsid chromium --new-window "$1" >/dev/null 2>&1 < /dev/null &',
923
+ "sh",
924
+ url
925
+ ]);
926
+ }
927
+ async run(argv, timeoutMs) {
928
+ const r = await this.process.exec(argv, timeoutMs ? { timeoutMs } : void 0);
929
+ if (r.exitCode !== 0) {
930
+ const err = new LizardError(`${argv[0]} ${argv[1] ?? ""} failed (exit ${r.exitCode}): ${(r.stderr || r.stdout).trim()}`);
931
+ if (r.exitCode === 127) err.code = "DESKTOP_NOT_SUPPORTED";
932
+ throw err;
933
+ }
934
+ return r;
935
+ }
936
+ };
937
+ function coord(n) {
938
+ if (!Number.isFinite(n) || n < 0) throw new LizardError(`Invalid screen coordinate: ${n}`);
939
+ return String(Math.round(n));
940
+ }
941
+ function decodeBase64(b64) {
942
+ if (!b64 || !/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) {
943
+ throw new LizardError("Screenshot returned no image");
944
+ }
945
+ return new Uint8Array(Buffer.from(b64, "base64"));
946
+ }
947
+
790
948
  // src/sandbox/client.ts
791
949
  var SandboxClient = class {
792
950
  static async createSandbox(template, timeoutMs, opts) {
@@ -888,6 +1046,7 @@ var Sandbox = class extends SandboxClient {
888
1046
  this.connectionConfig = new ConnectionConfig(opts);
889
1047
  this.fs = new Fs(this.sandboxId, this.connectionConfig);
890
1048
  this.process = new Process(this.sandboxId, this.connectionConfig);
1049
+ this.desktop = new Desktop(this.sandboxId, this.connectionConfig, this.process);
891
1050
  }
892
1051
  static async create(templateOrOpts, opts) {
893
1052
  const template = typeof templateOrOpts === "string" ? templateOrOpts : templateOrOpts?.template ?? this.defaultTemplate;
@@ -1936,6 +2095,7 @@ export {
1936
2095
  ConfigApplyError,
1937
2096
  ConflictError,
1938
2097
  DeployHandle,
2098
+ Desktop,
1939
2099
  DomainsAPI,
1940
2100
  GitHubAPI,
1941
2101
  Lizard,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lizard-build/sdk",
3
- "version": "0.1.47",
3
+ "version": "0.1.48",
4
4
  "description": "TypeScript SDK for Lizard: Kubernetes sandboxes, code execution and cloud apps.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",