@lizard-build/sdk 0.1.47 → 0.1.49

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,120 @@ 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
+ private readonly fs;
714
+ constructor(sandboxId: string, config: ConnectionConfig, process: Process, fs: Fs);
715
+ private get client();
716
+ private get path();
717
+ /**
718
+ * Start the desktop, publish its stream and return the URLs. Idempotent: calling
719
+ * it on a running desktop returns the same URLs (the size of a running desktop is
720
+ * not changed).
721
+ */
722
+ start(opts?: DesktopStartOpts): Promise<DesktopInfo>;
723
+ /** The desktop's state and URLs, without starting it. */
724
+ info(): Promise<DesktopInfo>;
725
+ /** Stop the desktop and unpublish its stream. Its URLs stay the same for the next start. */
726
+ stop(): Promise<void>;
727
+ /**
728
+ * A PNG of the whole screen. The desktop must be running.
729
+ *
730
+ * @example
731
+ * ```ts
732
+ * import { writeFileSync } from 'node:fs'
733
+ * writeFileSync('screen.png', await sandbox.desktop.screenshot())
734
+ * ```
735
+ */
736
+ screenshot(): Promise<Uint8Array>;
737
+ /** Click at (x, y). */
738
+ click(x: number, y: number, opts?: ClickOpts): Promise<void>;
739
+ /** Move the pointer to (x, y) without clicking. */
740
+ moveMouse(x: number, y: number): Promise<void>;
741
+ /** Press the left button at (fromX, fromY), move to (toX, toY) and release. */
742
+ drag(fromX: number, fromY: number, toX: number, toY: number): Promise<void>;
743
+ /**
744
+ * Type text into the focused window, as keystrokes. The text is passed as a
745
+ * single argument after `--`, never through a shell.
746
+ */
747
+ type(text: string): Promise<void>;
748
+ /**
749
+ * Press a key or a chord, in xdotool keysym syntax: `'Return'`, `'ctrl+l'`,
750
+ * `'alt+Tab'`. An array presses them one after another.
751
+ */
752
+ press(keys: string | string[]): Promise<void>;
753
+ /** Scroll with the mouse wheel at the pointer's current position. `amount` is wheel clicks. */
754
+ scroll(direction: ScrollDirection, amount?: number): Promise<void>;
755
+ /** Where the pointer is now. */
756
+ cursorPosition(): Promise<{
757
+ x: number;
758
+ y: number;
759
+ }>;
760
+ /**
761
+ * Open a URL in a new Chromium window on the desktop. Returns once Chromium has
762
+ * been launched, not when the page has loaded — take a screenshot to see it.
763
+ */
764
+ openUrl(url: string): Promise<void>;
765
+ private run;
766
+ }
767
+
645
768
  interface SandboxInfo {
646
769
  sandboxId: string;
647
770
  template: string;
@@ -837,6 +960,19 @@ declare class Sandbox extends SandboxClient {
837
960
  * ```
838
961
  */
839
962
  readonly process: Process;
963
+ /**
964
+ * Drive the graphical desktop of a `desktop`-template sandbox: stream it to a
965
+ * browser, take screenshots, click and type.
966
+ *
967
+ * @example
968
+ * ```ts
969
+ * const sandbox = await Sandbox.create('desktop', { project: 'my-project' })
970
+ * const { url } = await sandbox.desktop.start() // a credential: anyone with it controls the desktop
971
+ * await sandbox.desktop.click(100, 200)
972
+ * const png = await sandbox.desktop.screenshot()
973
+ * ```
974
+ */
975
+ readonly desktop: Desktop;
840
976
  protected readonly connectionConfig: ConnectionConfig;
841
977
  constructor(opts: {
842
978
  sandboxId: string;
@@ -1813,4 +1949,4 @@ declare class LizardCLI {
1813
1949
  }): Promise<CLIResult>;
1814
1950
  }
1815
1951
 
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 };
1952
+ 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,120 @@ 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
+ private readonly fs;
714
+ constructor(sandboxId: string, config: ConnectionConfig, process: Process, fs: Fs);
715
+ private get client();
716
+ private get path();
717
+ /**
718
+ * Start the desktop, publish its stream and return the URLs. Idempotent: calling
719
+ * it on a running desktop returns the same URLs (the size of a running desktop is
720
+ * not changed).
721
+ */
722
+ start(opts?: DesktopStartOpts): Promise<DesktopInfo>;
723
+ /** The desktop's state and URLs, without starting it. */
724
+ info(): Promise<DesktopInfo>;
725
+ /** Stop the desktop and unpublish its stream. Its URLs stay the same for the next start. */
726
+ stop(): Promise<void>;
727
+ /**
728
+ * A PNG of the whole screen. The desktop must be running.
729
+ *
730
+ * @example
731
+ * ```ts
732
+ * import { writeFileSync } from 'node:fs'
733
+ * writeFileSync('screen.png', await sandbox.desktop.screenshot())
734
+ * ```
735
+ */
736
+ screenshot(): Promise<Uint8Array>;
737
+ /** Click at (x, y). */
738
+ click(x: number, y: number, opts?: ClickOpts): Promise<void>;
739
+ /** Move the pointer to (x, y) without clicking. */
740
+ moveMouse(x: number, y: number): Promise<void>;
741
+ /** Press the left button at (fromX, fromY), move to (toX, toY) and release. */
742
+ drag(fromX: number, fromY: number, toX: number, toY: number): Promise<void>;
743
+ /**
744
+ * Type text into the focused window, as keystrokes. The text is passed as a
745
+ * single argument after `--`, never through a shell.
746
+ */
747
+ type(text: string): Promise<void>;
748
+ /**
749
+ * Press a key or a chord, in xdotool keysym syntax: `'Return'`, `'ctrl+l'`,
750
+ * `'alt+Tab'`. An array presses them one after another.
751
+ */
752
+ press(keys: string | string[]): Promise<void>;
753
+ /** Scroll with the mouse wheel at the pointer's current position. `amount` is wheel clicks. */
754
+ scroll(direction: ScrollDirection, amount?: number): Promise<void>;
755
+ /** Where the pointer is now. */
756
+ cursorPosition(): Promise<{
757
+ x: number;
758
+ y: number;
759
+ }>;
760
+ /**
761
+ * Open a URL in a new Chromium window on the desktop. Returns once Chromium has
762
+ * been launched, not when the page has loaded — take a screenshot to see it.
763
+ */
764
+ openUrl(url: string): Promise<void>;
765
+ private run;
766
+ }
767
+
645
768
  interface SandboxInfo {
646
769
  sandboxId: string;
647
770
  template: string;
@@ -837,6 +960,19 @@ declare class Sandbox extends SandboxClient {
837
960
  * ```
838
961
  */
839
962
  readonly process: Process;
963
+ /**
964
+ * Drive the graphical desktop of a `desktop`-template sandbox: stream it to a
965
+ * browser, take screenshots, click and type.
966
+ *
967
+ * @example
968
+ * ```ts
969
+ * const sandbox = await Sandbox.create('desktop', { project: 'my-project' })
970
+ * const { url } = await sandbox.desktop.start() // a credential: anyone with it controls the desktop
971
+ * await sandbox.desktop.click(100, 200)
972
+ * const png = await sandbox.desktop.screenshot()
973
+ * ```
974
+ */
975
+ readonly desktop: Desktop;
840
976
  protected readonly connectionConfig: ConnectionConfig;
841
977
  constructor(opts: {
842
978
  sandboxId: string;
@@ -1813,4 +1949,4 @@ declare class LizardCLI {
1813
1949
  }): Promise<CLIResult>;
1814
1950
  }
1815
1951
 
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 };
1952
+ 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,159 @@ 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 SCREENSHOT_PATH = /^\/tmp\/lizard-screenshot-[A-Za-z0-9_-]+\.png$/;
853
+ var TYPE_DELAY_MS = 12;
854
+ var Desktop = class {
855
+ constructor(sandboxId, config, process2, fs) {
856
+ this.sandboxId = sandboxId;
857
+ this.config = config;
858
+ this.process = process2;
859
+ this.fs = fs;
860
+ }
861
+ get client() {
862
+ return new PlatformClient(this.config);
863
+ }
864
+ get path() {
865
+ return `/api/sandboxes/${encodeURIComponent(this.sandboxId)}/desktop`;
866
+ }
867
+ /**
868
+ * Start the desktop, publish its stream and return the URLs. Idempotent: calling
869
+ * it on a running desktop returns the same URLs (the size of a running desktop is
870
+ * not changed).
871
+ */
872
+ start(opts = {}) {
873
+ const body = {};
874
+ if (opts.width !== void 0) body.width = opts.width;
875
+ if (opts.height !== void 0) body.height = opts.height;
876
+ return this.client.post(this.path, body);
877
+ }
878
+ /** The desktop's state and URLs, without starting it. */
879
+ info() {
880
+ return this.client.get(this.path);
881
+ }
882
+ /** Stop the desktop and unpublish its stream. Its URLs stay the same for the next start. */
883
+ async stop() {
884
+ await this.client.delete(this.path);
885
+ }
886
+ /**
887
+ * A PNG of the whole screen. The desktop must be running.
888
+ *
889
+ * @example
890
+ * ```ts
891
+ * import { writeFileSync } from 'node:fs'
892
+ * writeFileSync('screen.png', await sandbox.desktop.screenshot())
893
+ * ```
894
+ */
895
+ async screenshot() {
896
+ const r = await this.run(["lizard-desktop", "screenshot", "--path"]);
897
+ const path = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
898
+ if (!SCREENSHOT_PATH.test(path)) {
899
+ throw new LizardError(`Screenshot returned an unexpected path: ${path.slice(0, 200)}`);
900
+ }
901
+ try {
902
+ return await this.fs.readBytes(path);
903
+ } finally {
904
+ await this.fs.remove(path).catch(() => {
905
+ });
906
+ }
907
+ }
908
+ /** Click at (x, y). */
909
+ async click(x, y, opts = {}) {
910
+ const button = BUTTONS[opts.button ?? "left"];
911
+ if (!button) throw new LizardError(`Unknown mouse button: ${String(opts.button)}`);
912
+ const repeat = opts.double ? ["--repeat", "2", "--delay", "100"] : [];
913
+ await this.run(["xdotool", "mousemove", coord(x), coord(y), "click", ...repeat, button]);
914
+ }
915
+ /** Move the pointer to (x, y) without clicking. */
916
+ async moveMouse(x, y) {
917
+ await this.run(["xdotool", "mousemove", coord(x), coord(y)]);
918
+ }
919
+ /** Press the left button at (fromX, fromY), move to (toX, toY) and release. */
920
+ async drag(fromX, fromY, toX, toY) {
921
+ await this.run([
922
+ "xdotool",
923
+ "mousemove",
924
+ coord(fromX),
925
+ coord(fromY),
926
+ "mousedown",
927
+ "1",
928
+ "sleep",
929
+ "0.1",
930
+ "mousemove",
931
+ coord(toX),
932
+ coord(toY),
933
+ "sleep",
934
+ "0.1",
935
+ "mouseup",
936
+ "1"
937
+ ]);
938
+ }
939
+ /**
940
+ * Type text into the focused window, as keystrokes. The text is passed as a
941
+ * single argument after `--`, never through a shell.
942
+ */
943
+ async type(text) {
944
+ if (!text) return;
945
+ const timeoutMs = Math.min(6e5, Math.max(6e4, text.length * TYPE_DELAY_MS * 3));
946
+ await this.run(["xdotool", "type", "--delay", String(TYPE_DELAY_MS), "--", text], timeoutMs);
947
+ }
948
+ /**
949
+ * Press a key or a chord, in xdotool keysym syntax: `'Return'`, `'ctrl+l'`,
950
+ * `'alt+Tab'`. An array presses them one after another.
951
+ */
952
+ async press(keys) {
953
+ const list = Array.isArray(keys) ? keys : [keys];
954
+ if (list.length === 0 || list.some((k) => typeof k !== "string" || !k)) {
955
+ throw new LizardError("press() needs at least one non-empty key");
956
+ }
957
+ await this.run(["xdotool", "key", "--", ...list]);
958
+ }
959
+ /** Scroll with the mouse wheel at the pointer's current position. `amount` is wheel clicks. */
960
+ async scroll(direction, amount = 3) {
961
+ const button = SCROLL_BUTTONS[direction];
962
+ if (!button) throw new LizardError(`Unknown scroll direction: ${String(direction)}`);
963
+ if (!Number.isInteger(amount) || amount < 1) throw new LizardError("scroll amount must be a positive integer");
964
+ await this.run(["xdotool", "click", "--repeat", String(amount), "--delay", "30", button]);
965
+ }
966
+ /** Where the pointer is now. */
967
+ async cursorPosition() {
968
+ const r = await this.run(["xdotool", "getmouselocation", "--shell"]);
969
+ const x = /^X=(\d+)$/m.exec(r.stdout);
970
+ const y = /^Y=(\d+)$/m.exec(r.stdout);
971
+ if (!x || !y) throw new LizardError(`Unexpected xdotool output: ${r.stdout.trim()}`);
972
+ return { x: Number(x[1]), y: Number(y[1]) };
973
+ }
974
+ /**
975
+ * Open a URL in a new Chromium window on the desktop. Returns once Chromium has
976
+ * been launched, not when the page has loaded — take a screenshot to see it.
977
+ */
978
+ async openUrl(url) {
979
+ await this.run([
980
+ "sh",
981
+ "-c",
982
+ 'setsid chromium --new-window "$1" >/dev/null 2>&1 < /dev/null &',
983
+ "sh",
984
+ url
985
+ ]);
986
+ }
987
+ async run(argv, timeoutMs) {
988
+ const r = await this.process.exec(argv, timeoutMs ? { timeoutMs } : void 0);
989
+ if (r.exitCode !== 0) {
990
+ const err = new LizardError(`${argv[0]} ${argv[1] ?? ""} failed (exit ${r.exitCode}): ${(r.stderr || r.stdout).trim()}`);
991
+ if (r.exitCode === 127) err.code = "DESKTOP_NOT_SUPPORTED";
992
+ throw err;
993
+ }
994
+ return r;
995
+ }
996
+ };
997
+ function coord(n) {
998
+ if (!Number.isFinite(n) || n < 0) throw new LizardError(`Invalid screen coordinate: ${n}`);
999
+ return String(Math.round(n));
1000
+ }
1001
+
841
1002
  // src/sandbox/client.ts
842
1003
  var SandboxClient = class {
843
1004
  static async createSandbox(template, timeoutMs, opts) {
@@ -939,6 +1100,7 @@ var Sandbox = class extends SandboxClient {
939
1100
  this.connectionConfig = new ConnectionConfig(opts);
940
1101
  this.fs = new Fs(this.sandboxId, this.connectionConfig);
941
1102
  this.process = new Process(this.sandboxId, this.connectionConfig);
1103
+ this.desktop = new Desktop(this.sandboxId, this.connectionConfig, this.process, this.fs);
942
1104
  }
943
1105
  static async create(templateOrOpts, opts) {
944
1106
  const template = typeof templateOrOpts === "string" ? templateOrOpts : templateOrOpts?.template ?? this.defaultTemplate;
@@ -1988,6 +2150,7 @@ async function* readLines(body) {
1988
2150
  ConfigApplyError,
1989
2151
  ConflictError,
1990
2152
  DeployHandle,
2153
+ Desktop,
1991
2154
  DomainsAPI,
1992
2155
  GitHubAPI,
1993
2156
  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,159 @@ 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 SCREENSHOT_PATH = /^\/tmp\/lizard-screenshot-[A-Za-z0-9_-]+\.png$/;
801
+ var TYPE_DELAY_MS = 12;
802
+ var Desktop = class {
803
+ constructor(sandboxId, config, process2, fs) {
804
+ this.sandboxId = sandboxId;
805
+ this.config = config;
806
+ this.process = process2;
807
+ this.fs = fs;
808
+ }
809
+ get client() {
810
+ return new PlatformClient(this.config);
811
+ }
812
+ get path() {
813
+ return `/api/sandboxes/${encodeURIComponent(this.sandboxId)}/desktop`;
814
+ }
815
+ /**
816
+ * Start the desktop, publish its stream and return the URLs. Idempotent: calling
817
+ * it on a running desktop returns the same URLs (the size of a running desktop is
818
+ * not changed).
819
+ */
820
+ start(opts = {}) {
821
+ const body = {};
822
+ if (opts.width !== void 0) body.width = opts.width;
823
+ if (opts.height !== void 0) body.height = opts.height;
824
+ return this.client.post(this.path, body);
825
+ }
826
+ /** The desktop's state and URLs, without starting it. */
827
+ info() {
828
+ return this.client.get(this.path);
829
+ }
830
+ /** Stop the desktop and unpublish its stream. Its URLs stay the same for the next start. */
831
+ async stop() {
832
+ await this.client.delete(this.path);
833
+ }
834
+ /**
835
+ * A PNG of the whole screen. The desktop must be running.
836
+ *
837
+ * @example
838
+ * ```ts
839
+ * import { writeFileSync } from 'node:fs'
840
+ * writeFileSync('screen.png', await sandbox.desktop.screenshot())
841
+ * ```
842
+ */
843
+ async screenshot() {
844
+ const r = await this.run(["lizard-desktop", "screenshot", "--path"]);
845
+ const path = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
846
+ if (!SCREENSHOT_PATH.test(path)) {
847
+ throw new LizardError(`Screenshot returned an unexpected path: ${path.slice(0, 200)}`);
848
+ }
849
+ try {
850
+ return await this.fs.readBytes(path);
851
+ } finally {
852
+ await this.fs.remove(path).catch(() => {
853
+ });
854
+ }
855
+ }
856
+ /** Click at (x, y). */
857
+ async click(x, y, opts = {}) {
858
+ const button = BUTTONS[opts.button ?? "left"];
859
+ if (!button) throw new LizardError(`Unknown mouse button: ${String(opts.button)}`);
860
+ const repeat = opts.double ? ["--repeat", "2", "--delay", "100"] : [];
861
+ await this.run(["xdotool", "mousemove", coord(x), coord(y), "click", ...repeat, button]);
862
+ }
863
+ /** Move the pointer to (x, y) without clicking. */
864
+ async moveMouse(x, y) {
865
+ await this.run(["xdotool", "mousemove", coord(x), coord(y)]);
866
+ }
867
+ /** Press the left button at (fromX, fromY), move to (toX, toY) and release. */
868
+ async drag(fromX, fromY, toX, toY) {
869
+ await this.run([
870
+ "xdotool",
871
+ "mousemove",
872
+ coord(fromX),
873
+ coord(fromY),
874
+ "mousedown",
875
+ "1",
876
+ "sleep",
877
+ "0.1",
878
+ "mousemove",
879
+ coord(toX),
880
+ coord(toY),
881
+ "sleep",
882
+ "0.1",
883
+ "mouseup",
884
+ "1"
885
+ ]);
886
+ }
887
+ /**
888
+ * Type text into the focused window, as keystrokes. The text is passed as a
889
+ * single argument after `--`, never through a shell.
890
+ */
891
+ async type(text) {
892
+ if (!text) return;
893
+ const timeoutMs = Math.min(6e5, Math.max(6e4, text.length * TYPE_DELAY_MS * 3));
894
+ await this.run(["xdotool", "type", "--delay", String(TYPE_DELAY_MS), "--", text], timeoutMs);
895
+ }
896
+ /**
897
+ * Press a key or a chord, in xdotool keysym syntax: `'Return'`, `'ctrl+l'`,
898
+ * `'alt+Tab'`. An array presses them one after another.
899
+ */
900
+ async press(keys) {
901
+ const list = Array.isArray(keys) ? keys : [keys];
902
+ if (list.length === 0 || list.some((k) => typeof k !== "string" || !k)) {
903
+ throw new LizardError("press() needs at least one non-empty key");
904
+ }
905
+ await this.run(["xdotool", "key", "--", ...list]);
906
+ }
907
+ /** Scroll with the mouse wheel at the pointer's current position. `amount` is wheel clicks. */
908
+ async scroll(direction, amount = 3) {
909
+ const button = SCROLL_BUTTONS[direction];
910
+ if (!button) throw new LizardError(`Unknown scroll direction: ${String(direction)}`);
911
+ if (!Number.isInteger(amount) || amount < 1) throw new LizardError("scroll amount must be a positive integer");
912
+ await this.run(["xdotool", "click", "--repeat", String(amount), "--delay", "30", button]);
913
+ }
914
+ /** Where the pointer is now. */
915
+ async cursorPosition() {
916
+ const r = await this.run(["xdotool", "getmouselocation", "--shell"]);
917
+ const x = /^X=(\d+)$/m.exec(r.stdout);
918
+ const y = /^Y=(\d+)$/m.exec(r.stdout);
919
+ if (!x || !y) throw new LizardError(`Unexpected xdotool output: ${r.stdout.trim()}`);
920
+ return { x: Number(x[1]), y: Number(y[1]) };
921
+ }
922
+ /**
923
+ * Open a URL in a new Chromium window on the desktop. Returns once Chromium has
924
+ * been launched, not when the page has loaded — take a screenshot to see it.
925
+ */
926
+ async openUrl(url) {
927
+ await this.run([
928
+ "sh",
929
+ "-c",
930
+ 'setsid chromium --new-window "$1" >/dev/null 2>&1 < /dev/null &',
931
+ "sh",
932
+ url
933
+ ]);
934
+ }
935
+ async run(argv, timeoutMs) {
936
+ const r = await this.process.exec(argv, timeoutMs ? { timeoutMs } : void 0);
937
+ if (r.exitCode !== 0) {
938
+ const err = new LizardError(`${argv[0]} ${argv[1] ?? ""} failed (exit ${r.exitCode}): ${(r.stderr || r.stdout).trim()}`);
939
+ if (r.exitCode === 127) err.code = "DESKTOP_NOT_SUPPORTED";
940
+ throw err;
941
+ }
942
+ return r;
943
+ }
944
+ };
945
+ function coord(n) {
946
+ if (!Number.isFinite(n) || n < 0) throw new LizardError(`Invalid screen coordinate: ${n}`);
947
+ return String(Math.round(n));
948
+ }
949
+
790
950
  // src/sandbox/client.ts
791
951
  var SandboxClient = class {
792
952
  static async createSandbox(template, timeoutMs, opts) {
@@ -888,6 +1048,7 @@ var Sandbox = class extends SandboxClient {
888
1048
  this.connectionConfig = new ConnectionConfig(opts);
889
1049
  this.fs = new Fs(this.sandboxId, this.connectionConfig);
890
1050
  this.process = new Process(this.sandboxId, this.connectionConfig);
1051
+ this.desktop = new Desktop(this.sandboxId, this.connectionConfig, this.process, this.fs);
891
1052
  }
892
1053
  static async create(templateOrOpts, opts) {
893
1054
  const template = typeof templateOrOpts === "string" ? templateOrOpts : templateOrOpts?.template ?? this.defaultTemplate;
@@ -1936,6 +2097,7 @@ export {
1936
2097
  ConfigApplyError,
1937
2098
  ConflictError,
1938
2099
  DeployHandle,
2100
+ Desktop,
1939
2101
  DomainsAPI,
1940
2102
  GitHubAPI,
1941
2103
  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.49",
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",