## Summary
Adds an `E2B` client to both SDKs so a process can talk to several API
keys / domains / deployments without going through environment
variables. The client binds a connection config once and exposes the
resource surfaces off it; the named top-level exports are untouched and
keep reading the environment.
Nothing existing changes (changeset is `minor`): the default export is
still `Sandbox`, `Template(...)` keeps working, and `E2B` is a new named
export. Two follow-ups are tracked for v3: making `E2B` the default
export
([SDK-341](https://linear.app/e2b/issue/SDK-341/sdk-v3-js-make-e2b-the-default-export-instead-of-sandbox))
and dropping the `Template` Proxy in favour of `new Template()`
([SDK-342](https://linear.app/e2b/issue/SDK-342/sdk-v3-js-drop-the-template-proxy-require-new-template)).
```ts
import { E2B } from 'e2b'
const { Sandbox, Volume, Template, Secret } = new E2B({
apiKey: 'e2b_***',
domain: 'e2b.dev',
})
const sandbox = await Sandbox.create()
const volume = await Volume.create('my-volume')
const exists = await Template.exists('my-template')
await Template.build(Template().fromPythonImage('3'), 'my-env')
await Secret.create('openai-api-key', 'sk-***')
// Per-call options still win over the client's options.
await Sandbox.create({ apiKey: 'e2b_other***' })
```
```python
from e2b import E2B
client = E2B(api_key="e2b_***", domain="e2b.dev")
Sandbox, Volume, Template = client.Sandbox, client.Volume, client.Template
Secret = client.Secret
sandbox = Sandbox.create()
volume = Volume.create("my-volume")
exists = Template.exists("my-template")
secret = Secret.create("openai-api-key", "sk-***")
# Async variants are exposed too.
AsyncSandbox, AsyncTemplate = client.AsyncSandbox, client.AsyncTemplate
async_sandbox = await AsyncSandbox.create()
await AsyncTemplate.exists("my-template")
```
### Mechanism
`client.Sandbox` / `client.Volume` / `client.Template` / `client.Secret`
(plus the `Async*` variants in Python) are per-client subclasses of the
real classes, carrying the bound opts as class-level state. Nothing
process-global is mutated, so clients are isolated from each other and
from the default path, and `cls`/`this` dispatch is preserved (`create`
on a client class returns an instance of that client class).
```ts
// sandboxApi.ts / volume/index.ts / template/index.ts / secret.ts — one hook per class hierarchy
protected static readonly boundOpts?: ConnectionOpts // undefined on the base classes
protected static resolveOpts<T extends ConnectionOpts>(opts?: T) {
return ConnectionConfig.mergeOpts(this.boundOpts, opts) // { ...bound, ...definedPerCall }
}
// every static method that built a config from raw opts now does
- const config = new ConnectionConfig(opts)
+ const apiOpts = this.resolveOpts(opts)
+ const config = new ConnectionConfig(apiOpts)
```
```py
# sandbox/main.py, volume_sync.py, volume_async.py, template_{sync,async}/main.py, secret/base.py
_bound_api_params: ApiParams = {} # empty on the base classes
@classmethod
def _resolve_api_params(cls, **opts: Unpack[ApiParams]) -> ApiParams:
return merge_api_params(cls._bound_api_params, opts)
- config = ConnectionConfig(**opts)
+ config = ConnectionConfig(**cls._resolve_api_params(**opts))
```
`Template` used to be a factory function whose statics were pre-bound to
`TemplateBase`, which left no class for a client to subclass. It is now
the `TemplateBase` class itself, wrapped in a `Proxy` whose only trap
makes it callable without `new`, so `Template(...)` keeps working (no
breaking change) while `client.Template` is a plain subclass like
Sandbox/Volume and `Template.build(...)` resolves `this` naturally:
```ts
export function callableTemplate<T extends typeof TemplateBase>(cls: T) {
return new Proxy(cls, { apply: (target, _this, args) => new target(...args) })
}
export const Template = callableTemplate(TemplateBase) // Template() still returns a builder
this.Template = callableTemplate(class extends TemplateBase { boundOpts }) // client.Template
```
Because the trap only intercepts calls, `new Template()`, statics,
`instanceof` and subclassing all go straight to the class, and the
builder's default file context (`getCallerDirectory()`) still resolves
to the user's frame (the trap's frame is inside the SDK and filtered
like the old factory's).
Two side effects of routing everything through the hook:
- Static methods that resolved config off the base class had to move to
`this`/`cls`: `SandboxApi.createSandbox(...)` →
`this.createSandbox(...)` in JS, `new Volume(...)` → `new this(...)`,
and several Python `@staticmethod`s (`SandboxApi.list`,
`_cls_list_snapshots`, `delete_snapshot`, `Volume._class_get_info` /
`_class_list` / `destroy`, and the `Secret` operations) became
`@classmethod`s. Behavior for the top-level classes is unchanged since
their bound opts are empty.
- `DualMethod.__get__` (the descriptor behind `Volume.get_info` /
`Volume.list` working both on the class and on instances) now binds the
class-level function to the accessed class, so `client.Volume.list()`
sees the subclass' bound params instead of `Volume`'s.
Per-call values explicitly set to `undefined` / `None` are dropped when
merging, so they fall back to the client's opts rather than clearing
them into the env-var path.
### Tests
`packages/js-sdk/tests/client.test.ts` (MSW) and
`packages/python-sdk/tests/test_client.py` (local HTTP server, sync +
async) cover: the client's API key/domain being used instead of the env
vars, per-call precedence, rebinding the class (`const S =
client.Sandbox`), rebound `client.Template`, the client template builder
producing the same Dockerfile as the top-level one, two clients staying
isolated, generated-subclass instances, `client.Secret` (sync + async)
using the bound config, the top-level classes still using the env config
with empty bound opts and the default export still being `Sandbox`.
Link to Devin session:
https://app.devin.ai/sessions/772afa048b814ad784b5dde0a599df46
Requested by: @mishushakov
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mish@e2b.dev <mish@e2b.dev>
Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
The SDK and CLI homepage fields get utm_source=pypi/npm
(utm_campaign=package_homepage) so traffic from the registry pages
attributes to its real source instead of direct. Takes effect on the
next publish of each package.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
## Summary
Fixes the duplicate logo issue on NPM and PyPI caused by #1462. The
`#gh-light-mode-only` / `#gh-dark-mode-only` URL fragments are
GitHub-specific — NPM and PyPI ignore them and render both `<img>` tags.
Switches all three package READMEs (CLI, JS SDK, Python SDK) to
`<picture>` elements:
```html
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".../logo-white.png">
<source media="(prefers-color-scheme: light)" srcset=".../logo-black.png">
<img alt="E2B Logo" src=".../logo-black.png" width="200">
</picture>
```
- **GitHub**: `<picture>` + `prefers-color-scheme` handles theme
switching
- **NPM/PyPI**: `<picture>` not supported, falls back to the single
`<img>` (black logo)
Link to Devin session:
https://app.devin.ai/sessions/4983f23d23934d2c9a51733f5f9920f3
Requested by: @mlejva
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: vasek <vasek.mlejnsky@gmail.com>
## Summary
Replace the old `logo-circle.png` in the CLI, JS SDK, and Python SDK
READMEs with the new E2B wordmark logos that adapt to GitHub's theme
setting.
Each package README now uses a `<picture>` element:
```html
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".../logo-white.png">
<source media="(prefers-color-scheme: light)" srcset=".../logo-black.png">
<img alt="E2B Logo" src=".../logo-black.png" width="200">
</picture>
```
- **Light theme** → black logo (`logo-black.png`)
- **Dark theme** → white logo (`logo-white.png`)
- **NPM/PyPI** (no `<picture>` support) → falls back to the black logo
via the `<img>` tag
New logo assets added to `readme-assets/`: `logo-black.png`,
`logo-white.png`.
Includes a patch changeset for `@e2b/cli`, `e2b` (JS SDK), and
`@e2b/python-sdk`.
Link to Devin session:
https://app.devin.ai/sessions/4983f23d23934d2c9a51733f5f9920f3
Requested by: @mlejva
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: vasek <vasek.mlejnsky@gmail.com>
## Summary
- Made `runCode()` and `run_code()` references in READMEs link to the
[code interpreting docs](https://e2b.dev/docs/code-interpreting)
- Updated root README, js-sdk README, and python-sdk README
## Test plan
- [ ] Verify links render correctly on GitHub
- [ ] Confirm docs URL resolves
## Summary
- Updated root README, js-sdk README, and python-sdk README to show base
`e2b` SDK install and usage as the default
- Code-interpreter is now shown as an optional step for when
`runCode()`/`run_code()` is actually needed
- SDK links in descriptions now point to base `e2b` packages on npm/PyPI
## Why
The base `e2b` package covers commands, files, git, networking, and
sandbox lifecycle. Users who don't need code execution shouldn't be
directed to install `@e2b/code-interpreter` / `e2b-code-interpreter` as
their first step.
## Test plan
- [ ] Verify README renders correctly on GitHub
- [ ] Confirm base SDK examples use correct import syntax
- [ ] Confirm code-interpreter section still shows correct usage for
`runCode()`
Update broken downloads badge
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Documentation-only change that updates external badge image URLs and
styling; no runtime or build behavior is affected.
>
> **Overview**
> Fixes broken download badges in `README.md` and
`packages/python-sdk/README.md`.
>
> The PyPI badge is switched from shields.io to a Pepy monthly downloads
badge, and the NPM badge label/styling is updated to explicitly show
*monthly* downloads.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
811324dd583dd068f6a763489ac6c63483c47f87. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Introduce beta submodule with beta features - pause and resume
Update Sandbox list to also return paused sandboxes
---------
Co-authored-by: Tomas Valenta <valenta.and.thomas@gmail.com>