notte-sdk 0.0.22 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,8 +4,119 @@
4
4
 
5
5
  The official TypeScript SDK for Notte API, providing cloud-hosted browser sessions with precise control, LLM-powered web agents for automated tasks, serverless functions, and secure credential management.
6
6
 
7
+ ## Contributing
8
+
9
+ This directory is a standalone npm package. Node.js 22 and 24 are tested in CI;
10
+ Python tooling is not required to build or test it.
11
+
12
+ ```bash
13
+ cd node-sdk
14
+ npm ci
15
+ npm run typecheck
16
+ npm run test:unit -- --run
17
+ npx vitest run test/function-stream.local.test.ts # also part of npm run test:unit
18
+ npm run build
19
+ node scripts/check-package.js
20
+ npm pack --dry-run
21
+ ```
22
+
23
+ `npm run build` compiles the checked-in client without contacting the API.
24
+ To update the generated client, run `npm run generate:staging`, review the diff,
25
+ and commit it. `npm run check:staging` checks for drift without editing your files.
26
+
27
+ ### Generated resource methods
28
+
29
+ The SDK generates client-bound methods for `sessions`, `agents`, `functions`,
30
+ `vaults`, `personas`, and `profiles` from the checked-in OpenAPI-derived types
31
+ and operations. No per-call client or `throwOnError` is needed:
32
+
33
+ ```ts
34
+ const client = new NotteClient();
35
+ await client.sessions.stop(sessionId);
36
+ const vault = await client.vaults.create({ name: 'default' });
37
+ const copy = await client.functions.fork(functionId);
38
+ ```
39
+
40
+ Arguments are path IDs in URL order, then the request body (if any), query
41
+ parameters (if any), and optional `{ signal }` cancellation options. For example,
42
+ `client.sessions.stop(sessionId, { close_reason: 'manual' }, { signal })`.
43
+ Operations with required request headers require a final options object, for
44
+ example `client.sessions.createPayment(sessionId, body, undefined, { headers: { 'idempotency-key': key } })`.
45
+ Keep the same idempotency key when retrying the same payment request. The SDK
46
+ binds `x-notte-api-key` automatically; callers cannot override it through typed headers.
47
+ Methods return the API response body and throw the client's typed API errors.
48
+ The five existing `list()` methods still return arrays for compatibility;
49
+ the newly added `profiles.list()` returns the API pagination response.
50
+
51
+ These are API operations, not lifecycle helpers: they do not poll for completion,
52
+ decrypt credentials, or manage cleanup. Continue using `Session().use()`,
53
+ `Agent().run()`, `Function().run()`, and `Vault()` for those behaviors. Existing
54
+ standalone generated exports remain supported.
55
+
56
+ `npm run generate` refreshes this layer too. Use `npm run resources:generate`
57
+ to regenerate it offline, `npm run resources:check` to detect stale output, and
58
+ `npm run resources:test` for generator regressions. Method-name aliases live in
59
+ the generator; request shapes and routes come from generated OpenAPI types.
60
+
61
+ For the isolated live test, set `NOTTE_API_KEY` and
62
+ `NOTTE_API_URL=https://us-staging.notte.cc`, then run:
63
+
64
+ ```bash
65
+ npx vitest run test/function-create-run.integration.test.ts
66
+ ```
67
+
68
+ This test creates its own function and deletes it afterward. Other live suites
69
+ may require pre-existing fixtures; see [test documentation](test/README.md).
70
+ The `node sdk cicd` workflow runs on every PR touching `node-sdk/`:
71
+
72
+ - `tests` runs unit tests, local HTTP integration tests, type checks, and package
73
+ checks on Node.js 22 and 24, including for fork PRs.
74
+ - `integration-tests` runs the isolated live function lifecycle tests against
75
+ staging on same-repository PRs. Fork PRs skip this credentialed job.
76
+
77
+ Both jobs also run on main pushes and manual workflow runs on main. API drift
78
+ checks remain limited to main pushes and manual workflow runs on main.
79
+
80
+ ### Releases
81
+
82
+ The npm package remains `notte-sdk`. Its MIT license is in [LICENSE](LICENSE);
83
+ the repository root license does not replace this package's license.
84
+
85
+ The Node SDK is released together with the Python SDK, from the same tag and at
86
+ the same version. Cutting a release means creating a GitHub release tagged
87
+ `vX.Y.Z` on [nottelabs/notte](https://github.com/nottelabs/notte/releases):
88
+
89
+ - `pypi-release.yml` publishes the Python packages to PyPI as `X.Y.Z`.
90
+ - `node-sdk-publish.yml` sets this package's version to `X.Y.Z`, runs the
91
+ typecheck, unit tests, streaming test and export smoke test, publishes to npm
92
+ with provenance, then installs the published version back from npm to verify it.
93
+
94
+ `package.json` stays at `0.0.0-dev` on `main`; the version is only set in CI
95
+ from the tag, like the `.dev` placeholder in the Python `pyproject.toml` files.
96
+ Publishing uses npm trusted publishing (GitHub OIDC) bound to this repository,
97
+ workflow `node-sdk-publish.yml` and environment `npm`, so no npm token is stored
98
+ in the repository. To dry-run a release build locally, run `make release X.Y.Z`
99
+ in this directory; it never publishes.
100
+
101
+ Importing the SDK does not contact the npm registry. Constructing a `NotteClient`
102
+ performs a one-time, non-blocking check per process and warns when a newer
103
+ release exists, like the Python SDK. Set `NOTTE_SDK_DISABLE_VERSION_CHECK=1` to
104
+ disable it. To explicitly check for an update, call `await checkForLatestVersion()`
105
+ from `notte-sdk`.
106
+ API URLs must use HTTPS, except HTTP loopback addresses for local development.
107
+
7
108
  ## Features
8
109
 
110
+ ### Server-side proxy security
111
+
112
+ `createNotteProxy` from `notte-sdk/next` requires an `authenticate` callback at
113
+ runtime. Without one, requests return 401 even when a server API key is configured.
114
+ The callback must verify caller identity and authorize access to requested
115
+ resources. Returning a user-specific Notte API key isolates accounts; returning
116
+ void uses the shared server key and requires application-level ownership checks.
117
+ An endpoint allowlist is not a substitute for authorization. Relative HTTP proxy
118
+ clients poll agent status for completion because they cannot use backend WebSockets.
119
+
9
120
  - 🌐 **Cloud Browser Sessions** - Access remote browsers with full control
10
121
  - 🤖 **LLM-Powered Agents** - Intelligent web automation with natural language
11
122
  - 🔒 **Secret Vaults** - Enterprise-grade credential management with end-to-end encryption
@@ -14,6 +125,7 @@ The official TypeScript SDK for Notte API, providing cloud-hosted browser sessio
14
125
  - 📡 **Real-time Updates** - WebSocket support for live agent monitoring
15
126
  - 📝 **Type Safety** - Full TypeScript support with generated types
16
127
  - ⚡ **Serverless Functions** - Run pre-built automation workflows with simple API calls
128
+ - 🔍 **Web Search, Secrets and Usage** - Search the web, manage workspace secrets and read billing usage
17
129
  - 🔄 **Auto-sync** - Generated from OpenAPI spec, always up-to-date
18
130
 
19
131
  ## Installation
@@ -35,7 +147,7 @@ const notte = new NotteClient({
35
147
  });
36
148
 
37
149
  // Use session with automatic cleanup (mirrors Python's context manager)
38
- await notte.Session({ timeoutMinutes: 30 }).use(async (session) => {
150
+ await notte.Session({ idle_timeout_minutes: 5 }).use(async (session) => {
39
151
  const status = await session.status();
40
152
  console.log('Session status:', status);
41
153
  });
@@ -52,13 +164,13 @@ const notte = new NotteClient();
52
164
  await notte.Session().use(async (session) => {
53
165
  const agent = notte.Agent({ session, max_steps: 10 });
54
166
 
55
- const response = await agent.run(
56
- "Find the best Italian restaurant in San Francisco and book a table for 2 at 7pm today",
57
- (update) => {
167
+ const response = await agent.run({
168
+ task: "Find the best Italian restaurant in San Francisco and book a table for 2 at 7pm today",
169
+ updateHandler: (update) => {
58
170
  // Receive real-time updates via WebSocket
59
- console.log(`Step ${update.data.currentStep}: ${update.data.message}`);
171
+ console.log(`[${update.type}]`, update.data);
60
172
  }
61
- );
173
+ });
62
174
 
63
175
  console.log(`Agent completed: ${response.success ? 'Success' : 'Failed'}`);
64
176
  console.log(`Answer: ${response.answer}`);
@@ -74,10 +186,15 @@ The main client for interacting with the Notte API.
74
186
  ```typescript
75
187
  import { NotteClient } from 'notte-sdk';
76
188
 
77
- const notte = new NotteClient();
189
+ const notte = new NotteClient({
190
+ apiKey: process.env.NOTTE_API_KEY, // defaults to NOTTE_API_KEY
191
+ baseUrl: 'https://api.notte.cc', // defaults to NOTTE_API_URL or https://api.notte.cc
192
+ timeoutMs: 60_000, // per-request timeout, default 60 s
193
+ verbose: false, // log every request
194
+ });
78
195
 
79
196
  // Create sessions
80
- const session = notte.Session({ timeoutMinutes: 30 });
197
+ const session = notte.Session({ idle_timeout_minutes: 5, max_duration_minutes: 30 });
81
198
 
82
199
  // Create agents
83
200
  const agent = notte.Agent({ session, max_steps: 15 });
@@ -87,8 +204,32 @@ const vault = notte.Vault({ name: 'My Vault' });
87
204
 
88
205
  // Create personas
89
206
  const persona = notte.Persona({ create_vault: true });
207
+
208
+ // Check that the API is reachable (rejects with NotteAPIError otherwise)
209
+ await notte.healthCheck();
210
+ ```
211
+
212
+ #### Listing resources
213
+
214
+ Every resource has a `list()` namespace, mirroring `client.sessions.list()` and
215
+ friends in Python. Options are the generated query parameters of the matching
216
+ endpoint (`SessionListOptions`, `AgentListOptions`, `VaultListOptions`,
217
+ `FunctionListOptions`, `PersonaListOptions`).
218
+
219
+ ```typescript
220
+ const sessions = await notte.sessions.list({ only_active: true });
221
+ const agents = await notte.agents.list({ page_size: 10 });
222
+ const vaults = await notte.vaults.list();
223
+ const functions = await notte.functions.list();
224
+ const personas = await notte.personas.list({ only_active: true });
90
225
  ```
91
226
 
227
+ #### Database preview branches
228
+
229
+ Set `NOTTE_DB_PREVIEW_BRANCH` (or pass `dbPreview` to the client) to route every
230
+ request and websocket handshake to a database preview branch. This is an internal
231
+ option used when testing API changes.
232
+
92
233
  ### Session
93
234
 
94
235
  Manages browser sessions with automatic lifecycle management.
@@ -99,7 +240,7 @@ import { NotteClient } from 'notte-sdk';
99
240
  const notte = new NotteClient();
100
241
 
101
242
  // Context manager pattern (automatic start/stop)
102
- await notte.Session({ timeoutMinutes: 30 }).use(async (session) => {
243
+ await notte.Session({ idle_timeout_minutes: 5 }).use(async (session) => {
103
244
  // Session is automatically started
104
245
  console.log('Session ID:', session.getId());
105
246
 
@@ -120,7 +261,7 @@ try {
120
261
  }
121
262
 
122
263
  // Async iterator pattern
123
- for await (const session of notte.Session({ timeoutMinutes: 15 })) {
264
+ for await (const session of notte.Session({ max_duration_minutes: 15 })) {
124
265
  // Use session here
125
266
  const status = await session.status();
126
267
  console.log('Session active:', status);
@@ -128,15 +269,102 @@ for await (const session of notte.Session({ timeoutMinutes: 15 })) {
128
269
  }
129
270
  ```
130
271
 
272
+ #### Browser control
273
+
274
+ Drive the page with typed actions, exactly like `session.execute(...)` in Python.
275
+ Element ids (`B1`, `I2`, ...) must come from a live `observe()` call.
276
+
277
+ ```typescript
278
+ import { NotteClient, actions, ActionExecutionError } from 'notte-sdk';
279
+
280
+ const notte = new NotteClient();
281
+
282
+ await notte.Session().use(async (session) => {
283
+ await session.execute(actions.goto({ url: 'https://www.notte.cc' }));
284
+
285
+ const observation = await session.observe({ perception_type: 'deep' });
286
+ console.log(observation.space);
287
+
288
+ // Plain objects work too; the `type` field is a discriminated union
289
+ await session.execute({ type: 'click', id: 'B1' });
290
+ await session.execute(actions.fill({ selector: 'internal:text="Email"', value: 'me@example.com' }));
291
+
292
+ // A failed action throws ActionExecutionError with the server-side error type
293
+ try {
294
+ await session.execute(actions.click({ id: 'B999' }));
295
+ } catch (error) {
296
+ if (error instanceof ActionExecutionError) console.log(error.errorType, error.userMessage);
297
+ }
298
+
299
+ // Or inspect the result yourself
300
+ const result = await session.execute(actions.click({ id: 'B999' }), { raiseOnFailure: false });
301
+ console.log(result.success, result.message);
302
+
303
+ // Run JavaScript in the page and fetch through the page (cookies, proxy, fingerprint).
304
+ // Relative URLs resolve against the page; plaintext http targets are refused unless allowInsecure is set.
305
+ const title = await session.evaluateJs('document.title');
306
+ const summary = await session.fetch('/api/rest_v1/page/summary/Main_Page').then(r => r.json());
307
+
308
+ // Scrape the current page
309
+ const markdown = await session.scrape({ only_main_content: true });
310
+
311
+ // Cookies
312
+ const cookies = await session.getCookies();
313
+ await session.setCookies(cookies);
314
+ });
315
+ ```
316
+
317
+ #### Playwright access
318
+
319
+ `playwright-core` is an optional peer dependency (`npm install playwright-core`),
320
+ the counterpart of `pip install notte-sdk[playwright]`:
321
+
322
+ ```typescript
323
+ await notte.Session().use(async (session) => {
324
+ const page = await session.page(); // connected over CDP, cached
325
+ await page.goto('https://www.notte.cc');
326
+ await page.screenshot({ path: 'screenshot.png' });
327
+
328
+ // Or connect your own tooling
329
+ const cdpUrl = await session.cdpUrl();
330
+ });
331
+ ```
332
+
333
+ #### Replays and cookie files
334
+
335
+ ```typescript
336
+ // Persist login state between runs: loaded on start, appended on stop
337
+ const session = notte.Session({ cookie_file: './cookies.json' });
338
+
339
+ await session.use(async () => { /* ... */ });
340
+
341
+ // Replays are generated after stop(); replay() polls until ready (240 s by default)
342
+ const replay = await session.replay();
343
+ await session.downloadReplay('./session.mp4');
344
+ ```
345
+
131
346
  #### Session Methods
132
347
 
133
- - `start()` - Start the session
134
- - `stop()` - Stop the session
348
+ - `start()` - Start the session (retries 5xx errors 3 times, waits 30 s on cluster overload)
349
+ - `stop(closeReason?)` - Stop the session
135
350
  - `status()` - Get session status
136
- - `viewer()` - Open the live session viewer in the local default browser
137
351
  - `use(callback)` - Context manager pattern
138
352
  - `getId()` - Get session ID
139
353
  - `isSessionActive()` - Check if session is running
354
+ - `execute(action, options?)` - Execute a typed action; throws `ActionExecutionError` on failure unless `raiseOnFailure: false`
355
+ - `observe(options?)` - Observe the page (`perception_type`, `min_nb_actions`, `max_nb_actions`)
356
+ - `scrape(options?)` - Scrape the page as markdown, images, or structured data (see [Scraping](#scraping))
357
+ - `evaluateJs(code, options?)` - Evaluate JavaScript and return the stringified result
358
+ - `fetch(url, options?)` - Issue an HTTP request from inside the page
359
+ - `setCookies(cookies)` / `setCookiesFromFile(path)` / `getCookies()` - Manage cookies
360
+ - `cdpUrl()` - Chrome DevTools Protocol websocket URL
361
+ - `page()` - Playwright `Page` connected over CDP (requires `playwright-core`)
362
+ - `debugInfo()` / `debugTabInfo(tabIdx?)` - Debug URLs and tab metadata
363
+ - `replay(options?)` / `downloadReplay(path?)` - Session recording
364
+ - `getScript(options?)` - Python code reproducing the session
365
+ - `viewer()` - Open the live session viewer in the local default browser
366
+ - `storage` - The `RemoteFileStorage` attached with the `storage` option
367
+ - `client.sessions.list(options?)` - List sessions
140
368
 
141
369
  ### Agent
142
370
 
@@ -153,25 +381,25 @@ await notte.Session().use(async (session) => {
153
381
  max_steps: 10
154
382
  });
155
383
 
156
- // Non-blocking: start agent and get ID
157
- const agentId = await agent.start("Navigate to Google and search for 'TypeScript'");
158
- console.log('Agent started:', agentId);
384
+ // Non-blocking: start agent and get its ID
385
+ await agent.start({ task: "Navigate to Google and search for 'TypeScript'" });
386
+ console.log('Agent started:', agent.agentId);
159
387
 
160
388
  // Check agent status
161
389
  const status = await agent.status();
162
390
  console.log('Agent status:', status);
163
391
 
164
392
  // Blocking: run agent and wait for completion with live updates
165
- const response = await agent.run(
166
- "Find the latest TypeScript documentation",
167
- (update) => {
393
+ const response = await agent.run({
394
+ task: "Find the latest TypeScript documentation",
395
+ updateHandler: (update) => {
168
396
  if (update.type === 'step') {
169
397
  console.log(`Step update:`, update.data);
170
398
  } else if (update.type === 'completion') {
171
399
  console.log(`Completed:`, update.data);
172
400
  }
173
401
  }
174
- );
402
+ });
175
403
 
176
404
  console.log('Final result:', response);
177
405
  });
@@ -179,12 +407,15 @@ await notte.Session().use(async (session) => {
179
407
 
180
408
  #### Agent Methods
181
409
 
182
- - `start(task)` - Start agent with task (non-blocking)
183
- - `run(task, onUpdate?)` - Start agent and wait for completion (blocking)
410
+ - `start(request)` - Start agent with `{ task, url?, ... }` (non-blocking)
411
+ - `run(request)` - Start agent and wait for completion (blocking); pass `updateHandler` for live updates
184
412
  - `status()` - Get agent status
185
413
  - `stop()` - Stop the agent
186
- - `getId()` - Get agent ID
187
- - `isRunning()` - Check if agent is running
414
+ - `agentId` - The agent ID (read-only property, available after `start()` / `run()`)
415
+ - `sessionId` - The session ID the agent runs in (read-only property)
416
+ - `hasStarted()` - Whether `start()`/`run()` was called on this instance (local check)
417
+ - `isActive()` - Whether the agent is still active on the server (fetches the status)
418
+ - `isRunning()` - Deprecated alias of `hasStarted()`; it does not contact the API
188
419
 
189
420
  #### Structured answers with Zod
190
421
 
@@ -231,15 +462,6 @@ await vault.addCredentials('https://github.com/', {
231
462
  const strongPassword = vault.generatePassword(20, true); // 20 chars with special chars
232
463
  const simplePassword = vault.generatePassword(12, false); // 12 chars, no special chars
233
464
 
234
- // Add credit card information
235
- await vault.setCreditCard({
236
- card_holder_name: 'John Doe',
237
- card_number: '4111111111111111',
238
- card_cvv: '123',
239
- expiry_month: '12',
240
- expiry_year: '2025'
241
- });
242
-
243
465
  // Use with agents for automatic credential management
244
466
  await notte.Session().use(async (session) => {
245
467
  const agent = notte.Agent({
@@ -259,22 +481,21 @@ const credentials = await existingVault.listCredentials();
259
481
 
260
482
  // Cleanup
261
483
  await vault.deleteCredentials('https://github.com/');
262
- await vault.deleteCreditCard();
263
484
  await vault.stop(); // Deletes entire vault
264
485
  ```
265
486
 
266
487
  #### Vault Methods
267
488
 
268
- - `addCredentials(url, credentials)` - Store credentials for a URL
269
- - `getCredentials(url)` - Retrieve credentials for a URL
489
+ - `addCredentials(url, credentials)` - Store credentials for a URL (validated client-side like Python: root domain, exactly one of `email`/`username`, base32 `mfa_secret`)
490
+ - `addCredentialsFromEnv(url)` - Read `{DOMAIN}_EMAIL` / `{DOMAIN}_USERNAME` / `{DOMAIN}_PASSWORD` / `{DOMAIN}_MFA_SECRET` from the environment
491
+ - `getCredentials(url)` - Retrieve credentials for a URL; throws `NotteAPIError` when none exist
492
+ - `hasCredential(url)` - Whether credentials exist for a URL
270
493
  - `deleteCredentials(url)` - Delete credentials for a URL
271
494
  - `listCredentials()` - List all stored credentials
272
- - `setCreditCard(card)` - Store credit card information
273
- - `getCreditCard()` - Retrieve credit card information
274
- - `deleteCreditCard()` - Delete credit card information
275
495
  - `generatePassword(length?, includeSpecialChars?)` - Generate secure passwords
276
496
  - `delete()` - Delete the entire vault
277
497
  - `stop()` - Stop and delete the vault
498
+ - `client.vaults.list(options?)` - List vaults
278
499
 
279
500
  #### Security Features
280
501
 
@@ -292,15 +513,12 @@ import { NotteClient } from 'notte-sdk';
292
513
 
293
514
  const notte = new NotteClient();
294
515
 
295
- // Create a new persona with vault and phone number
296
- const persona = notte.Persona({
297
- create_vault: true,
298
- create_phone_number: true
299
- });
516
+ // Create a new persona with a vault
517
+ const persona = notte.Persona({ create_vault: true });
300
518
 
301
- // Get persona information
302
- console.log(`Email: ${persona.info.email}`);
303
- console.log(`Phone: ${persona.info.phone_number}`);
519
+ // Get persona information (the first call initializes the persona)
520
+ const info = await persona.get();
521
+ console.log(`Email: ${info.email}`);
304
522
 
305
523
  // Read emails sent to the persona
306
524
  const emails = await persona.emails({
@@ -323,9 +541,9 @@ await notte.Session().use(async (session) => {
323
541
  vault_id: persona.vault.vaultId // Agent will use persona's vault
324
542
  });
325
543
 
326
- const result = await agent.run(
327
- `Create an account on GitHub using the persona credentials`
328
- );
544
+ const result = await agent.run({
545
+ task: `Create an account on GitHub using the persona credentials`
546
+ });
329
547
  console.log(`Account created: ${result.success}`);
330
548
  });
331
549
 
@@ -345,10 +563,9 @@ await persona.stop(); // Deletes persona and all associated data
345
563
 
346
564
  #### Persona Methods
347
565
 
566
+ - `get()` - Fetch (and initialize) the persona information
348
567
  - `emails(options?)` - Read emails sent to the persona
349
568
  - `sms(options?)` - Read SMS messages sent to the persona
350
- - `createNumber(options?)` - Create a phone number for the persona
351
- - `deleteNumber()` - Delete the persona's phone number
352
569
  - `addCredentials(url)` - Add auto-generated credentials to the persona's vault
353
570
  - `delete()` - Delete the persona
354
571
  - `stop()` - Stop and delete the persona
@@ -368,7 +585,7 @@ const sms = await persona.sms(options);
368
585
 
369
586
  #### Persona Features
370
587
 
371
- - **Complete Digital Identity** - Unique email address and phone number
588
+ - **Complete Digital Identity** - Unique email address
372
589
  - **2FA Support** - Receive and read SMS verification codes automatically
373
590
  - **Automated Account Creation** - Seamless integration with agents for signup flows
374
591
  - **Vault Integration** - Optional secure credential storage
@@ -390,16 +607,17 @@ const fn = notte.NotteFunction({
390
607
  decryption_key: 'optional-decryption-key' // optional, for encrypted outputs
391
608
  });
392
609
 
393
- // Run the function with input variables
610
+ // Stream logs to console.log and wait for the final result (default)
394
611
  const result = await fn.run({
395
612
  url: 'https://example.com',
396
613
  query: 'extract pricing information'
397
614
  });
398
615
 
399
616
  console.log('Run ID:', result.function_run_id);
617
+ console.log('Result:', result.result);
400
618
 
401
619
  // Retrieve the run result / metadata later
402
- const metadata = await fn.retrieve(result.function_run_id);
620
+ const metadata = await fn.getRun(result.function_run_id);
403
621
  console.log('Status:', metadata.status); // 'active' | 'closed' | 'failed'
404
622
  console.log('Result:', metadata);
405
623
  ```
@@ -424,28 +642,60 @@ fail explicitly if that runtime is unavailable or at capacity; they do not fall
424
642
  back to Lambda. Browser sessions keep their own duration limits. Existing
425
643
  schedules continue to use the standard runtime during the pilot.
426
644
 
427
- #### Polling for completion
645
+ #### Create a run before execution
428
646
 
429
- Since functions run asynchronously, you can poll for the result:
647
+ `createRun()` is optional: use it when you need a run ID before execution starts.
648
+ It only creates the cloud run record. Pass the returned ID as `functionRunId` to
649
+ execute that record; omitting `functionRunId` makes `run()` create a new run.
430
650
 
431
651
  ```typescript
432
652
  const fn = notte.NotteFunction({ function_id: 'your-function-id' });
433
- const { function_run_id } = await fn.run({ url: 'https://example.com' });
434
-
435
- // Poll until complete
436
- let metadata;
437
- do {
438
- await new Promise(r => setTimeout(r, 2000)); // wait 2s between checks
439
- metadata = await fn.retrieve(function_run_id);
440
- } while (metadata.status === 'active');
441
-
442
- if (metadata.status === 'closed') {
443
- console.log('Function completed successfully:', metadata);
444
- } else {
445
- console.error('Function failed:', metadata);
446
- }
653
+ const { function_run_id } = await fn.createRun();
654
+ console.log('Run ID before execution:', function_run_id);
655
+
656
+ const pending = fn.run(
657
+ { url: 'https://example.com' },
658
+ { functionRunId: function_run_id },
659
+ );
660
+
661
+ // Do other work here, then wait for completion.
662
+ const result = await pending;
663
+ console.log(result.result);
664
+
665
+ const metadata = await fn.getRun(function_run_id);
666
+ console.log(metadata.status);
447
667
  ```
448
668
 
669
+ `getRun()` fetches the current status and stored result without waiting for execution
670
+ to finish. `retrieve()` remains a compatible alias. Execution results contain the
671
+ function's structured output; the stored `metadata.result` is serialized JSON.
672
+
673
+ Keeping the promise lets your JavaScript continue while the HTTP request remains
674
+ open. Keep the process running until it completes. This is not a detached job.
675
+ With `{ stream: false }`, the SDK returns JSON without live logs, but standard
676
+ execution still waits for completion.
677
+
678
+ #### Streaming options and migration
679
+
680
+ **Breaking change:** `run()` now defaults to `stream: true`, matching the Python SDK.
681
+ It consumes the event stream, prints logs as they arrive, and resolves with the
682
+ final response, including `status` and `result`. Existing callers that require a
683
+ JSON response without live logs must add `{ stream: false }` as the second argument.
684
+
685
+ Failed executions throw by default. Set `raiseOnFailure: false` to inspect the
686
+ failed response yourself. Transport and malformed-stream errors still throw.
687
+ Use `onLog` to route logs to your application (or `onLog: () => {}` to silence them):
688
+
689
+ ```typescript
690
+ const result = await fn.run({ url: 'https://example.com' }, {
691
+ onLog: message => console.log('[function]', message),
692
+ raiseOnFailure: false,
693
+ });
694
+ ```
695
+
696
+ Options belong in the second argument; a `stream` field in the first argument
697
+ is a script input and does not change streaming behavior.
698
+
449
699
  #### Downloading the function code
450
700
 
451
701
  ```typescript
@@ -472,24 +722,153 @@ const fn = notte.NotteFunction({
472
722
  const code = await fn.download();
473
723
  ```
474
724
 
725
+ #### Creating and managing functions
726
+
727
+ Upload a Python script to create a function, like `NotteFunction(path=...)` in Python.
728
+ Creation is lazy: the upload happens on the first call that needs the function id.
729
+
730
+ ```typescript
731
+ const fn = notte.NotteFunction({ path: './my_function.py', name: 'price-monitor', description: 'Checks prices' });
732
+
733
+ const details = await fn.get(); // metadata, versions and download link
734
+ await fn.update({ path: './my_function_v2.py' }); // upload a new version
735
+ await fn.updateMetadata({ name: 'price-monitor-v2' });
736
+ await fn.rollback({ version: details.latest_version });
737
+
738
+ await fn.setSchedule({ cron: '0 9 * * *', variables: { url: 'https://example.com' } });
739
+ await fn.deleteSchedule();
740
+
741
+ const runs = await fn.runs(); // all runs, including completed ones
742
+ const functions = await notte.functions.list();
743
+
744
+ await fn.delete();
745
+ ```
746
+
747
+ `run()` waits at most 300 s by default; pass `timeoutMs` to change it. A failed
748
+ run throws `FailedToRunCloudFunctionError` unless `raiseOnFailure: false`.
749
+
475
750
  #### Function Methods
476
751
 
477
- - `run(variables?)` - Start a function run with optional input variables
478
- - `retrieve(functionRunId)` - Get metadata/result for a specific run
752
+ - `run(variables?, options?)` - Stream logs and wait for completion; use `stream: false` for the backend's JSON response without live logs. Options: `runtime`, `functionRunId`, `onLog`, `raiseOnFailure`, `timeoutMs`
753
+ - `get(options?)` - Function metadata, versions and download link
754
+ - `update(options)` / `updateMetadata(body)` - Upload a new version / change name, description
755
+ - `delete()` - Delete the function
756
+ - `setSchedule(body)` / `deleteSchedule()` - Manage the cron schedule
757
+ - `rollback(options)` - Roll back to a previous version
758
+ - `runs(options?)` - List runs (all runs by default, `only_active: true` for active ones)
759
+ - `createRun()` - Create a cloud run record without executing it
760
+ - `getRun(functionRunId)` - Get current metadata/result for a specific run
761
+ - `retrieve(functionRunId)` - Compatibility alias for `getRun()`
479
762
  - `download(options?)` - Download the function code, optionally writing it to `options.path`
480
763
  - `getUrl(options?)` - Get the (decrypted) download url of the function code
481
764
  - `getFunctionId()` - Get the function ID
482
765
  - `functionId` - The function ID (read-only property)
483
766
  - `decryptionKey` - The decryption key, if provided (read-only property)
484
767
 
768
+ ### File Storage
769
+
770
+ Upload files for a session to use and download files the session produced,
771
+ the counterpart of `client.FileStorage()` in Python.
772
+
773
+ ```typescript
774
+ import { NotteClient, FileExistsError } from 'notte-sdk';
775
+
776
+ const notte = new NotteClient();
777
+ const storage = notte.FileStorage();
778
+ let sessionId = '';
779
+ let fileId = '';
780
+
781
+ await notte.Session({ storage }).use(async (session) => {
782
+ sessionId = session.getId()!;
783
+
784
+ // Upload from a path, a Blob or a Uint8Array
785
+ const uploaded = await storage.upload('./invoice.pdf');
786
+ fileId = uploaded.id;
787
+
788
+ // ... let the session / an agent download something ...
789
+
790
+ const downloads = await storage.list({ source: 'session_download' });
791
+ for (const file of downloads.files) {
792
+ const path = await storage.download(file.id, './downloads');
793
+ console.log('saved', path);
794
+ }
795
+
796
+ const info = await storage.metadata(uploaded.id);
797
+ await storage.delete(uploaded.id);
798
+ });
799
+
800
+ // Files of a closed session
801
+ const files = notte.FileStorage(sessionId);
802
+ await files.download(fileId, './downloads', { force: true }); // overwrite instead of FileExistsError
803
+ ```
804
+
805
+ `notte.Files(sessionId)` exposes the same operations returning `Blob`s instead of writing to disk.
806
+
807
+ ### Web Search
808
+
809
+ Search the public web with `POST /search`. The default `outputType` returns
810
+ ranked results; `sourcedAnswer` returns a written answer with its sources.
811
+
812
+ ```typescript
813
+ const { results } = await notte.search('notte browser automation', { depth: 'fast' });
814
+ results.forEach(item => console.log(item.name, item.url));
815
+
816
+ const { answer, sources } = await notte.search('what is notte?', { outputType: 'sourcedAnswer' });
817
+ console.log(answer, sources.map(s => s.url));
818
+ ```
819
+
820
+ Extra fields such as `maxResults` or `includeDomains` are forwarded to the search
821
+ provider. `SearchOptions` and the `SearchResultsResponse` /
822
+ `SearchSourcedAnswerResponse` shapes are exported.
823
+
824
+ ### Anything API
825
+
826
+ `notte.anything.start({ task })` (`POST /anything/start`) turns a plain-English
827
+ description of a web task into a deployed, reusable function. Building is slow and
828
+ billable; prefer `notte.functions.list()` when a ready-made function already fits.
829
+ The API response is not typed by the OpenAPI spec and is exposed as an opaque
830
+ `AnythingStartResponse` record (it references the built function, e.g. `function_id`).
831
+
832
+ ```typescript
833
+ const started = await notte.anything.start({ task: 'fetch the top 3 hacker news posts' });
834
+ console.log(started);
835
+ ```
836
+
837
+ ### Secrets
838
+
839
+ Workspace secrets (`/secrets`) are scoped by namespace: `llm_provider` keys are
840
+ used by agents and scraping, `function_env` values are injected into function runs.
841
+ Listing never returns values, only metadata with a `key_hint`.
842
+
843
+ ```typescript
844
+ const meta = await notte.secrets.store({ namespace: 'function_env', name: 'API_TOKEN', value: 'xyz' });
845
+ const { value } = await notte.secrets.get('API_TOKEN', 'function_env');
846
+ const secrets = await notte.secrets.list({ namespace: 'function_env' });
847
+ await notte.secrets.delete(meta.id);
848
+ ```
849
+
850
+ ### Usage
851
+
852
+ Read billing usage for the current (or a given) monthly period, and page through
853
+ per-request usage logs.
854
+
855
+ ```typescript
856
+ const usage = await notte.usage.get();
857
+ console.log(`${usage.session_count} sessions, $${usage.total_cost} this period`);
858
+
859
+ const page = await notte.usage.logs({ endpoint: 'sessions.start', page: 1, page_size: 50 });
860
+ page.items.forEach(log => console.log(log.created_at, log.endpoint, log.duration_ms));
861
+ ```
862
+
485
863
  ## Advanced Usage
486
864
 
487
865
  ### Session Configuration
488
866
 
489
867
  ```typescript
490
868
  const session = notte.Session({
491
- timeoutMinutes: 45, // Session timeout (default: 30)
492
- // Add other session options as supported by the API
869
+ idle_timeout_minutes: 5, // Close the session after this much inactivity
870
+ max_duration_minutes: 45, // Hard limit on the session lifetime
871
+ // Add other session options as supported by the API (SessionOptions)
493
872
  });
494
873
  ```
495
874
 
@@ -504,36 +883,87 @@ const agent = notte.Agent({
504
883
 
505
884
  ### Error Handling
506
885
 
886
+ Every error thrown by the SDK extends `NotteError`, mirroring `notte_sdk.errors`
887
+ in Python. Generated API calls reject with a `NotteAPIError` carrying the request
888
+ `path`, the HTTP `statusCode` and the parsed response body in `error`; the API can
889
+ flag failures as `NotteAPIExecutionError`. Client-side problems use dedicated
890
+ classes: `AuthenticationError` (missing key), `InvalidRequestError`,
891
+ `NotteTimeoutError` (request or wait deadline exceeded),
892
+ `FailedToRunCloudFunctionError`, `ScrapeFailedError` and `ActionExecutionError`.
893
+
507
894
  ```typescript
895
+ import { NotteAPIError, NotteError, NotteTimeoutError } from 'notte-sdk';
896
+
508
897
  try {
509
898
  await notte.Session().use(async (session) => {
510
899
  const agent = notte.Agent({ session });
511
- const response = await agent.run("Complete a complex task");
900
+ const response = await agent.run({ task: "Complete a complex task" });
512
901
 
513
902
  if (!response.success) {
514
- console.error('Agent failed:', response.error);
903
+ console.error('Agent failed:', response.answer);
515
904
  }
516
905
  });
517
906
  } catch (error) {
518
- console.error('Session error:', error);
907
+ if (error instanceof NotteAPIError) {
908
+ console.error(`API ${error.statusCode} on ${error.path}:`, error.apiMessage ?? error.error);
909
+ } else if (error instanceof NotteTimeoutError) {
910
+ console.error('Timed out:', error.message);
911
+ } else if (error instanceof NotteError) {
912
+ console.error('SDK error:', error.message);
913
+ } else {
914
+ throw error;
915
+ }
519
916
  }
520
917
  ```
521
918
 
919
+ ### Timeouts and retries
920
+
921
+ CAPTCHA solving has a separate `captchaTimeoutSeconds` budget (default 180).
922
+ `session.execute()` polls pending solves using requests of at most 10 seconds,
923
+ then resumes an ordinary action only when the backend confirms it did not execute.
924
+ Actions that already ran are never replayed. Navigation, closure, provider failure,
925
+ and deadline expiry preserve the normal `raiseOnFailure` behavior. Ordinary HTTP
926
+ transport failures are not automatically retried. Deploy backend CAPTCHA polling
927
+ support before upgrading the SDK.
928
+
929
+ The client applies a per-request timeout (`timeoutMs`, default 60 000 ms; `0`
930
+ disables it) through response-body consumption and rejects with `NotteTimeoutError`
931
+ when it elapses. Explicit streams use the caller's signal for body cancellation;
932
+ function runs supply their own execution deadline. `retry()` wraps an async function so it is re-attempted on failure, like the Python
933
+ `@retry` decorator:
934
+
935
+ ```typescript
936
+ import { NotteClient, retry } from 'notte-sdk';
937
+
938
+ const notte = new NotteClient({ timeoutMs: 30_000 });
939
+
940
+ const start = retry(() => notte.Session().start(), {
941
+ maxTries: 3,
942
+ delayMs: 1_000,
943
+ shouldRetry: error => error instanceof NotteTimeoutError,
944
+ });
945
+ await start();
946
+ ```
947
+
522
948
  ### WebSocket Updates
523
949
 
524
- The `agent.run()` method provides real-time updates via WebSocket:
950
+ The `agent.run()` method provides real-time updates via WebSocket through the
951
+ `updateHandler` option:
525
952
 
526
953
  ```typescript
527
- const response = await agent.run("Your task", (update) => {
528
- switch (update.type) {
529
- case 'step':
530
- console.log(`Step ${update.data.currentStep}:`, update.data);
531
- break;
532
- case 'completion':
533
- console.log('Task completed:', update.data);
534
- break;
535
- default:
536
- console.log('Update:', update);
954
+ const response = await agent.run({
955
+ task: "Your task",
956
+ updateHandler: (update) => {
957
+ switch (update.type) {
958
+ case 'step':
959
+ console.log('Step:', update.data);
960
+ break;
961
+ case 'completion':
962
+ console.log('Task completed:', update.data);
963
+ break;
964
+ default:
965
+ console.log('Update:', update);
966
+ }
537
967
  }
538
968
  });
539
969
  ```
@@ -571,7 +1001,7 @@ from notte_sdk import NotteClient
571
1001
  notte = NotteClient()
572
1002
 
573
1003
  # Session context manager
574
- with notte.Session(timeout_minutes=2) as session:
1004
+ with notte.Session(idle_timeout_minutes=2) as session:
575
1005
  status = session.status()
576
1006
  print(status)
577
1007
 
@@ -589,7 +1019,7 @@ import { NotteClient } from 'notte-sdk';
589
1019
  const notte = new NotteClient();
590
1020
 
591
1021
  // Session context manager equivalent
592
- await notte.Session({ timeoutMinutes: 2 }).use(async (session) => {
1022
+ await notte.Session({ idle_timeout_minutes: 2 }).use(async (session) => {
593
1023
  const status = await session.status();
594
1024
  console.log(status);
595
1025
  });
@@ -597,7 +1027,7 @@ await notte.Session({ timeoutMinutes: 2 }).use(async (session) => {
597
1027
  // Agent usage
598
1028
  await notte.Session().use(async (session) => {
599
1029
  const agent = notte.Agent({ session, max_steps: 10 });
600
- const response = await agent.run("Find the best italian restaurant in SF");
1030
+ const response = await agent.run({ task: "Find the best italian restaurant in SF" });
601
1031
  console.log(`Agent completed: ${response.success}, answer: ${response.answer}`);
602
1032
  });
603
1033
  ```
@@ -635,7 +1065,7 @@ npm test
635
1065
  npm run test -- --coverage
636
1066
 
637
1067
  # Run specific test file
638
- npm run test src/test/client.test.ts
1068
+ npm run test test/client.test.ts
639
1069
  ```
640
1070
 
641
1071
  ### Regenerating from API
@@ -654,15 +1084,15 @@ This will fetch the latest API specification and update the generated client cod
654
1084
  The SDK is built with full TypeScript support:
655
1085
 
656
1086
  ```typescript
657
- import { NotteClient, SessionStatus, AgentResponse } from 'notte-sdk';
1087
+ import { NotteClient, type SessionResponse, type LegacyAgentStatusResponse } from 'notte-sdk';
658
1088
 
659
1089
  const notte = new NotteClient();
660
1090
 
661
1091
  // All types are properly inferred
662
1092
  await notte.Session().use(async (session) => {
663
- const status: SessionStatus = await session.status();
1093
+ const status: SessionResponse = await session.status();
664
1094
  const agent = notte.Agent({ session });
665
- const response: AgentResponse = await agent.run("task");
1095
+ const response: LegacyAgentStatusResponse = await agent.run({ task: "task" });
666
1096
  });
667
1097
  ```
668
1098
 
@@ -670,24 +1100,35 @@ await notte.Session().use(async (session) => {
670
1100
 
671
1101
  ### Types
672
1102
 
673
- The SDK exports all generated types from the OpenAPI specification:
1103
+ The SDK exports all generated types from the OpenAPI specification alongside the
1104
+ hand-written option types:
674
1105
 
675
1106
  ```typescript
676
1107
  import type {
677
1108
  NotteClientConfig,
678
1109
  SessionOptions,
679
- SessionStatus,
1110
+ SessionResponse,
1111
+ SessionListOptions,
680
1112
  AgentConstructor,
681
1113
  AgentRunRequest,
682
1114
  AgentUpdateHandler,
1115
+ AgentListOptions,
683
1116
  VaultConstructor,
684
- CredentialsDict,
685
- CreditCardDict,
1117
+ VaultListOptions,
1118
+ CredentialsDictInput,
686
1119
  Credential,
687
1120
  PersonaConstructor,
688
1121
  MessageReadOptions,
689
1122
  PersonaListOptions,
690
- CreatePhoneNumberOptions
1123
+ FunctionConstructor,
1124
+ FunctionListOptions,
1125
+ RetryOptions,
1126
+ NotteAPIErrorBody,
1127
+ SearchOptions,
1128
+ SearchResultsResponse,
1129
+ AnythingStartResponse,
1130
+ SecretMetadata,
1131
+ UsageResponse,
691
1132
  } from 'notte-sdk';
692
1133
  ```
693
1134
 
@@ -698,6 +1139,57 @@ MIT
698
1139
  ## Support
699
1140
 
700
1141
  For issues and questions:
701
- - GitHub Issues: [Report issues](https://github.com/notte/sdk/issues)
1142
+ - GitHub Issues: [Report issues](https://github.com/nottelabs/notte/issues)
702
1143
  - Documentation: [docs.notte.cc](https://docs.notte.cc)
703
1144
  - Python SDK: [notte-sdk](https://pypi.org/project/notte-sdk/)
1145
+
1146
+ ## Managed authentication
1147
+
1148
+ Use existing connection IDs with a session. `start()` waits for authentication by
1149
+ default, and `.use()` always invokes its callback only after the session is ready:
1150
+
1151
+ ```typescript
1152
+ const session = client.Session({ auth_ids: ["<connection-id>"], auth_retry: 2 });
1153
+ await session.use(async (ready) => {
1154
+ await ready.execute({ type: "goto", url: "https://example.com" });
1155
+ });
1156
+ ```
1157
+
1158
+ For a nonblocking start, wait explicitly before browser actions or CDP access:
1159
+
1160
+ ```typescript
1161
+ const abortController = new AbortController();
1162
+ const session = client.Session({ auth_ids: ["<connection-id>"] });
1163
+ await session.start({ wait_for_authentication: false });
1164
+ try {
1165
+ await session.waitForAuth({ timeoutMs: 615_000, signal: abortController.signal });
1166
+ await session.execute({ type: "goto", url: "https://example.com" });
1167
+ } finally {
1168
+ await session.stop();
1169
+ }
1170
+ ```
1171
+
1172
+ Waiting polls session readiness through short requests. It never restarts the
1173
+ session or submits another login. `auth_retry` controls server-side login retries
1174
+ (0 to 2) and is omitted from requests without `auth_ids`. A failed or cancelled
1175
+ startup wait closes the created session. A standalone `waitForAuth()` leaves
1176
+ cleanup to the caller.
1177
+
1178
+ Connection operations are available through `client.managedAuth`:
1179
+
1180
+ ```typescript
1181
+ const check = await client.managedAuth.checkConnection("<connection-id>");
1182
+ // checkConnection only verifies; refreshConnection can log in if needed.
1183
+ const operation = await client.managedAuth.refreshConnection("<connection-id>", {
1184
+ wait: false,
1185
+ auth_retry: 2,
1186
+ });
1187
+ const completed = await client.managedAuth.waitForAuth(operation);
1188
+ // To explicitly request a new login:
1189
+ await client.managedAuth.reauthenticateConnection("<connection-id>");
1190
+ ```
1191
+
1192
+ Refresh and reauthentication wait by default. Use `getOperation(operation.id)`
1193
+ for individual status reads. Cancelling an operation wait does not cancel the
1194
+ server operation. An HTTP 409 remains an ownership conflict and is not retried.
1195
+ These helpers require a backend with the managed-auth lifecycle endpoints.