diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9e38480..34eae37 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -104,7 +104,7 @@ npm run build
- LLM tests also create sandboxes by default and require `E2B_API_KEY` plus the relevant provider credentials such as `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`.
- If you already have a runtime server running, set `TEST_WS_URL` to reuse it instead of creating a fresh sandbox.
- Some LLM tests also require `TEST_S3_BUCKET` for artifact upload verification.
-- If you want sandbox tests to run against a dev build instead of `npx -y runtimeuse`, set `RUNTIMEUSE_RUN_COMMAND`. A convenient way to get that command is `packages/runtimeuse` -> `npm run dev-publish`.
+- If you want sandbox tests to run against a dev build instead of `npx -y runtimeuse@latest`, set `RUNTIMEUSE_RUN_COMMAND`. A convenient way to get that command is `packages/runtimeuse` -> `npm run dev-publish`.
## Docs Development
diff --git a/README.md b/README.md
index 4ec4d7b..2788539 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ Run AI agents inside sandboxes and communicate with them over WebSocket.
```bash
export OPENAI_API_KEY=your_openai_api_key
-npx -y runtimeuse
+npx -y runtimeuse@latest
```
This starts a WebSocket server on port 8080 using the default OpenAI handler. For fuller Claude-based sandbox examples, see [`examples/`](./examples).
diff --git a/docs/content/docs/agent-runtime.mdx b/docs/content/docs/agent-runtime.mdx
index 1975f8a..b9521ec 100644
--- a/docs/content/docs/agent-runtime.mdx
+++ b/docs/content/docs/agent-runtime.mdx
@@ -8,10 +8,10 @@ The [agent runtime](https://www.npmjs.com/package/runtimeuse) is the process tha
## CLI
```bash
-npx -y runtimeuse # OpenAI handler on port 8080
-npx -y runtimeuse --agent claude # Claude handler
-npx -y runtimeuse --port 3000 # custom port
-npx -y runtimeuse --handler ./my-handler.js # custom handler entrypoint
+npx -y runtimeuse@latest # OpenAI handler on port 8080
+npx -y runtimeuse@latest --agent claude # Claude handler
+npx -y runtimeuse@latest --port 3000 # custom port
+npx -y runtimeuse@latest --handler ./my-handler.js # custom handler entrypoint
```
## Built-in Handlers
@@ -25,7 +25,7 @@ Requires `OPENAI_API_KEY` to be set in the environment. The handler runs the age
```bash
export OPENAI_API_KEY=your_openai_api_key
-npx -y runtimeuse
+npx -y runtimeuse@latest
```
### Claude Handler
@@ -37,7 +37,7 @@ npm install -g @anthropic-ai/claude-code
export ANTHROPIC_API_KEY=your_anthropic_api_key
export IS_SANDBOX=1
export CLAUDE_SKIP_ROOT_CHECK=1
-npx -y runtimeuse --agent claude
+npx -y runtimeuse@latest --agent claude
```
## Programmatic Startup
diff --git a/docs/content/docs/quickstart.mdx b/docs/content/docs/quickstart.mdx
index f27ba74..86d83ae 100644
--- a/docs/content/docs/quickstart.mdx
+++ b/docs/content/docs/quickstart.mdx
@@ -12,13 +12,13 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
```bash
npm install -g @anthropic-ai/claude-code
export ANTHROPIC_API_KEY=your_anthropic_api_key
- npx -y runtimeuse --agent claude
+ npx -y runtimeuse@latest --agent claude
```
This starts the Claude Code agent on port `8080`. To use OpenAI agent instead:
```bash
export OPENAI_API_KEY=your_openai_api_key
- npx -y runtimeuse
+ npx -y runtimeuse@latest
```
```python
@@ -47,7 +47,7 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
"CLAUDE_SKIP_ROOT_CHECK": "1",
}
)
- .set_start_cmd("npx -y runtimeuse --agent claude", wait_for_port(8080))
+ .set_start_cmd("npx -y runtimeuse@latest --agent claude", wait_for_port(8080))
)
sandbox = Sandbox.create(template="runtimeuse-quickstart-claude", api_key=e2b_api_key)
@@ -95,7 +95,7 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
sandbox.process.execute_session_command(
"runtimeuse",
SessionExecuteRequest(
- command="npx -y runtimeuse --agent claude",
+ command="npx -y runtimeuse@latest --agent claude",
run_async=True,
),
)
diff --git a/docs/public/terminal.svg b/docs/public/terminal.svg
index 516f143..5f1260c 100644
--- a/docs/public/terminal.svg
+++ b/docs/public/terminal.svg
@@ -8,7 +8,7 @@
$
- npx -y runtimeuse --agent=claude
+ npx -y runtimeuse@latest --agent=claude
diff --git a/examples/daytona-quickstart.py b/examples/daytona-quickstart.py
index 58cde2d..e8d589e 100644
--- a/examples/daytona-quickstart.py
+++ b/examples/daytona-quickstart.py
@@ -102,7 +102,7 @@ async def _start_server_and_wait(sandbox: Sandbox) -> str:
exec_resp = sandbox.process.execute_session_command(
_SESSION_ID,
SessionExecuteRequest(
- command=f"npx -y runtimeuse --agent claude",
+ command=f"npx -y runtimeuse@latest --agent claude",
run_async=True,
),
)
diff --git a/examples/e2b-quickstart.py b/examples/e2b-quickstart.py
index d25416b..1fce101 100644
--- a/examples/e2b-quickstart.py
+++ b/examples/e2b-quickstart.py
@@ -38,7 +38,7 @@ def _get_env_or_fail(name: str) -> str:
def _create_template_with_alias(alias: str):
anthropic_api_key = _get_env_or_fail("ANTHROPIC_API_KEY")
- start_cmd = "npx -y runtimeuse --agent claude"
+ start_cmd = "npx -y runtimeuse@latest --agent claude"
template = (
Template()
diff --git a/packages/runtimeuse-client-python/.env.example b/packages/runtimeuse-client-python/.env.example
index 5749c9c..a028880 100644
--- a/packages/runtimeuse-client-python/.env.example
+++ b/packages/runtimeuse-client-python/.env.example
@@ -10,9 +10,9 @@ OPENAI_API_KEY="your-openai-api-key"
ANTHROPIC_API_KEY="your-anthropic-api-key"
# Optional: override the runtime start command used by sandbox tests.
-# Defaults to: npx -y runtimeuse
+# Defaults to: npx -y runtimeuse@latest
# Useful with `packages/runtimeuse/scripts/dev-publish.sh`, which prints a curl/unzip command.
-RUNTIMEUSE_RUN_COMMAND="npx -y runtimeuse"
+RUNTIMEUSE_RUN_COMMAND="npx -y runtimeuse@latest"
# Optional: reuse the E2B template between test runs.
E2B_REUSE_TEMPLATE=false
diff --git a/packages/runtimeuse-client-python/README.md b/packages/runtimeuse-client-python/README.md
index 9ddf0e4..bbf98c5 100644
--- a/packages/runtimeuse-client-python/README.md
+++ b/packages/runtimeuse-client-python/README.md
@@ -30,7 +30,7 @@ WORKDIR = "/runtimeuse"
async def main():
# Start the runtime in a sandbox (provider-specific)
sandbox = Sandbox.create()
- sandbox.run("npx -y runtimeuse")
+ sandbox.run("npx -y runtimeuse@latest")
ws_url = sandbox.get_url(8080)
client = RuntimeUseClient(ws_url=ws_url)
@@ -173,27 +173,26 @@ except CancelledException:
### Types
-| Class | Description |
-| ----------------------------------------- | ------------------------------------------------------ |
-| `QueryOptions` | Configuration for `client.query()` (prompt options, callbacks, timeout) |
-| `QueryResult` | Return type of `query()` (`.data`, `.metadata`) |
-| `ResultMessageInterface` | Wire-format result message from the runtime |
-| `TextResult` | Result variant when no output schema is specified (`.text`) |
+| Class | Description |
+| ----------------------------------------- | ------------------------------------------------------------------------ |
+| `QueryOptions` | Configuration for `client.query()` (prompt options, callbacks, timeout) |
+| `QueryResult` | Return type of `query()` (`.data`, `.metadata`) |
+| `ResultMessageInterface` | Wire-format result message from the runtime |
+| `TextResult` | Result variant when no output schema is specified (`.text`) |
| `StructuredOutputResult` | Result variant when an output schema is specified (`.structured_output`) |
-
-| `AssistantMessageInterface` | Intermediate assistant text messages |
-| `ArtifactUploadRequestMessageInterface` | Runtime requesting a presigned URL for artifact upload |
-| `ArtifactUploadResponseMessageInterface` | Response with presigned URL sent back to runtime |
-| `ErrorMessageInterface` | Error from the agent runtime |
-| `CommandInterface` | Pre/post invocation shell command |
-| `RuntimeEnvironmentDownloadableInterface` | File to download into the runtime before invocation |
+| `AssistantMessageInterface` | Intermediate assistant text messages |
+| `ArtifactUploadRequestMessageInterface` | Runtime requesting a presigned URL for artifact upload |
+| `ArtifactUploadResponseMessageInterface` | Response with presigned URL sent back to runtime |
+| `ErrorMessageInterface` | Error from the agent runtime |
+| `CommandInterface` | Pre/post invocation shell command |
+| `RuntimeEnvironmentDownloadableInterface` | File to download into the runtime before invocation |
### Exceptions
-| Class | Description |
-| -------------------- | ------------------------------------------- |
+| Class | Description |
+| -------------------- | --------------------------------------------------------------------------------- |
| `AgentRuntimeError` | Raised when the agent runtime returns an error (carries `.error` and `.metadata`) |
-| `CancelledException` | Raised when `client.abort()` is called during a query |
+| `CancelledException` | Raised when `client.abort()` is called during a query |
## Related Docs
diff --git a/packages/runtimeuse-client-python/test/sandbox_factories/e2b.py b/packages/runtimeuse-client-python/test/sandbox_factories/e2b.py
index e6db777..5244286 100644
--- a/packages/runtimeuse-client-python/test/sandbox_factories/e2b.py
+++ b/packages/runtimeuse-client-python/test/sandbox_factories/e2b.py
@@ -10,7 +10,7 @@
_logger = logging.getLogger(__name__)
-_DEFAULT_RUN_COMMAND = "npx -y runtimeuse@latest"
+_DEFAULT_RUN_COMMAND = "npx -y runtimeuse@latest@latest"
def _get_env_or_fail(name: str) -> str:
diff --git a/packages/runtimeuse/README.md b/packages/runtimeuse/README.md
index da81446..eb49156 100644
--- a/packages/runtimeuse/README.md
+++ b/packages/runtimeuse/README.md
@@ -16,7 +16,7 @@ Run the runtime inside any sandbox:
```bash
export OPENAI_API_KEY=your_openai_api_key
-npx -y runtimeuse
+npx -y runtimeuse@latest
```
This starts a WebSocket server on port 8080 using the OpenAI agent handler (default). You can choose between built-in handlers:
@@ -27,8 +27,8 @@ This starts a WebSocket server on port 8080 using the OpenAI agent handler (defa
The Claude handler requires the `claude` CLI to be installed in the sandbox environment.
```bash
-npx -y runtimeuse # OpenAI (default)
-npx -y runtimeuse --agent claude # Claude
+npx -y runtimeuse@latest # OpenAI (default)
+npx -y runtimeuse@latest --agent claude # Claude
```
Use it programmatically:
@@ -113,7 +113,11 @@ sender.sendErrorMessage("Something went wrong", { code: "TIMEOUT" });
```typescript
type AgentResult =
| { type: "text"; text: string; metadata?: Record }
- | { type: "structured_output"; structuredOutput: Record; metadata?: Record };
+ | {
+ type: "structured_output";
+ structuredOutput: Record;
+ metadata?: Record;
+ };
```
## Server Options
diff --git a/packages/runtimeuse/src/cli.test.ts b/packages/runtimeuse/src/cli.test.ts
new file mode 100644
index 0000000..03c1af3
--- /dev/null
+++ b/packages/runtimeuse/src/cli.test.ts
@@ -0,0 +1,66 @@
+import { describe, it, expect } from "vitest";
+import { parseArgs } from "./cli.js";
+
+describe("parseArgs", () => {
+ const noopHelp = (): never => {
+ throw new Error("help called");
+ };
+
+ it("parses --key value (space-separated)", () => {
+ expect(parseArgs(["--agent", "claude"], noopHelp)).toEqual({
+ agent: "claude",
+ });
+ });
+
+ it("parses --key=value (equals-separated)", () => {
+ expect(parseArgs(["--agent=claude"], noopHelp)).toEqual({
+ agent: "claude",
+ });
+ });
+
+ it("handles a mix of space and equals styles", () => {
+ expect(
+ parseArgs(["--agent=claude", "--port", "3000"], noopHelp),
+ ).toEqual({ agent: "claude", port: "3000" });
+ });
+
+ it("handles equals style followed by space style", () => {
+ expect(
+ parseArgs(["--port=8080", "--handler", "./my-handler.js"], noopHelp),
+ ).toEqual({ port: "8080", handler: "./my-handler.js" });
+ });
+
+ it("handles value containing an equals sign", () => {
+ expect(parseArgs(["--handler=path/to/file=v2.js"], noopHelp)).toEqual({
+ handler: "path/to/file=v2.js",
+ });
+ });
+
+ it("returns empty object for no args", () => {
+ expect(parseArgs([], noopHelp)).toEqual({});
+ });
+
+ it("ignores bare flags without a following value", () => {
+ expect(parseArgs(["--verbose"], noopHelp)).toEqual({});
+ });
+
+ it("last value wins when a key is repeated", () => {
+ expect(
+ parseArgs(["--port", "3000", "--port=4000"], noopHelp),
+ ).toEqual({ port: "4000" });
+ });
+
+ it("calls onHelp for -h", () => {
+ expect(() => parseArgs(["-h"], noopHelp)).toThrow("help called");
+ });
+
+ it("calls onHelp for --help", () => {
+ expect(() => parseArgs(["--help"], noopHelp)).toThrow("help called");
+ });
+
+ it("skips non-flag arguments", () => {
+ expect(parseArgs(["positional", "--port", "8080"], noopHelp)).toEqual({
+ port: "8080",
+ });
+ });
+});
diff --git a/packages/runtimeuse/src/cli.ts b/packages/runtimeuse/src/cli.ts
index 445a0d0..1120b28 100644
--- a/packages/runtimeuse/src/cli.ts
+++ b/packages/runtimeuse/src/cli.ts
@@ -21,16 +21,23 @@ Options:
process.exit(0);
}
-function parseArgs(args: string[]): Record {
+export function parseArgs(
+ args: string[],
+ onHelp: () => never = usage,
+): Record {
const result: Record = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "-h" || arg === "--help") {
- usage();
+ onHelp();
}
- if (arg.startsWith("--") && i + 1 < args.length) {
- const key = arg.slice(2);
- result[key] = args[++i];
+ if (arg.startsWith("--")) {
+ const eqIdx = arg.indexOf("=");
+ if (eqIdx !== -1) {
+ result[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
+ } else if (i + 1 < args.length) {
+ result[arg.slice(2)] = args[++i];
+ }
}
}
return result;
@@ -60,8 +67,9 @@ function getBuiltinHandler(agent: BuiltinAgent): AgentHandler {
async function loadHandler(handlerPath: string): Promise {
const resolved = path.resolve(handlerPath);
const mod = await import(resolved);
- const handler: AgentHandler | undefined =
- mod.default?.run ? mod.default : mod.handler;
+ const handler: AgentHandler | undefined = mod.default?.run
+ ? mod.default
+ : mod.handler;
if (!handler?.run) {
console.error(
@@ -99,7 +107,9 @@ async function main() {
await server.startListening();
}
-main().catch((err) => {
- console.error(err);
- process.exit(1);
-});
+if (!process.env.VITEST) {
+ main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+ });
+}