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); + }); +}