@e2b/python-sdk@2.35.0
1057 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4fcf7cb150 |
feat: sync API specs from infra and belt with Copybara (#1564)
The specs in `spec/` were copied from their source repos by hand and had
drifted ~2,400 lines behind infra, so they are now imported with
Copybara (`copy.bara.sky`, run in a pinned Docker image by
`scripts/fetch-spec.sh`): `make codegen` re-fetches them at the commits
pinned in `spec/infra-ref` and `spec/belt-ref` before generating, and
the generated-files CI check fails if the tracked copies don't match the
pins. Regenerating from the current pins picks up the accumulated spec
changes in the generated JS/Python clients (renamed request schemas,
`SandboxNetworkConfig`, `SandboxIam` workload identity,
`FILE_TYPE_SYMLINK`, access-token auth deprecation, volume path-metadata
tweaks). The one handwritten SDK change follows from that: the public
`FileType` enums gain a `SYMLINK` member (JS and both Python surfaces)
so entries envd reports as symlinks show up in `files.list()` and
`getInfo()`/`get_info()` instead of being silently skipped as unknown
types. The custom `spec/remove_extra_tags.py` tag-filtering script is
replaced by Redocly CLI's `filter-in` decorator (`redocly.yaml`), which
produces identical generated JS output; a `filter-out` decorator
additionally drops any operation or component schema the upstream specs
mark `x-not-implemented: true` (currently the SOCKS5
`SandboxEgressProxyConfig`/`egressProxy` surface, which infra flagged as
spec-only); each SDK's bundle now goes to its own gitignored
`spec/openapi_generated.<api>.yml` instead of both pipelines overwriting
one shared file; Python client models now list fields in spec order
instead of alphabetical (mechanical reordering only — construct models
with keyword args). Spec fetches try whatever GitHub token is available
and fall back to the tracked copies with a warning (the public infra
specs also fetch anonymously); in CI a short-lived belt-scoped token is
minted from the org-wide Autofixer GitHub App (no new secrets), so fork
PRs simply fall back for the belt spec; the CI workflows also cache the
Copybara image alongside the codegen image, and the previously ignored
`CODEGEN_IMAGE` env is honored by the Makefile.
## Usage
```sh
# update the specs: bump a pin, then regenerate
echo <infra-commit-sha> > spec/infra-ref
make codegen
# fetch a single spec without regenerating
pnpm fetch:api-spec # spec/openapi.yml from infra
pnpm fetch:envd-spec # spec/envd/ from infra
pnpm fetch:volume-spec # spec/openapi-volumecontent.yml from belt
# try the latest spec without touching the pin
E2B_INFRA_REF=main pnpm fetch:api-spec
# change which endpoint tags an SDK exposes
$EDITOR redocly.yaml && make codegen
```
```ts
// symlinks are now visible in the filesystem API (JS; same shape in Python)
const entries = await sandbox.files.list('/home/user')
const link = entries.find((e) => e.type === FileType.SYMLINK)
console.log(link?.symlinkTarget)
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
ada1744cf1 |
test(js-sdk): run the template test suite on Bun (#1600)
## Description Adds `--project template` to `test:bun` so the Bun CI leg runs the template suite, matching the Deno leg (#1595). No code changes are needed: the template suite previously failed under Bun because Bun's JavaScriptCore elides tail-call frames and the fixed-depth stack walk attributed build errors one frame past the user's call site (the workaround attempt in #1596 was closed in favor of #1599). With #1599's boundary-based frame selection (now merged), the suite passes under Bun as-is. The CI workflow already passes `E2B_API_KEY`/`E2B_DOMAIN` to the Bun leg, and the matrix comment (updated in #1595) already covers Bun re-running API-backed suites, so `package.json` is the only change. ## Testing Full `test:bun` (unit + connectionConfig + template) green locally on Bun 1.3.14 against the real API: 530 passed, 35 skipped, 0 failed — including all 34 stack-trace/caller-directory tests that pin exact user call-site line/columns, the frames Bun used to elide. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1ae3f92090 |
feat(js-sdk): run the full unit test suite in Cloudflare workerd (#1593)
## What Promotes `test:cf` from a single dist smoke test to the **full unit + connectionConfig suite running inside Cloudflare's workerd** (`@cloudflare/vitest-pool-workers`) — the same coverage `test:bun` and `test:deno` get. Locally: **74 files / 393 tests green** against prod sandboxes. The real-deploy suite (`test:cf:deploy`) is unchanged and keeps covering the built bundle on actual Cloudflare infrastructure (the pool can't reproduce bundling bugs like #1579). ## SDK fixes the suite surfaced 1. **Dropped-connection mapping for Workers** (`src/envd/rpc.ts`): workerd surfaces a sandbox connection drop as `Network connection lost`, which fell through to a cryptic `SandboxError`. It's now matched like the Node/Bun/Deno variants, so killing a sandbox mid-request surfaces as the health-checked `TimeoutError`: ```ts const cmd = await sandbox.commands.run('sleep 60', { background: true }) await sandbox.kill() await cmd.wait() // now rejects with TimeoutError('…sandbox was killed or reached its end of life…') on Workers too ``` 2. **Double connection release on stream cancel** (`src/connectionConfig.ts`): `wrapStreamWithConnectionCleanup` claimed its `release` was idempotent but had no guard — cancelling a streamed download while a read was in flight ran `cleanup()` twice (both the `cancel` callback and the pending `pull` resolving `done` fire). workerd's stream scheduling hits this deterministically; the pooled connection was double-released. Both are runtime-behavior fixes specific to the JS fetch/streams stack — no Python SDK equivalent applies. ## Test adjustments - **boot_id reads** in the two "filesystem-only pause" tests now use `commands.run('cat …')` instead of `files.read`: envd's non-gzip download path serves procfs files as an empty 200 (filed as e2b-dev/infra#3363 — Go `ServeContent` sizes them by stat, which is 0). Only clients that don't negotiate gzip (workerd's fetch) observe it; the command path sidesteps the bug while keeping the reboot assertion on all runtimes. - **runtime.test.ts** Node-host detection scenarios skip under workerd via the existing host guard (same treatment as Bun/Deno). - **Pool config filters expected unhandled-rejection shapes** via vitest's `onUnhandledError` (not the blanket `dangerouslyIgnoreUnhandledErrors`): workerd reports a rejection as unhandled unless a handler attaches within the same microtask drain — even inline `await expect(op()).rejects` trips it — and vitest never processes the `rejectionhandled` retraction on any runtime, so the suite's deliberate rejections false-positive ~60× per run. A diagnostic pairing `unhandledrejection` with `rejectionhandled` confirmed all of them are handled-late false positives (zero genuine leaks). The filter drops only the shapes the tests provoke (SDK error classes, `ConnectError`, `AbortError`, workerd's `Network connection lost.`, one test stub); unknown rejection shapes and uncaught exceptions still fail the run — verified with a planted never-handled `TypeError` (exit 1). ## CI Rebased onto #1588's per-runtime matrix: the `cloudflare` leg (already ubuntu-only there) now runs the full suite; no extra jobs added. The stale `tests/integration` exclude was dropped after #1591 removed that suite. ## Notes - Suite config needs `nodejs_compat_populate_process_env` + `E2B_API_KEY`/`E2B_DOMAIN` miniflare bindings so the SDK and tests read env like on Node. - The deleted `tests/runtimes/cloudflare/run.test.ts` (dist smoke) is fully subsumed: lifecycle coverage by the suite, bundle coverage by `test:cf:deploy` + `tests/bundle/edgeCompat.test.ts`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3f46d56026 |
fix(sdk): select stack-trace frames by SDK boundary instead of fixed depth (#1599)
## Description Template build stack traces were captured by walking a fixed number of frames (`STACK_TRACE_DEPTH` plus `±1` arithmetic at ~15 call sites), which broke whenever the frame count between `new Error()` and user code shifted — TS class-field initializer frames (#1539) and Bun's tail-call frame elision were both this bug. This PR makes two related changes: 1. **Boundary-based frame selection.** The caller's frame is now the first one whose file lies outside the SDK package, making extra transpiler frames and elided delegating frames irrelevant. In the JS SDK, frame parsing is delegated to `error-stack-parser-es` (ESM-only, so it's a devDependency inlined into both dist formats via tsdown `noExternal` — the engines range includes Node versions without `require(esm)`); the Python SDK equivalently walks `f_back` until `co_filename` leaves the `e2b` package root, in the shared builder used by both sync and async. If no user frame is identifiable (e.g. the SDK is bundled into the caller's own file), capture degrades to no trace rather than a wrong frame. 2. **Dead machinery removed.** Because boundary capture resolves through SDK-internal delegation (`remove()` → `runCmd()`, `fromDockerfile()` → parser) to the user's call site on its own, the suppress/override collection machinery (`runInNewStackTraceContext`, `runInStackTraceOverrideContext`, the enabled/override flags, and their Python equivalents) became redundant and is removed — superseding the approach in #1596. Error `.stack` synthesis (keeping the `Name: message` header and the throw site on `cause`) was prototyped here and backed out — it will come as a follow-up PR. ## Usage No API changes — build errors now point at the user's call site regardless of runtime or transpiler: ```ts const template = Template() .fromBaseImage() .runCmd('./does-not-exist') // ← build failures point exactly here await Template.build(template, 'my-template') ``` ## Testing - JS: `unit` + `template` vitest projects green against the real API (incl. 27 per-method stacktrace tests pinning exact call-site line/columns, `bunInstall` now covered); edge-compat bundle test and CLI build verified; built CJS/ESM dists smoke-tested with `require()`/`import()`. - Python: all 184 template tests green (shared + sync + async, incl. both `test_stacktrace.py` suites, `bun_install` now covered); `ruff` and `ty` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5e141a765f |
fix(js-sdk): use commands.run in Sandbox.getHost() example (#1531) (#1550)
The JSDoc `@example` on the public `Sandbox.getHost()` method calls
`sandbox.commands.exec(...)`, but the `Commands` class has no `exec`
method. It
exposes `run`. Copy-pasting the documented snippet therefore throws:
```
TypeError: sandbox.commands.exec is not a function
```
### Where
`packages/js-sdk/src/sandbox/index.ts`, in the `getHost()` doc comment:
```ts
/**
* ...
* @example
* ```ts
* const sandbox = await Sandbox.create()
* // Start an HTTP server
* await sandbox.commands.exec('python3 -m http.server 3000') // <- no such method
* // Get the hostname of the HTTP server
* const serverURL = sandbox.getHost(3000)
* ```
*/
```
The `Commands` class (`packages/js-sdk/src/sandbox/commands/index.ts`)
exposes
`list`, `sendStdin`, `closeStdin`, `kill`, `connect`, and `run` (four
`run`
overloads), plus a private `start`. There is no `exec`. The correct
method here
is `run`, which is what every other example already uses, including the
sibling
`@example` in this same file (the `commands.run(...)` snippet a few
methods up)
and both Python SDK mirrors (`get_host` in `sandbox_sync/main.py` and
`sandbox_async/main.py` already use `commands.run`).
### Fix
One token, `exec` -> `run`:
```ts
- await sandbox.commands.exec('python3 -m http.server 3000')
+ await sandbox.commands.run('python3 -m http.server 3000')
```
Documentation only. No behavior or type change.
### Parity with the Python SDK
The repo guidelines ask that SDK changes be mirrored across the JS and
Python
SDKs. Here the Python `get_host` examples already use `commands.run`
correctly,
so this defect exists only in the JS SDK doc comment and no Python
change is
needed to reach parity.
### Tests
This is a JSDoc `@example` correction with no runtime code path to
exercise, so
it adds no test, matching the repo's existing precedent for
documentation-only
fixes (e.g. `.changeset/sandbox-list-docstring.md`, and merged doc-fix
PRs such
as #1511 / #1500 / #1260, none of which added a regression test).
Correctness is
that the example now names the real public API: after the change,
`commands.exec`
no longer appears anywhere in the SDK source, and `commands.run` matches
the
`Commands` class and the sibling examples.
Offline gates run locally (Node 20, pnpm 9.15.5):
```
pnpm --filter e2b run lint # oxlint, clean
pnpm --filter e2b run typecheck # tsc --noEmit, clean
pnpm --filter e2b run build # tsc + tsup, ESM/CJS/DTS built
prettier --check src/sandbox/index.ts # clean
```
A changeset (`e2b`, patch) is included.
---
## Linked issues
- None. There is no existing GitHub issue for this; it is a self-evident
public
doc-example defect (the documented snippet throws at runtime). Not
filing a
separate issue for a one-token doc fix.
## Pre-flight checklist (repo AGENTS.md / CLAUDE.md gates)
- [x] `pnpm run format` - `prettier --check` clean on the changed file
- [x] `pnpm run lint` - oxlint clean (exit 0)
- [x] `pnpm run typecheck` - tsc --noEmit clean (exit 0)
- [x] `pnpm run build` - tsc + tsup clean
- [x] Changeset generated - `.changeset/fix-gethost-example-command.md`
(`e2b`: patch)
- [x] Conventional Commit message (`fix(js-sdk): ...`, reuses `js-sdk`
scope)
- [ ] Test added - not applicable (doc-only `@example`; see Tests
section for precedent)
- [ ] DCO / CLA - no sign-off required by this repo; CLA is signed via
`@cla-bot`
on the PR after opening (as on prior PRs #1518 / #1519 / #1507)
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Co-authored-by: Anas Khan <anxkhn28@gmail.com>
|
||
|
|
9ee4414e6d |
feat(js-sdk): run the template test suite on Deno (#1595)
## What Extends the Deno vitest run (#1585) with the `template` project and fixes the real runtime bug the suite surfaced. Split out of #1594 (Bun counterpart: #1596). ```jsonc // packages/js-sdk/package.json "test:deno": "deno run -A npm:vitest run --project unit --project connectionConfig --project template", ``` ## Bug — Deno: template uploads used chunked transfer encoding Deno's native `fetch` ignores an explicit `Content-Length` header on stream bodies and falls back to `Transfer-Encoding: chunked` — exactly the failure #1243 fixed for Node, since S3-compatible presigned PUT URLs reject chunked uploads with 501. `uploadFile` now streams the spooled archive through **undici's `fetch`** (via the existing `loadUndici()` helper — undici 8 where it imports, undici 7 on Bun, global `fetch` where undici isn't resolvable, e.g. bundled apps), which honors the `Content-Length` header on stream bodies on every runtime. One upload path, no runtime sniffing. Approaches rejected along the way, all verified empirically with 1GB uploads + RSS sampling: - **File-backed `Blob` body (`fs.openAsBlob`)** — lazy on Node/Bun, but Deno's shim reads the whole file into memory eagerly (denoland/deno#32316), and Bun infers an unstrippable MIME type from the extension whose `Content-Type` breaks presigned signatures (403 against production storage). - **`node:http(s)` on Deno** — works (and is memory-bounded), but can't be unified: Bun's `node:http` ignores abort signals, and it's a second code path. Known caveat: Deno's `Readable.toWeb` shim has no backpressure, so the archive is buffered in memory during upload on Deno (Node and Bun stream in lockstep with the socket). Filed upstream as denoland/deno#36275 — accepted as Deno's to fix rather than worked around here. As part of this, `tarFileStream` became `spoolTarArchive`, returning `{ path, size, cleanup }` with caller-owned cleanup instead of a self-deleting read stream. `tests/template/uploadFile.test.ts` also asserts no `Content-Type` header is sent. ## Python SDK parity Intentionally none: `upload_file` already sends a sized file body via httpx. ## Testing - Real template builds (`tests/template/build.test.ts`, against prod S3 presigned URLs) green under **Node, Deno, and Bun** - `uploadFile` + `spoolTarArchive` suites green under Node, Deno, and Bun - `tests/template/abortSignal.test.ts` green under Deno - `pnpm build`, `lint`, `typecheck`, `prettier --check` clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5417dd4f9f |
fix(deps): resolve all open Dependabot alerts (#1598)
## Summary Fixes all 8 open [Dependabot alerts](https://github.com/e2b-dev/E2B/security/dependabot), all in `pnpm-lock.yaml`: | Package | Severity | Alerts | Before | After | How | |---|---|---|---|---|---| | `@vitest/browser` | critical | #328 | 4.1.8 | 4.1.10 | updated the vitest family in js-sdk and cli devDeps (4.1.10 peer-requires `vitest@4.1.10` exactly) | | `tar` | critical/high/medium ×4 | #324–#327 | 7.5.16 | 7.5.21 | bumped the js-sdk runtime dep floor to `^7.5.19` + repo-wide override | | `sharp` | high | #329 | 0.34.5 | 0.35.3 | new override (pinned exactly by miniflare, dev-only) | | `shell-quote` | high | #323 | 1.8.4 | 1.10.0 | widened existing override (dev-only, via npm-run-all) | | `brace-expansion` | high | #322 | 2.1.0 | 2.1.2 | widened existing override | The only runtime-dependency change is `tar` in the js-sdk (used for template build contexts), so a patch changeset for `e2b` is included. The CLI bundles the SDK and its dependencies into `dist/index.js`, so the published CLI also ships the vulnerable `tar` — a patch changeset for `@e2b/cli` is included to rebundle it. Everything else is dev tooling or lockfile-only. ## Verification - `pnpm run lint` and `pnpm run typecheck` pass (the 7 python-sdk ty diagnostics pre-exist on main) - js-sdk: unit + connectionConfig (393 passed) and template projects (132 passed, exercises the new `tar` end-to-end against the real API) on vitest 4.1.10; `pnpm run build` clean - js-sdk `test:cf` passes — miniflare/workerd boots with sharp 0.35.3 - cli: full suite green (103 passed) on vitest 4.1.10 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
67bf112efc |
test(js-sdk): rename deprecated test.scoped() to test.override() (#1597)
vitest 4.1 deprecates `test.scoped()` in favor of `test.override()`, emitting 15 warnings during test collection in CI. This renames all `sandboxTest.scoped()` fixture overrides to `sandboxTest.override()` across the six affected test files (network, snapshot, internetAccess, secure, files/signing, commands/envVars). It's a pure rename — the vitest 4.1.8 types confirm an identical signature — and `vitest list` on all six files now collects with zero deprecation warnings. Test-only change, so no changeset. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e00503b090 |
fix(ci): recover release 30006966441 and retry lockfile update with backoff (#1589)
## What happened
Release run
[30006966441](https://github.com/e2b-dev/E2B/actions/runs/30006966441)
successfully published **e2b@2.35.3** and **@e2b/cli@2.15.0** to npm and
pushed both tags, but then failed on the **Update lock file** step:
`pnpm i` ran ~6 seconds after `npm publish` and the registry had not
propagated the new version yet (`ERR_PNPM_NO_MATCHING_VERSION: No
matching version found for e2b@^2.35.3 — the latest release of e2b is
"2.35.2"`). Because that step failed, the **Commit new versions** step
was skipped, leaving main with stale versions and unconsumed changesets.
Auditing the rest of the publish path for similar races also turned up a
long-dead step: the `@e2b/sdk` alias republish.
## Changes
**Commit 1 — replay the missing release commit.** Reproduces exactly
what the bot would have committed: `pnpm run version` (consumes the
three changesets, bumps js-sdk 2.35.2 → 2.35.3 and cli 2.14.0 → 2.15.0)
followed by `pnpm i --no-link --no-frozen-lockfile` (now succeeds — the
registry has long since propagated). The only commit that landed on main
after the release was dispatched
([
|
||
|
|
aa3c2593b9 |
test(js-sdk): remove unused integration test suite (#1591)
## Summary Removes `packages/js-sdk/tests/integration/` — the suite was never wired into CI: no workflow references `test:integration` or sets the `E2B_INTEGRATION_TEST` env var that gated every test. The tests were also stale, referencing hardcoded template IDs (`en716jw99aj63v1k8ugh`, `integration-test-v1`) that likely no longer exist and passing `timeoutMs: 120` (120 ms). Also removes the `test:integration` script, the `integration` vitest project, and the unused `isIntegrationTest` helper from `tests/setup.ts`. Test-only change, no changeset needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e29d406887 |
feat(js-sdk): run the vitest unit suite on Deno (#1585)
## Description `pnpm test:deno` now runs the full vitest suite — the `unit` and `connectionConfig` projects, 421 sandbox/files/commands/pty/git/api/config tests — under the Deno runtime via `deno run -A npm:vitest run --project unit --project connectionConfig`, replacing the previous single dist-based smoke test (superseded — the suite covers the SDK under Deno far more thoroughly). The CI step runs on ubuntu only and covers the same projects as the Bun suite step from #1584, and the Deno pin is bumped from 1.46.3 to 2.8.1 (`setup-deno@v2`) since vitest needs Deno 2's Node compat. Also drops the `edge` vitest project: `tests/runtimes/edge/` no longer exists, so it matched zero files. Rebased on main after #1584: the off-Node fetch-caching fix originally in this PR was superseded by #1584's late-binding fix, which also makes the whole suite (including the per-proxy cache tests) pass under Deno with no test changes — so this PR is pure test/CI wiring. Verified locally on Deno 2.8.1: unit project green (349 passed, 0 failed, 29 skipped — same skips as Node), connectionConfig project green (43 passed), and Node suite green. ## Usage ```bash cd packages/js-sdk pnpm test:deno ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d417e9c4e6 |
test(js-sdk): Cloudflare Workers smoke tests (workerd pool + real deploy) (#1586)
Adds two Cloudflare Workers smoke suites for the JS SDK, both exercising the built `dist/index.mjs`: `pnpm test:cf` runs the sandbox lifecycle inside workerd via `@cloudflare/vitest-pool-workers`, and `pnpm test:cf:deploy` deploys a worker to an ephemeral Cloudflare preview account (`wrangler deploy --temporary` in the suite's global setup — no Cloudflare credentials needed) and asserts the same lifecycle against the live `workers.dev` URL, deleting the worker in teardown. The pool suite immediately caught a runtime-detection bug: Node-compat shims populate `process.release.name` inside Workers, so `getRuntime()` misdetected Workers as Node and loaded `undici`; explicit runtime markers now take precedence over the generic Node check (unit-tested, changeset included). Both suites run in CI after the build step, alongside the Bun and Deno suites (deploy suite on ubuntu only). > [!IMPORTANT] > Merge #1583 first: the deploy suite reproduces the exact #1579 startup crash (Cloudflare rejects the upload with validation error 10021, `createRequire` receiving undefined `import.meta.url`) and stays red until that fix lands. Verified green end-to-end with #1583 applied. Usage: ```bash cd packages/js-sdk && pnpm build # sandbox lifecycle inside local workerd (vitest-pool-workers) pnpm test:cf # deploy to a temporary Cloudflare preview account, test the live worker, delete it E2B_API_KEY=... pnpm test:cf:deploy ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a406f78658 |
feat(js-sdk): run the full test suite under Bun (#1584)
## What
Runs the JS SDK's full vitest suite (the `unit` and `connectionConfig`
projects — 419 tests) under the Bun runtime, replacing the previous
single `bun:test` smoke test (superseded — the suite covers the SDK
under Bun far more thoroughly).
- `pnpm test:bun` → `bunx --bun vitest run --project unit --project
connectionConfig`
- CI step in `js_sdk_tests.yml` (ubuntu only for now); the old smoke
test and its Windows Bun install are removed
## SDK fixes surfaced by running the suite under Bun
1. **Late-bind `globalThis.fetch` on non-Node runtimes**
(`src/api/http2.ts`, `src/envd/http2.ts`). The factories previously
returned the bare global `fetch` reference, so:
- every per-proxy cache entry was the identical function, and
- a `fetch` swapped in *after* client creation (msw, instrumentation,
test stubs) was either ignored or — worse — a temporary stub was
captured permanently in the module-level fetcher cache.
They now return a closure that reads `globalThis.fetch` at call time.
2. **Pin abort reasons to their `AbortController`**
(`src/connectionConfig.ts`). Bun (observed on 1.3.14) holds
`AbortSignal.reason` weakly: a timeout `DOMException` constructed inside
a `setTimeout` callback gets garbage-collected, so consumers saw
`signal.reason === undefined` instead of a `TimeoutError`. Reasons are
now also stored on the controller, keeping them alive, and a losing
(post-abort) call never overwrites the pin. No behavior change on other
runtimes.
```ts
// Before (on Bun): sandbox operations that timed out aborted with
reason undefined
// After: they abort with DOMException('Request handshake timed out
after 30000ms', 'TimeoutError')
const sbx = await Sandbox.create({ requestTimeoutMs: 30_000 })
```
## Test changes
- `tests/envd/http2.test.ts`: the "uses global fetch outside Node" test
now asserts late-binding behavior (a fetch stubbed after fetcher
creation is picked up) instead of reference identity.
- `tests/volume/volume.test.ts`: the msw-mocked `format: 'stream'` read
is split into its own test and skipped on Bun — reading `response.body`
of an msw-intercepted fetch via a reader yields an immediately-done
stream there (msw/Bun incompatibility; `.text()`/`.blob()` work). Real
network streams on Bun work and are covered by the sandbox `files.read`
tests that now run under Bun.
## Verification
Locally on Bun 1.3.14 (macOS arm64) and Node 22:
- `pnpm test:bun`: 73 files passed, 389 tests passed / 30 skipped, 0
failed
- `npx vitest run --project unit --project connectionConfig` (Node): 389
passed / 29 skipped, 0 failed
- browser project (chromium via playwright): passed
- `pnpm run format` / `lint` / `typecheck`: clean
Python SDK parity: not applicable — the changes are JS-runtime-specific
(Bun/global-fetch handling).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
ab5f7666c9 | [skip ci] Release new versions | ||
|
|
f10989813c |
fix(js-sdk): drop bare require calls that crash edge runtimes at import (#1583)
Fixes #1579. Bare `require` references in the SDK's ESM source made tsdown emit an eager `createRequire(import.meta.url)` shim at module scope in `dist/index.mjs`, which throws in Cloudflare Workers (workerd) where `import.meta.url` is undefined in bundled code — so `import 'e2b'` crashed before any API call. `sha256` now uses WebCrypto directly (the `node:crypto` fallback was dead code, since package engines require Node ≥ 20.18.1 and `globalThis.crypto` exists on all supported runtimes), and `getCallerDirectory` loads `fileURLToPath` via a static top-level `import url from 'node:url'`, matching the existing sibling `node:fs`/`node:os`/`node:path` imports in the same file. `dynamicRequire` is removed entirely (no remaining callers), and a new bundle test (`tests/bundle/edgeCompat.test.ts`) fails the suite if a `require` shim ever reappears in `dist/index.mjs` — it skips locally when `dist/` hasn't been built and throws in CI, where the workflow always builds first. Verified against the issue's repro in real workerd via wrangler: `e2b@2.35.1` reproduces the crash, while this build imports cleanly and runs a full sandbox lifecycle from inside a Worker. Also verified: chromium browser test, Bun runtime test, signing/secure tests (WebCrypto signatures accepted end-to-end), and the full template suite (134 tests) against live infra. ### Usage No API changes — importing the SDK in a Cloudflare Worker (with `nodejs_compat`) now works again: ```ts import { Sandbox } from 'e2b' export default { async fetch(request: Request, env: Env) { const sandbox = await Sandbox.create({ apiKey: env.E2B_API_KEY }) const result = await sandbox.commands.run('echo hello from workerd') await sandbox.kill() return Response.json({ stdout: result.stdout }) }, } ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
43db96a0ef | [skip ci] Release new versions | ||
|
|
e5a4bd655d | Use undici8.8 when on node >= 22.19 (#1575) | ||
|
|
50de0af442 | [skip ci] Release new versions | ||
|
|
95e4dc2832 |
feat(sdk): add sandbox fork to JS and Python SDKs (#1554)
## Summary
Adds SDK support for the new `POST /sandboxes/{sandboxID}/fork` endpoint
(e2b-dev/infra#3202): checkpoint a running sandbox in place (briefly
paused, snapshotted with full memory state, and resumed — its ID and
expiration stay untouched) and boot `count` new sandboxes from that
snapshot.
- **spec**: adds `SandboxForkRequest` / `SandboxForkResult` schemas and
the `/sandboxes/{sandboxID}/fork` path (mirroring the infra spec); JS
and Python API clients regenerated via `make codegen`.
- **js-sdk**: `sandbox.fork(opts)` instance method and
`Sandbox.fork(sandboxId, opts)` static method. Returns
`Promise<Array<Sandbox | Error>>` — one entry per requested fork, each
either a connected `Sandbox` instance or an `Error` describing why that
fork failed to start (`Promise.allSettled`-style, matching the per-fork
results of the API). Per-fork error codes go through the same code→class
mapping as other API errors (extracted from `handleApiError` into
`apiErrorFromCode`), so e.g. a per-fork 429 (sandbox limit) surfaces as
`RateLimitError`. `SandboxForkOpts` extends the full `ConnectionOpts`
(like `SandboxConnectOpts`), so `proxy`, `logger`, `apiUrl`, etc. work
with fork-by-ID. `timeoutMs` defaults to 5 minutes like
`create`/`connect`; `count` defaults to 1 and is validated client-side
(`InvalidArgumentError` for `count < 1`); a whole-request 404 maps to
`SandboxNotFoundError` (the source sandbox is the missing resource —
same semantics as `pause`/`connect`/`setTimeout`), carrying the API
error message when present; per-fork 404 error codes map to generic
`NotFoundError` (the missing resource is fork-internal, e.g. the
snapshot).
- **python-sdk**: `sandbox.fork(timeout=..., count=...)` /
`Sandbox.fork(sandbox_id, ...)` and the `AsyncSandbox` equivalents (same
`@class_method_variant` instance/static pattern as `connect`/`pause`),
returning `List[Union[Sandbox, Exception]]`. Per-fork errors map through
the shared `api_exception_from_code` (extracted from
`handle_api_exception`). `timeout` is in seconds per Python SDK
convention; an explicit `timeout=0` is preserved. Whole-request 404
raises `SandboxNotFoundException`; per-fork 404 codes map to generic
`NotFoundException`.
- **changesets**: minor bumps for `e2b` and `@e2b/python-sdk`.
## Usage
JS:
```ts
const sandbox = await Sandbox.create()
const [fork1, fork2] = await sandbox.fork({ count: 2, timeoutMs: 60_000 })
if (fork1 instanceof Sandbox) {
await fork1.commands.run('echo "hello from fork"')
}
// or by ID
const forks = await Sandbox.fork(sandbox.sandboxId, { count: 2 })
```
Python (sync / async):
```python
sandbox = Sandbox.create()
fork1, fork2 = sandbox.fork(count=2, timeout=60)
if isinstance(fork1, Sandbox):
fork1.commands.run('echo "hello from fork"')
# or by ID
forks = Sandbox.fork(sandbox.sandbox_id, count=2)
```
```python
sandbox = await AsyncSandbox.create()
fork1, fork2 = await sandbox.fork(count=2)
```
## Notes
- The JS option is named `timeoutMs` (milliseconds) to match
`SandboxOpts.timeoutMs` / `SandboxConnectOpts.timeoutMs`; the API
receives seconds via `timeoutToSeconds` as elsewhere.
- Failed forks are returned as error **values** in the array rather than
rejected promises, so a partial failure doesn't throw away the
successful forks and there are no unhandled-rejection hazards. A
per-fork error message includes the API error code only when the API
returned one.
## Test plan
- [x] `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` pass at
the repo root (`ty` diagnostics identical to baseline)
- [x] Offline tests pass: `count < 1` → `InvalidArgumentError` /
`InvalidArgumentException` in JS, Python sync, and Python async;
`handleApiError` suite passes after the `apiErrorFromCode` extraction
(plus a behavior-parity check of the Python `handle_api_exception`
refactor)
- [ ] Integration tests (single fork with FS state inheritance +
independence, multi-fork with unique IDs, fork-by-ID, fork of killed
sandbox → `SandboxNotFoundError`) are written but currently fail against
prod with 404 because the fork endpoint (e2b-dev/infra#3202) is not
deployed yet — they should pass once it lands.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
8c87016a57 | [skip ci] Release new versions | ||
|
|
2c77fc00bb |
feat(sdk): add name filter to snapshot list (#1523)
Adds an optional `name` filter to `Sandbox.listSnapshots()` / `Sandbox.list_snapshots()`, mirroring the infra snapshots list endpoint ([e2b-dev/infra#3184](https://github.com/e2b-dev/infra/pull/3184)). The filter accepts a snapshot name or ID, optionally tag-qualified (e.g. `"my-snapshot"`, `"my-team/my-snapshot"` or `"my-snapshot:v1"`); unknown names return an empty list. It's a flat top-level option alongside the existing `sandboxId` filter (non-breaking) and can be combined with it — the backend applies both with AND, matching the `metadata`+`state` behavior of `Sandbox.list()`. Applied equivalently across the OpenAPI spec, generated clients, and the JS + Python sync/async SDKs, with tests and a changeset. ## Usage ```ts // JS/TS const paginator = Sandbox.listSnapshots({ name: 'my-snapshot' }) const snapshots = await paginator.nextItems() // combine filters (snapshots from a sandbox matching a name) Sandbox.listSnapshots({ sandboxId: 'sandbox-id', name: 'my-snapshot' }) ``` ```python # Python (sync) paginator = Sandbox.list_snapshots(name="my-snapshot") snapshots = paginator.next_items() # Python (async) paginator = AsyncSandbox.list_snapshots(name="my-snapshot") snapshots = await paginator.next_items() ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
78a91ab72f | [skip ci] Release new versions | ||
|
|
64e9bc02b6 |
fix(js-sdk): unpin useDefineForClassFields — make caller-directory resolution emit-invariant (#1539)
Follow-up to #1536, which pinned `useDefineForClassFields: false` in the js-sdk tsconfig because raising `target` to `es2022` flips the default to `true`, and that broke the template builder. This PR fixes the root cause and removes the pin, so the SDK now compiles with the standard es2022 `[[Define]]` class-field semantics. ## Root cause `TemplateBase` resolved its default `fileContextPath` in a **class field initializer**: ```ts private fileContextPath: PathLike = runtime === 'browser' ? '.' : (getCallerDirectory(STACK_TRACE_DEPTH) ?? '.') ``` With native class fields (define semantics), V8 evaluates field initializers in an extra `<instance_members_initializer>` stack frame: ``` at getCallerDirectory (utils.ts) at <instance_members_initializer> (index.ts) ← extra frame under define semantics at new TemplateBase (index.ts) at Template (index.ts) at user code ← fixed-depth walk lands one frame short ``` `getCallerDirectory` walks the stack at a fixed depth, so it landed on the SDK's own `src/template` directory instead of the caller's — `.copy('folder/*', …)` then globbed against the wrong base dir (`Error: No files found in .../src/template/...`), and the resulting client-side failure mis-attributed build-step stack traces (the two `stacktrace.test.ts` failures were cascades of this one bug). ## Fix Move the default resolution into the constructor body, where the stack shape is identical under both emits: ```ts constructor(options?: TemplateOptions) { this.fileContextPath = options?.fileContextPath ?? (runtime === 'browser' ? '.' : (getCallerDirectory(STACK_TRACE_DEPTH) ?? '.')) ``` The call is now emit-invariant (same `STACK_TRACE_DEPTH`), so the tsconfig pin is removed. The method-level `getCallerFrame` call sites were never affected — method bodies don't change shape with class-field semantics. Only the js-sdk is touched: the Python SDKs resolve the caller via `inspect` and don't have this failure mode, and the CLI bundle doesn't include `TemplateBase`. ## Usage example Fixes relative-path resolution for SDK consumers whose toolchain emits native class fields (e.g. esbuild/vitest with `target: es2022+`): ```ts // user-project/scripts/template.ts const template = Template() .fromBaseImage() .copy('assets/*', '/app/assets') // now resolves against user-project/scripts/, // not the SDK's own directory ``` ## Verification - `tests/template/stacktrace.test.ts` — 30/30 pass with the flag defaulted (`true`), and still 30/30 when explicitly set back to `false` (emit-invariance) - `tests/template/build.test.ts` — 4/4 pass against the real backend (real `.copy` glob + build) - Smoke-tested built `dist/index.mjs` and `dist/index.js` from an external directory: `fileContextPath` resolves to the importing script's directory in both - Unit project A/B: identical results with and without this change (remaining failures are pre-existing `E2B_API_KEY`-gated live tests) - `pnpm run typecheck`, `lint`, `format` ✅ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dbc6bfa161 | [skip ci] Release new versions | ||
|
|
09e12b3f65 |
feat(sdk): set-once integration attribution via ConnectionConfig.setIntegration (#1524)
Replaces the per-call `integration` connection option with a set-once, process-wide setter — `ConnectionConfig.setIntegration()` in JS and `ConnectionConfig.set_integration()` in Python — so integrations wrapping the SDK tag themselves once at startup and every request carries the identifier in the `User-Agent` header, with no threading through individual SDK calls. The setter is internal and hidden from generated docs; the `integration` option is removed from `ConnectionConfigOpts` (kept as a deprecated alias of `ConnectionOpts`) and from the Python constructor, and the round-trip machinery from #1459 is no longer needed since rebuilt configs read the process-wide value. User-Agent handling now follows a single rule in both SDKs via one shared helper per SDK: an explicitly provided `User-Agent` always wins, otherwise the SDK sends its own tagged with the current integration — and SDK-built values are recomputed whenever a config is rebuilt, so clearing or changing the integration propagates. Tests cover attribution, clearing, config rebuilds, and custom User-Agent precedence in both SDKs, with changesets for `e2b` and `@e2b/python-sdk` (minor). CLI attribution using this setter will follow in a separate PR. Usage (internal integrations only): ```ts import { ConnectionConfig } from 'e2b' ConnectionConfig.setIntegration('e2b-code-interpreter/0.1.0') // once at startup ``` ```python from e2b import ConnectionConfig ConnectionConfig.set_integration("e2b-code-interpreter/0.1.0") # once at startup ``` A caller-supplied `User-Agent` (via `headers`/`apiHeaders`) is preserved in both SDKs: ```ts const sbx = await Sandbox.create({ apiHeaders: { 'User-Agent': 'my-app/1.0' } }) // requests carry: my-app/1.0 ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
07041ccffc |
test: skip live volume tests unless ENABLE_VOLUME_TESTS is set (#1526)
Live volume tests create real volumes against the API; this gates them
behind an `ENABLE_VOLUME_TESTS` env var so they skip by default. In the
JS SDK, the `volumeTest` fixture is chained with
`.skipIf(process.env.ENABLE_VOLUME_TESTS === undefined)`, skipping all
of `tests/volume/file.test.ts`. In the Python SDK, the `volume` and
`async_volume` fixtures call `pytest.skip` when the env var is unset,
gating `tests/{sync/volume_sync,async/volume_async}/test_file.py`.
Mocked and unit volume tests (msw-based `volume.test.ts`,
`test_volume.py`, `test_volume_content.py`, `test_volume_client.py`,
`test_volume_connection_config.py`) still run unconditionally. To run
the live tests: `ENABLE_VOLUME_TESTS=1 pnpm run test` or
`ENABLE_VOLUME_TESTS=1 poetry run pytest`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
0bd06d86d2 |
chore(js-sdk,cli): modernize tsconfig and adopt TypeScript 7 (side-by-side) (#1536)
Supersedes #1516 (same modernization at TypeScript 6.0). Rebased onto `main` now that the build runs on **tsdown** (#1515). ## What & why Adopt **TypeScript 7** for both packages and modernize the compiler config. TypeScript 7.0's native compiler [ships no programmatic API yet](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/#running-side-by-side-with-typescript-6.0) (it lands in 7.1), so anything built on the TS compiler API breaks on it — here that's tsdown's `.d.ts` generation and the codegen scripts (`openapi-typescript`, `json-schema-to-typescript`). Per the official guidance, TS 7 is installed **side-by-side** with TS 6: ```json "@typescript/native": "npm:typescript@^7.0.2", // native tsc — used for type-checking "typescript": "npm:@typescript/typescript6@^6.0.2" // TS6 w/ compiler API — used by tooling ``` - `tsc --noEmit` (typecheck) → **native TypeScript 7.0.2** (verified: `tsc --version` → 7.0.2) - `import 'typescript'` → **TypeScript 6.0** *with* the compiler API → tsdown dts + codegen keep working - Bonus: tsdown's dts no longer prints the "TypeScript 7.0 does not yet have a stable API and is experimental" warning (it's on the 6.0 API now) **Internal build-config change only — no public API or runtime behavior changes.** ## Compiler options: before → after ### `packages/js-sdk/tsconfig.json` | option | before | after | |---|---|---| | `target` | `es6` | `es2022` | | `lib` | `["dom","ESNext"]` | `["dom","es2022"]` | | `module` | _(unset)_ | `esnext` | | `moduleResolution` | `node` | `bundler` | | `allowJs` | `true` | **removed** (no `.js` sources) | | `allowSyntheticDefaultImports` | `true` | **removed** (implied by `esModuleInterop`) | | `useDefineForClassFields` | _(false, implied by es6)_ | **`false` (now explicit)** — see note | ### `packages/cli/tsconfig.json` | option | before | after | |---|---|---| | `moduleResolution` | `node` | `bundler` | | `strictNullChecks`, `strictFunctionTypes`, `strictBindCallApply`, `strictPropertyInitialization`, `noImplicitThis`, `alwaysStrict` | `true` | **removed** (implied by `strict`) | | `downlevelIteration` | `true` | **removed** (removed in TS 7; no-op at `es2022`) | | `baseUrl` | `"."` | **removed** (removed in TS 7) | | `paths` | `{ e2b }` | `{ src, "src/*", e2b }` (replaces `baseUrl` for the existing `src/...` import style) | | `outDir` | `"dist"` | **removed** (unused under `tsc --noEmit`) | | `exclude` | _(none)_ | `["dist","node_modules"]` (so the built bundle is never type-checked) | `target`/`lib` for the CLI were already `es2022`. ## Notes / decisions - **Why side-by-side, not a plain `typescript@7` bump:** TS 7.0 is the native (Go) compiler rewrite — feature-identical to 6.0 for type-checking, no programmatic API until 7.1. A plain bump crashed both codegen tools (`Cannot read properties of undefined (reading 'createKeywordTypeNode')`). Side-by-side gives native-TS-7 checking while keeping the TS-6 API for tooling. Once 7.1 ships the API and the tools update, this collapses back to a single `typescript@7` dep. - **`useDefineForClassFields: false` is pinned explicitly.** Raising js-sdk's `target` to `es2022` flips this default to `true`, changing class-field emit and shifting stack frames. The template builder resolves the caller's directory and per-step traces via **fixed-depth** stack walking (`getCallerDirectory` in `src/template/index.ts`), so the extra frames threw it off by one — resolving `.copy('folder/*', …)` against the wrong base dir and mis-attributing build steps (`tests/template/build.test.ts` + `stacktrace.test.ts`). Pinning `false` keeps the exact pre-existing field semantics (es6 already implied `false`); adopting `define` semantics should be a separate, deliberately tested change. - **Target stays at `es2022`, not `es2023`.** `engines` still allow Node 20 (`>=20.18.1 <21 || >=22`). - **`moduleResolution: "bundler"`** typechecks + builds cleanly in both packages. The CLI's `baseUrl`-based bare imports (`from 'src/user'`, `from 'src'`) are preserved via `paths`; the bundled output still resolves them (build verified, binary smoke-tested). ## Not done (intentionally) - **`verbatimModuleSyntax`** — ~177 `import type` conversions; left as a follow-up. - **Shared `tsconfig.base.json`** — the two configs diverge too much to factor out cleanly. ## Verification - `pnpm run typecheck` ✅ both packages, on **native TS 7.0.2** - `pnpm run build` ✅ both packages (js-sdk ESM + CJS + **DTS**; cli CJS; binary smoke-tested) - codegen ✅ `openapi-typescript` + `json2ts` run and produce identical output (idempotent) - `pnpm run lint` ✅ both packages - `pnpm run test` — `template/build` + `template/stacktrace` now pass (`stacktrace` verified locally 30/30); remaining local failures are all `E2B_API_KEY`-gated live tests, unaffected by this change 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
49367c8491 |
build: switch from tsup to tsdown (#1515)
Switches the build tooling for `packages/js-sdk` and `packages/cli` from `tsup` (esbuild) to `tsdown` (rolldown), replacing each `tsup.config.js` with a `tsdown.config.ts` and updating the `build`/`dev` scripts and devDependencies. The published artifact layout is intentionally unchanged — the SDK still ships `dist/index.js` (CJS), `dist/index.mjs` (ESM) and `dist/index.d.ts`/`.d.mts`, and the CLI still ships an executable `dist/index.js` plus `dist/templates` — kept identical via `fixedExtension: false`. CLI dependency bundling is preserved by mapping the old `noExternal` to tsdown's `deps.alwaysBundle` (still excluding the ESM-only, dynamically-imported `inquirer`), and template copying moves from an `onSuccess` shell step to tsdown's `copy` option. Also aligns Node versions: `engines.node` for both packages is set to `20 || >=22`, the CLI build targets `node20`, and the pinned `nodejs` in `.tool-versions` is bumped to `22.11.0`. The large `pnpm-lock.yaml` diff is expected — it swaps the tsup/esbuild dependency tree for tsdown's rolldown tree (no lockfile format change). ## Verification - Both packages build cleanly with output filenames identical to the previous tsup builds. - `typecheck`, `lint` (oxlint) and `build` pass for both packages; the built CLI runs (`--version`). - Built js-sdk imports correctly in both CJS (`require`) and ESM (`import`), exposing the default `Sandbox` export and all named exports. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e6c4e7e9d5 |
chore(js): modernize Connect/Protobuf and React test deps (#1512)
## What Modernizes the JS SDK's dependencies while remaining fully compatible with the current supported Node range (`>=20.18.1`) — no engine changes and no breaking impact for consumers. - **`@connectrpc/connect` / `@connectrpc/connect-web`:** `2.0.0-rc.3` → `^2.1.2` (off the pre-release pin onto the stable line, and switched to a `^` range). - **`@bufbuild/protobuf`:** `^2.6.2` → `^2.12.1`. - **React test deps:** `react` / `@types/react` → `^19.2.0`, and `react-dom` / `@types/react-dom` added at `^19.2.0` (previously auto-installed as v18 peers). Dev/test-only — no runtime impact. - **CI:** standardized `actions/setup-node` (mixed v3/v4/v6) to `v6` across all workflows; the three `@v3` uses were on the deprecated Node16 action runtime. No public SDK API changes — the sandbox filesystem and command RPCs use the same Connect transport configuration. ## Why undici / Node floor were dropped from this PR An earlier revision also bumped `undici` 7 → 8 and raised the Node floor to `>=22.19.0`. Usage data shows **Node 20 is still the single largest SDK runtime (~39% of sandbox creations)**, so dropping it would break the largest consumer segment via `engine-strict` install failures. undici 8 was the *only* change forcing Node 22, and undici `7.28.0` (already the latest 7.x) supports Node 20 — so undici stays at `^7.28.0` and the engine floor is unchanged. undici 8 is a good candidate for a future major once Node 20 usage declines. ## Verification - typecheck, lint (oxlint), and build pass - 22 mocked Connect/undici transport unit tests pass - 106 live filesystem/command tests pass over connectrpc `2.1.2` + undici `7.28.0` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0feb926937 | [skip ci] Release new versions | ||
|
|
2b7dd17f10 |
feat(sdk): add gzip option to template copy layer (#1482)
Adds a `gzip` option to the template `.copy()` / `copyItems` layer that
controls whether copied files are gzipped before upload, threaded from
the copy call through the build-time tar stream in both the JS SDK and
the sync/async Python SDKs. It is enabled by default to preserve
existing behavior, so passing `gzip: false` (`gzip=False`) uploads an
uncompressed tar — useful for already-compressed payloads where gzip
adds CPU cost without shrinking the upload. The option name matches
node-tar's own `gzip` option and the existing sandbox filesystem `gzip`
kwarg. Gzip is deliberately excluded from the file cache hash, so
toggling it does not bust the build cache. Tests in both SDKs were
updated for the new argument and extended with `gzip: false` cases
asserting the archive is not gzipped yet still extracts, and a changeset
(`minor` for both packages) is included.
> [!NOTE]
> The server that extracts these uploaded archives lives in another repo
and must auto-detect compression (peek the gzip `0x1f 0x8b` magic)
rather than assuming gzip; confirm it handles plain tars before release.
## Usage
```ts
// JS/TS
template.copy('model.bin', '/app/', { gzip: false })
template.copyItems([{ src: 'a.bin', dest: '/app/', gzip: false }])
```
```python
# Python (sync & async)
template.copy('model.bin', '/app/', gzip=False)
template.copy_items([{ 'src': 'a.bin', 'dest': '/app/', 'gzip': False }])
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
a39db3bb36 |
chore: switch from eslint to oxlint (#1514)
Replaces ESLint (and its `@typescript-eslint/*` and `unused-imports` plugins) with [oxlint](https://oxc.rs) across the `js-sdk` and `cli` packages. A root `.oxlintrc.json` replaces the three `.eslintrc.cjs` files, the package `lint` scripts now run `oxlint`, the related devDependencies are swapped for `oxlint`, and the lint CI path filter is updated accordingly. Formatting rules (`quotes`/`semi`/`linebreak-style`) are dropped because Prettier already enforces them, and `no-unused-vars` is set to error to preserve the previous unused-imports check. The one behavior change is that `@typescript-eslint/member-ordering` has no oxlint equivalent and is no longer enforced. `lint`, `typecheck`, and `prettier` all pass clean for both packages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c385566c29 |
fix(python-sdk): correct Sandbox.list() docstring (also lists paused) (#1511)
Integration branch PR for #1500. Merges the docstring fix into `main`. Once #1500 is merged into `python-sdk-list-docstring-base`, this PR will carry those changes into `main`. --------- Co-authored-by: Leinux <tristone13th@outlook.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f160f08c7b |
Keep integration attribution on connection config (#1459)
moves integration attirbution to more private thing to avoid confusing people with first class kwargs |
||
|
|
bb45f185f1 |
Introduce generic paginator base class for JS and Python SDKs (#1491)
Extracts the cursor-based pagination state machine into a reusable base
class — `Paginator` in the JS SDK's `utils`, `PaginatorBase` in
`e2b/utils.py` — that owns `hasNext`/`nextToken` and the `x-next-token`
header handling, and migrates the sandbox and snapshot paginators onto
it. Each concrete paginator now just implements `nextItems`/`next_items`
to fetch its own page, so future list endpoints (templates, builds,
etc.) can add pagination by subclassing without reimplementing the
bookkeeping. Applied equivalently to the JS SDK and both Python sync and
async implementations, with unit tests covering the shared base. There
are no public API changes — `Sandbox.list()` / `listSnapshots()` and the
existing paginator types behave identically.
## Usage (unchanged)
```ts
const paginator = Sandbox.list()
while (paginator.hasNext) {
const sandboxes = await paginator.nextItems()
console.log(sandboxes)
}
```
```python
paginator = Sandbox.list()
while paginator.has_next:
sandboxes = paginator.next_items()
print(sandboxes)
```
|
||
|
|
bb1696871b |
Stream template build-context upload from disk instead of buffering in memory (#1435)
## Summary Template builds previously buffered the entire gzipped build-context tar archive in memory before uploading it. This PR spools the archive to a temporary file and streams it from disk during upload — in the JS SDK and both sync and async Python SDKs — so memory usage no longer scales with the size of the build context. The upload keeps an explicit `Content-Length` header (taken from the spooled file's size), which S3 presigned PUT URLs require — they reject `Transfer-Encoding: chunked` with `501 NotImplemented` (#1243). ## Changes - **JS** (`packages/js-sdk/src/template/`): `tarFileStream`/`tarFileStreamUpload` are replaced by `tarFileToStream`, which writes the archive to a temp file and returns a self-cleaning read stream plus its `size`. The spooled temp file deletes itself once the stream is closed (consumed, errored, or destroyed) via the stream's `close` event — mirroring the Python SDK's `tar_file_stream`. `buildApi` streams this body with `duplex: 'half'` and an explicit `Content-Length` from `size`; if `fetch` throws before consuming the body, it destroys the stream to trigger the same cleanup. There is no separate cleanup callback, so a cleanup failure can no longer mask the upload result. - **Python** (`packages/python-sdk/e2b/template/utils.py`, `template_async/build_api.py`, `template_sync/build_api.py`): `tar_file_stream` now writes to a `tempfile.TemporaryFile` instead of `io.BytesIO` and returns the file object positioned at the start; the upload streams from it with an explicit `Content-Length` and closes it (deleting the temp file) when done. - Tests updated for the new return shapes (JS `tarFileToStream.test.ts`, `uploadFile.test.ts`; Python upload/tar tests), including assertions that the spooled archive is removed on both the consume and destroy paths. ## Usage No API changes — `Template.build()` / template builds behave the same, just without holding the build context in memory: ```ts await Template.build(template, { alias: 'my-template' }) ``` ```python Template.build(template, alias="my-template") ``` Split out of #1433, which covers streaming for sandbox/volume file uploads and downloads. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ec260376dc | [skip ci] Release new versions | ||
|
|
de0c401626 | fix(sdk): correct filesystem watch handle callback and timeout behavior (#1480) | ||
|
|
7e7e9514df |
feat(sdk): filesystem-only auto-pause via lifecycle.onTimeout object form (#1471)
## Filesystem-only auto-pause (`onTimeout` object form)
Adds an object form to the sandbox **lifecycle** `onTimeout`
(`on_timeout` in Python) that controls the snapshot kind taken when a
sandbox auto-pauses on timeout, via `keepMemory` (`keep_memory`).
`onTimeout` now accepts either the existing bare action (`'pause'` /
`'kill'`) or the object form `{ action, keepMemory }`. When `keepMemory`
is `false` (with `action: 'pause'`), a timeout auto-pause takes a
**filesystem-only** snapshot (no memory) instead of a full memory one,
so the sandbox cold-boots (reboots) from disk on resume — losing running
processes and open connections. Defaults to `true` (full memory
snapshot), so existing callers are unaffected. **The bare string form is
unchanged.**
It's the create-time / auto-pause counterpart to the explicit
`pause(keepMemory=false)` from #1465: same `keepMemory` naming, mapped
onto the `autoPauseMemory` create field.
### Type safety
The object form is a **discriminated union** on `action`: `keepMemory`
is only valid with `action: 'pause'`. Pairing it with `action: 'kill'`
is a **compile-time type error** (TS) / static error (`ty`), and is
additionally rejected at runtime (`InvalidArgumentError` /
`InvalidArgumentException`) for untyped callers.
### Behavior & validation
- `keepMemory` only applies to a `pause` action.
- **Incompatible with auto-resume** — auto-resume wakes a paused sandbox
on inbound traffic by restoring its memory snapshot in place; a
filesystem-only snapshot has no memory to restore (resuming cold-boots
it), so it must be resumed explicitly via `connect()`. Combining
`keepMemory: false` with `autoResume` is rejected client-side.
### Usage
```ts
// JS/TS — filesystem-only auto-pause on timeout
const sbx = await Sandbox.create({
lifecycle: { onTimeout: { action: 'pause', keepMemory: false } },
})
// bare string form still works (full memory snapshot)
const sbx2 = await Sandbox.create({ lifecycle: { onTimeout: 'pause' } })
```
```python
# Python
sbx = Sandbox.create(
lifecycle={"on_timeout": {"action": "pause", "keep_memory": False}}
)
```
### Changes
- `spec/openapi.yml`: `autoPauseMemory` on the create body (+
regenerated JS/Python clients).
- JS `SandboxOnTimeout` discriminated union (`'pause' | 'kill' | {
action: 'pause'; keepMemory? } | { action: 'kill' }`) and the Python
`SandboxOnTimeoutPause` / `SandboxOnTimeoutKill` TypedDicts, wired
through `createSandbox` / `_create_sandbox` (sync + async) to
`autoPauseMemory`, with the client-side guards.
- Tests: payload serialization + validation (offline, incl. the `action:
'kill'` type/runtime guard) and live cold-boot e2e in both SDKs;
changeset (`e2b` + `@e2b/python-sdk`, minor).
### Backend dependency
The live e2e tests exercise the real auto-pause→cold-boot path and
require the infra-side `autoPauseMemory` support (e2b-dev/infra#3055),
now merged and deployed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
cb5a3870b6 |
feat(sdk): filesystem-only snapshots (pause memory:false) (#1465)
## Summary
Adds an optional **`memory`** flag to `pause` in both the JS and Python
SDKs. When `memory` is `false`, the pause captures **only the
filesystem** (no memory snapshot); resuming such a snapshot **cold-boots
(reboots)** the sandbox from disk — losing in-memory state, running
processes, and open connections. Defaults to `true` (full memory
snapshot), so existing callers are unaffected.
This is the SDK surface for the filesystem-only snapshot feature on the
infra side.
## Usage
```ts
// JS / TS
const sbx = await Sandbox.create()
await sbx.pause({ memory: false }) // filesystem-only snapshot
const resumed = await sbx.connect() // resumes by cold-booting from disk
```
```python
# Python (sync)
sbx = Sandbox()
sbx.pause(memory=False) # filesystem-only snapshot
resumed = sbx.connect() # resumes by cold-booting from disk
# Python (async)
sbx = await AsyncSandbox.create()
await sbx.pause(memory=False)
resumed = await sbx.connect()
```
`memory` defaults to `true` — `pause()` / `pause({})` behave exactly as
before.
## What changed
- **spec**: optional `memory: boolean` (default `true`) on `POST
/sandboxes/{sandboxID}/pause` (`SandboxPauseRequest`); both API clients
regenerated via `make codegen`.
- **JS**: `Sandbox.pause` / `betaPause` accept `{ memory }` →
`SandboxApi.pause` sends the request body.
- **Python**: `pause(memory=...)` / `beta_pause` → `_cls_pause` (sync +
async) sends `SandboxPauseRequest(memory=...)`.
- **Tests**: filesystem-only pause+resume reboots the guest while the
filesystem survives — JS (`tests/sandbox/snapshot.test.ts`) and Python
sync + async. All pass against a local stack; `format` / `lint` /
`typecheck` clean.
- **Changeset**: `minor` for `e2b` and `@e2b/python-sdk`.
## Note (related infra observation, not addressed here)
While testing, a filesystem-only **resume cold-boots into a different
default exec context** (`root` / `/root`) than a memory resume (`user` /
`/home/user`). The filesystem itself is fully intact; tests use absolute
paths to be robust to this. Worth confirming on the infra reboot path
whether the template's default user should be restored after a cold
boot.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
31a93bed0c | [skip ci] Release new versions | ||
|
|
2a98cce8c7 |
fix(js-sdk): stop CommandHandle.disconnect() leaking the output subscription (#1474)
## Description
This PR fixes two related issues in the command handle's event handling.
### 1. JS `CommandHandle.disconnect()` leaked the output subscription
`disconnect()` was fire-and-forget — it only triggered the transport
abort and relied entirely on HTTP/2 abort propagation to stop events,
which is unreliable under keepalive: `onStdout`/`onStderr`/`onPty` could
keep firing for output produced after `disconnect()` returned.
`disconnect()` now sets a cooperative `disconnected` flag and aborts the
transport. The flag is checked before every callback dispatch in the
event loop, so once `disconnect()` returns no callback fires for output
that arrives (or was buffered) after the call — even if the underlying
abort hasn't torn the stream down yet. It does **not** wait for the
event handler to drain, so it returns promptly even for an idle command
(e.g. `sleep`) whose stream produces no further output, never blocks on
an in-flight callback, and does not deadlock when awaited from inside a
callback.
The async Python SDK was already correct here (`disconnect()` cancels
the event-handling task), and the sync Python SDK has no background
subscription (events are consumed only while the caller iterates). The
added Python tests confirm both.
### 2. Exit code was lost when a disconnected consumer stopped on a
flushed `end`-event chunk
When the `end` event flushes trailing decoder bytes (an incomplete
multibyte sequence → replacement character) and the consumer stops
iterating on the first flushed chunk, the generator was aborted before
the result was assigned, so `wait()` failed as if the process never
produced a result. The `end` handler now records the result **before**
yielding the flushed chunks, across the JS, async Python, and sync
Python SDKs.
## Usage
```js
const handle = await sandbox.commands.run(daemon, { background: true, stdin: true, onStdout })
await sandbox.commands.sendStdin(handle.pid, 'turn1\n')
await handle.disconnect() // resolves promptly; onStdout will not fire again
await sandbox.commands.sendStdin(handle.pid, 'turn2\n') // turn2 output never reaches onStdout
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
dabac31cab |
Reuse toUploadBody in JS volume writeFile (#1473)
Replace the inlined stream/buffer logic in the JS volume `writeFile` with the shared `toUploadBody` helper, matching the `sandbox/filesystem` write path and dropping the now-unused local `runtime` and `toBlob` imports. The helper already returns a `ReadableStream` only when the body should be streamed (non-browser stream input) and otherwise buffers into a Blob, so deriving `isStream` from `body instanceof ReadableStream` is byte-for-byte equivalent to the old check. This is a pure refactor with no behavior change, keeping the two write paths from drifting. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7d4d620fa8 | [skip ci] Release new versions | ||
|
|
c1415f3ec7 |
Stream volume file uploads and downloads instead of buffering in memory (#1453)
Follow-up to #1433. Builds on the shared streaming infrastructure introduced there (`FILE_TIMEOUT_MS`, request-controller/stream-cleanup helpers in `connectionConfig`, `io_utils` chunk iterators, the `runtime` guard) and applies the same streaming model to volumes. > [!NOTE] > Based on `mishushakov/stream-write-file-upload` (#1433). Merge that PR first; this PR's diff will then retarget to `main` automatically. ## What changed - **`Volume.writeFile()` / `Volume.write_file()`** — stream the request body instead of buffering it in memory. - JS: `ReadableStream` data is streamed outside the browser (half-duplex); browsers still buffer since they can't stream request bodies. - Python: file-like objects are streamed in chunks (async wraps them in an async iterator; sync passes them to httpx directly, text-mode IO is encoded chunk-by-chunk). - **`Volume.readFile(format="stream")` / `read_file(format="stream")`** — the request timeout now bounds only the initial handshake, not the body read, matching the sandbox `files.read` stream path. A dropped connection during the handshake surfaces the same typed, health-checked error; JS supports `signal` to cancel an in-flight stream and cancels unconsumed bodies on error so the pooled connection is released. ## Usage JS — stream a file straight to a volume without buffering: ```ts import { createReadStream } from 'node:fs' import { Readable } from 'node:stream' const stream = Readable.toWeb(createReadStream('large-input.bin')) await volume.writeFile('/data/large-input.bin', stream) // read back as a stream; the body lives until consumed/cancelled const out = await volume.readFile('/data/large-input.bin', { format: 'stream' }) for await (const chunk of out) { // process chunk } ``` Python — stream a file-like object: ```python with open("large-input.bin", "rb") as f: volume.write_file("/data/large-input.bin", f) # streamed, not read() into memory for chunk in volume.read_file("/data/large-input.bin", format="stream"): ... # process chunk ``` ## Testing - `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` pass. - Added volume streaming tests (JS `tests/volume/file.test.ts`; Python sync/async `test_file.py` text-stream cases). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
60feee3cf6 |
Stream SDK file uploads and downloads instead of buffering in memory (#1433)
## Description Removes full in-memory buffering from the SDK **sandbox** file-transfer paths, in both JS and Python (sync + async). **Streamed uploads** — `Sandbox.files.write` / `write_files` streams `ReadableStream` (JS, outside the browser) and file-like (Python) input to the sandbox with chunk-by-chunk gzip compression, instead of buffering the whole body in memory. `useOctetStream`/`use_octet_stream` now defaults to auto-detect — octet-stream when any entry is streamable (so streamed uploads aren't silently buffered), `multipart/form-data` otherwise; browsers always use `multipart/form-data` since streaming request bodies aren't supported there. A streamed upload is bounded by a per-chunk timeout on the wire (Python's per-write `httpx` timeout, default the request timeout); a stalled upload the wire can't observe is bounded server-side. On Python's `AsyncSandbox`, the blocking file reads and gzip compression of a streamed upload now run in a worker thread so a large upload doesn't stall the event loop. **Streamed downloads** — `Sandbox.files.read(format="stream")` now streams the response body from the sandbox instead of downloading it into memory before iterating (Python sync + async), and the 60s request timeout no longer kills the stream while it's being consumed: - The request timeout now bounds only the initial handshake. - The body is bounded by a per-chunk **idle-read timeout** on the wire — a per-`read()` option (`streamIdleTimeoutMs` in JS, `stream_idle_timeout` in Python; default the request timeout — 60s — `0`/`None` to disable). It's armed only while waiting on a network read and cleared the moment a chunk arrives, so it aborts only when the server stops sending mid-stream; a slow or paused consumer never trips it (a held-but-unread stream is reclaimed server-side, not by this timer). - A dropped connection during the handshake surfaces the same typed, health-checked error as non-stream reads. In JS, `signal` can still cancel an in-flight stream. - The stream holds its pooled connection until it is consumed to the end, cancelled/closed, errors, or the idle timeout fires — consume it fully, use the context manager, or close it. (This replaces the earlier GC-finalizer net.) Python returns a `FileStreamReader`/`AsyncFileStreamReader` supporting deterministic cleanup via `close()`/`aclose()` and (async) context-manager use; both still satisfy `Iterator[bytes]`/`AsyncIterator[bytes]`, so existing iteration is unchanged. **Empty files** — JS `Sandbox.files.read()` with `blob` or `stream` format now returns a format-correct empty value (empty `Blob` / empty `ReadableStream`) for empty files instead of `""`. > [!NOTE] > The equivalent **volume** streaming changes (`Volume.writeFile`/`write_file`, `Volume.readFile`/`read_file` streams) live in a follow-up PR, #1453, which is based on this branch. ## Usage ```ts // JS: upload a large file without holding it in memory const file = createReadStream('large.bin') await sandbox.files.write('large.bin', Readable.toWeb(file), { gzip: true }) // JS: consume a download for longer than 60s without it being killed const stream = await sandbox.files.read('large.bin', { format: 'stream' }) for await (const chunk of stream) { /* ... */ } // JS: tune (or disable) the per-chunk idle-read timeout for a read const stream = await sandbox.files.read('large.bin', { format: 'stream', streamIdleTimeoutMs: 120_000, // 0 to disable }) // JS: empty files now return format-correct empty values const blob = await sandbox.files.read('empty.txt', { format: 'blob' }) // Blob (size 0), not '' ``` ```python # Python: streamed upload and download with open("large.bin", "rb") as f: sandbox.files.write("large.bin", f, gzip=True) for chunk in sandbox.files.read("large.bin", format="stream"): ... # Python: deterministic cleanup when not reading the stream to the end with sandbox.files.read("large.bin", format="stream") as stream: first_chunk = next(iter(stream)) # connection released on block exit # Python: tune (or disable) the per-chunk idle-read timeout for a read for chunk in sandbox.files.read( "large.bin", format="stream", stream_idle_timeout=120.0 # None to disable ): ... ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3cb6ca5f92 |
[skip ci] Release new versions (sync repo with published packages) (#1468)
## Why The Release run [27844631141](https://github.com/e2b-dev/E2B/actions/runs/27844631141/job/82412481993) **published all packages successfully** but then failed at the final *"Commit new versions"* step — the version-bump commit-back to \`main\` was rejected as non-fast-forward (another PR landed on \`main\` during the release window). As a result the registries are ahead of the repo: | Package | Published | Repo (main) before this PR | |---|---|---| | \`e2b\` (JS) | 2.30.4 (npm) | 2.30.3 | | \`@e2b/cli\` | 2.12.2 (npm) | 2.12.1 | | \`e2b\` (Python) | 2.29.4 (PyPI) | 2.29.3 | The changeset \`fix-logo-pypi-npm.md\` was also never consumed and is still on \`main\`. ## What this PR does Replays exactly what the failed *"Commit new versions"* step would have committed — i.e. \`pnpm run version\` (changeset version + \`postVersion\` poetry sync) + lockfile update: - Bumps \`e2b\` → 2.30.4, \`@e2b/cli\` → 2.12.2, \`@e2b/python-sdk\` → 2.29.4 (matching what's already published) - Deletes the consumed changeset \`fix-logo-pypi-npm.md\` - Updates \`pnpm-lock.yaml\` (CLI's \`e2b\` dep → 2.30.4) No new packages are published by merging this — it only syncs the repo to the registries. **Do not re-run the Release workflow** for this changeset; the versions already exist on npm/PyPI. |
||
|
|
726ced6ec5 |
docs: fix duplicate logo on NPM/PyPI by switching to <picture> element (#1466)
## 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> |
||
|
|
2c48e927c2 | [skip ci] Release new versions | ||
|
|
0a5d52478c |
docs: update package logos with theme-aware dark/light variants (#1462)
## 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> |