notte-sdk 0.0.21 → 1.9.0

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/dist/index.mjs CHANGED
@@ -1,28 +1,38 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
7
-
8
1
  // src/version.ts
9
- var SDK_VERSION = true ? "0.0.21" : "dev";
2
+ var SDK_VERSION = true ? "1.9.0" : "dev";
10
3
  var SDK_PACKAGE_NAME = true ? "notte-sdk" : "notte-sdk";
11
4
 
12
5
  // src/version-check.ts
13
6
  var DISABLE_ENV = "NOTTE_SDK_DISABLE_VERSION_CHECK";
14
7
  var VERSION_CHECK_TIMEOUT_MS = 1500;
15
- var hasStartedVersionCheck = false;
8
+ var versionCheckPerformed = false;
9
+ var cachedLatestVersion;
16
10
  function startVersionCheck() {
17
- if (hasStartedVersionCheck || !shouldCheckVersion()) {
11
+ if (versionCheckPerformed || !shouldCheckVersion()) {
18
12
  return;
19
13
  }
20
- hasStartedVersionCheck = true;
14
+ versionCheckPerformed = true;
21
15
  const timer = setTimeout(() => {
22
16
  void checkForLatestVersion().catch(() => void 0);
23
17
  }, 0);
24
18
  timer.unref?.();
25
19
  }
20
+ function isVersionOlder(current, latest) {
21
+ return isVersionGreater(latest, current);
22
+ }
23
+ function upgradeSuggestion(currentVersion = SDK_VERSION) {
24
+ if (!cachedLatestVersion || !isPublishedVersion(currentVersion)) {
25
+ return void 0;
26
+ }
27
+ return isVersionOlder(currentVersion, cachedLatestVersion) ? cachedLatestVersion : void 0;
28
+ }
29
+ function createUpgradeErrorMessage(errorContext, latestVersion, originalError, currentVersion = SDK_VERSION, packageName = SDK_PACKAGE_NAME) {
30
+ let message = `${errorContext}. This might be due to API schema changes. Current SDK version: ${currentVersion}, Latest available: ${latestVersion}. Either you made a mistake in the request arguments, or you should upgrade to the latest ${packageName} version by running: 'npm install ${packageName}@${latestVersion}'`;
31
+ if (originalError) {
32
+ message += `. Original error: ${originalError}`;
33
+ }
34
+ return message;
35
+ }
26
36
  async function checkForLatestVersion(options = {}) {
27
37
  const currentVersion = options.currentVersion ?? SDK_VERSION;
28
38
  const packageName = options.packageName ?? SDK_PACKAGE_NAME;
@@ -33,11 +43,15 @@ async function checkForLatestVersion(options = {}) {
33
43
  return;
34
44
  }
35
45
  const latestVersion = await fetchLatestVersion(packageName, fetchFn);
36
- if (!latestVersion || !isVersionGreater(latestVersion, currentVersion)) {
46
+ if (!latestVersion) {
47
+ return;
48
+ }
49
+ cachedLatestVersion = latestVersion;
50
+ if (!isVersionGreater(latestVersion, currentVersion)) {
37
51
  return;
38
52
  }
39
53
  warnFn(
40
- `[${packageName}] A newer version is available: ${currentVersion} -> ${latestVersion}. Update with: npm install ${packageName}@latest`
54
+ `\u26A0\uFE0F You are using ${packageName} version ${currentVersion}, but version ${latestVersion} is available on npm. Run 'npm install ${packageName}@${latestVersion}' to avoid any interruptions.`
41
55
  );
42
56
  }
43
57
  function shouldCheckVersion(currentVersion = SDK_VERSION, fetchFn = globalThis.fetch, env = getEnv()) {
@@ -53,7 +67,8 @@ async function fetchLatestVersion(packageName, fetchFn) {
53
67
  try {
54
68
  const response = await fetchFn(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, {
55
69
  headers: {
56
- accept: "application/json"
70
+ accept: "application/json",
71
+ "user-agent": `${packageName}/${SDK_VERSION} (https://github.com/nottelabs/notte)`
57
72
  },
58
73
  signal: controller?.signal
59
74
  });
@@ -157,7 +172,7 @@ function createSseClient({
157
172
  ...options
158
173
  }) {
159
174
  let lastEventId;
160
- const sleep = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
175
+ const sleep2 = sseSleepFn ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
161
176
  const createStream = async function* () {
162
177
  let retryDelay = sseDefaultRetryDelay ?? 3e3;
163
178
  let attempt = 0;
@@ -261,7 +276,7 @@ function createSseClient({
261
276
  break;
262
277
  }
263
278
  const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4);
264
- await sleep(backoff);
279
+ await sleep2(backoff);
265
280
  }
266
281
  }
267
282
  };
@@ -658,9 +673,7 @@ var mergeHeaders = (...headers) => {
658
673
  return mergedHeaders;
659
674
  };
660
675
  var Interceptors = class {
661
- constructor() {
662
- this.fns = [];
663
- }
676
+ fns = [];
664
677
  clear() {
665
678
  this.fns = [];
666
679
  }
@@ -916,7 +929,7 @@ var createClient = (config = {}) => {
916
929
  };
917
930
 
918
931
  // src/lib/client/client.gen.ts
919
- var client = createClient(createConfig({ baseUrl: "https://us-staging.notte.cc" }));
932
+ var client = createClient(createConfig({ baseUrl: "https://api.notte.cc" }));
920
933
 
921
934
  // src/lib/client/sdk.gen.ts
922
935
  var listSessions = (options) => (options?.client ?? client).get({
@@ -1014,6 +1027,31 @@ var sessionReplay = (options) => (options.client ?? client).get({
1014
1027
  url: "/sessions/{session_id}/replay",
1015
1028
  ...options
1016
1029
  });
1030
+ var listSessionFiles = (options) => (options.client ?? client).get({
1031
+ security: [{ scheme: "bearer", type: "http" }],
1032
+ url: "/sessions/{session_id}/files",
1033
+ ...options
1034
+ });
1035
+ var uploadSessionFile = (options) => (options.client ?? client).post({
1036
+ ...formDataBodySerializer,
1037
+ security: [{ scheme: "bearer", type: "http" }],
1038
+ url: "/sessions/{session_id}/files",
1039
+ ...options,
1040
+ headers: {
1041
+ "Content-Type": null,
1042
+ ...options.headers
1043
+ }
1044
+ });
1045
+ var deleteSessionFile = (options) => (options.client ?? client).delete({
1046
+ security: [{ scheme: "bearer", type: "http" }],
1047
+ url: "/sessions/{session_id}/files/{file_id}",
1048
+ ...options
1049
+ });
1050
+ var downloadSessionFile = (options) => (options.client ?? client).get({
1051
+ security: [{ scheme: "bearer", type: "http" }],
1052
+ url: "/sessions/{session_id}/files/{file_id}",
1053
+ ...options
1054
+ });
1017
1055
  var agentStart = (options) => (options.client ?? client).post({
1018
1056
  security: [{ scheme: "bearer", type: "http" }],
1019
1057
  url: "/agents/start",
@@ -1418,46 +1456,6 @@ var getUsageLogs = (options) => (options?.client ?? client).get({
1418
1456
  url: "/usage/logs",
1419
1457
  ...options
1420
1458
  });
1421
- var fileDownload = (options) => (options.client ?? client).get({
1422
- security: [{ scheme: "bearer", type: "http" }],
1423
- url: "/storage/{session_id}/downloads/{filename}",
1424
- ...options
1425
- });
1426
- var fileUploadDownloadedFile = (options) => (options.client ?? client).post({
1427
- ...formDataBodySerializer,
1428
- security: [{ scheme: "bearer", type: "http" }],
1429
- url: "/storage/{session_id}/downloads/{filename}",
1430
- ...options,
1431
- headers: {
1432
- "Content-Type": null,
1433
- ...options.headers
1434
- }
1435
- });
1436
- var fileListDownloads = (options) => (options.client ?? client).get({
1437
- security: [{ scheme: "bearer", type: "http" }],
1438
- url: "/storage/{session_id}/downloads",
1439
- ...options
1440
- });
1441
- var fileDownloadUploadedFile = (options) => (options.client ?? client).get({
1442
- security: [{ scheme: "bearer", type: "http" }],
1443
- url: "/storage/uploads/{filename}",
1444
- ...options
1445
- });
1446
- var fileUpload = (options) => (options.client ?? client).post({
1447
- ...formDataBodySerializer,
1448
- security: [{ scheme: "bearer", type: "http" }],
1449
- url: "/storage/uploads/{filename}",
1450
- ...options,
1451
- headers: {
1452
- "Content-Type": null,
1453
- ...options.headers
1454
- }
1455
- });
1456
- var fileListUploads = (options) => (options?.client ?? client).get({
1457
- security: [{ scheme: "bearer", type: "http" }],
1458
- url: "/storage/uploads",
1459
- ...options
1460
- });
1461
1459
  var anythingStart = (options) => (options.client ?? client).post({
1462
1460
  security: [{ scheme: "bearer", type: "http" }],
1463
1461
  url: "/anything/start",
@@ -1521,6 +1519,582 @@ var scrapeFromHtml = (options) => (options.client ?? client).post({
1521
1519
  }
1522
1520
  });
1523
1521
 
1522
+ // src/session.ts
1523
+ import { existsSync } from "fs";
1524
+ import { writeFile as writeFile2 } from "fs/promises";
1525
+ import { resolve as resolve2 } from "path";
1526
+
1527
+ // src/errors.ts
1528
+ var NotteError = class extends Error {
1529
+ constructor(message, options) {
1530
+ super(message, options);
1531
+ this.name = new.target.name;
1532
+ }
1533
+ };
1534
+ function describeBody(error) {
1535
+ try {
1536
+ return JSON.stringify(error);
1537
+ } catch {
1538
+ return String(error);
1539
+ }
1540
+ }
1541
+ var NotteAPIError = class _NotteAPIError extends NotteError {
1542
+ path;
1543
+ statusCode;
1544
+ error;
1545
+ response;
1546
+ constructor(path, statusCode, error, response) {
1547
+ super(`Request to \`${path}\` failed with status code ${statusCode}: ${describeBody(error)}`);
1548
+ this.path = path;
1549
+ this.statusCode = statusCode;
1550
+ this.error = error;
1551
+ this.response = response;
1552
+ }
1553
+ /** The API's own error message, when it sent one. */
1554
+ get apiMessage() {
1555
+ const message = this.error.message ?? this.error.detail;
1556
+ return typeof message === "string" ? message : void 0;
1557
+ }
1558
+ static fromBody(path, statusCode, body, response) {
1559
+ return new _NotteAPIError(path, statusCode, normalizeErrorBody(body), response);
1560
+ }
1561
+ };
1562
+ var NotteAPIExecutionError = class extends NotteAPIError {
1563
+ constructor(path, statusCode, error, response) {
1564
+ super(path, statusCode, error, response);
1565
+ this.message = `Error on ${path}: ${describeBody(error)}`;
1566
+ }
1567
+ };
1568
+ var AuthenticationError = class extends NotteError {
1569
+ constructor(message) {
1570
+ super(`Authentication failed. ${message}`);
1571
+ }
1572
+ };
1573
+ var InvalidRequestError = class extends NotteError {
1574
+ constructor(message) {
1575
+ super(`Invalid request. ${message}`);
1576
+ }
1577
+ };
1578
+ var NotteTimeoutError = class extends NotteError {
1579
+ };
1580
+ var FailedToRunCloudFunctionError = class extends NotteError {
1581
+ functionId;
1582
+ functionRunId;
1583
+ response;
1584
+ constructor(functionId, functionRunId, response) {
1585
+ super(
1586
+ `Function ${functionId} run ${functionRunId} failed: ${typeof response === "string" ? response : describeBody(response ?? {})}`
1587
+ );
1588
+ this.functionId = functionId;
1589
+ this.functionRunId = functionRunId;
1590
+ this.response = response;
1591
+ }
1592
+ };
1593
+ var ScrapeFailedError = class extends NotteError {
1594
+ structuredError;
1595
+ constructor(error) {
1596
+ super(`Scrape failed: ${error ?? "unknown error"}`);
1597
+ this.structuredError = error;
1598
+ }
1599
+ };
1600
+ var ActionExecutionError = class _ActionExecutionError extends NotteError {
1601
+ errorType;
1602
+ devMessage;
1603
+ userMessage;
1604
+ agentMessage;
1605
+ shouldRetryLater;
1606
+ shouldNotifyTeam;
1607
+ constructor(detail) {
1608
+ super(detail.dev_message || detail.user_message || detail.error_type);
1609
+ this.errorType = detail.error_type;
1610
+ this.devMessage = detail.dev_message;
1611
+ this.userMessage = detail.user_message;
1612
+ this.agentMessage = detail.agent_message;
1613
+ this.shouldRetryLater = detail.should_retry_later ?? false;
1614
+ this.shouldNotifyTeam = detail.should_notify_team ?? false;
1615
+ }
1616
+ static fromMessage(message, errorType = "NotteBaseError") {
1617
+ return new _ActionExecutionError({
1618
+ error_type: errorType,
1619
+ dev_message: message,
1620
+ user_message: message,
1621
+ agent_message: message
1622
+ });
1623
+ }
1624
+ };
1625
+ var EvaluateJsNoDataError = class extends NotteError {
1626
+ constructor() {
1627
+ super("evaluate_js returned no data. Upgrade the Notte API to a build that returns the evaluated value.");
1628
+ }
1629
+ };
1630
+ var FetchResponseDecodeError = class extends NotteError {
1631
+ constructor(reason) {
1632
+ super(`Failed to decode the response returned by the page fetch: ${reason}`);
1633
+ }
1634
+ };
1635
+ function normalizeErrorBody(body) {
1636
+ if (body && typeof body === "object" && !Array.isArray(body)) {
1637
+ return body;
1638
+ }
1639
+ if (body === void 0 || body === null || body === "") {
1640
+ return {};
1641
+ }
1642
+ return { message: typeof body === "string" ? body : String(body) };
1643
+ }
1644
+ var DEFAULT_RETRY_MESSAGE = "An error occurred while executing the function. Try again later...";
1645
+ function retry(fn, options) {
1646
+ const {
1647
+ maxTries,
1648
+ delayMs = 5e3,
1649
+ errorMessage = DEFAULT_RETRY_MESSAGE,
1650
+ shouldRetry = () => true,
1651
+ onRetry = (error, attempt, total) => {
1652
+ console.warn(`Failed to execute ${fn.name || "function"}: ${String(error)} (attempt ${attempt}/${total})`);
1653
+ }
1654
+ } = options;
1655
+ if (!Number.isInteger(maxTries) || maxTries < 1) {
1656
+ throw new InvalidRequestError("maxTries must be a positive integer");
1657
+ }
1658
+ return async (...args) => {
1659
+ let lastError;
1660
+ for (let attempt = 1; attempt <= maxTries; attempt += 1) {
1661
+ try {
1662
+ return await fn(...args);
1663
+ } catch (error) {
1664
+ lastError = error;
1665
+ if (attempt === maxTries || !shouldRetry(error, attempt)) {
1666
+ break;
1667
+ }
1668
+ onRetry(error, attempt, maxTries);
1669
+ await sleep(delayMs);
1670
+ }
1671
+ }
1672
+ throw new Error(errorMessage, { cause: lastError });
1673
+ };
1674
+ }
1675
+ function sleep(ms) {
1676
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1677
+ }
1678
+
1679
+ // src/actions.ts
1680
+ function builder(type) {
1681
+ return (...[input]) => ({ ...input ?? {}, type });
1682
+ }
1683
+ var goto = builder("goto");
1684
+ var gotoNewTab = builder("goto_new_tab");
1685
+ var closeTab = builder("close_tab");
1686
+ var switchTab = builder("switch_tab");
1687
+ var goBack = builder("go_back");
1688
+ var goForward = builder("go_forward");
1689
+ var reload = builder("reload");
1690
+ var wait = builder("wait");
1691
+ var pressKey = builder("press_key");
1692
+ var scrollUp = builder("scroll_up");
1693
+ var scrollDown = builder("scroll_down");
1694
+ var captchaSolve = builder("captcha_solve");
1695
+ var help = builder("help");
1696
+ var completion = builder("completion");
1697
+ var scrape = builder("scrape");
1698
+ var emailRead = builder("email_read");
1699
+ var emailVerificationRead = builder("email_verification_read");
1700
+ var smsRead = builder("sms_read");
1701
+ var evaluateJs = builder("evaluate_js");
1702
+ var formFill = builder("form_fill");
1703
+ var click = builder("click");
1704
+ var fill = builder("fill");
1705
+ var multiFactorFill = builder("multi_factor_fill");
1706
+ var fallbackFill = builder("fallback_fill");
1707
+ var check = builder("check");
1708
+ var selectDropdownOption = builder("select_dropdown_option");
1709
+ var uploadFile = builder("upload_file");
1710
+ var downloadFile = builder("download_file");
1711
+ var actions = {
1712
+ goto,
1713
+ gotoNewTab,
1714
+ closeTab,
1715
+ switchTab,
1716
+ goBack,
1717
+ goForward,
1718
+ reload,
1719
+ wait,
1720
+ pressKey,
1721
+ scrollUp,
1722
+ scrollDown,
1723
+ captchaSolve,
1724
+ help,
1725
+ completion,
1726
+ scrape,
1727
+ emailRead,
1728
+ emailVerificationRead,
1729
+ smsRead,
1730
+ evaluateJs,
1731
+ formFill,
1732
+ click,
1733
+ fill,
1734
+ multiFactorFill,
1735
+ fallbackFill,
1736
+ check,
1737
+ selectDropdownOption,
1738
+ uploadFile,
1739
+ downloadFile
1740
+ };
1741
+ function isCaptchaSolveAction(action) {
1742
+ return action.type === "captcha_solve";
1743
+ }
1744
+
1745
+ // src/page-fetch.ts
1746
+ var CONTENT_TYPE = "content-type";
1747
+ function hasHeader(headers, name) {
1748
+ return Object.keys(headers).some((key) => key.toLowerCase() === name);
1749
+ }
1750
+ function urlencode(mapping) {
1751
+ const search = new URLSearchParams();
1752
+ for (const [key, value] of Object.entries(mapping)) {
1753
+ if (Array.isArray(value)) {
1754
+ for (const item of value) search.append(key, String(item));
1755
+ } else {
1756
+ search.append(key, String(value));
1757
+ }
1758
+ }
1759
+ return search.toString();
1760
+ }
1761
+ function withParams(url, params) {
1762
+ const hashIndex = url.indexOf("#");
1763
+ const fragment = hashIndex >= 0 ? url.slice(hashIndex) : "";
1764
+ const base = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
1765
+ const queryIndex = base.indexOf("?");
1766
+ const path = queryIndex >= 0 ? base.slice(0, queryIndex) : base;
1767
+ const existing = queryIndex >= 0 ? base.slice(queryIndex + 1) : "";
1768
+ let query = urlencode(params);
1769
+ if (existing) {
1770
+ query = `${existing}&${query}`;
1771
+ }
1772
+ return `${path}?${query}${fragment}`;
1773
+ }
1774
+ function buildFetchScript(url, options = {}) {
1775
+ const { method = "GET", headers, params, json, data, timeoutMs, allowInsecure = false } = options;
1776
+ const redirect = options.redirect ?? (allowInsecure ? "follow" : "manual");
1777
+ if (json !== void 0 && data !== void 0) {
1778
+ throw new InvalidRequestError("pass either json or data, not both");
1779
+ }
1780
+ if (timeoutMs !== void 0 && !(timeoutMs > 0)) {
1781
+ throw new InvalidRequestError("timeout must be positive");
1782
+ }
1783
+ let requestUrl = url;
1784
+ if (params && Object.keys(params).length > 0) {
1785
+ requestUrl = withParams(url, params);
1786
+ }
1787
+ const requestHeaders = { ...headers ?? {} };
1788
+ let body;
1789
+ if (json !== void 0) {
1790
+ body = JSON.stringify(json);
1791
+ if (!hasHeader(requestHeaders, CONTENT_TYPE)) {
1792
+ requestHeaders["Content-Type"] = "application/json";
1793
+ }
1794
+ } else if (data !== void 0 && typeof data !== "string") {
1795
+ body = urlencode(data);
1796
+ if (!hasHeader(requestHeaders, CONTENT_TYPE)) {
1797
+ requestHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1798
+ }
1799
+ } else if (data !== void 0) {
1800
+ body = data;
1801
+ }
1802
+ const init = {
1803
+ method: method.toUpperCase(),
1804
+ headers: requestHeaders,
1805
+ credentials: "include",
1806
+ redirect
1807
+ };
1808
+ if (body !== void 0) {
1809
+ if (init.method === "GET" || init.method === "HEAD") {
1810
+ throw new InvalidRequestError(`${String(init.method)} requests cannot have a body`);
1811
+ }
1812
+ init.body = body;
1813
+ }
1814
+ let abort = "";
1815
+ if (timeoutMs !== void 0) {
1816
+ abort = `const controller = new AbortController();setTimeout(() => controller.abort(), ${Math.trunc(timeoutMs)});init.signal = controller.signal;`;
1817
+ }
1818
+ return `(async () => {const init = ${JSON.stringify(init)};${abort}const target = new URL(${JSON.stringify(requestUrl)}, location.href);const allowInsecure = ${allowInsecure ? "true" : "false"};if (target.protocol !== 'https:' && !allowInsecure) { throw new Error('Refusing to fetch ' + target.origin + ' over plaintext; pass allowInsecure: true to override');}const response = await fetch(target.toString(), init);if (response.type === 'opaqueredirect') { throw new Error('The request to ' + target.origin + ' was redirected; redirects are not followed because the page cookies could be sent in cleartext. Fetch the final URL directly or pass allowInsecure: true');}if (!allowInsecure && new URL(response.url).protocol !== 'https:') { throw new Error('Refusing to read a response served over plaintext from ' + new URL(response.url).origin + '; pass allowInsecure: true to override');}const bytes = new Uint8Array(await response.arrayBuffer());let binary = '';for (let i = 0; i < bytes.length; i += 0x8000) { binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));}const headers = {};response.headers.forEach((value, key) => { headers[key] = value; });return JSON.stringify({status: response.status, url: response.url, headers: headers, body_b64: btoa(binary)});})()`;
1819
+ }
1820
+ var REASON_PHRASES = {
1821
+ 100: "Continue",
1822
+ 101: "Switching Protocols",
1823
+ 102: "Processing",
1824
+ 103: "Early Hints",
1825
+ 200: "OK",
1826
+ 201: "Created",
1827
+ 202: "Accepted",
1828
+ 203: "Non-Authoritative Information",
1829
+ 204: "No Content",
1830
+ 205: "Reset Content",
1831
+ 206: "Partial Content",
1832
+ 207: "Multi-Status",
1833
+ 208: "Already Reported",
1834
+ 226: "IM Used",
1835
+ 300: "Multiple Choices",
1836
+ 301: "Moved Permanently",
1837
+ 302: "Found",
1838
+ 303: "See Other",
1839
+ 304: "Not Modified",
1840
+ 305: "Use Proxy",
1841
+ 307: "Temporary Redirect",
1842
+ 308: "Permanent Redirect",
1843
+ 400: "Bad Request",
1844
+ 401: "Unauthorized",
1845
+ 402: "Payment Required",
1846
+ 403: "Forbidden",
1847
+ 404: "Not Found",
1848
+ 405: "Method Not Allowed",
1849
+ 406: "Not Acceptable",
1850
+ 407: "Proxy Authentication Required",
1851
+ 408: "Request Timeout",
1852
+ 409: "Conflict",
1853
+ 410: "Gone",
1854
+ 411: "Length Required",
1855
+ 412: "Precondition Failed",
1856
+ 413: "Request Entity Too Large",
1857
+ 414: "Request-URI Too Long",
1858
+ 415: "Unsupported Media Type",
1859
+ 416: "Requested Range Not Satisfiable",
1860
+ 417: "Expectation Failed",
1861
+ 418: "I'm a Teapot",
1862
+ 421: "Misdirected Request",
1863
+ 422: "Unprocessable Entity",
1864
+ 423: "Locked",
1865
+ 424: "Failed Dependency",
1866
+ 425: "Too Early",
1867
+ 426: "Upgrade Required",
1868
+ 428: "Precondition Required",
1869
+ 429: "Too Many Requests",
1870
+ 431: "Request Header Fields Too Large",
1871
+ 451: "Unavailable For Legal Reasons",
1872
+ 500: "Internal Server Error",
1873
+ 501: "Not Implemented",
1874
+ 502: "Bad Gateway",
1875
+ 503: "Service Unavailable",
1876
+ 504: "Gateway Timeout",
1877
+ 505: "HTTP Version Not Supported",
1878
+ 506: "Variant Also Negotiates",
1879
+ 507: "Insufficient Storage",
1880
+ 508: "Loop Detected",
1881
+ 510: "Not Extended",
1882
+ 511: "Network Authentication Required"
1883
+ };
1884
+ var PageFetchHTTPError = class extends NotteError {
1885
+ response;
1886
+ constructor(response) {
1887
+ const kind = response.status < 500 ? "Client Error" : "Server Error";
1888
+ super(`${response.status} ${kind}: ${response.statusText} for url: ${response.url}`);
1889
+ this.response = response;
1890
+ }
1891
+ };
1892
+ function encodingFor(headers, content) {
1893
+ const contentType = Object.entries(headers).find(([key]) => key.toLowerCase() === CONTENT_TYPE)?.[1] ?? "";
1894
+ for (const param of contentType.split(";").slice(1)) {
1895
+ const [name, ...rest] = param.split("=");
1896
+ const value = rest.join("=");
1897
+ if (name?.trim().toLowerCase() === "charset" && value) {
1898
+ return value.trim().replace(/^["']|["']$/g, "");
1899
+ }
1900
+ }
1901
+ try {
1902
+ new TextDecoder("utf-8", { fatal: true }).decode(content);
1903
+ } catch {
1904
+ return void 0;
1905
+ }
1906
+ return "utf-8";
1907
+ }
1908
+ var PageFetchResponse = class {
1909
+ status;
1910
+ statusText;
1911
+ headers;
1912
+ /** The final URL after redirects. */
1913
+ url;
1914
+ /** The declared charset, `utf-8` when the body is valid utf-8, else `undefined`. */
1915
+ encoding;
1916
+ content;
1917
+ constructor(init) {
1918
+ this.status = init.status;
1919
+ this.statusText = REASON_PHRASES[init.status] ?? "";
1920
+ this.headers = new Headers(init.headers);
1921
+ this.url = init.url;
1922
+ this.content = init.content;
1923
+ this.encoding = encodingFor(init.headers, init.content);
1924
+ }
1925
+ get ok() {
1926
+ return this.status >= 200 && this.status < 300;
1927
+ }
1928
+ /** The raw body bytes. */
1929
+ bytes() {
1930
+ return this.content;
1931
+ }
1932
+ async arrayBuffer() {
1933
+ return this.content.slice().buffer;
1934
+ }
1935
+ async text() {
1936
+ let decoder;
1937
+ try {
1938
+ decoder = new TextDecoder(this.encoding ?? "utf-8");
1939
+ } catch {
1940
+ decoder = new TextDecoder("utf-8");
1941
+ }
1942
+ return decoder.decode(this.content);
1943
+ }
1944
+ async json() {
1945
+ return JSON.parse(await this.text());
1946
+ }
1947
+ /** Throw `PageFetchHTTPError` on a 4xx/5xx status, like `requests.Response.raise_for_status()`. */
1948
+ raiseForStatus() {
1949
+ if (this.status >= 400) {
1950
+ throw new PageFetchHTTPError(this);
1951
+ }
1952
+ return this;
1953
+ }
1954
+ };
1955
+ var BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/;
1956
+ function decodeBase64(value) {
1957
+ if (!BASE64_PATTERN.test(value) || value.length % 4 !== 0) {
1958
+ throw new Error("body_b64 is not valid base64");
1959
+ }
1960
+ return new Uint8Array(Buffer.from(value, "base64"));
1961
+ }
1962
+ function responseFromEvaluated(raw) {
1963
+ let payload;
1964
+ try {
1965
+ payload = JSON.parse(raw);
1966
+ } catch (error) {
1967
+ throw new FetchResponseDecodeError(error instanceof Error ? error.message : String(error));
1968
+ }
1969
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
1970
+ throw new FetchResponseDecodeError("envelope is not an object");
1971
+ }
1972
+ const envelope = payload;
1973
+ try {
1974
+ if (!("status" in envelope)) {
1975
+ throw new Error("'status'");
1976
+ }
1977
+ const status = Number(envelope.status);
1978
+ if (!Number.isInteger(status)) {
1979
+ throw new Error(`invalid status: ${String(envelope.status)}`);
1980
+ }
1981
+ const rawHeaders = envelope.headers ?? {};
1982
+ if (typeof rawHeaders !== "object" || Array.isArray(rawHeaders)) {
1983
+ throw new Error("headers is not an object");
1984
+ }
1985
+ const headers = {};
1986
+ for (const [key, value] of Object.entries(rawHeaders)) {
1987
+ headers[String(key)] = String(value);
1988
+ }
1989
+ const content = decodeBase64(String(envelope.body_b64 ?? ""));
1990
+ const url = String(envelope.url ?? "");
1991
+ return new PageFetchResponse({ status, headers, url, content });
1992
+ } catch (error) {
1993
+ throw new FetchResponseDecodeError(error instanceof Error ? error.message : String(error));
1994
+ }
1995
+ }
1996
+
1997
+ // src/cookies.ts
1998
+ import { readFile, writeFile } from "fs/promises";
1999
+ import { resolve } from "path";
2000
+ async function readCookiesFile(cookieFile) {
2001
+ const cookies = JSON.parse(await readFile(cookieFile, "utf-8"));
2002
+ if (!Array.isArray(cookies)) {
2003
+ throw new InvalidRequestError(`Cookie file ${cookieFile} must contain a JSON list of cookies`);
2004
+ }
2005
+ return cookies;
2006
+ }
2007
+ async function createOrAppendCookiesToFile(cookieFile, cookies) {
2008
+ console.info(`\u{1F36A} Automatically saving cookies to ${cookieFile}`);
2009
+ await withFileLock(cookieFile, async () => {
2010
+ let existing = [];
2011
+ try {
2012
+ existing = await readCookiesFile(cookieFile);
2013
+ } catch (error) {
2014
+ if (!isNotFound(error)) {
2015
+ throw error;
2016
+ }
2017
+ }
2018
+ existing.push(...cookies);
2019
+ await writeFile(cookieFile, JSON.stringify(existing), "utf-8");
2020
+ });
2021
+ }
2022
+ var fileLocks = /* @__PURE__ */ new Map();
2023
+ async function withFileLock(path, fn) {
2024
+ const key = resolve(path);
2025
+ const previous = fileLocks.get(key) ?? Promise.resolve();
2026
+ let release;
2027
+ const current = new Promise((done) => {
2028
+ release = done;
2029
+ });
2030
+ const tail = previous.then(() => current);
2031
+ fileLocks.set(key, tail);
2032
+ await previous;
2033
+ try {
2034
+ return await fn();
2035
+ } finally {
2036
+ release();
2037
+ if (fileLocks.get(key) === tail) {
2038
+ fileLocks.delete(key);
2039
+ }
2040
+ }
2041
+ }
2042
+ function isNotFound(error) {
2043
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
2044
+ }
2045
+
2046
+ // src/scrape.ts
2047
+ function isZodSchema(value) {
2048
+ return typeof value === "object" && value !== null && typeof value.parse === "function";
2049
+ }
2050
+ function requiresSchema(options) {
2051
+ return Boolean(options.response_format || options.instructions);
2052
+ }
2053
+ async function buildScrapeBody(options) {
2054
+ const { raiseOnFailure: _raise, json_schema, response_format, ...rest } = options;
2055
+ const body = { ...rest };
2056
+ if (isZodSchema(response_format)) {
2057
+ if (json_schema) {
2058
+ body.response_format = json_schema;
2059
+ } else {
2060
+ const { z: z2 } = await import("zod");
2061
+ body.response_format = z2.toJSONSchema(response_format);
2062
+ }
2063
+ } else if (response_format) {
2064
+ body.response_format = response_format;
2065
+ }
2066
+ return body;
2067
+ }
2068
+ function processScrapeResponse(scrapeResponse, options) {
2069
+ if (!scrapeResponse) {
2070
+ throw new ScrapeFailedError("empty response");
2071
+ }
2072
+ const raiseOnFailure = options.raiseOnFailure ?? true;
2073
+ if (options.only_images) {
2074
+ return scrapeResponse.images ?? [];
2075
+ }
2076
+ if (requiresSchema(options)) {
2077
+ const structured = scrapeResponse.structured;
2078
+ if (!structured) {
2079
+ throw new ScrapeFailedError("the API returned no structured data. This should not happen, please report it.");
2080
+ }
2081
+ const success = structured.success ?? false;
2082
+ const schema = isZodSchema(options.response_format) ? options.response_format : void 0;
2083
+ if (!success) {
2084
+ if (raiseOnFailure) {
2085
+ throw new ScrapeFailedError(structured.error ?? void 0);
2086
+ }
2087
+ return { success, error: structured.error ?? null, data: null };
2088
+ }
2089
+ const data = schema && structured.data != null ? schema.parse(structured.data) : structured.data;
2090
+ if (raiseOnFailure) {
2091
+ return data;
2092
+ }
2093
+ return { success, error: structured.error ?? null, data };
2094
+ }
2095
+ return scrapeResponse.markdown;
2096
+ }
2097
+
1524
2098
  // src/utils.ts
1525
2099
  import { spawn } from "child_process";
1526
2100
  function formatError(error) {
@@ -1563,55 +2137,64 @@ function openBrowser(url) {
1563
2137
  }
1564
2138
 
1565
2139
  // src/session.ts
1566
- import { z } from "zod";
1567
- function processScrapeResponse(scrapeResponse, options) {
1568
- if (options.only_images && scrapeResponse.images) {
1569
- return scrapeResponse.images;
1570
- }
1571
- if (requiresSchema(options)) {
1572
- const structured = scrapeResponse.structured;
1573
- if (!structured) {
1574
- throw new Error("Failed to scrape structured data. This should not happen. Please report this issue.");
1575
- }
1576
- if (options.response_format && typeof options.response_format.parse === "function") {
1577
- if (structured.data) {
1578
- try {
1579
- const validatedData = options.response_format.parse(structured.data);
1580
- return validatedData;
1581
- } catch (error) {
1582
- throw new Error(`Schema validation failed: ${error instanceof Error ? error.message : String(error)}`);
1583
- }
1584
- }
1585
- return structured;
1586
- }
1587
- return structured;
1588
- }
1589
- if (options.response_format && typeof options.response_format.parse === "function") {
1590
- try {
1591
- const validatedData = options.response_format.parse(scrapeResponse);
1592
- return validatedData;
1593
- } catch (error) {
1594
- throw new Error(`Schema validation failed: ${error instanceof Error ? error.message : String(error)}`);
1595
- }
1596
- }
1597
- return scrapeResponse.markdown;
2140
+ var SESSION_START_TRIES = 3;
2141
+ var CLUSTER_OVERLOAD_RETRY_DELAY_MS = 3e4;
2142
+ var CAPTCHA_SOLVE_TIMEOUT_MS = 1e5;
2143
+ var CAPTCHA_SOLVE_TRIES = 3;
2144
+ function observeServerOwnedDialog(_dialog) {
1598
2145
  }
1599
- function requiresSchema(options) {
1600
- return !!(options.response_format || options.instructions);
2146
+ function installServerOwnedDialogPolicy(browser) {
2147
+ for (const context of browser.contexts()) {
2148
+ context.on("dialog", observeServerOwnedDialog);
2149
+ }
1601
2150
  }
1602
2151
  var Session = class {
2152
+ client;
2153
+ options;
2154
+ openViewer;
2155
+ legacyHeadless;
2156
+ defaultRaiseOnFailure;
2157
+ defaultPerceptionType;
2158
+ cookieFile;
2159
+ fileStorage;
2160
+ sessionId = null;
2161
+ isActive = false;
2162
+ response = null;
2163
+ playwrightBrowser = null;
2164
+ playwrightPage = null;
1603
2165
  constructor(client2, options = {}) {
1604
- this.sessionId = null;
1605
- this.isActive = false;
1606
- this.response = null;
1607
2166
  this.client = client2;
1608
- const { open_viewer = false, headless, ...sessionOptions } = options;
1609
- this.options = sessionOptions;
2167
+ const {
2168
+ open_viewer = false,
2169
+ headless,
2170
+ raiseOnFailure = true,
2171
+ perception_type = "fast",
2172
+ cookie_file,
2173
+ storage,
2174
+ ...sessionOptions
2175
+ } = options;
2176
+ this.options = storage ? { ...sessionOptions, use_file_storage: true } : sessionOptions;
1610
2177
  this.openViewer = open_viewer;
1611
2178
  this.legacyHeadless = headless;
1612
- }
2179
+ this.defaultRaiseOnFailure = raiseOnFailure;
2180
+ this.defaultPerceptionType = perception_type;
2181
+ this.cookieFile = cookie_file;
2182
+ this.fileStorage = storage;
2183
+ }
2184
+ // #######################################################################
2185
+ // ############################# Session #################################
2186
+ // #######################################################################
1613
2187
  /**
1614
- * Start the session - creates a new session on the server
2188
+ * Start the session: creates a new session on the server.
2189
+ *
2190
+ * Retries up to `SESSION_START_TRIES` times on 5xx errors, waiting
2191
+ * `CLUSTER_OVERLOAD_RETRY_DELAY_MS` on HTTP 529 (cluster overload). 4xx
2192
+ * errors are never retried.
2193
+ *
2194
+ * Prefer `session.use(async session => { ... })`, which also stops the session.
2195
+ * @example
2196
+ * const session = client.Session();
2197
+ * await session.start();
1615
2198
  */
1616
2199
  async start() {
1617
2200
  if (this.legacyHeadless === false) {
@@ -1626,93 +2209,171 @@ var Session = class {
1626
2209
  if (this.isActive) {
1627
2210
  throw new Error("Session is already active");
1628
2211
  }
1629
- let shouldOpenViewer = false;
2212
+ const body = this.options;
2213
+ let tries = SESSION_START_TRIES;
2214
+ const origTries = tries;
2215
+ let sessionData;
2216
+ while (tries > 0) {
2217
+ tries -= 1;
2218
+ try {
2219
+ const response = await sessionStart({ client: this.client.getClient(), body, throwOnError: true });
2220
+ sessionData = response.data;
2221
+ break;
2222
+ } catch (error) {
2223
+ if (tries === 0 || !(error instanceof NotteAPIError) || error.statusCode < 500) {
2224
+ throw error;
2225
+ }
2226
+ const retryStr = `${origTries - tries}/${origTries - 1}`;
2227
+ if (error.statusCode === 529) {
2228
+ console.warn(
2229
+ `Failed to start session due to cluster overload, retrying in ${CLUSTER_OVERLOAD_RETRY_DELAY_MS / 1e3} seconds (${retryStr})...`
2230
+ );
2231
+ await sleep(CLUSTER_OVERLOAD_RETRY_DELAY_MS);
2232
+ } else {
2233
+ console.warn(`Failed to start session: retrying (${retryStr})`);
2234
+ }
2235
+ }
2236
+ }
2237
+ if (!sessionData) {
2238
+ throw new Error("Failed to start session: empty response");
2239
+ }
2240
+ this.sessionId = sessionData.session_id;
2241
+ this.isActive = true;
2242
+ this.response = sessionData;
2243
+ this.fileStorage = this.fileStorage?.forSession(sessionData.session_id);
1630
2244
  try {
1631
- const response = await sessionStart({
1632
- client: this.client.getClient(),
1633
- body: this.options
1634
- });
1635
- if (response?.error) {
1636
- throw new Error(`Failed to create session: ${formatError(response.error)}`);
2245
+ if (this.openViewer) {
2246
+ this.openViewerInBrowser();
2247
+ }
2248
+ if (this.cookieFile !== void 0) {
2249
+ if (existsSync(this.cookieFile)) {
2250
+ console.info(`\u{1F36A} Automatically loading cookies from ${this.cookieFile}`);
2251
+ await this.setCookiesFromFile(this.cookieFile);
2252
+ } else {
2253
+ console.warn(`\u{1F36A} Cookie file ${this.cookieFile} not found, skipping cookie loading`);
2254
+ }
1637
2255
  }
1638
- const sessionData = response.data;
1639
- this.sessionId = sessionData.session_id;
1640
- this.isActive = true;
1641
- this.response = sessionData;
1642
- shouldOpenViewer = this.openViewer;
1643
2256
  } catch (error) {
1644
- throw new Error(`Failed to start session: ${error instanceof Error ? error.message : String(error)}`);
1645
- }
1646
- if (shouldOpenViewer) {
1647
- this.openViewerInBrowser();
2257
+ try {
2258
+ await this.stop("error");
2259
+ } catch (stopError) {
2260
+ console.error(`[Session] Failed to stop ${sessionData.session_id} after a start failure: ${String(stopError)}`);
2261
+ }
2262
+ throw error;
1648
2263
  }
1649
2264
  }
1650
2265
  /**
1651
- * Stop the session - terminates the session on the server
2266
+ * Stop the session and clean up resources (including any Playwright
2267
+ * connection opened with `page()`). When a `cookie_file` was given, the
2268
+ * session cookies are appended to it first. A session the API reports as
2269
+ * already stopped is tolerated with a warning.
2270
+ *
2271
+ * @example
2272
+ * await session.stop();
1652
2273
  */
1653
2274
  async stop(closeReason = "manual") {
1654
2275
  if (!this.isActive || !this.sessionId) {
1655
2276
  return;
1656
2277
  }
1657
- const response = await sessionStop({
1658
- client: this.client.getClient(),
1659
- path: {
1660
- session_id: this.sessionId
1661
- },
1662
- query: {
1663
- close_reason: closeReason
2278
+ const sessionId = this.sessionId;
2279
+ await this.closePlaywright();
2280
+ if (this.cookieFile !== void 0) {
2281
+ try {
2282
+ const cookies = await this.getCookies();
2283
+ await createOrAppendCookiesToFile(this.cookieFile, cookies);
2284
+ } catch (error) {
2285
+ console.error(`\u{1F36A} Error saving cookies to ${this.cookieFile}: ${String(error)}`);
2286
+ }
2287
+ }
2288
+ try {
2289
+ const response = await sessionStop({
2290
+ client: this.client.getClient(),
2291
+ path: { session_id: sessionId },
2292
+ query: { close_reason: closeReason },
2293
+ throwOnError: true
2294
+ });
2295
+ if (response.data.status !== "closed") {
2296
+ throw new Error(`[Session] ${sessionId} failed to stop`);
2297
+ }
2298
+ this.response = response.data;
2299
+ } catch (error) {
2300
+ const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
2301
+ if (message.includes("already stopped") || message.includes("already closed")) {
2302
+ console.warn(`Session ${sessionId} was already stopped`);
2303
+ } else {
2304
+ throw error;
1664
2305
  }
1665
- });
1666
- if (response?.error) {
1667
- throw new Error(`Failed to stop session: ${formatError(response.error)}`);
1668
2306
  }
1669
- this.response = response.data;
1670
2307
  this.isActive = false;
1671
2308
  this.sessionId = null;
1672
2309
  }
2310
+ async closePlaywright() {
2311
+ const browser = this.playwrightBrowser;
2312
+ this.playwrightBrowser = null;
2313
+ this.playwrightPage = null;
2314
+ if (browser) {
2315
+ try {
2316
+ await browser.close();
2317
+ } catch (error) {
2318
+ console.warn(`[Session] Failed to close the Playwright connection: ${String(error)}`);
2319
+ }
2320
+ }
2321
+ }
1673
2322
  /**
1674
- * Get session status
2323
+ * Get the current status of the session. Like Python, this keeps working
2324
+ * after `stop()` (for example to read the recorded `steps`).
2325
+ *
2326
+ * @returns Current session metadata, including lifecycle status and recorded steps. Works after `stop()`.
2327
+ * @throws Error if this session has never been started.
2328
+ * @throws NotteAPIError if the status request fails.
2329
+ * @example
2330
+ * const status = await session.status();
2331
+ * console.log(status.status); // 'active'
1675
2332
  */
1676
2333
  async status() {
1677
- if (!this.sessionId) {
1678
- throw new Error("Session not started");
1679
- }
1680
2334
  const response = await sessionStatus({
1681
2335
  client: this.client.getClient(),
1682
- path: {
1683
- session_id: this.sessionId
1684
- }
2336
+ path: { session_id: this.lastSessionId() },
2337
+ throwOnError: true
1685
2338
  });
1686
- if (response?.error) {
1687
- throw new Error(`Failed to get session status: ${formatError(response.error)}`);
1688
- }
1689
2339
  return response.data;
1690
2340
  }
1691
- /**
1692
- * Get session ID
1693
- */
2341
+ /** Session ID while the session is active, `null` otherwise. */
1694
2342
  getId() {
1695
2343
  return this.sessionId;
1696
2344
  }
1697
- /**
1698
- * Check if session is active
1699
- */
2345
+ /** Whether the session has been started and not stopped yet. */
1700
2346
  isSessionActive() {
1701
2347
  return this.isActive;
1702
2348
  }
1703
- /**
1704
- * Get the last response from session operations
1705
- */
2349
+ /** The last `SessionResponse` received from the API (start, stop or status). */
1706
2350
  getResponse() {
1707
2351
  return this.response;
1708
2352
  }
2353
+ /** The file storage attached to the session, when one was given. */
2354
+ get storage() {
2355
+ return this.fileStorage;
2356
+ }
2357
+ requireSessionId() {
2358
+ if (!this.sessionId) {
2359
+ throw new Error("Session not started");
2360
+ }
2361
+ return this.sessionId;
2362
+ }
2363
+ /** Session ID of the active session, or of the last one when it was stopped. */
2364
+ lastSessionId() {
2365
+ const sessionId = this.sessionId ?? this.response?.session_id;
2366
+ if (!sessionId) {
2367
+ throw new Error("Session not started");
2368
+ }
2369
+ return sessionId;
2370
+ }
1709
2371
  /**
1710
2372
  * Open the live viewer for this session in the local default browser.
2373
+ * @throws If the session has not started or no viewer URL is available.
1711
2374
  */
1712
2375
  viewer() {
1713
- if (!this.sessionId) {
1714
- throw new Error("Session not started");
1715
- }
2376
+ this.requireSessionId();
1716
2377
  const viewerUrl = this.response?.viewer_url;
1717
2378
  if (!viewerUrl) {
1718
2379
  throw new Error("No viewer URL available for this session");
@@ -1728,154 +2389,381 @@ var Session = class {
1728
2389
  openBrowser(viewerUrl);
1729
2390
  }
1730
2391
  /**
1731
- * Set cookies for the session
2392
+ * Get presigned URLs for the session replay. Recordings are finalized after
2393
+ * the session is closed, so by default this polls on 404 until the replay is
2394
+ * ready. It keeps working after `stop()`.
2395
+ *
2396
+ * Throws an `Error` when the API reports the session is still active and a
2397
+ * `NotteTimeoutError` when the replay is not ready within `timeoutMs`.
2398
+ * @example
2399
+ * await session.stop();
2400
+ * const replay = await session.replay();
2401
+ * console.log(replay.mp4_url);
1732
2402
  */
1733
- async setCookies(cookies) {
1734
- if (!this.sessionId) {
1735
- throw new Error("Session not started");
2403
+ async replay(options = {}) {
2404
+ const { wait: wait2 = true, timeoutMs = 24e4, pollIntervalMs = 5e3 } = options;
2405
+ const sessionId = this.lastSessionId();
2406
+ const request = async () => {
2407
+ const response = await sessionReplay({
2408
+ client: this.client.getClient(),
2409
+ path: { session_id: sessionId },
2410
+ throwOnError: true
2411
+ });
2412
+ if (!response.data) {
2413
+ throw new Error("Failed to get session replay: empty response");
2414
+ }
2415
+ return response.data;
2416
+ };
2417
+ if (!wait2) {
2418
+ return request();
2419
+ }
2420
+ const deadline = Date.now() + timeoutMs;
2421
+ for (; ; ) {
2422
+ try {
2423
+ return await request();
2424
+ } catch (error) {
2425
+ if (!(error instanceof NotteAPIError) || error.statusCode !== 404) {
2426
+ throw error;
2427
+ }
2428
+ if (this.isActive && (error.apiMessage ?? "").includes("still active")) {
2429
+ throw new Error(`Session ${sessionId} is still active \u2014 close the session first to generate the replay.`, {
2430
+ cause: error
2431
+ });
2432
+ }
2433
+ if (Date.now() + pollIntervalMs > deadline) {
2434
+ throw new NotteTimeoutError(`Replay for session ${sessionId} not ready within ${timeoutMs}ms`, { cause: error });
2435
+ }
2436
+ await sleep(pollIntervalMs);
2437
+ }
2438
+ }
2439
+ }
2440
+ /**
2441
+ * Download the MP4 replay to a local file, like `ReplayResponse.download()`
2442
+ * in Python. Resolves with the absolute path of the written file and throws
2443
+ * when the replay has no `mp4_url`.
2444
+ *
2445
+ * @example
2446
+ * await session.stop();
2447
+ * const file = await session.downloadReplay('session.mp4');
2448
+ */
2449
+ async downloadReplay(path = "replay.mp4", options = {}) {
2450
+ const replay = await this.replay(options);
2451
+ if (!replay.mp4_url) {
2452
+ throw new Error("No mp4_url available for download");
2453
+ }
2454
+ const response = await fetch(replay.mp4_url);
2455
+ if (!response.ok) {
2456
+ throw new Error(`Failed to download replay: HTTP ${response.status}`);
1736
2457
  }
2458
+ const target = resolve2(path);
2459
+ await writeFile2(target, new Uint8Array(await response.arrayBuffer()));
2460
+ return target;
2461
+ }
2462
+ // #######################################################################
2463
+ // ############################# Cookies #################################
2464
+ // #######################################################################
2465
+ /**
2466
+ * Upload cookies to the session.
2467
+ *
2468
+ * @example
2469
+ * await session.setCookies([{ name: 'token', value: 'abc', domain: 'example.com', path: '/', httpOnly: false }]);
2470
+ */
2471
+ async setCookies(cookies) {
1737
2472
  const response = await sessionCookiesSet({
1738
2473
  client: this.client.getClient(),
1739
- path: {
1740
- session_id: this.sessionId
1741
- },
1742
- body: {
1743
- cookies
1744
- }
2474
+ path: { session_id: this.requireSessionId() },
2475
+ body: { cookies },
2476
+ throwOnError: true
1745
2477
  });
1746
- if (response?.error) {
1747
- throw new Error(`Failed to set cookies: ${formatError(response.error)}`);
1748
- }
1749
2478
  return response.data;
1750
2479
  }
1751
2480
  /**
1752
- * Set cookies from a file
2481
+ * Upload cookies from a JSON file (a list of cookies, like `getCookies()` returns).
2482
+ *
2483
+ * @example
2484
+ * await session.setCookiesFromFile('cookies.json');
1753
2485
  */
1754
2486
  async setCookiesFromFile(cookieFile) {
1755
- const fs = await import("fs/promises");
1756
- const cookieData = await fs.readFile(cookieFile, "utf-8");
1757
- const cookies = JSON.parse(cookieData);
1758
- return this.setCookies(cookies);
2487
+ return this.setCookies(await readCookiesFile(cookieFile));
1759
2488
  }
1760
2489
  /**
1761
- * Get cookies from the session
2490
+ * Get the cookies of the session.
2491
+ *
2492
+ * @example
2493
+ * const cookies = await session.getCookies();
2494
+ * await fs.writeFile('cookies.json', JSON.stringify(cookies));
1762
2495
  */
1763
2496
  async getCookies() {
1764
- if (!this.sessionId) {
1765
- throw new Error("Session not started");
1766
- }
1767
2497
  const response = await sessionCookiesGet({
1768
2498
  client: this.client.getClient(),
1769
- path: {
1770
- session_id: this.sessionId
1771
- }
2499
+ path: { session_id: this.requireSessionId() },
2500
+ throwOnError: true
1772
2501
  });
1773
- if (response?.error) {
1774
- throw new Error(`Failed to get cookies: ${formatError(response.error)}`);
1775
- }
1776
2502
  return response.data.cookies;
1777
2503
  }
2504
+ // #######################################################################
2505
+ // ############################# Debug / CDP #############################
2506
+ // #######################################################################
2507
+ /** Detailed debug information for the session (debug URL, websocket URLs, tabs). */
2508
+ async debugInfo() {
2509
+ const response = await sessionDebugInfo({
2510
+ client: this.client.getClient(),
2511
+ path: { session_id: this.requireSessionId() },
2512
+ throwOnError: true
2513
+ });
2514
+ return response.data;
2515
+ }
1778
2516
  /**
1779
- * Execute an action on the page
2517
+ * Debug information for one tab of the session. The API client has no
2518
+ * dedicated tab endpoint, so this reads the tab from `debugInfo().tabs`.
1780
2519
  */
1781
- async execute(action, raiseOnFailure = true) {
1782
- if (!this.sessionId) {
2520
+ async debugTabInfo(tabIdx = 0) {
2521
+ const debug = await this.debugInfo();
2522
+ const tab = debug.tabs[tabIdx];
2523
+ if (!tab) {
2524
+ throw new Error(`Tab ${tabIdx} not found: the session has ${debug.tabs.length} tab(s)`);
2525
+ }
2526
+ return tab;
2527
+ }
2528
+ /**
2529
+ * Get the Chrome DevTools Protocol WebSocket URL for the session.
2530
+ *
2531
+ * A `cdp_url` given at construction (another browser provider) is returned
2532
+ * untouched. Notte-served URLs, from the start response or the debug info,
2533
+ * carry the database preview branch when one is configured.
2534
+ *
2535
+ * @returns The CDP WebSocket URL for connecting a browser automation client.
2536
+ * @throws Error if no session ID or CDP URL is available.
2537
+ * @example
2538
+ * const browser = await chromium.connectOverCDP(await session.cdpUrl());
2539
+ */
2540
+ async cdpUrl() {
2541
+ if (!this.response) {
1783
2542
  throw new Error("Session not started");
1784
2543
  }
1785
- const response = await pageExecute({
1786
- client: this.client.getClient(),
1787
- path: {
1788
- session_id: this.sessionId
1789
- },
1790
- body: action
1791
- });
1792
- if (response?.error) {
1793
- throw new Error(`Failed to execute action: ${formatError(response.error)}`);
2544
+ if (this.options.cdp_url) {
2545
+ return this.options.cdp_url;
1794
2546
  }
1795
- const result = response.data;
1796
- if (raiseOnFailure && !result.success) {
1797
- throw new Error(`Action execution failed: ${result.message}`);
2547
+ if (this.response.cdp_url) {
2548
+ return this.client.withDbPreview(this.response.cdp_url);
1798
2549
  }
1799
- return result;
2550
+ const debug = await this.debugInfo();
2551
+ return this.client.withDbPreview(debug.ws.cdp);
1800
2552
  }
1801
2553
  /**
1802
- * Observe the current page state
2554
+ * Get a Playwright page connected to the session via CDP. The connection is
2555
+ * established lazily on first access, cached, and closed by `stop()`.
2556
+ * Native dialogs are left to the Notte backend.
2557
+ *
2558
+ * Requires the optional `playwright-core` peer dependency
2559
+ * (`npm install playwright-core`).
2560
+ *
2561
+ * @example
2562
+ * const page = await session.page();
2563
+ * await page.goto('https://www.google.com');
2564
+ * await page.screenshot({ path: 'screenshot.png' });
1803
2565
  */
1804
- async observe(perceptionType = "fast") {
1805
- if (!this.sessionId) {
1806
- throw new Error("Session not started");
2566
+ async page() {
2567
+ if (this.playwrightPage) {
2568
+ return this.playwrightPage;
1807
2569
  }
1808
- const response = await pageObserve({
1809
- client: this.client.getClient(),
1810
- path: {
1811
- session_id: this.sessionId
1812
- },
1813
- body: {
1814
- perception_type: perceptionType
2570
+ let playwright;
2571
+ try {
2572
+ playwright = await import("playwright-core");
2573
+ } catch (error) {
2574
+ throw new Error("Playwright not installed. Run `npm install playwright-core` to use `session.page()`.", {
2575
+ cause: error
2576
+ });
2577
+ }
2578
+ try {
2579
+ if (!this.playwrightBrowser) {
2580
+ const cdpUrl = await this.cdpUrl();
2581
+ this.playwrightBrowser = await playwright.chromium.connectOverCDP(cdpUrl);
2582
+ installServerOwnedDialogPolicy(this.playwrightBrowser);
1815
2583
  }
1816
- });
1817
- if (response?.error) {
1818
- throw new Error(`Failed to observe page: ${formatError(response.error)}`);
2584
+ const page = this.playwrightBrowser.contexts()[0]?.pages()[0];
2585
+ if (!page) {
2586
+ throw new Error("the browser has no open page");
2587
+ }
2588
+ this.playwrightPage = page;
2589
+ return page;
2590
+ } catch (error) {
2591
+ throw new Error(`Failed to access the playwright page from CDP: ${error instanceof Error ? error.message : String(error)}`, {
2592
+ cause: error
2593
+ });
1819
2594
  }
1820
- return response.data;
1821
2595
  }
1822
2596
  /**
1823
- * Get session replay data
2597
+ * Get the workflow code generated from the session steps
2598
+ * (`GET /sessions/{id}/workflow/code`).
2599
+ *
2600
+ * @example
2601
+ * const { python_script } = await session.getScript();
1824
2602
  */
1825
- async replay() {
1826
- if (!this.sessionId) {
1827
- throw new Error("Session not started");
1828
- }
1829
- const response = await sessionReplay({
2603
+ async getScript(options = {}) {
2604
+ const { as_workflow = true, infer_response_format } = options;
2605
+ const response = await getSessionScript({
1830
2606
  client: this.client.getClient(),
1831
- path: {
1832
- session_id: this.sessionId
1833
- }
2607
+ path: { session_id: this.lastSessionId() },
2608
+ query: { as_workflow, ...infer_response_format === void 0 ? {} : { infer_response_format } },
2609
+ throwOnError: true
1834
2610
  });
1835
- if (response?.error) {
1836
- throw new Error(`Failed to get session replay: ${formatError(response.error)}`);
1837
- }
1838
2611
  return response.data;
1839
2612
  }
1840
- async scrape(options = {}) {
1841
- if (!this.sessionId) {
1842
- throw new Error("Session not started");
1843
- }
1844
- const apiOptions = { ...options };
1845
- if (options.response_format && typeof options.response_format.parse === "function") {
1846
- if (options.json_schema) {
1847
- apiOptions.response_format = options.json_schema;
1848
- } else {
1849
- apiOptions.response_format = z.toJSONSchema(options.response_format);
1850
- }
1851
- if (!apiOptions.instructions) {
1852
- const schemaStr = JSON.stringify(apiOptions.response_format);
1853
- if (schemaStr.includes("plans") || schemaStr.includes("plan")) {
1854
- apiOptions.instructions = "Extract pricing plans from the page";
1855
- } else if (schemaStr.includes("products") || schemaStr.includes("product")) {
1856
- apiOptions.instructions = "Extract product information from the page";
1857
- } else if (schemaStr.includes("articles") || schemaStr.includes("article")) {
1858
- apiOptions.instructions = "Extract article information from the page";
1859
- } else {
1860
- apiOptions.instructions = "Extract structured data from the page based on the provided schema";
2613
+ // #######################################################################
2614
+ // ############################# Page ####################################
2615
+ // #######################################################################
2616
+ /**
2617
+ * Execute an action on the current page.
2618
+ *
2619
+ * When the action fails and `raiseOnFailure` is true (the default, see the
2620
+ * session option), the structured `exception_detail` returned by the API is
2621
+ * rehydrated into an `ActionExecutionError` whose `errorType` names the
2622
+ * server-side error class. `captcha_solve` actions get a 100 s request
2623
+ * timeout and are retried up to 3 times on HTTP 408.
2624
+ *
2625
+ * @param action - Action type, target, and action-specific arguments.
2626
+ * @param options - Override failure handling for this call; otherwise use the session setting.
2627
+ * @returns The execution response, including `success` and failure details when throwing is disabled.
2628
+ * @throws ActionExecutionError if the action fails and failure throwing is enabled.
2629
+ * @throws NotteAPIError if the API request fails.
2630
+ * @example
2631
+ * import { actions } from 'notte-sdk';
2632
+ *
2633
+ * await session.execute({ type: 'goto', url: 'https://www.notte.cc' });
2634
+ * await session.execute(actions.fill({ selector: "input[name='email']", value: 'user@example.com' }));
2635
+ * const result = await session.execute({ type: 'click', id: 'B1' }, { raiseOnFailure: false });
2636
+ * if (!result.success) console.log(result.message);
2637
+ */
2638
+ async execute(action, options = {}) {
2639
+ const sessionId = this.requireSessionId();
2640
+ const raiseOnFailure = typeof options === "boolean" ? options : options.raiseOnFailure ?? this.defaultRaiseOnFailure;
2641
+ const isCaptcha = isCaptchaSolveAction(action);
2642
+ let result;
2643
+ let lastError;
2644
+ for (let attempt = 0; attempt < CAPTCHA_SOLVE_TRIES; attempt += 1) {
2645
+ try {
2646
+ const response = await pageExecute({
2647
+ client: this.client.getClient(),
2648
+ path: { session_id: sessionId },
2649
+ body: action,
2650
+ headers: isCaptcha ? { [TIMEOUT_HEADER]: String(CAPTCHA_SOLVE_TIMEOUT_MS) } : void 0,
2651
+ throwOnError: true
2652
+ });
2653
+ result = response.data;
2654
+ break;
2655
+ } catch (error) {
2656
+ if (isCaptcha && error instanceof NotteAPIError && error.statusCode === 408) {
2657
+ lastError = error;
2658
+ console.warn("Solve captcha action timed out. This can happen for long and complex captchas. Retrying...");
2659
+ continue;
1861
2660
  }
2661
+ throw error;
2662
+ }
2663
+ }
2664
+ if (!result) {
2665
+ throw lastError ?? new Error(`Failed to execute action '${action.type}'. This should not happen. Please report this issue.`);
2666
+ }
2667
+ if (raiseOnFailure && !result.success) {
2668
+ console.error(`\u{1F6A8} Execution failed with message: '${result.message}'`);
2669
+ if (result.exception_detail) {
2670
+ throw new ActionExecutionError(result.exception_detail);
1862
2671
  }
2672
+ const fallbackMessage = String(result.message ?? "").trim() || `Failed to execute action: ${action.type}`;
2673
+ throw ActionExecutionError.fromMessage(fallbackMessage);
2674
+ }
2675
+ return result;
2676
+ }
2677
+ async evaluateJs(code, options = {}) {
2678
+ const raiseOnFailure = options.raiseOnFailure ?? true;
2679
+ const result = await this.execute({ type: "evaluate_js", code }, { raiseOnFailure });
2680
+ if (!raiseOnFailure) {
2681
+ return result;
2682
+ }
2683
+ if (!result.data) {
2684
+ throw new EvaluateJsNoDataError();
2685
+ }
2686
+ return result.data.markdown;
2687
+ }
2688
+ /**
2689
+ * Issue an HTTP request from the page the session is on and return the response.
2690
+ *
2691
+ * The request runs inside the browser through `fetch()`, so it carries the
2692
+ * page's cookies, the session's proxy and the browser's own network
2693
+ * fingerprint. A relative `url` resolves against the current page, which
2694
+ * also makes it same-origin; a cross-origin URL is subject to CORS exactly
2695
+ * as in a browser tab, so `goto` the target origin first. Redirects are
2696
+ * followed and the final URL is on `response.url`. A non-2xx status is
2697
+ * returned, not thrown; `response.raiseForStatus()` throws `PageFetchHTTPError`.
2698
+ *
2699
+ * `json` is serialised as the body with an `application/json` content type,
2700
+ * `data` as a form body when it is an object or verbatim when it is a string.
2701
+ *
2702
+ * @param url - Absolute URL or a path relative to the current page.
2703
+ * @param options - HTTP method, headers, and request body settings.
2704
+ * @returns A response with status, headers, and body readers. Non-2xx responses are not automatically thrown.
2705
+ * @throws ActionExecutionError if browser-side execution fails.
2706
+ * @example
2707
+ * await session.execute({ type: 'goto', url: 'https://en.wikipedia.org/wiki/Main_Page' });
2708
+ * const summary = await (await session.fetch('/api/rest_v1/page/summary/Main_Page')).json();
2709
+ */
2710
+ async fetch(url, options = {}) {
2711
+ const script = buildFetchScript(url, options);
2712
+ return responseFromEvaluated(await this.evaluateJs(script));
2713
+ }
2714
+ /**
2715
+ * Observe the current page: the list of actions that can be taken, a
2716
+ * screenshot and page metadata.
2717
+ *
2718
+ * The `perception_type` defaults to the session option (`fast`). A bare
2719
+ * string argument is accepted as the perception type.
2720
+ *
2721
+ * @param options - Observation settings, including an optional URL, or a perception-mode string.
2722
+ * @returns Structured page analysis, available actions, and page metadata.
2723
+ * @throws NotteAPIError if observation fails.
2724
+ * @example
2725
+ * const obs = await session.observe();
2726
+ * console.log(obs.space.description);
2727
+ * const deep = await session.observe({ perception_type: 'deep', max_nb_actions: 50 });
2728
+ */
2729
+ async observe(options = {}) {
2730
+ const body = typeof options === "string" ? { perception_type: options } : { ...options };
2731
+ if (body.perception_type == null) {
2732
+ body.perception_type = this.defaultPerceptionType;
1863
2733
  }
2734
+ const response = await pageObserve({
2735
+ client: this.client.getClient(),
2736
+ path: { session_id: this.requireSessionId() },
2737
+ body,
2738
+ throwOnError: true
2739
+ });
2740
+ return response.data;
2741
+ }
2742
+ async scrape(options = {}) {
2743
+ const sessionId = this.requireSessionId();
2744
+ const body = await buildScrapeBody(options);
1864
2745
  const response = await pageScrape({
1865
2746
  client: this.client.getClient(),
1866
- path: {
1867
- session_id: this.sessionId
1868
- },
1869
- body: apiOptions
2747
+ path: { session_id: sessionId },
2748
+ body,
2749
+ throwOnError: true
1870
2750
  });
1871
- if (response?.error) {
1872
- throw new Error(`Failed to scrape page: ${JSON.stringify(response.error)}`);
1873
- }
1874
2751
  return processScrapeResponse(response.data, options);
1875
2752
  }
2753
+ // #######################################################################
2754
+ // ############################# Lifecycle ###############################
2755
+ // #######################################################################
1876
2756
  /**
1877
- * Context manager pattern - automatically start and stop session
1878
- * Usage: await session.use(async (session) => { ... })
2757
+ * Context manager pattern: start the session, run the callback and stop the
2758
+ * session, with close reason `error` when the callback throws.
2759
+ *
2760
+ * @param callback - Async work to perform with the started session.
2761
+ * @returns The callback's return value after session cleanup has been attempted.
2762
+ * @throws Error if startup fails, the callback throws, or cleanup fails after a successful callback.
2763
+ * @example
2764
+ * await client.Session().use(async session => {
2765
+ * await session.execute({ type: 'goto', url: 'https://www.notte.cc' });
2766
+ * });
1879
2767
  */
1880
2768
  async use(callback) {
1881
2769
  await this.start();
@@ -1884,6 +2772,7 @@ var Session = class {
1884
2772
  return await callback(this);
1885
2773
  } catch (error) {
1886
2774
  callbackFailed = true;
2775
+ console.warn(`Session exiting because of exception: ${String(error)}`);
1887
2776
  throw error;
1888
2777
  } finally {
1889
2778
  try {
@@ -1897,8 +2786,13 @@ var Session = class {
1897
2786
  }
1898
2787
  }
1899
2788
  /**
1900
- * Async iterator pattern for session lifecycle management
1901
- * Usage: for await (const session of sessionInstance) { ... }
2789
+ * Async iterator pattern for session lifecycle management.
2790
+ *
2791
+ * ```ts
2792
+ * for await (const session of client.Session()) {
2793
+ * await session.execute({ type: 'goto', url: 'https://www.notte.cc' });
2794
+ * }
2795
+ * ```
1902
2796
  */
1903
2797
  async *[Symbol.asyncIterator]() {
1904
2798
  await this.start();
@@ -1912,12 +2806,12 @@ var Session = class {
1912
2806
 
1913
2807
  // src/agent.ts
1914
2808
  import WebSocket from "ws";
1915
- import { z as z2 } from "zod";
2809
+ import { z } from "zod";
1916
2810
  function prepareAgentRequest(data) {
1917
2811
  const apiData = { ...data };
1918
2812
  const rf = data.response_format;
1919
2813
  if (rf && typeof rf.parse === "function" && typeof rf.safeParse === "function") {
1920
- apiData.response_format = z2.toJSONSchema(rf);
2814
+ apiData.response_format = z.toJSONSchema(rf);
1921
2815
  return { apiData, zodSchema: rf };
1922
2816
  }
1923
2817
  return { apiData, zodSchema: null };
@@ -2023,9 +2917,12 @@ var defaultStepLogger = (stepData, stepCounter, agentId) => {
2023
2917
  console.log("");
2024
2918
  };
2025
2919
  var Agent = class {
2920
+ client;
2921
+ request;
2922
+ response = null;
2923
+ websocket = null;
2924
+ existingAgent;
2026
2925
  constructor(client2, options) {
2027
- this.response = null;
2028
- this.websocket = null;
2029
2926
  this.client = client2;
2030
2927
  if ("agent_id" in options && options.agent_id) {
2031
2928
  if (options.session) {
@@ -2112,22 +3009,29 @@ var Agent = class {
2112
3009
  if (!this.response) {
2113
3010
  throw new Error("Agent not started");
2114
3011
  }
3012
+ const deadline = Date.now() + 3e5;
2115
3013
  try {
2116
- const result = await this.watchLogs(log, updateHandler);
2117
- if (result) {
3014
+ const result = await this.watchLogs(log, updateHandler, deadline);
3015
+ if (result && result.status !== "active") {
2118
3016
  return result;
2119
3017
  }
2120
- await new Promise((resolve) => setTimeout(resolve, 3e3));
2121
- return await this.status();
2122
3018
  } catch (error) {
2123
- console.error(`[Agent] ${this.agentId} failed to complete in time. Try running agent.status() after a few seconds.`);
2124
- return await this.status();
3019
+ console.warn(`[Agent] ${this.agentId} log stream unavailable; polling for completion.`);
3020
+ }
3021
+ while (Date.now() < deadline) {
3022
+ const result = await this.status(AbortSignal.timeout(Math.max(1, deadline - Date.now())));
3023
+ if (result.status !== "active") {
3024
+ updateHandler?.({ type: "completion", data: result, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
3025
+ return result;
3026
+ }
3027
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
2125
3028
  }
3029
+ throw new Error(`Agent ${this.agentId} did not complete before the polling timeout; retrieve status() later.`);
2126
3030
  }
2127
3031
  /**
2128
3032
  * Watch logs via WebSocket - mirrors Python watch_logs method
2129
3033
  */
2130
- async watchLogs(log = true, updateHandler = null) {
3034
+ async watchLogs(log = true, updateHandler = null, deadline = Date.now() + 3e5) {
2131
3035
  if (this.existingAgent) {
2132
3036
  throw new Error("You cannot call watchLogs() on an agent instantiated from agent id");
2133
3037
  }
@@ -2135,18 +3039,39 @@ var Agent = class {
2135
3039
  throw new Error("Agent not started");
2136
3040
  }
2137
3041
  const config = this.client.getConfig();
2138
- const token = config.apiKey;
3042
+ if (config.baseUrl?.startsWith("/")) return null;
2139
3043
  const agentId = this.response.agent_id;
2140
3044
  const sessionId = this.response.session_id;
2141
- const wsUrl = config.baseUrl?.replace("https://", "wss://").replace("http://", "ws://") + `/agents/${agentId}/debug/logs?token=${token}&session_id=${sessionId}`;
2142
- return new Promise((resolve, reject) => {
3045
+ const debug = await sessionDebugInfo({
3046
+ client: this.client.getClient(),
3047
+ path: { session_id: sessionId },
3048
+ throwOnError: true,
3049
+ signal: AbortSignal.timeout(Math.max(1, deadline - Date.now()))
3050
+ });
3051
+ const wsUrl = new URL(this.client.withDbPreview(debug.data.ws.logs));
3052
+ const token = wsUrl.searchParams.get("token");
3053
+ if (!token || token === config.apiKey || wsUrl.protocol !== "wss:") return null;
3054
+ wsUrl.pathname = `/agents/${encodeURIComponent(agentId)}/debug/logs`;
3055
+ wsUrl.searchParams.set("session_id", sessionId);
3056
+ return new Promise((resolve3, reject) => {
2143
3057
  this.websocket = new WebSocket(wsUrl);
3058
+ const socket = this.websocket;
3059
+ const timeout = setTimeout(() => finish(null), Math.max(0, deadline - Date.now()));
3060
+ let settled = false;
3061
+ const finish = (result) => {
3062
+ if (settled) return;
3063
+ settled = true;
3064
+ clearTimeout(timeout);
3065
+ if (this.websocket === socket) this.websocket = null;
3066
+ if (socket.readyState !== WebSocket.CLOSED) socket.terminate();
3067
+ resolve3(result);
3068
+ };
2144
3069
  let counter = 0;
2145
3070
  this.websocket.on("open", () => {
2146
3071
  });
2147
3072
  this.websocket.on("error", (error) => {
2148
3073
  console.error(`Connection error: ${agentId} ${error.message}`);
2149
- resolve(null);
3074
+ finish(null);
2150
3075
  });
2151
3076
  this.websocket.on("message", (data) => {
2152
3077
  try {
@@ -2168,8 +3093,7 @@ var Agent = class {
2168
3093
  } else if (!finalStatus.success) {
2169
3094
  console.error(finalStatus.answer);
2170
3095
  }
2171
- this.disconnectWebSocket();
2172
- resolve(finalStatus);
3096
+ finish(finalStatus);
2173
3097
  return;
2174
3098
  }
2175
3099
  if (log) {
@@ -2189,26 +3113,20 @@ var Agent = class {
2189
3113
  }
2190
3114
  });
2191
3115
  this.websocket.on("close", () => {
2192
- this.websocket = null;
2193
- resolve(null);
3116
+ finish(null);
2194
3117
  });
2195
- setTimeout(() => {
2196
- if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
2197
- this.disconnectWebSocket();
2198
- resolve(null);
2199
- }
2200
- }, 3e5);
2201
3118
  });
2202
3119
  }
2203
3120
  /**
2204
3121
  * Get agent status - mirrors Python status method
2205
3122
  */
2206
- async status() {
3123
+ async status(signal) {
2207
3124
  if (!this.response) {
2208
3125
  throw new Error("Agent not started or not accessible");
2209
3126
  }
2210
3127
  const response = await agentStatus({
2211
3128
  client: this.client.getClient(),
3129
+ signal,
2212
3130
  path: {
2213
3131
  agent_id: this.response.agent_id
2214
3132
  }
@@ -2277,24 +3195,126 @@ var Agent = class {
2277
3195
  return this.response.session_id;
2278
3196
  }
2279
3197
  /**
2280
- * Check if agent is running - convenience method
3198
+ * Whether `start()`/`run()` was called on this instance (or it was built from an agent id).
3199
+ * This is a local check and does not contact the API.
2281
3200
  */
2282
- isRunning() {
3201
+ hasStarted() {
2283
3202
  return this.response !== null;
2284
3203
  }
3204
+ /**
3205
+ * Whether the agent is still active on the server. Fetches the current status,
3206
+ * like calling `agent.status()` in Python; an agent that was never started is not active.
3207
+ */
3208
+ async isActive() {
3209
+ if (!this.response) {
3210
+ return false;
3211
+ }
3212
+ const status = await this.status();
3213
+ return status.status === "active";
3214
+ }
3215
+ /**
3216
+ * @deprecated Only reports whether this instance was started; it does not
3217
+ * contact the API. Use `hasStarted()` for that check or `isActive()` for the
3218
+ * live server status.
3219
+ */
3220
+ isRunning() {
3221
+ return this.hasStarted();
3222
+ }
2285
3223
  };
2286
3224
 
2287
3225
  // src/vaults.ts
3226
+ import { getDomain } from "tldts";
3227
+ var CREDENTIAL_FIELDS = ["email", "username", "mfa_secret", "password"];
3228
+ function extractHostname(url) {
3229
+ const trimmed = url.trim();
3230
+ if (trimmed.length === 0) {
3231
+ return "";
3232
+ }
3233
+ const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed);
3234
+ const candidates = hasScheme ? [trimmed] : [`https://${trimmed}`];
3235
+ for (const candidate of candidates) {
3236
+ try {
3237
+ const { hostname } = new URL(candidate);
3238
+ if (hostname.length > 0 && !hostname.startsWith(".")) {
3239
+ return hostname.toLowerCase();
3240
+ }
3241
+ } catch {
3242
+ }
3243
+ }
3244
+ return "";
3245
+ }
3246
+ function getRootDomain(url) {
3247
+ const hostname = extractHostname(url);
3248
+ if (hostname.length === 0) {
3249
+ return "";
3250
+ }
3251
+ if (hostname.startsWith("[") || /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
3252
+ return hostname;
3253
+ }
3254
+ const domain = getDomain(hostname, { allowPrivateDomains: false, detectIp: false });
3255
+ if (domain) {
3256
+ return domain.toLowerCase();
3257
+ }
3258
+ return hostname.toLowerCase();
3259
+ }
3260
+ function validateUrl(url) {
3261
+ const domain = getRootDomain(url);
3262
+ if (domain.length === 0) {
3263
+ throw new InvalidRequestError(`Invalid URL: ${url}. Please provide a valid URL with a domain name.`);
3264
+ }
3265
+ return domain;
3266
+ }
3267
+ function isValidMfaSecret(secret) {
3268
+ if (secret.length === 0) {
3269
+ return false;
3270
+ }
3271
+ let padded = secret.toUpperCase();
3272
+ if (padded.length % 8 !== 0) {
3273
+ padded += "=".repeat(8 - padded.length % 8);
3274
+ }
3275
+ let end = padded.length;
3276
+ while (end > 0 && padded[end - 1] === "=") {
3277
+ end -= 1;
3278
+ }
3279
+ const stripped = padded.slice(0, end);
3280
+ const padding = padded.length - stripped.length;
3281
+ return /^[A-Z2-7]*$/.test(stripped) && [0, 1, 3, 4, 6].includes(padding);
3282
+ }
3283
+ function validateMfaSecret(secret) {
3284
+ if (!isValidMfaSecret(secret)) {
3285
+ throw new InvalidRequestError("Invalid MFA secret code: did you try to store an OTP instead of a secret?");
3286
+ }
3287
+ }
3288
+ function validateCredentials(credentials) {
3289
+ const hasUsername = credentials.username !== void 0 && credentials.username !== null;
3290
+ const hasEmail = credentials.email !== void 0 && credentials.email !== null;
3291
+ if (hasUsername && hasEmail) {
3292
+ throw new InvalidRequestError("Can only set either username or email");
3293
+ }
3294
+ if (!hasUsername && !hasEmail) {
3295
+ throw new InvalidRequestError("Need to have either username or email set");
3296
+ }
3297
+ if (credentials.mfa_secret !== void 0 && credentials.mfa_secret !== null) {
3298
+ validateMfaSecret(credentials.mfa_secret);
3299
+ }
3300
+ return credentials;
3301
+ }
2288
3302
  var NotteVault = class {
3303
+ client;
3304
+ _vaultId = null;
3305
+ initPromise = null;
3306
+ createData = null;
2289
3307
  constructor(client2, options = {}) {
2290
- this._vaultId = null;
2291
- this.initPromise = null;
2292
- this.createData = null;
2293
3308
  this.client = client2;
2294
- if (options && "vault_id" in options && options.vault_id) {
3309
+ if (options && "vault_id" in options && options.vault_id !== void 0) {
3310
+ if (options.vault_id.length === 0) {
3311
+ throw new InvalidRequestError("Vault ID cannot be empty");
3312
+ }
2295
3313
  this.initPromise = this.initExistingVault(options.vault_id);
3314
+ this.initPromise.catch(() => void 0);
2296
3315
  } else {
2297
- this.createData = options || {};
3316
+ const { vault_id: _ignored, ...createData } = options ?? {};
3317
+ this.createData = createData;
2298
3318
  }
2299
3319
  }
2300
3320
  async initExistingVault(vaultId) {
@@ -2305,29 +3325,20 @@ var NotteVault = class {
2305
3325
  async initNewVault(createData) {
2306
3326
  const response = await vaultCreate({
2307
3327
  client: this.client.getClient(),
2308
- body: createData
3328
+ body: createData,
3329
+ throwOnError: true
2309
3330
  });
2310
- if (response?.error) {
2311
- throw new Error(`Failed to create vault: ${formatError(response.error)}`);
2312
- }
2313
3331
  const vault = response.data;
2314
3332
  console.warn(`[Vault] ${vault.vault_id} created since no vault id was provided. Please store this to retrieve it later.`);
2315
3333
  this._vaultId = vault.vault_id;
2316
3334
  return this._vaultId;
2317
3335
  }
2318
3336
  async verifyVaultExists(vaultId) {
2319
- if (vaultId.length === 0) {
2320
- throw new Error("Vault ID cannot be empty");
2321
- }
2322
- const response = await vaultCredentialsList({
3337
+ await vaultCredentialsList({
2323
3338
  client: this.client.getClient(),
2324
- path: {
2325
- vault_id: vaultId
2326
- }
3339
+ path: { vault_id: vaultId },
3340
+ throwOnError: true
2327
3341
  });
2328
- if (response?.error) {
2329
- throw new Error(`Failed to verify vault exists: ${formatError(response.error)}`);
2330
- }
2331
3342
  }
2332
3343
  async ensureInitialized() {
2333
3344
  if (this._vaultId) {
@@ -2342,257 +3353,215 @@ var NotteVault = class {
2342
3353
  this.initPromise = this.initNewVault(createData);
2343
3354
  return await this.initPromise;
2344
3355
  }
2345
- throw new Error("Vault not initialized");
3356
+ throw new InvalidRequestError("Vault not initialized");
2346
3357
  }
3358
+ /** ID of the remote vault. Throws until the first API operation has created or verified it. */
2347
3359
  get vaultId() {
2348
3360
  if (!this._vaultId) {
2349
- throw new Error("Vault not initialized. Use await on vault operations first.");
3361
+ throw new InvalidRequestError("Vault not initialized. Await a vault operation first.");
2350
3362
  }
2351
3363
  return this._vaultId;
2352
3364
  }
2353
3365
  /**
2354
- * Start the vault - no-op for compatibility with Python interface
3366
+ * Start the vault. No-op kept for parity with Python's `SyncResource` interface.
2355
3367
  */
2356
3368
  start() {
2357
3369
  }
2358
3370
  /**
2359
- * Stop the vault - deletes it and all credentials
3371
+ * Stop the vault: deletes it together with every credential it holds.
3372
+ *
3373
+ * ```ts
3374
+ * await vault.stop();
3375
+ * ```
2360
3376
  */
2361
3377
  async stop() {
2362
3378
  const vaultId = await this.ensureInitialized();
2363
- console.log(`[Vault] ${vaultId} deleted. All credentials have been deleted.`);
3379
+ console.info(`[Vault] ${vaultId} deleted. All credentials have been deleted.`);
2364
3380
  await this.delete();
2365
3381
  }
2366
3382
  /**
2367
- * Add or update credentials for a URL
3383
+ * Add or update the credentials stored for a website. The URL is reduced to
3384
+ * its root domain (`https://github.com/login` → `github.com`) before it is
3385
+ * sent, like Python's `AddCredentialsRequest`.
3386
+ *
3387
+ * ```ts
3388
+ * await vault.addCredentials('https://github.com/', {
3389
+ * email: 'me@example.org',
3390
+ * password: 'secret', // pragma: allowlist secret
3391
+ * mfa_secret: 'JBSWY3DPEHPK3PXP', // pragma: allowlist secret
3392
+ * });
3393
+ * ```
2368
3394
  */
2369
3395
  async addCredentials(url, credentials) {
2370
- if (credentials.mfa_secret) {
2371
- this.validateMfaSecret(credentials.mfa_secret);
2372
- }
3396
+ const domain = validateUrl(url);
3397
+ validateCredentials(credentials);
2373
3398
  const vaultId = await this.ensureInitialized();
2374
- const response = await vaultCredentialsAdd({
3399
+ await vaultCredentialsAdd({
2375
3400
  client: this.client.getClient(),
2376
- path: {
2377
- vault_id: vaultId
2378
- },
2379
- body: { url, credentials }
3401
+ path: { vault_id: vaultId },
3402
+ body: { url: domain, credentials },
3403
+ throwOnError: true
2380
3404
  });
2381
- if (response?.error) {
2382
- throw new Error(`Failed to add credentials: ${formatError(response.error)}`);
2383
- }
2384
3405
  }
2385
3406
  /**
2386
- * Get credentials for a URL
3407
+ * Get the credentials stored for a website. Rejects with a `NotteAPIError`
3408
+ * when the vault holds no credentials for that domain, like Python's
3409
+ * `get_credentials`.
3410
+ *
3411
+ * ```ts
3412
+ * const { email, password } = await vault.getCredentials('https://github.com/login');
3413
+ * ```
2387
3414
  */
2388
3415
  async getCredentials(url) {
2389
- try {
2390
- const vaultId = await this.ensureInitialized();
2391
- const response = await vaultCredentialsGet({
2392
- client: this.client.getClient(),
2393
- path: {
2394
- vault_id: vaultId
2395
- },
2396
- query: { url }
2397
- });
2398
- if (response?.error) {
2399
- return null;
2400
- }
2401
- const credentialsResponse = response.data;
2402
- return credentialsResponse.credentials;
2403
- } catch (error) {
2404
- return null;
2405
- }
3416
+ const domain = validateUrl(url);
3417
+ const vaultId = await this.ensureInitialized();
3418
+ const response = await vaultCredentialsGet({
3419
+ client: this.client.getClient(),
3420
+ path: { vault_id: vaultId },
3421
+ query: { url: domain },
3422
+ throwOnError: true
3423
+ });
3424
+ return response.data.credentials;
2406
3425
  }
2407
3426
  /**
2408
- * Delete credentials for a URL
3427
+ * Check whether the vault holds credentials for a website, the counterpart
3428
+ * of `BaseVault.has_credential`. Never rejects for a missing credential.
3429
+ *
3430
+ * ```ts
3431
+ * if (!(await vault.hasCredential('https://github.com/'))) {
3432
+ * await vault.addCredentialsFromEnv('https://github.com/');
3433
+ * }
3434
+ * ```
3435
+ */
3436
+ async hasCredential(url) {
3437
+ const domain = getRootDomain(url);
3438
+ const credentials = await this.listCredentials();
3439
+ return credentials.some((credential) => credential.url === url || credential.url === domain);
3440
+ }
3441
+ /**
3442
+ * Delete the credentials stored for a website.
3443
+ *
3444
+ * ```ts
3445
+ * await vault.deleteCredentials('https://github.com/');
3446
+ * ```
2409
3447
  */
2410
3448
  async deleteCredentials(url) {
3449
+ const domain = validateUrl(url);
2411
3450
  const vaultId = await this.ensureInitialized();
2412
- const response = await vaultCredentialsDelete({
3451
+ await vaultCredentialsDelete({
2413
3452
  client: this.client.getClient(),
2414
- path: {
2415
- vault_id: vaultId
2416
- },
2417
- query: { url }
3453
+ path: { vault_id: vaultId },
3454
+ query: { url: domain },
3455
+ throwOnError: true
2418
3456
  });
2419
- if (response?.error) {
2420
- throw new Error(`Failed to delete credentials: ${formatError(response.error)}`);
2421
- }
2422
3457
  }
2423
3458
  /**
2424
- * Set credit card information
3459
+ * List the websites the vault holds credentials for. Passwords are never returned.
3460
+ *
3461
+ * ```ts
3462
+ * const credentials = await vault.listCredentials();
3463
+ * console.log(credentials.map(c => c.url));
3464
+ * ```
2425
3465
  */
2426
- async setCreditCard(creditCard) {
3466
+ async listCredentials() {
2427
3467
  const vaultId = await this.ensureInitialized();
2428
- const response = await vaultCreditCardSet({
3468
+ const response = await vaultCredentialsList({
2429
3469
  client: this.client.getClient(),
2430
- path: {
2431
- vault_id: vaultId
2432
- },
2433
- body: { credit_card: creditCard }
3470
+ path: { vault_id: vaultId },
3471
+ throwOnError: true
2434
3472
  });
2435
- if (response?.error) {
2436
- throw new Error(`Failed to set credit card: ${formatError(response.error)}`);
2437
- }
3473
+ return response.data.credentials;
2438
3474
  }
2439
3475
  /**
2440
- * Get credit card information
2441
- */
2442
- async getCreditCard() {
2443
- const vaultId = await this.ensureInitialized();
2444
- const response = await vaultCreditCardGet({
2445
- client: this.client.getClient(),
2446
- path: {
2447
- vault_id: vaultId
2448
- }
2449
- });
2450
- if (response?.error) {
2451
- throw new Error(`Failed to get credit card: ${formatError(response.error)}`);
2452
- }
2453
- const creditCardResponse = response.data;
2454
- return creditCardResponse.credit_card;
2455
- }
2456
- /**
2457
- * List all credentials in the vault
2458
- */
2459
- async listCredentials() {
2460
- const vaultId = await this.ensureInitialized();
2461
- const response = await vaultCredentialsList({
2462
- client: this.client.getClient(),
2463
- path: {
2464
- vault_id: vaultId
2465
- }
2466
- });
2467
- if (response?.error) {
2468
- throw new Error(`Failed to list credentials: ${formatError(response.error)}`);
2469
- }
2470
- const credentialsResponse = response.data;
2471
- return credentialsResponse.credentials;
2472
- }
2473
- /**
2474
- * Delete credit card information
2475
- */
2476
- async deleteCreditCard() {
2477
- const vaultId = await this.ensureInitialized();
2478
- const response = await vaultCreditCardDelete({
2479
- client: this.client.getClient(),
2480
- path: {
2481
- vault_id: vaultId
2482
- }
2483
- });
2484
- if (response?.error) {
2485
- throw new Error(`Failed to delete credit card: ${formatError(response.error)}`);
2486
- }
2487
- }
2488
- /**
2489
- * Delete the entire vault
3476
+ * Delete the vault and every credential it holds.
2490
3477
  */
2491
3478
  async delete() {
2492
3479
  const vaultId = await this.ensureInitialized();
2493
- const response = await vaultDelete({
3480
+ await vaultDelete({
2494
3481
  client: this.client.getClient(),
2495
- path: {
2496
- vault_id: vaultId
2497
- }
3482
+ path: { vault_id: vaultId },
3483
+ throwOnError: true
2498
3484
  });
2499
- if (response?.error) {
2500
- throw new Error(`Failed to delete vault: ${formatError(response.error)}`);
2501
- }
2502
3485
  }
2503
3486
  /**
2504
- * Add credentials from environment variables
2505
- * Mirrors Python's add_credentials_from_env method
3487
+ * Add credentials read from environment variables, the counterpart of
3488
+ * `BaseVault.add_credentials_from_env`. For `https://github.com/` the
3489
+ * variables are `GITHUB_COM_EMAIL`, `GITHUB_COM_USERNAME`,
3490
+ * `GITHUB_COM_MFA_SECRET` and `GITHUB_COM_PASSWORD`.
3491
+ *
3492
+ * ```ts
3493
+ * process.env.GITHUB_COM_EMAIL = 'me@example.org';
3494
+ * process.env.GITHUB_COM_PASSWORD = 'secret'; // pragma: allowlist secret
3495
+ * await vault.addCredentialsFromEnv('https://github.com/');
3496
+ * ```
2506
3497
  */
2507
3498
  async addCredentialsFromEnv(url) {
2508
- const domain = this.extractDomainFromUrl(url);
2509
- const envPrefix = domain.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase();
2510
- const email = process.env[`${envPrefix}_EMAIL`] || process.env[`${envPrefix}_USERNAME`];
2511
- const password = process.env[`${envPrefix}_PASSWORD`];
2512
- if (!email || !password) {
2513
- throw new Error(`Environment variables ${envPrefix}_EMAIL/USERNAME and ${envPrefix}_PASSWORD are required`);
2514
- }
2515
- const credentials = { email, password };
2516
- const mfaSecret = process.env[`${envPrefix}_MFA_SECRET`];
2517
- if (mfaSecret) {
2518
- this.validateMfaSecret(mfaSecret);
2519
- credentials.mfa_secret = mfaSecret;
2520
- }
2521
- await this.addCredentials(url, credentials);
2522
- }
2523
- /**
2524
- * Extract domain from URL for environment variable naming
2525
- */
2526
- extractDomainFromUrl(url) {
2527
- try {
2528
- const urlObj = new URL(url);
2529
- return urlObj.hostname.replace(/^www\./, "");
2530
- } catch {
2531
- const match = url.match(/(?:https?:\/\/)?(?:www\.)?([^\/]+)/);
2532
- return match ? match[1] : url;
2533
- }
2534
- }
2535
- /**
2536
- * Validate MFA secret format
2537
- * Mirrors Python's MFA secret validation
2538
- */
2539
- validateMfaSecret(mfaSecret) {
2540
- if (/^\d+$/.test(mfaSecret)) {
2541
- throw new Error("MFA secret cannot be all numbers. Please provide a valid base32 secret.");
3499
+ const rootDomain = validateUrl(url);
3500
+ const prefix = rootDomain.replace(/\./g, "_").toUpperCase();
3501
+ const credentials = {};
3502
+ const variableNames = [];
3503
+ for (const field of CREDENTIAL_FIELDS) {
3504
+ const variable = `${prefix}_${field.toUpperCase()}`;
3505
+ variableNames.push(variable);
3506
+ const value = process.env[variable];
3507
+ if (value !== void 0) {
3508
+ credentials[field] = value;
3509
+ }
2542
3510
  }
2543
- if (mfaSecret.length < 16) {
2544
- throw new Error("MFA secret must be at least 16 characters long");
3511
+ if (Object.keys(credentials).length === 0) {
3512
+ throw new InvalidRequestError(
3513
+ `No credentials found in the environment for ${url}. Please set the following variables: ${variableNames.join(", ")}`
3514
+ );
2545
3515
  }
3516
+ await this.addCredentials(url, credentials);
2546
3517
  }
2547
3518
  /**
2548
- * Generate a secure random password
2549
- * Mirrors Python's generate_password method
3519
+ * Generate a secure random password, the counterpart of Python's
3520
+ * `generate_password`. The result always contains a lowercase letter, an
3521
+ * uppercase letter and a digit, plus a special character unless
3522
+ * `includeSpecialChars` is false.
3523
+ *
3524
+ * ```ts
3525
+ * const password = vault.generatePassword(24);
3526
+ * ```
2550
3527
  */
2551
3528
  generatePassword(length = 20, includeSpecialChars = true) {
2552
3529
  const minRequiredLength = includeSpecialChars ? 4 : 3;
2553
3530
  if (length < minRequiredLength) {
2554
- throw new Error(`Password length must be at least ${minRequiredLength} characters`);
3531
+ throw new InvalidRequestError(`Password length must be at least ${minRequiredLength} characters`);
2555
3532
  }
2556
3533
  const lowercase = "abcdefghijklmnopqrstuvwxyz";
2557
3534
  const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
2558
3535
  const digits = "0123456789";
2559
3536
  const specialChars = "!@#$%^&*()_+-=[]|;:,.<>?";
2560
- const crypto = globalThis.crypto || __require("crypto");
2561
- const array = new Uint8Array(length);
2562
- crypto.getRandomValues(array);
2563
- const allowedChars = includeSpecialChars ? "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
2564
- let password = Array.from(array, (byte) => allowedChars[byte % allowedChars.length]).join("").slice(0, length);
2565
- const passwordArray = password.split("");
2566
- if (!includeSpecialChars) {
2567
- for (let i = 0; i < passwordArray.length; i++) {
2568
- if (specialChars.includes(passwordArray[i])) {
2569
- const alphanumeric = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
2570
- passwordArray[i] = alphanumeric[Math.floor(Math.random() * alphanumeric.length)];
3537
+ const alphanumeric = uppercase + lowercase + digits;
3538
+ const allowedChars = includeSpecialChars ? `${alphanumeric}-_` : alphanumeric;
3539
+ const randomIndex = (size) => {
3540
+ const limit = Math.floor(4294967296 / size) * size;
3541
+ const buffer = new Uint32Array(1);
3542
+ for (; ; ) {
3543
+ const [value] = globalThis.crypto.getRandomValues(buffer);
3544
+ if (value < limit) {
3545
+ return value % size;
2571
3546
  }
2572
3547
  }
2573
- }
3548
+ };
3549
+ const passwordArray = Array.from({ length }, () => allowedChars[randomIndex(allowedChars.length)]);
3550
+ const pick = (chars) => chars[randomIndex(chars.length)];
2574
3551
  let maxIterations = 10;
2575
3552
  while (maxIterations-- > 0) {
2576
3553
  let allRequirementsMet = true;
2577
- if (!passwordArray.some((c) => lowercase.includes(c))) {
2578
- const index = Math.floor(Math.random() * passwordArray.length);
2579
- passwordArray[index] = lowercase[Math.floor(Math.random() * lowercase.length)];
2580
- allRequirementsMet = false;
2581
- }
2582
- if (!passwordArray.some((c) => uppercase.includes(c))) {
2583
- const index = Math.floor(Math.random() * passwordArray.length);
2584
- passwordArray[index] = uppercase[Math.floor(Math.random() * uppercase.length)];
2585
- allRequirementsMet = false;
2586
- }
2587
- if (!passwordArray.some((c) => digits.includes(c))) {
2588
- const index = Math.floor(Math.random() * passwordArray.length);
2589
- passwordArray[index] = digits[Math.floor(Math.random() * digits.length)];
2590
- allRequirementsMet = false;
2591
- }
2592
- if (includeSpecialChars && !passwordArray.some((c) => specialChars.includes(c))) {
2593
- const index = Math.floor(Math.random() * passwordArray.length);
2594
- passwordArray[index] = specialChars[Math.floor(Math.random() * specialChars.length)];
2595
- allRequirementsMet = false;
3554
+ const requirements = [
3555
+ [true, lowercase],
3556
+ [true, uppercase],
3557
+ [true, digits],
3558
+ [includeSpecialChars, specialChars]
3559
+ ];
3560
+ for (const [required, chars] of requirements) {
3561
+ if (required && !passwordArray.some((c) => chars.includes(c))) {
3562
+ passwordArray[randomIndex(passwordArray.length)] = pick(chars);
3563
+ allRequirementsMet = false;
3564
+ }
2596
3565
  }
2597
3566
  if (allRequirementsMet) {
2598
3567
  break;
@@ -2604,10 +3573,15 @@ var NotteVault = class {
2604
3573
 
2605
3574
  // src/personas.ts
2606
3575
  var NottePersona = class {
3576
+ client;
3577
+ _initRequest;
3578
+ response = null;
3579
+ initPromise = null;
3580
+ _vault;
3581
+ ownsPersona;
2607
3582
  constructor(client2, options = {}) {
2608
- this.response = null;
2609
- this.initPromise = null;
2610
3583
  this.client = client2;
3584
+ this.ownsPersona = !("persona_id" in options && options.persona_id);
2611
3585
  if (options && "persona_id" in options && options.persona_id) {
2612
3586
  this.initPromise = this.initExistingPersona(options.persona_id);
2613
3587
  this._initRequest = {};
@@ -2692,13 +3666,23 @@ var NottePersona = class {
2692
3666
  }
2693
3667
  return this.response.persona_id;
2694
3668
  }
3669
+ /**
3670
+ * Public, human-readable email address of the persona
3671
+ */
3672
+ get email() {
3673
+ return this.info.email;
3674
+ }
3675
+ /**
3676
+ * Internal UUID-backed mailbox address of the persona
3677
+ */
3678
+ get internalEmail() {
3679
+ return this.info.internal_email;
3680
+ }
2695
3681
  /**
2696
3682
  * Start the persona - ensures initialization
2697
3683
  */
2698
3684
  async start() {
2699
- if (this.response === null) {
2700
- await this.create();
2701
- }
3685
+ await this.ensureInitialized();
2702
3686
  }
2703
3687
  /**
2704
3688
  * Stop the persona - deletes it
@@ -2724,14 +3708,7 @@ var NottePersona = class {
2724
3708
  if (this.response !== null) {
2725
3709
  throw new Error(`Persona ${this.personaId} already initialized`);
2726
3710
  }
2727
- const response = await personaCreate({
2728
- client: this.client.getClient(),
2729
- body: this._initRequest
2730
- });
2731
- if (response?.error) {
2732
- throw new Error(`Failed to create persona: ${formatError(response.error)}`);
2733
- }
2734
- this.response = response.data;
3711
+ await this.ensureInitialized();
2735
3712
  }
2736
3713
  /**
2737
3714
  * Delete the persona from the notte console
@@ -2807,13 +3784,15 @@ var NottePersona = class {
2807
3784
  return this.response;
2808
3785
  }
2809
3786
  /**
2810
- * Context manager for automatic cleanup
3787
+ * Run a callback and automatically delete a persona created by this instance.
3788
+ * Personas loaded by an existing ID are retained after the callback finishes.
2811
3789
  */
2812
3790
  async use(callback) {
3791
+ await this.ensureInitialized();
2813
3792
  try {
2814
3793
  return await callback(this);
2815
3794
  } finally {
2816
- if (this._initRequest && !("persona_id" in this._initRequest)) {
3795
+ if (this.ownsPersona) {
2817
3796
  try {
2818
3797
  await this.delete();
2819
3798
  } catch (error) {
@@ -2825,7 +3804,8 @@ var NottePersona = class {
2825
3804
  };
2826
3805
 
2827
3806
  // src/functions.ts
2828
- import { writeFile } from "fs/promises";
3807
+ import { readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
3808
+ import { basename } from "path";
2829
3809
 
2830
3810
  // src/encryption.ts
2831
3811
  import { createDecipheriv, createHmac, pbkdf2Sync, timingSafeEqual } from "crypto";
@@ -2837,6 +3817,7 @@ var FERNET_IV_LENGTH = 16;
2837
3817
  var FERNET_HMAC_LENGTH = 32;
2838
3818
  var FERNET_HEADER_LENGTH = 1 + 8 + FERNET_IV_LENGTH;
2839
3819
  var Encryption = class {
3820
+ rootKey;
2840
3821
  constructor(rootKey) {
2841
3822
  if (!rootKey) {
2842
3823
  throw new Error("Root key cannot be empty");
@@ -2878,140 +3859,385 @@ function decryptFernet(token, key) {
2878
3859
 
2879
3860
  // src/functions.ts
2880
3861
  var DOWNLOAD_TIMEOUT_MS = 3e4;
3862
+ var FUNCTION_RUN_TIMEOUT_MS = 5 * 60 * 1e3;
3863
+ var FUNCTION_RUN_ENDPOINTS = {
3864
+ /** `POST` creates a run record without executing it (`CreateFunctionRunRequest`). */
3865
+ create: "/functions/{function_id}/runs/create",
3866
+ /** `POST` executes a previously created run (`StartFunctionRunRequest`). */
3867
+ start: "/functions/{function_id}/runs/{run_id}"
3868
+ };
3869
+ var RUN_API_KEY_HEADER = "x-notte-api-key";
3870
+ var FUNCTION_RUNTIMES = ["standard", "extended"];
3871
+ var FUNCTION_RUN_STATUSES = ["closed", "active", "failed"];
2881
3872
  var NotteFunction = class {
3873
+ client;
3874
+ _functionId = null;
3875
+ initPromise = null;
3876
+ createData = null;
3877
+ decryptionKey;
2882
3878
  constructor(client2, options) {
2883
3879
  this.client = client2;
2884
- this.functionId = options.function_id;
2885
3880
  this.decryptionKey = options.decryption_key;
3881
+ if (options.function_id !== void 0) {
3882
+ if (options.function_id.length === 0) {
3883
+ throw new InvalidRequestError("function_id cannot be empty");
3884
+ }
3885
+ this._functionId = options.function_id;
3886
+ } else if (options.path !== void 0) {
3887
+ assertPythonPath(options.path);
3888
+ this.createData = options;
3889
+ } else {
3890
+ throw new InvalidRequestError("Either 'function_id' or 'path' must be provided");
3891
+ }
3892
+ }
3893
+ /** The function ID. Throws before a `path`-constructed function has been uploaded. */
3894
+ get functionId() {
3895
+ if (this._functionId === null) {
3896
+ throw new InvalidRequestError(
3897
+ "Function not initialized. Await a function operation (for example `await fn.get()`) before reading its ID."
3898
+ );
3899
+ }
3900
+ return this._functionId;
3901
+ }
3902
+ /** Alias of the `functionId` accessor. */
3903
+ getFunctionId() {
3904
+ return this.functionId;
3905
+ }
3906
+ async ensureInitialized() {
3907
+ if (this._functionId !== null) {
3908
+ return this._functionId;
3909
+ }
3910
+ if (this.initPromise === null) {
3911
+ const createData = this.createData;
3912
+ this.createData = null;
3913
+ if (createData === null) {
3914
+ throw new InvalidRequestError("Function not initialized");
3915
+ }
3916
+ this.initPromise = this.createFunction(createData);
3917
+ }
3918
+ return this.initPromise;
3919
+ }
3920
+ async createFunction(data) {
3921
+ const file = await readPythonFile(data.path);
3922
+ const response = await functionCreate({
3923
+ client: this.client.getClient(),
3924
+ throwOnError: true,
3925
+ body: { file, name: data.name, description: data.description, shared: data.shared }
3926
+ });
3927
+ this._functionId = response.data.function_id;
3928
+ console.info(`[Function] ${this._functionId} created successfully.`);
3929
+ return this._functionId;
2886
3930
  }
2887
3931
  /**
2888
- * Get metadata for a specific function run
3932
+ * Get the function metadata together with its download URL, the
3933
+ * counterpart of `client.functions.get()` in Python.
3934
+ *
3935
+ * @param options - Optional version selector; omitted means the latest version.
3936
+ * @returns Function metadata, version history, and the download link returned by the API.
3937
+ * @throws NotteAPIError if the function cannot be retrieved.
3938
+ * @example
3939
+ * const { latest_version, versions } = await fn.get();
2889
3940
  */
2890
- async retrieve(functionRunId) {
2891
- try {
2892
- const response = await functionRunGetMetadata({
2893
- client: this.client.getClient(),
2894
- path: {
2895
- function_id: this.functionId,
2896
- run_id: functionRunId
2897
- }
2898
- });
2899
- if (response?.error) {
2900
- throw new Error(`Failed to get function run metadata: ${formatError(response.error)}`);
2901
- }
2902
- return response.data;
2903
- } catch (error) {
2904
- if (error instanceof Error && error.message.startsWith("Failed to get function run metadata:")) {
2905
- throw error;
2906
- }
2907
- throw new Error(`Failed to get function run metadata: ${error instanceof Error ? error.message : String(error)}`);
3941
+ async get(options = {}) {
3942
+ const functionId = await this.ensureInitialized();
3943
+ const response = await functionDownloadUrl({
3944
+ client: this.client.getClient(),
3945
+ throwOnError: true,
3946
+ path: { function_id: functionId },
3947
+ query: options.version !== void 0 ? { version: options.version } : {}
3948
+ });
3949
+ return response.data;
3950
+ }
3951
+ /**
3952
+ * Upload a new version of the code, mirroring `function.update(path=...)`.
3953
+ *
3954
+ * @param options - Local Python file path, optional version, and code-access settings.
3955
+ * @returns Updated function metadata, including its latest version.
3956
+ * @throws InvalidRequestError if the path is missing or does not end in `.py`.
3957
+ * @throws NotteAPIError if the upload is rejected.
3958
+ * @example
3959
+ * const updated = await fn.update({ path: './scraper.py' });
3960
+ * console.log(updated.latest_version);
3961
+ */
3962
+ async update(options) {
3963
+ if (options.path === void 0) {
3964
+ throw new InvalidRequestError("'path' must be provided");
2908
3965
  }
3966
+ const functionId = await this.ensureInitialized();
3967
+ const file = await readPythonFile(options.path);
3968
+ const response = await functionUpdate({
3969
+ client: this.client.getClient(),
3970
+ throwOnError: true,
3971
+ path: { function_id: functionId },
3972
+ query: {
3973
+ restricted: options.restricted ?? true,
3974
+ ...options.version !== void 0 ? { version: options.version } : {}
3975
+ },
3976
+ body: { file }
3977
+ });
3978
+ console.info(`[Function] ${functionId} updated successfully to version ${response.data.latest_version}.`);
3979
+ return response.data;
3980
+ }
3981
+ /**
3982
+ * Update the function metadata (name, description, default runtime, ...).
3983
+ *
3984
+ * @param options - Metadata fields to update, such as name, description, or default runtime.
3985
+ * @returns Updated function metadata.
3986
+ * @throws NotteAPIError if the metadata update is rejected.
3987
+ * @example
3988
+ * await fn.updateMetadata({ name: 'Price monitor', default_runtime: 'extended' });
3989
+ */
3990
+ async updateMetadata(options) {
3991
+ const functionId = await this.ensureInitialized();
3992
+ const response = await functionMetadataUpdate({
3993
+ client: this.client.getClient(),
3994
+ throwOnError: true,
3995
+ path: { function_id: functionId },
3996
+ body: options
3997
+ });
3998
+ return response.data;
3999
+ }
4000
+ /**
4001
+ * Delete the function from the Notte console.
4002
+ *
4003
+ * @returns The API's function-deletion confirmation.
4004
+ * @throws NotteAPIError if the function cannot be deleted.
4005
+ * @example
4006
+ * await fn.delete();
4007
+ */
4008
+ async delete() {
4009
+ const functionId = await this.ensureInitialized();
4010
+ const response = await functionDelete({
4011
+ client: this.client.getClient(),
4012
+ throwOnError: true,
4013
+ path: { function_id: functionId }
4014
+ });
4015
+ console.info(`[Function] ${functionId} deleted successfully.`);
4016
+ return response.data;
4017
+ }
4018
+ /**
4019
+ * Schedule the function to run on a cron expression.
4020
+ *
4021
+ * @param options - Cron expression and optional variables passed to each scheduled execution.
4022
+ * @returns The saved schedule configuration.
4023
+ * @throws NotteAPIError if the schedule cannot be saved.
4024
+ * @example
4025
+ * await fn.setSchedule({ cron: '0 9 * * *', variables: { url: 'https://example.com' } });
4026
+ */
4027
+ async setSchedule(options) {
4028
+ const functionId = await this.ensureInitialized();
4029
+ const response = await functionScheduleSet({
4030
+ client: this.client.getClient(),
4031
+ throwOnError: true,
4032
+ path: { function_id: functionId },
4033
+ body: { cron: options.cron, variables: options.variables ?? null }
4034
+ });
4035
+ return response.data;
4036
+ }
4037
+ /**
4038
+ * Remove the function schedule.
4039
+ *
4040
+ * @returns The API's schedule-deletion confirmation.
4041
+ * @throws NotteAPIError if schedule deletion fails.
4042
+ * @example
4043
+ * await fn.deleteSchedule();
4044
+ */
4045
+ async deleteSchedule() {
4046
+ const functionId = await this.ensureInitialized();
4047
+ const response = await functionScheduleDelete({
4048
+ client: this.client.getClient(),
4049
+ throwOnError: true,
4050
+ path: { function_id: functionId }
4051
+ });
4052
+ return response.data;
4053
+ }
4054
+ /**
4055
+ * Roll the function back to a previous version.
4056
+ *
4057
+ * @param options - Target version and optional code-access restriction, which defaults to true.
4058
+ * @returns Updated function metadata after rollback.
4059
+ * @throws NotteAPIError if the requested version cannot be restored.
4060
+ * @example
4061
+ * const { versions } = await fn.get();
4062
+ * await fn.rollback({ version: versions[0] });
4063
+ */
4064
+ async rollback(options) {
4065
+ const functionId = await this.ensureInitialized();
4066
+ const response = await functionRollback({
4067
+ client: this.client.getClient(),
4068
+ throwOnError: true,
4069
+ path: { function_id: functionId },
4070
+ query: { restricted: options.restricted ?? true },
4071
+ body: { version: options.version }
4072
+ });
4073
+ return response.data;
4074
+ }
4075
+ /**
4076
+ * Get metadata for a specific function run.
4077
+ *
4078
+ * @param functionRunId - ID of a run belonging to this function.
4079
+ * @returns The run's current status, logs, and result metadata. This does not wait for completion.
4080
+ * @throws NotteAPIError if the run cannot be retrieved.
4081
+ * @example
4082
+ * const run = await fn.getRun(result.function_run_id);
4083
+ * console.log(run.status, run.logs);
4084
+ */
4085
+ async getRun(functionRunId) {
4086
+ const functionId = await this.ensureInitialized();
4087
+ const response = await functionRunGetMetadata({
4088
+ client: this.client.getClient(),
4089
+ throwOnError: true,
4090
+ path: { function_id: functionId, run_id: functionRunId }
4091
+ });
4092
+ return response.data;
4093
+ }
4094
+ /**
4095
+ * Compatibility alias for getRun().
4096
+ * @deprecated Use getRun() instead. Retained for backwards compatibility.
4097
+ */
4098
+ async retrieve(functionRunId) {
4099
+ return this.getRun(functionRunId);
4100
+ }
4101
+ /**
4102
+ * Create a cloud run record without starting execution, like
4103
+ * `function.create_run()` in Python. Use this only when you need the ID
4104
+ * before execution, then pass it to `run(variables, { functionRunId })`.
4105
+ * `run()` otherwise creates a new run on its own.
4106
+ *
4107
+ * @param options - Run-record settings; `local` defaults to false. Node execution remains cloud-only.
4108
+ * @returns A created run record and its ID. No function code has executed yet.
4109
+ * @throws NotteAPIError if run creation is rejected.
4110
+ * @example
4111
+ * const created = await fn.createRun();
4112
+ * const result = await fn.run({ url: 'https://example.com' }, { functionRunId: created.function_run_id });
4113
+ */
4114
+ async createRun(options = {}) {
4115
+ const functionId = await this.ensureInitialized();
4116
+ return this.postRunEndpoint({
4117
+ url: FUNCTION_RUN_ENDPOINTS.create,
4118
+ path: { function_id: functionId },
4119
+ body: { local: options.local ?? false }
4120
+ });
2909
4121
  }
2910
4122
  /**
2911
4123
  * List the runs of this function.
2912
4124
  *
2913
- * `only_active` is sent as `false` unless the caller says otherwise, and that
2914
- * is deliberate rather than redundant. `GET /functions/{function_id}/runs`
2915
- * now defaults it to `false` server-side as well, but it used to default to
2916
- * `true`: the request model was a bare subclass of the session one, where
2917
- * "only active" reads sensibly as "list my running sessions" and, for "list
2918
- * this function's runs", was a trap. Hitting the endpoint with no query
2919
- * parameters returned only the runs executing right now - an empty list for
2920
- * any function that had finished. That cost us a bug in the anything-api
2921
- * console, where a function with 15 recorded runs displayed "Nothing has run
2922
- * this endpoint yet".
2923
- *
2924
- * Sending it explicitly pins this method to the documented behaviour against
2925
- * whichever API version it is talking to, including deployments that predate
2926
- * the server-side change. Callers who want only in-flight runs pass
2927
- * `only_active: true`.
4125
+ * Includes completed runs by default. Set `only_active: true` to retrieve
4126
+ * only in-flight runs. Use the response pagination metadata to request
4127
+ * additional pages.
4128
+ *
4129
+ * @param options - Pagination and filtering options. `only_active` defaults to false, including completed runs.
4130
+ * @returns One page of run records and pagination metadata; additional pages require another call.
4131
+ * @throws NotteAPIError if the run history cannot be retrieved.
4132
+ * @example
4133
+ * const { items } = await fn.runs({ page_size: 20 });
2928
4134
  */
2929
4135
  async runs(options = {}) {
2930
- try {
2931
- const response = await listFunctionRunsByFunctionId({
2932
- client: this.client.getClient(),
2933
- path: {
2934
- function_id: this.functionId
2935
- },
2936
- query: {
2937
- ...options,
2938
- only_active: options.only_active ?? false
2939
- }
2940
- });
2941
- if (response?.error) {
2942
- throw new Error(`Failed to list function runs: ${formatError(response.error)}`);
2943
- }
2944
- return response.data;
2945
- } catch (error) {
2946
- if (error instanceof Error && error.message.startsWith("Failed to list function runs:")) {
2947
- throw error;
2948
- }
2949
- throw new Error(`Failed to list function runs: ${error instanceof Error ? error.message : String(error)}`);
2950
- }
4136
+ const functionId = await this.ensureInitialized();
4137
+ const response = await listFunctionRunsByFunctionId({
4138
+ client: this.client.getClient(),
4139
+ throwOnError: true,
4140
+ path: { function_id: functionId },
4141
+ query: { ...options, only_active: options.only_active ?? false }
4142
+ });
4143
+ return response.data;
2951
4144
  }
2952
4145
  /**
2953
- * Start a function run
4146
+ * Run the function on the cloud and wait for its result, streaming logs by
4147
+ * default like `function.run(**variables)` in Python.
4148
+ *
4149
+ * Pass `{ stream: false }` to receive the backend's JSON response without
4150
+ * live logs; execution still blocks until completion. Pass `functionRunId`
4151
+ * to execute a record returned by `createRun()`; otherwise a new run record
4152
+ * is created first, exactly like Python. A failed execution throws
4153
+ * `FailedToRunCloudFunctionError` unless `raiseOnFailure` is false, and the
4154
+ * wait is aborted with `NotteTimeoutError` after `timeoutMs`.
4155
+ *
4156
+ * @param variables - JSON-serializable keyword arguments passed to the Python function's `run` entry point.
4157
+ * @param options - Log streaming, failure handling, runtime, run ID, and timeout settings.
4158
+ * @returns The completed execution response. The function's return value is in `result`. Disabling streaming still waits for completion.
4159
+ * @throws FailedToRunCloudFunctionError if execution fails and `raiseOnFailure` is true.
4160
+ * @throws NotteTimeoutError if the execution request exceeds `timeoutMs`.
4161
+ * @throws InvalidRequestError if the run options are invalid.
4162
+ * @example
4163
+ * const result = await fn.run({ url: 'https://example.com' });
4164
+ * console.log(result.result);
4165
+ *
4166
+ * await fn.run({ url: 'https://example.com' }, { runtime: 'extended', onLog: line => logger.info(line) });
2954
4167
  */
2955
- async run(variables = {}) {
2956
- const apiKey = this.client.getConfig().apiKey || "";
2957
- if (!apiKey) {
2958
- throw new Error("API key is required. Provide it via config.apiKey or set the NOTTE_API_KEY environment variable.");
2959
- }
4168
+ async run(variables = {}, options = {}) {
4169
+ const stream = options.stream ?? true;
4170
+ const timeoutMs = options.timeoutMs ?? FUNCTION_RUN_TIMEOUT_MS;
4171
+ if (!(timeoutMs > 0)) {
4172
+ throw new InvalidRequestError("timeoutMs must be a positive number");
4173
+ }
4174
+ if (options.runtime !== void 0 && !FUNCTION_RUNTIMES.includes(options.runtime)) {
4175
+ throw new InvalidRequestError(`runtime must be one of ${FUNCTION_RUNTIMES.join(", ")}, got '${String(options.runtime)}'`);
4176
+ }
4177
+ const functionId = await this.ensureInitialized();
4178
+ const functionRunId = options.functionRunId ?? (await this.createRun()).function_run_id;
4179
+ const body = {
4180
+ workflow_id: functionId,
4181
+ variables,
4182
+ stream,
4183
+ ...options.runtime !== void 0 ? { runtime: options.runtime } : {}
4184
+ };
4185
+ const headers = { [TIMEOUT_HEADER]: "0" };
4186
+ const apiKey = this.client.getConfig().apiKey;
4187
+ if (apiKey) {
4188
+ headers[RUN_API_KEY_HEADER] = apiKey;
4189
+ }
4190
+ const endpointPath = FUNCTION_RUN_ENDPOINTS.start.replace("{function_id}", functionId).replace("{run_id}", functionRunId);
4191
+ const parseAs = stream ? "stream" : "json";
4192
+ const controller = new AbortController();
4193
+ let timedOut = false;
4194
+ const timer = Number.isFinite(timeoutMs) ? setTimeout(() => {
4195
+ timedOut = true;
4196
+ controller.abort(new DOMException(`Function run timed out after ${timeoutMs}ms`, "TimeoutError"));
4197
+ }, timeoutMs) : void 0;
4198
+ timer?.unref?.();
2960
4199
  try {
2961
- const response = await functionRunStart({
2962
- client: this.client.getClient(),
2963
- path: {
2964
- function_id: this.functionId
2965
- },
2966
- headers: {
2967
- "x-notte-api-key": apiKey,
2968
- "x-notte-request-origin": "sdk-node",
2969
- "x-notte-sdk-version": SDK_VERSION
2970
- },
2971
- body: {
2972
- workflow_id: this.functionId,
2973
- variables,
2974
- stream: false
2975
- }
4200
+ const raw = await this.postRunEndpoint({
4201
+ url: FUNCTION_RUN_ENDPOINTS.start,
4202
+ path: { function_id: functionId, run_id: functionRunId },
4203
+ body: { ...body, function_run_id: functionRunId },
4204
+ parseAs,
4205
+ headers,
4206
+ signal: controller.signal
2976
4207
  });
2977
- if (response?.error) {
2978
- throw new Error(`Failed to start function run: ${formatError(response.error)}`);
4208
+ const result = stream ? await readFunctionStream(raw, options.onLog ?? console.log, endpointPath, controller.signal) : parseFunctionRunResult(raw, endpointPath);
4209
+ if (result.status === "failed" && (options.raiseOnFailure ?? true)) {
4210
+ throw new FailedToRunCloudFunctionError(functionId, result.function_run_id, result);
2979
4211
  }
2980
- return response.data;
4212
+ return result;
2981
4213
  } catch (error) {
2982
- if (error instanceof Error && error.message.startsWith("Failed to start function run:")) {
2983
- throw error;
4214
+ if (timedOut) {
4215
+ throw new NotteTimeoutError(`Function ${functionId} run timed out after ${timeoutMs}ms`, { cause: error });
2984
4216
  }
2985
- throw new Error(`Failed to run function: ${error instanceof Error ? error.message : String(error)}`);
4217
+ throw error;
4218
+ } finally {
4219
+ clearTimeout(timer);
2986
4220
  }
2987
4221
  }
2988
4222
  /**
2989
4223
  * Get the download URL of the function code, decrypting it when the API
2990
4224
  * returns an encrypted one (Notte managed functions).
4225
+ *
4226
+ * @param options - Optional version and decryption-key override.
4227
+ * @returns An HTTP(S) URL for downloading the function source.
4228
+ * @throws InvalidRequestError if an encrypted URL has no usable decryption key.
4229
+ * @throws NotteError if decryption does not produce an HTTP(S) URL.
4230
+ * @example
4231
+ * const url = await fn.getUrl({ version: 'v1' });
2991
4232
  */
2992
4233
  async getUrl(options = {}) {
2993
- let response;
2994
- try {
2995
- response = await functionDownloadUrl({
2996
- client: this.client.getClient(),
2997
- path: {
2998
- function_id: this.functionId
2999
- },
3000
- query: options.version ? { version: options.version } : {}
3001
- });
3002
- } catch (error) {
3003
- throw new Error(`Failed to get function download url: ${error instanceof Error ? error.message : String(error)}`);
3004
- }
3005
- if (response?.error) {
3006
- throw new Error(`Failed to get function download url: ${formatError(response.error)}`);
3007
- }
3008
- const url = response.data.url;
4234
+ const { url } = await this.get(options);
3009
4235
  if (isHttpUrl(url)) {
3010
4236
  return url;
3011
4237
  }
3012
4238
  const decryptionKey = options.decryption_key ?? this.decryptionKey;
3013
- if (!decryptionKey) {
3014
- throw new Error(
4239
+ if (decryptionKey === void 0) {
4240
+ throw new InvalidRequestError(
3015
4241
  "Decryption key is required to decrypt the function download url. Set it when creating the function, i.e. client.NotteFunction({ function_id: '<your-function-id>', decryption_key: '<your-key>' })."
3016
4242
  );
3017
4243
  }
@@ -3019,100 +4245,837 @@ var NotteFunction = class {
3019
4245
  try {
3020
4246
  decrypted = new Encryption(decryptionKey).decrypt(url);
3021
4247
  } catch (error) {
3022
- throw new Error(`Failed to decrypt the function download url: ${error instanceof Error ? error.message : String(error)}. Contact support@notte.cc if you need help.`);
4248
+ throw new InvalidRequestError(
4249
+ `Failed to decrypt the function download url: ${error instanceof Error ? error.message : String(error)}. Contact support@notte.cc if you need help.`
4250
+ );
3023
4251
  }
3024
4252
  if (!isHttpUrl(decrypted)) {
3025
- throw new Error("Failed to decrypt the function download url: the decrypted value is not a URL. Contact support@notte.cc if you need help.");
4253
+ throw new NotteError(
4254
+ "Failed to decrypt the function download url: the decrypted value is not a URL. Contact support@notte.cc if you need help."
4255
+ );
3026
4256
  }
3027
4257
  return decrypted;
3028
4258
  }
3029
4259
  /**
3030
4260
  * Download the function code as a python file. Returns the code, and writes
3031
4261
  * it to `path` when one is provided.
4262
+ *
4263
+ * @param options - Optional version, decryption key, and local `.py` destination path.
4264
+ * @returns The Python source text, including when a local destination is supplied.
4265
+ * @throws InvalidRequestError if the destination does not end in `.py` or the decryption key is invalid.
4266
+ * @throws NotteError if the source download fails. Local filesystem failures also propagate.
4267
+ * @example
4268
+ * const code = await fn.download({ path: './scraper.py' });
3032
4269
  */
3033
4270
  async download(options = {}) {
3034
4271
  const { path } = options;
3035
- if (path !== void 0 && !path.endsWith(".py")) {
3036
- throw new Error(`Code file path must end with .py, got '${path}'`);
4272
+ if (path !== void 0) {
4273
+ assertPythonPath(path);
3037
4274
  }
3038
4275
  const url = await this.getUrl(options);
3039
4276
  let response;
3040
4277
  try {
3041
4278
  response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
3042
4279
  } catch (error) {
3043
- throw new Error(`Failed to download function code: ${error instanceof Error ? error.message : String(error)}`);
4280
+ throw new NotteError(
4281
+ `Failed to download function code in ${DOWNLOAD_TIMEOUT_MS / 1e3} seconds: ${error instanceof Error ? error.message : String(error)}`,
4282
+ { cause: error }
4283
+ );
3044
4284
  }
3045
4285
  if (!response.ok) {
3046
- throw new Error(`Failed to download function code: ${response.status} ${response.statusText}`);
4286
+ throw new NotteError(`Failed to download function code: ${response.status} ${response.statusText}`);
3047
4287
  }
3048
4288
  const code = await response.text();
3049
4289
  if (path !== void 0) {
3050
- await writeFile(path, code, "utf8");
4290
+ await writeFile3(path, code, "utf8");
3051
4291
  }
3052
4292
  return code;
3053
4293
  }
3054
4294
  /**
3055
- * Get the function ID
4295
+ * Call one of the run endpoints that the OpenAPI spec does not describe
4296
+ * (`FUNCTION_RUN_ENDPOINTS`). The generated client has no operation for
4297
+ * them, so this is the single place where they are hand-rolled; the request
4298
+ * and response shapes are transcribed from `notte_sdk.types`
4299
+ * (`CreateFunctionRunRequest` / `CreateFunctionRunResponse` and
4300
+ * `StartFunctionRunRequest` / `FunctionRunResponse`). Replace the callers
4301
+ * with generated operations once the spec exposes these paths.
3056
4302
  */
3057
- getFunctionId() {
3058
- return this.functionId;
4303
+ async postRunEndpoint(request) {
4304
+ const response = await this.client.getClient().post({
4305
+ url: request.url,
4306
+ path: request.path,
4307
+ body: request.body,
4308
+ headers: { "Content-Type": "application/json", ...request.headers },
4309
+ parseAs: request.parseAs ?? "json",
4310
+ signal: request.signal,
4311
+ throwOnError: true
4312
+ });
4313
+ return response.data;
3059
4314
  }
3060
4315
  };
3061
4316
  function isHttpUrl(value) {
3062
4317
  return value.startsWith("https://") || value.startsWith("http://");
3063
4318
  }
4319
+ function assertPythonPath(path) {
4320
+ if (!path.endsWith(".py")) {
4321
+ throw new InvalidRequestError(`Code file path must end with .py, got '${path}'`);
4322
+ }
4323
+ }
4324
+ async function readPythonFile(path) {
4325
+ assertPythonPath(path);
4326
+ let bytes;
4327
+ try {
4328
+ bytes = await readFile2(path);
4329
+ } catch (error) {
4330
+ throw new InvalidRequestError(
4331
+ `The file '${path}' could not be read: ${error instanceof Error ? error.message : String(error)}`
4332
+ );
4333
+ }
4334
+ return new File([new Uint8Array(bytes)], basename(path), { type: "text/x-python" });
4335
+ }
4336
+ function unwrapRuntimeEnvelope(candidate, path) {
4337
+ const { statusCode, body } = candidate;
4338
+ if (typeof statusCode !== "number" || statusCode < 400 || !("body" in candidate)) {
4339
+ return;
4340
+ }
4341
+ let parsed = body;
4342
+ if (typeof body === "string") {
4343
+ try {
4344
+ parsed = JSON.parse(body);
4345
+ } catch {
4346
+ parsed = body;
4347
+ }
4348
+ }
4349
+ throw NotteAPIError.fromBody(path, statusCode, parsed);
4350
+ }
4351
+ function parseFunctionRunResult(value, path) {
4352
+ if (typeof value !== "object" || value === null) {
4353
+ throw new NotteError("Invalid function result: expected an object");
4354
+ }
4355
+ const candidate = value;
4356
+ unwrapRuntimeEnvelope(candidate, path);
4357
+ const status = candidate.status;
4358
+ if (typeof candidate.function_run_id !== "string" || typeof candidate.function_id !== "string" || typeof status !== "string" || !FUNCTION_RUN_STATUSES.includes(status) || !("result" in candidate)) {
4359
+ throw new NotteError("Invalid function result: missing function_id, function_run_id, status or result");
4360
+ }
4361
+ return {
4362
+ function_id: candidate.function_id,
4363
+ function_run_id: candidate.function_run_id,
4364
+ session_id: typeof candidate.session_id === "string" ? candidate.session_id : null,
4365
+ result: candidate.result,
4366
+ status
4367
+ };
4368
+ }
4369
+ async function readFunctionStream(body, onLog, path, signal) {
4370
+ if (!body?.getReader) {
4371
+ throw new NotteError("Missing function response stream");
4372
+ }
4373
+ const reader = body.getReader();
4374
+ const onAbort = () => {
4375
+ reader.cancel(signal?.reason).catch(() => {
4376
+ });
4377
+ };
4378
+ signal?.addEventListener("abort", onAbort, { once: true });
4379
+ const decoder = new TextDecoder();
4380
+ let buffer = "";
4381
+ let data = [];
4382
+ let result;
4383
+ let sawFrame = false;
4384
+ let raw = "";
4385
+ const dispatch = () => {
4386
+ if (!data.length) return;
4387
+ const payload = data.join("\n");
4388
+ data = [];
4389
+ let event;
4390
+ try {
4391
+ event = JSON.parse(payload);
4392
+ } catch (error) {
4393
+ throw new NotteError(`Malformed function stream event: ${payload}`, { cause: error });
4394
+ }
4395
+ if (event.type === "log") onLog(String(event.message ?? ""));
4396
+ if (event.type === "error") throw new NotteError(`Function stream error: ${String(event.message ?? "")}`);
4397
+ if (event.type === "result") {
4398
+ const value = typeof event.message === "string" ? JSON.parse(event.message) : event.message;
4399
+ result = parseFunctionRunResult(value, path);
4400
+ }
4401
+ };
4402
+ const line = (value) => {
4403
+ if (value === "") dispatch();
4404
+ else if (value.startsWith("data:")) {
4405
+ sawFrame = true;
4406
+ data.push(value.slice(5).replace(/^ /, ""));
4407
+ }
4408
+ };
4409
+ try {
4410
+ while (true) {
4411
+ const { value, done } = await reader.read();
4412
+ const text = decoder.decode(value, { stream: !done });
4413
+ buffer += text;
4414
+ if (!sawFrame) raw += text;
4415
+ let end;
4416
+ while ((end = buffer.indexOf("\n")) !== -1) {
4417
+ line(buffer.slice(0, end).replace(/\r$/, ""));
4418
+ buffer = buffer.slice(end + 1);
4419
+ }
4420
+ if (done) break;
4421
+ }
4422
+ if (buffer) line(buffer.replace(/\r$/, ""));
4423
+ dispatch();
4424
+ if (!result && !sawFrame && raw.trim().length > 0) {
4425
+ let parsed;
4426
+ try {
4427
+ parsed = JSON.parse(raw);
4428
+ } catch {
4429
+ throw new NotteError(`Function stream ended without a result: ${raw.slice(0, 200)}`);
4430
+ }
4431
+ result = parseFunctionRunResult(parsed, path);
4432
+ }
4433
+ if (!result) throw new NotteError("Function stream ended without a result");
4434
+ return result;
4435
+ } finally {
4436
+ signal?.removeEventListener("abort", onAbort);
4437
+ await reader.cancel().catch(() => {
4438
+ });
4439
+ reader.releaseLock();
4440
+ }
4441
+ }
4442
+
4443
+ // src/files.ts
4444
+ import { createWriteStream } from "fs";
4445
+ import { link, mkdir, readFile as readFile3, rename, rm, stat } from "fs/promises";
4446
+ import { basename as basename2, extname, join } from "path";
4447
+ import { randomBytes } from "crypto";
4448
+ import { Readable } from "stream";
4449
+ import { pipeline } from "stream/promises";
4450
+ var FileNotFoundError = class extends NotteError {
4451
+ };
4452
+ var FileExistsError = class extends NotteError {
4453
+ };
4454
+ var METADATA_PAGE_SIZE = 1e3;
4455
+ var MIME_TYPES_BY_EXTENSION = {
4456
+ ".csv": "text/csv",
4457
+ ".gif": "image/gif",
4458
+ ".htm": "text/html",
4459
+ ".html": "text/html",
4460
+ ".jpeg": "image/jpeg",
4461
+ ".jpg": "image/jpeg",
4462
+ ".json": "application/json",
4463
+ ".md": "text/markdown",
4464
+ ".pdf": "application/pdf",
4465
+ ".png": "image/png",
4466
+ ".svg": "image/svg+xml",
4467
+ ".txt": "text/plain",
4468
+ ".webp": "image/webp",
4469
+ ".xml": "application/xml",
4470
+ ".zip": "application/zip"
4471
+ };
4472
+ function guessMimeType(filename) {
4473
+ return MIME_TYPES_BY_EXTENSION[extname(filename).toLowerCase()] ?? "application/octet-stream";
4474
+ }
4475
+ function sanitizeFilename(filename) {
4476
+ const segment = filename.split(/[\\/]+/).filter(Boolean).pop() ?? "";
4477
+ return segment.replace(/^[a-zA-Z]:/, "");
4478
+ }
4479
+ function toFile(file, filename) {
4480
+ if (file instanceof File && file.name === filename) {
4481
+ return file;
4482
+ }
4483
+ const type = file instanceof Blob && file.type ? file.type : guessMimeType(filename);
4484
+ return new File([file], filename, { type });
4485
+ }
4486
+ var SessionFiles = class {
4487
+ constructor(client2, sessionId) {
4488
+ this.client = client2;
4489
+ this.sessionId = sessionId;
4490
+ }
4491
+ /**
4492
+ * Upload bytes as a session file. `filename` is what the browser sees when
4493
+ * the file is later attached to a page.
4494
+ *
4495
+ * @param file - Bytes to upload as a Blob, File, or buffer.
4496
+ * @param filename - Stored filename, defaulting to `file`.
4497
+ * @returns Metadata for the uploaded file, including its ID, name, and size.
4498
+ * @throws NotteAPIError if the storage API request fails.
4499
+ */
4500
+ async upload(file, filename = "file") {
4501
+ const response = await uploadSessionFile({
4502
+ client: this.client,
4503
+ path: { session_id: this.sessionId },
4504
+ body: { file: toFile(file, filename) },
4505
+ throwOnError: true
4506
+ });
4507
+ return response.data;
4508
+ }
4509
+ /** List the session's non-expired files, newest first.
4510
+ *
4511
+ * @param options - Optional source filter and pagination. Defaults to all sources, limit 100, and offset 0.
4512
+ * @returns One page of non-expired files and the total count; pagination is not automatic.
4513
+ * @throws NotteAPIError if the storage API request fails.
4514
+ */
4515
+ async list(options = {}) {
4516
+ const response = await listSessionFiles({
4517
+ client: this.client,
4518
+ path: { session_id: this.sessionId },
4519
+ query: { limit: options.limit ?? 100, offset: options.offset ?? 0, source: options.source },
4520
+ throwOnError: true
4521
+ });
4522
+ return response.data;
4523
+ }
4524
+ /**
4525
+ * Find a file's metadata by ID by scanning the session listing. Rejects with
4526
+ * `FileNotFoundError` when the ID is unknown or expired.
4527
+ *
4528
+ * @param fileId - ID of a file belonging to this session.
4529
+ * @returns The matching file's metadata.
4530
+ * @throws NotteAPIError if the storage API request fails.
4531
+ * @throws FileNotFoundError if the file ID is unknown or expired.
4532
+ */
4533
+ async metadata(fileId) {
4534
+ let offset = 0;
4535
+ while (true) {
4536
+ const page = await this.list({ limit: METADATA_PAGE_SIZE, offset });
4537
+ const match = page.files.find((file) => file.id === fileId);
4538
+ if (match) {
4539
+ return match;
4540
+ }
4541
+ offset += page.files.length;
4542
+ if (offset >= page.total || page.files.length === 0) {
4543
+ throw new FileNotFoundError(`File ${fileId} was not found in session ${this.sessionId}`);
4544
+ }
4545
+ }
4546
+ }
4547
+ /** Download a file's bytes as a `Blob`.
4548
+ *
4549
+ * @param fileId - ID of a file belonging to this session.
4550
+ * @returns The downloaded bytes as a Blob; no local file is written.
4551
+ * @throws NotteAPIError if the storage API request fails.
4552
+ */
4553
+ async download(fileId) {
4554
+ const response = await downloadSessionFile({
4555
+ client: this.client,
4556
+ path: { session_id: this.sessionId, file_id: fileId },
4557
+ parseAs: "blob",
4558
+ throwOnError: true
4559
+ });
4560
+ return response.data;
4561
+ }
4562
+ /** Stream a file's bytes. Used by `RemoteFileStorage.download()` to write to disk without buffering. */
4563
+ async stream(fileId) {
4564
+ const response = await downloadSessionFile({
4565
+ client: this.client,
4566
+ path: { session_id: this.sessionId, file_id: fileId },
4567
+ parseAs: "stream",
4568
+ throwOnError: true
4569
+ });
4570
+ const body = response.data;
4571
+ if (!body) {
4572
+ throw new NotteError(`Download of file ${fileId} returned no body`);
4573
+ }
4574
+ return body;
4575
+ }
4576
+ /** Delete a session file.
4577
+ *
4578
+ * @param fileId - ID of the session file to delete.
4579
+ * @returns Resolves when deletion succeeds.
4580
+ * @throws NotteAPIError if the storage API request fails.
4581
+ */
4582
+ async delete(fileId) {
4583
+ await deleteSessionFile({
4584
+ client: this.client,
4585
+ path: { session_id: this.sessionId, file_id: fileId },
4586
+ throwOnError: true
4587
+ });
4588
+ }
4589
+ };
4590
+ var RemoteFileStorage = class _RemoteFileStorage {
4591
+ constructor(client2, sessionId) {
4592
+ this.client = client2;
4593
+ this._sessionId = sessionId ?? null;
4594
+ }
4595
+ _sessionId;
4596
+ /** Bind the storage to a session. Called by `Session.start()` when the storage was passed to the session. */
4597
+ setSessionId(sessionId) {
4598
+ this._sessionId = sessionId;
4599
+ }
4600
+ /**
4601
+ * Bind this storage once, cloning it when another session already owns it.
4602
+ *
4603
+ * ```ts
4604
+ * const first = storage.forSession('session-a'); // first === storage
4605
+ * const second = storage.forSession('session-b'); // a clone sharing the client
4606
+ * ```
4607
+ */
4608
+ forSession(sessionId) {
4609
+ if (this._sessionId === null) {
4610
+ this._sessionId = sessionId;
4611
+ return this;
4612
+ }
4613
+ if (this._sessionId === sessionId) {
4614
+ return this;
4615
+ }
4616
+ return new _RemoteFileStorage(this.client, sessionId);
4617
+ }
4618
+ /** ID of the bound session. Throws `InvalidRequestError` while the storage is unbound. */
4619
+ get sessionId() {
4620
+ if (this._sessionId === null) {
4621
+ throw new InvalidRequestError("A session ID is required for every file operation");
4622
+ }
4623
+ return this._sessionId;
4624
+ }
4625
+ /** Whether the storage has been bound to a session yet. */
4626
+ get isBound() {
4627
+ return this._sessionId !== null;
4628
+ }
4629
+ files() {
4630
+ return new SessionFiles(this.client.getClient(), this.sessionId);
4631
+ }
4632
+ /**
4633
+ * Upload a file to the session. A string is read as a local path and its
4634
+ * basename becomes the uploaded filename unless `uploadFileName` is set.
4635
+ *
4636
+ * @param file - Local file path, Blob, File, or buffer to upload.
4637
+ * @param uploadFileName - Optional stored filename. Local paths default to their basename; File objects keep their name.
4638
+ * @returns Metadata for the uploaded file, including its ID, name, and size.
4639
+ * @throws NotteAPIError if the storage API request fails.
4640
+ * @throws InvalidRequestError if storage has not been bound to a session.
4641
+ * @throws InvalidRequestError if a supplied local path is not a file.
4642
+ * @example
4643
+ * await storage.upload('./data/resume.pdf');
4644
+ * await storage.upload(Buffer.from('hello'), 'hello.txt');
4645
+ */
4646
+ async upload(file, uploadFileName) {
4647
+ const files = this.files();
4648
+ if (typeof file === "string") {
4649
+ const info = await stat(file).catch(() => null);
4650
+ if (!info?.isFile()) {
4651
+ throw new InvalidRequestError(`Cannot upload file ${file}: it is not a file`);
4652
+ }
4653
+ const bytes = await readFile3(file);
4654
+ return files.upload(bytes, uploadFileName ?? basename2(file));
4655
+ }
4656
+ const filename = uploadFileName ?? (file instanceof File ? file.name : "file");
4657
+ return files.upload(file, filename);
4658
+ }
4659
+ /**
4660
+ * List the session's files.
4661
+ *
4662
+ * @param options - Optional source filter and pagination. Defaults to all sources, limit 100, and offset 0.
4663
+ * @returns One page of non-expired files and the total count; pagination is not automatic.
4664
+ * @throws NotteAPIError if the storage API request fails.
4665
+ * @throws InvalidRequestError if storage has not been bound to a session.
4666
+ * @example
4667
+ * const downloads = await storage.list({ source: 'session_download', limit: 1000 });
4668
+ */
4669
+ async list(options = {}) {
4670
+ return this.files().list(options);
4671
+ }
4672
+ /** Metadata of a file by ID. Rejects with `FileNotFoundError` when unknown.
4673
+ *
4674
+ * @param fileId - ID of a file belonging to this session.
4675
+ * @returns The matching file's metadata.
4676
+ * @throws NotteAPIError if the storage API request fails.
4677
+ * @throws FileNotFoundError if the file ID is unknown or expired.
4678
+ * @throws InvalidRequestError if storage has not been bound to a session.
4679
+ */
4680
+ async metadata(fileId) {
4681
+ return this.files().metadata(fileId);
4682
+ }
4683
+ /**
4684
+ * Download a file to `<localDir>/<filename>` and return the local path. The
4685
+ * file is written atomically (temporary file + rename). Rejects with
4686
+ * `FileExistsError` when the destination exists unless `force` is set.
4687
+ *
4688
+ * @param fileId - ID of the file to download.
4689
+ * @param localDir - Destination directory, created if needed. Defaults to the current directory.
4690
+ * @param options - Set `force` to overwrite an existing destination; defaults to false.
4691
+ * @returns The local file path after the download has been written successfully.
4692
+ * @throws NotteAPIError if the storage API request fails.
4693
+ * @throws InvalidRequestError if storage has not been bound to a session.
4694
+ * @throws FileExistsError if the destination exists and `force` is false. Filesystem errors also propagate.
4695
+ * @example
4696
+ * const path = await storage.download(fileId, './downloads', { force: true });
4697
+ */
4698
+ async download(fileId, localDir = ".", options = {}) {
4699
+ const files = this.files();
4700
+ const metadata = await files.metadata(fileId);
4701
+ const safeName = sanitizeFilename(metadata.filename);
4702
+ if (safeName === "" || safeName === "." || safeName === "..") {
4703
+ throw new NotteError(`Unsafe filename returned for file ${fileId}: ${JSON.stringify(metadata.filename)}`);
4704
+ }
4705
+ await mkdir(localDir, { recursive: true });
4706
+ const destination = join(localDir, safeName);
4707
+ if (!options.force) {
4708
+ const exists = await stat(destination).then(() => true, () => false);
4709
+ if (exists) {
4710
+ throw new FileExistsError(`${destination} already exists; pass force: true to overwrite it`);
4711
+ }
4712
+ }
4713
+ const body = await files.stream(fileId);
4714
+ const temporary = join(localDir, `.${safeName}.${randomBytes(8).toString("hex")}.part`);
4715
+ try {
4716
+ await pipeline(
4717
+ Readable.fromWeb(body),
4718
+ createWriteStream(temporary, { flags: "wx" })
4719
+ );
4720
+ if (options.force) {
4721
+ await rename(temporary, destination);
4722
+ } else {
4723
+ try {
4724
+ await link(temporary, destination);
4725
+ } catch (error) {
4726
+ if (error.code === "EEXIST") {
4727
+ throw new FileExistsError(`${destination} already exists; pass force: true to overwrite it`);
4728
+ }
4729
+ throw error;
4730
+ }
4731
+ await rm(temporary, { force: true });
4732
+ }
4733
+ } catch (error) {
4734
+ await rm(temporary, { force: true });
4735
+ throw error;
4736
+ }
4737
+ return destination;
4738
+ }
4739
+ /** Download a file's bytes as a `Blob` without touching the local filesystem. */
4740
+ async downloadBlob(fileId) {
4741
+ return this.files().download(fileId);
4742
+ }
4743
+ /** Stream a file's bytes without buffering the whole file in memory. */
4744
+ async stream(fileId) {
4745
+ return this.files().stream(fileId);
4746
+ }
4747
+ /** Delete a session file.
4748
+ *
4749
+ * @param fileId - ID of the session file to delete.
4750
+ * @returns Resolves when deletion succeeds.
4751
+ * @throws NotteAPIError if the storage API request fails.
4752
+ * @throws InvalidRequestError if storage has not been bound to a session.
4753
+ */
4754
+ async delete(fileId) {
4755
+ await this.files().delete(fileId);
4756
+ }
4757
+ };
4758
+
4759
+ // src/anything.ts
4760
+ var NotteAnything = class {
4761
+ client;
4762
+ constructor(client2) {
4763
+ this.client = client2;
4764
+ }
4765
+ /**
4766
+ * Start building a function for `task`. Building is slow (minutes) and
4767
+ * billable; prefer the marketplace (`client.functions.list()`) when a
4768
+ * ready-made function already fits.
4769
+ *
4770
+ * ```ts
4771
+ * const result = await client.anything.start({ task: 'get the weather in Paris' });
4772
+ * if (typeof result.function_id === 'string') {
4773
+ * const fn = client.NotteFunction({ function_id: result.function_id });
4774
+ * }
4775
+ * ```
4776
+ *
4777
+ * Rejects with a `NotteAPIError` when the API answers with a non-2xx status.
4778
+ */
4779
+ async start(body) {
4780
+ const response = await anythingStart({
4781
+ client: this.client.getClient(),
4782
+ body,
4783
+ throwOnError: true
4784
+ });
4785
+ return response.data ?? {};
4786
+ }
4787
+ };
4788
+
4789
+ // src/secrets.ts
4790
+ var NotteSecrets = class {
4791
+ client;
4792
+ constructor(client2) {
4793
+ this.client = client2;
4794
+ }
4795
+ /**
4796
+ * List secret metadata (never the values), optionally filtered by namespace.
4797
+ *
4798
+ * ```ts
4799
+ * const secrets = await client.secrets.list({ namespace: 'llm_provider' });
4800
+ * secrets.forEach(s => console.log(s.name, s.key_hint));
4801
+ * ```
4802
+ */
4803
+ async list(options = {}) {
4804
+ const response = await listSecrets({ client: this.client.getClient(), query: options, throwOnError: true });
4805
+ return response.data?.items ?? [];
4806
+ }
4807
+ /**
4808
+ * Store (or overwrite) a secret and return its metadata.
4809
+ *
4810
+ * ```ts
4811
+ * const meta = await client.secrets.store({ namespace: 'function_env', name: 'API_TOKEN', value: 'xyz' });
4812
+ * console.log(meta.id, meta.key_hint);
4813
+ * ```
4814
+ */
4815
+ async store(body) {
4816
+ const response = await storeSecret({ client: this.client.getClient(), body, throwOnError: true });
4817
+ return response.data;
4818
+ }
4819
+ /**
4820
+ * Read the value of a secret by name within a namespace.
4821
+ *
4822
+ * ```ts
4823
+ * const { value } = await client.secrets.get('API_TOKEN', 'function_env');
4824
+ * ```
4825
+ */
4826
+ async get(name, namespace) {
4827
+ const response = await getSecret({
4828
+ client: this.client.getClient(),
4829
+ path: { name },
4830
+ query: { namespace },
4831
+ throwOnError: true
4832
+ });
4833
+ return response.data;
4834
+ }
4835
+ /**
4836
+ * Delete a secret by id (the `id` field of `SecretMetadata`).
4837
+ *
4838
+ * ```ts
4839
+ * const meta = await client.secrets.store({ namespace: 'function_env', name: 'TMP', value: 'x' });
4840
+ * await client.secrets.delete(meta.id);
4841
+ * ```
4842
+ */
4843
+ async delete(secretId) {
4844
+ await deleteSecret({ client: this.client.getClient(), path: { secret_id: secretId }, throwOnError: true });
4845
+ }
4846
+ };
4847
+
4848
+ // src/usage.ts
4849
+ var NotteUsage = class {
4850
+ client;
4851
+ constructor(client2) {
4852
+ this.client = client2;
4853
+ }
4854
+ /**
4855
+ * Usage summary for a monthly period (current month by default).
4856
+ *
4857
+ * ```ts
4858
+ * const usage = await client.usage.get({ period: 'May 2025' });
4859
+ * console.log(`${usage.session_count} sessions, $${usage.total_cost}`);
4860
+ * ```
4861
+ */
4862
+ async get(options = {}) {
4863
+ const response = await getUsage({ client: this.client.getClient(), query: options, throwOnError: true });
4864
+ return response.data;
4865
+ }
4866
+ /**
4867
+ * Paginated per-request usage logs, optionally filtered by endpoint.
4868
+ *
4869
+ * ```ts
4870
+ * const page = await client.usage.logs({ endpoint: 'sessions.start', page: 1, page_size: 50 });
4871
+ * page.items.forEach(log => console.log(log.created_at, log.endpoint, log.duration_ms));
4872
+ * ```
4873
+ */
4874
+ async logs(options = {}) {
4875
+ const response = await getUsageLogs({ client: this.client.getClient(), query: options, throwOnError: true });
4876
+ return response.data;
4877
+ }
4878
+ };
3064
4879
 
3065
4880
  // src/client.ts
3066
- import { z as z3 } from "zod";
3067
- var NotteClient = class {
4881
+ var DEFAULT_NOTTE_API_URL = "https://api.notte.cc";
4882
+ var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
4883
+ var REQUEST_ORIGIN = "sdk-node";
4884
+ var TIMEOUT_HEADER = "x-notte-timeout-ms";
4885
+ var DB_PREVIEW_ENV = "NOTTE_DB_PREVIEW_BRANCH";
4886
+ var DB_PREVIEW_HEADER = "x-db-preview";
4887
+ var EXECUTION_ERROR_CLASS = "NotteApiExecutionError";
4888
+ function isRelativeProxyUrl(baseUrl) {
4889
+ return /^\/(?!\/)/.test(baseUrl);
4890
+ }
4891
+ var NotteClient3 = class {
4892
+ config;
4893
+ client = createClient();
3068
4894
  constructor(config = {}) {
3069
4895
  const apiKey = config.apiKey || process.env.NOTTE_API_KEY;
3070
- const isProxyMode = config.baseUrl && !config.baseUrl.startsWith("https://");
4896
+ const baseUrl = config.baseUrl || process.env.NOTTE_API_URL || DEFAULT_NOTTE_API_URL;
4897
+ const dbPreview = config.dbPreview || process.env[DB_PREVIEW_ENV] || void 0;
4898
+ const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
4899
+ if (!(timeoutMs >= 0)) {
4900
+ throw new InvalidRequestError("timeoutMs must be a non-negative number");
4901
+ }
4902
+ const isProxyMode = isRelativeProxyUrl(baseUrl);
4903
+ if (!isProxyMode) {
4904
+ const url = new URL(baseUrl);
4905
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
4906
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
4907
+ throw new InvalidRequestError(
4908
+ "API base URL must use HTTPS (HTTP is only allowed on loopback for local development)"
4909
+ );
4910
+ }
4911
+ }
3071
4912
  if (!apiKey && !isProxyMode) {
3072
- throw new Error("API key is required. Provide it via config.apiKey or set the NOTTE_API_KEY environment variable.");
4913
+ throw new AuthenticationError(
4914
+ "NOTTE_API_KEY needs to be provided. Pass config.apiKey or set the NOTTE_API_KEY environment variable."
4915
+ );
3073
4916
  }
3074
- this.config = {
3075
- baseUrl: config.baseUrl || process.env.NOTTE_API_URL || "https://api.notte.cc",
3076
- apiKey,
3077
- ...config
3078
- };
3079
- client.setConfig({
4917
+ this.config = { baseUrl, apiKey, timeoutMs, dbPreview, verbose: config.verbose ?? false };
4918
+ if (baseUrl !== DEFAULT_NOTTE_API_URL && !isProxyMode) {
4919
+ console.warn(`NOTTE_API_URL is set to: ${baseUrl}`);
4920
+ }
4921
+ this.client.setConfig({
3080
4922
  baseUrl: this.config.baseUrl,
3081
- redirect: "manual"
4923
+ redirect: "manual",
4924
+ throwOnError: true
3082
4925
  });
3083
- client.interceptors.request.use((request) => {
3084
- request.headers.set("Authorization", `Bearer ${this.config.apiKey}`);
3085
- request.headers.set("x-notte-request-origin", "sdk-node");
3086
- request.headers.set("x-notte-sdk-version", SDK_VERSION);
3087
- return request;
4926
+ this.client.interceptors.request.use((request) => {
4927
+ const headers = new Headers(request.headers);
4928
+ if (this.config.apiKey) headers.set("Authorization", `Bearer ${this.config.apiKey}`);
4929
+ headers.set("x-notte-request-origin", REQUEST_ORIGIN);
4930
+ headers.set("x-notte-sdk-version", SDK_VERSION);
4931
+ if (this.config.dbPreview) headers.set(DB_PREVIEW_HEADER, this.config.dbPreview);
4932
+ const override = headers.get(TIMEOUT_HEADER);
4933
+ headers.delete(TIMEOUT_HEADER);
4934
+ const timeoutMs2 = override !== null ? Number(override) : this.config.timeoutMs;
4935
+ if (this.config.verbose) {
4936
+ console.info(`Making \`${request.method}\` request to \`${request.url}\``);
4937
+ }
4938
+ if (!(timeoutMs2 > 0)) {
4939
+ return new Request(request, { headers });
4940
+ }
4941
+ const deadline = withTimeout(request.signal, timeoutMs2);
4942
+ const timed = new Request(request, { headers, signal: deadline.signal });
4943
+ pendingDeadlines.set(timed, deadline.release);
4944
+ return timed;
3088
4945
  });
3089
- client.interceptors.response.use(async (response, request, opts) => {
3090
- if ((response.status === 307 || response.status === 308) && response.headers.get("location")) {
3091
- const location = response.headers.get("location");
3092
- const headers = new Headers(request.headers);
3093
- if (new URL(location).origin !== new URL(request.url).origin) {
4946
+ this.client.interceptors.response.use(async (response, request, opts) => {
4947
+ const initialUrl = new URL(request.url);
4948
+ let currentUrl = initialUrl;
4949
+ const headers = new Headers(request.headers);
4950
+ const apiUrl = new URL(this.config.baseUrl, initialUrl);
4951
+ const executionPrefix = `${apiUrl.pathname.replace(/\/$/, "")}/functions/`;
4952
+ const executionPath = initialUrl.pathname.slice(executionPrefix.length);
4953
+ const runtimeHandoff = request.method === "POST" && initialUrl.origin === apiUrl.origin && initialUrl.pathname.startsWith(executionPrefix) && /^[^/]+\/runs\/[^/]+$/.test(executionPath) && !executionPath.endsWith("/create");
4954
+ for (let hop = 0; (response.status === 307 || response.status === 308) && response.headers.get("location"); hop++) {
4955
+ if (hop >= 10) throw new InvalidRequestError("Too many API redirects");
4956
+ const location = new URL(response.headers.get("location"), currentUrl);
4957
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(location.hostname);
4958
+ if (location.protocol !== "https:" && !(location.protocol === "http:" && loopback)) {
4959
+ throw new InvalidRequestError(`Refusing to follow a redirect to an insecure URL: ${location.origin}`);
4960
+ }
4961
+ if (location.username || location.password) {
4962
+ throw new InvalidRequestError("Refusing to follow a redirect containing URL credentials");
4963
+ }
4964
+ if (location.origin !== currentUrl.origin) {
3094
4965
  headers.delete("Authorization");
4966
+ const localHandoff = location.protocol === "http:" && initialUrl.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(initialUrl.hostname) && loopback;
4967
+ if (!(hop === 0 && runtimeHandoff && (location.protocol === "https:" || localHandoff))) {
4968
+ headers.delete("x-notte-api-key");
4969
+ }
3095
4970
  }
4971
+ if (hop > 0) headers.delete("x-notte-api-key");
4972
+ await response.body?.cancel();
3096
4973
  const headerRecord = {};
3097
4974
  headers.forEach((v, k) => {
3098
4975
  headerRecord[k] = v;
3099
4976
  });
3100
- const redirected = await fetch(location, {
4977
+ response = await (opts.fetch ?? fetch)(location.toString(), {
3101
4978
  method: request.method,
3102
4979
  headers: headerRecord,
3103
- body: opts.serializedBody ?? void 0
4980
+ body: opts.serializedBody ?? void 0,
4981
+ signal: request.signal,
4982
+ redirect: "manual"
3104
4983
  });
3105
- return redirected;
4984
+ currentUrl = location;
3106
4985
  }
3107
4986
  const ct = response.headers.get("content-type") ?? "";
3108
- if (ct.split(";")[0]?.trim() === "text/plain") {
4987
+ if (opts.parseAs !== "stream" && ct.split(";")[0]?.trim() === "text/plain") {
3109
4988
  const body = await response.arrayBuffer();
3110
- const headers = new Headers(response.headers);
3111
- headers.set("content-type", "application/json");
3112
- return new Response(body, { status: response.status, statusText: response.statusText, headers });
4989
+ const headers2 = new Headers(response.headers);
4990
+ headers2.set("content-type", "application/json");
4991
+ return new Response(body, { status: response.status, statusText: response.statusText, headers: headers2 });
3113
4992
  }
3114
4993
  return response;
3115
4994
  });
4995
+ this.client.interceptors.response.use(async (response, request, opts) => {
4996
+ try {
4997
+ const parseAs = opts.parseAs === "auto" ? getParseAs(response.headers.get("content-type")) : opts.parseAs;
4998
+ if (parseAs !== "stream" && response.body) {
4999
+ const body = await response.arrayBuffer();
5000
+ const buffered = new Response(body, {
5001
+ status: response.status,
5002
+ statusText: response.statusText,
5003
+ headers: response.headers
5004
+ });
5005
+ Object.defineProperty(buffered, "url", { value: response.url });
5006
+ return buffered;
5007
+ }
5008
+ return response;
5009
+ } finally {
5010
+ releaseDeadline(request);
5011
+ }
5012
+ });
5013
+ this.client.interceptors.error.use((error, response, request) => {
5014
+ releaseDeadline(request);
5015
+ return this.toTypedError(error, response, request);
5016
+ });
5017
+ startVersionCheck();
5018
+ }
5019
+ /**
5020
+ * Map the raw value the generated client rejects with into a `NotteError`.
5021
+ * HTTP failures become `NotteAPIError` (or `NotteAPIExecutionError` when the
5022
+ * API flags them), timeouts become `NotteTimeoutError`, anything else is
5023
+ * returned untouched.
5024
+ */
5025
+ toTypedError(error, response, request) {
5026
+ if (error instanceof NotteAPIError) {
5027
+ return error;
5028
+ }
5029
+ if (request?.signal.aborted) {
5030
+ error = request.signal.reason ?? error;
5031
+ }
5032
+ if (isTimeoutAbort(error)) {
5033
+ const target = request ? ` to \`${requestPath(request)}\`` : "";
5034
+ return new NotteTimeoutError(`Request${target} ${error.message}`, { cause: error });
5035
+ }
5036
+ if (request?.signal.aborted) return error;
5037
+ if (response) {
5038
+ const path = requestPath(request, response);
5039
+ const body = normalizeErrorBody(error);
5040
+ if (response.status === 422) {
5041
+ const latest = upgradeSuggestion();
5042
+ const message = typeof body.message === "string" ? body.message : "";
5043
+ const looksLikeSchemaMismatch = message.includes("extra_forbidden") || message.includes("Extra inputs are not permitted") || message.includes("'type':");
5044
+ if (latest && looksLikeSchemaMismatch) {
5045
+ return new NotteAPIError(
5046
+ path,
5047
+ response.status,
5048
+ { ...body, message: createUpgradeErrorMessage("API returned 422 validation error", latest, message) },
5049
+ response
5050
+ );
5051
+ }
5052
+ }
5053
+ if (response.headers.get("x-error-class") === EXECUTION_ERROR_CLASS) {
5054
+ return new NotteAPIExecutionError(path, response.status, body, response);
5055
+ }
5056
+ return new NotteAPIError(path, response.status, body, response);
5057
+ }
5058
+ return error;
5059
+ }
5060
+ /**
5061
+ * Add the database preview branch to a websocket URL served by the Notte API.
5062
+ * The websocket handshake reads the branch from the query string, so without
5063
+ * it a preview-branch session is looked up in the default database.
5064
+ */
5065
+ withDbPreview(url) {
5066
+ if (!this.config.dbPreview) {
5067
+ return url;
5068
+ }
5069
+ const parsed = new URL(url);
5070
+ parsed.searchParams.set("db_preview", this.config.dbPreview);
5071
+ return parsed.toString();
5072
+ }
5073
+ /**
5074
+ * Health check the Notte API. Resolves when the API answers 200 and rejects
5075
+ * with a `NotteAPIError` (or a network error) otherwise.
5076
+ */
5077
+ async healthCheck() {
5078
+ await healthCheck({ client: this.client });
3116
5079
  }
3117
5080
  /**
3118
5081
  * Create a new session
@@ -3120,6 +5083,24 @@ var NotteClient = class {
3120
5083
  Session(options = {}) {
3121
5084
  return new Session(this, options);
3122
5085
  }
5086
+ /** Access files owned by a specific session, including closed sessions. */
5087
+ Files(sessionId) {
5088
+ return new SessionFiles(this.getClient(), sessionId);
5089
+ }
5090
+ /**
5091
+ * File storage to attach to a session, the counterpart of `client.FileStorage()`.
5092
+ *
5093
+ * ```ts
5094
+ * const storage = client.FileStorage();
5095
+ * await client.Session({ storage }).use(async session => {
5096
+ * await storage.upload('./invoice.pdf');
5097
+ * const files = await storage.list({ source: 'session_download' });
5098
+ * });
5099
+ * ```
5100
+ */
5101
+ FileStorage(sessionId) {
5102
+ return new RemoteFileStorage(this, sessionId);
5103
+ }
3123
5104
  /**
3124
5105
  * Create a new agent
3125
5106
  */
@@ -3144,6 +5125,66 @@ var NotteClient = class {
3144
5125
  NotteFunction(options) {
3145
5126
  return new NotteFunction(this, options);
3146
5127
  }
5128
+ /** Alias of `NotteFunction`, matching `client.Function` in Python. */
5129
+ Function(options) {
5130
+ return this.NotteFunction(options);
5131
+ }
5132
+ /** Sessions listing, the counterpart of `client.sessions.list()`. */
5133
+ get sessions() {
5134
+ return {
5135
+ list: async (options = {}) => {
5136
+ const response = await listSessions({ client: this.getClient(), query: options });
5137
+ return response.data?.items ?? [];
5138
+ }
5139
+ };
5140
+ }
5141
+ /** Agents listing, the counterpart of `client.agents.list()`. */
5142
+ get agents() {
5143
+ return {
5144
+ list: async (options = {}) => {
5145
+ const response = await listAgents({ client: this.getClient(), query: options });
5146
+ return response.data?.items ?? [];
5147
+ }
5148
+ };
5149
+ }
5150
+ /** Vaults listing, the counterpart of `client.vaults.list()`. */
5151
+ get vaults() {
5152
+ return {
5153
+ list: async (options = {}) => {
5154
+ const response = await listVaults({ client: this.getClient(), query: options });
5155
+ return response.data?.items ?? [];
5156
+ }
5157
+ };
5158
+ }
5159
+ /** Functions listing, the counterpart of `client.functions.list()`. */
5160
+ get functions() {
5161
+ return {
5162
+ list: async (options = {}) => {
5163
+ const response = await listFunctions({ client: this.getClient(), query: options });
5164
+ return response.data?.items ?? [];
5165
+ }
5166
+ };
5167
+ }
5168
+ async search(query, options = {}) {
5169
+ const response = await searchWeb({
5170
+ client: this.getClient(),
5171
+ body: { ...options, q: query },
5172
+ throwOnError: true
5173
+ });
5174
+ return response.data;
5175
+ }
5176
+ /** Anything API (`POST /anything/start`). */
5177
+ get anything() {
5178
+ return new NotteAnything(this);
5179
+ }
5180
+ /** Workspace secrets (`/secrets`). */
5181
+ get secrets() {
5182
+ return new NotteSecrets(this);
5183
+ }
5184
+ /** Usage and billing (`/usage`, `/usage/logs`). */
5185
+ get usage() {
5186
+ return new NotteUsage(this);
5187
+ }
3147
5188
  /**
3148
5189
  * Personas client for listing and managing personas
3149
5190
  */
@@ -3154,9 +5195,6 @@ var NotteClient = class {
3154
5195
  client: this.getClient(),
3155
5196
  query: options || {}
3156
5197
  });
3157
- if (response?.error) {
3158
- throw new Error(`Failed to list personas: ${JSON.stringify(response.error)}`);
3159
- }
3160
5198
  return response.data?.items || [];
3161
5199
  }
3162
5200
  };
@@ -3165,49 +5203,80 @@ var NotteClient = class {
3165
5203
  * Get the configured client instance
3166
5204
  */
3167
5205
  getClient() {
3168
- return client;
5206
+ return this.client;
3169
5207
  }
3170
5208
  async scrape(url, options = {}) {
3171
- const apiOptions = { ...options };
3172
- if (options.response_format && typeof options.response_format.parse === "function") {
3173
- if (options.json_schema) {
3174
- apiOptions.response_format = options.json_schema;
3175
- } else {
3176
- apiOptions.response_format = z3.toJSONSchema(options.response_format);
3177
- }
3178
- if (!apiOptions.instructions) {
3179
- const schemaStr = JSON.stringify(apiOptions.response_format);
3180
- if (schemaStr.includes("plans") || schemaStr.includes("plan")) {
3181
- apiOptions.instructions = "Extract pricing plans from the page";
3182
- } else if (schemaStr.includes("products") || schemaStr.includes("product")) {
3183
- apiOptions.instructions = "Extract product information from the page";
3184
- } else if (schemaStr.includes("articles") || schemaStr.includes("article")) {
3185
- apiOptions.instructions = "Extract article information from the page";
3186
- } else {
3187
- apiOptions.instructions = "Extract structured data from the page based on the provided schema";
3188
- }
3189
- }
3190
- }
5209
+ const body = await buildScrapeBody(options);
3191
5210
  const response = await scrapeWebpage({
3192
5211
  client: this.getClient(),
3193
- body: {
3194
- url,
3195
- ...apiOptions
3196
- }
5212
+ body: { url, ...body }
3197
5213
  });
3198
- if (response?.error) {
3199
- throw new Error(`Failed to scrape webpage: ${JSON.stringify(response.error)}`);
3200
- }
3201
- const scrapeResponse = response.data;
3202
- return processScrapeResponse(scrapeResponse, options);
5214
+ return processScrapeResponse(response.data, options);
3203
5215
  }
3204
5216
  /**
3205
5217
  * Get the client configuration
3206
5218
  */
3207
5219
  getConfig() {
3208
- return this.config;
5220
+ return { ...this.config };
3209
5221
  }
3210
5222
  };
5223
+ function requestPath(request, response) {
5224
+ const url = request?.url ?? response?.url;
5225
+ if (!url) {
5226
+ return "unknown";
5227
+ }
5228
+ try {
5229
+ return new URL(url).pathname;
5230
+ } catch {
5231
+ return url;
5232
+ }
5233
+ }
5234
+ function isTimeoutAbort(error) {
5235
+ return typeof error === "object" && error !== null && error.name === "TimeoutError";
5236
+ }
5237
+ function timeoutReason(timeoutMs) {
5238
+ return new DOMException(`timed out after ${timeoutMs}ms`, "TimeoutError");
5239
+ }
5240
+ var pendingDeadlines = /* @__PURE__ */ new WeakMap();
5241
+ function releaseDeadline(request) {
5242
+ if (!request) return;
5243
+ const release = pendingDeadlines.get(request);
5244
+ if (release) {
5245
+ pendingDeadlines.delete(request);
5246
+ release();
5247
+ }
5248
+ }
5249
+ function withTimeout(signal, timeoutMs) {
5250
+ const controller = new AbortController();
5251
+ const timer = setTimeout(() => controller.abort(timeoutReason(timeoutMs)), timeoutMs);
5252
+ timer.unref?.();
5253
+ let detach = () => void 0;
5254
+ let composite;
5255
+ const signalAny = AbortSignal.any;
5256
+ if (typeof signalAny === "function") {
5257
+ composite = signalAny.call(AbortSignal, signal ? [signal, controller.signal] : [controller.signal]);
5258
+ } else {
5259
+ composite = controller.signal;
5260
+ if (signal) {
5261
+ if (signal.aborted) {
5262
+ controller.abort(signal.reason);
5263
+ } else {
5264
+ const forward = () => controller.abort(signal.reason);
5265
+ signal.addEventListener("abort", forward, { once: true });
5266
+ detach = () => signal.removeEventListener("abort", forward);
5267
+ }
5268
+ }
5269
+ }
5270
+ let released = false;
5271
+ const release = () => {
5272
+ if (released) return;
5273
+ released = true;
5274
+ clearTimeout(timer);
5275
+ detach();
5276
+ };
5277
+ composite.addEventListener("abort", release, { once: true });
5278
+ return { signal: composite, release };
5279
+ }
3211
5280
 
3212
5281
  // src/proxy/types.ts
3213
5282
  var NotteProxyAuthError = class extends Error {
@@ -3218,32 +5287,65 @@ var NotteProxyAuthError = class extends Error {
3218
5287
  };
3219
5288
 
3220
5289
  // src/index.ts
3221
- startVersionCheck();
3222
5290
  var createClient2 = (config) => {
5291
+ const client2 = createClient({ baseUrl: config?.baseUrl || DEFAULT_NOTTE_API_URL });
3223
5292
  if (config?.baseUrl) {
3224
- client.setConfig({ baseUrl: config.baseUrl });
5293
+ client2.setConfig({ baseUrl: config.baseUrl });
3225
5294
  }
3226
5295
  if (config?.token) {
3227
- client.interceptors.request.use((request) => {
5296
+ client2.interceptors.request.use((request) => {
3228
5297
  request.headers.set("Authorization", `Bearer ${config.token}`);
3229
5298
  return request;
3230
5299
  });
3231
5300
  }
3232
- return client;
5301
+ return client2;
3233
5302
  };
3234
5303
  export {
5304
+ ActionExecutionError,
3235
5305
  Agent,
5306
+ AuthenticationError,
5307
+ CAPTCHA_SOLVE_TIMEOUT_MS,
5308
+ CLUSTER_OVERLOAD_RETRY_DELAY_MS,
5309
+ CREDENTIAL_FIELDS,
5310
+ DEFAULT_NOTTE_API_URL,
5311
+ DEFAULT_REQUEST_TIMEOUT_MS,
3236
5312
  Encryption,
3237
- NotteClient,
5313
+ EvaluateJsNoDataError,
5314
+ FUNCTION_RUN_ENDPOINTS,
5315
+ FUNCTION_RUN_TIMEOUT_MS,
5316
+ FailedToRunCloudFunctionError,
5317
+ FetchResponseDecodeError,
5318
+ FileExistsError,
5319
+ FileNotFoundError,
5320
+ InvalidRequestError,
5321
+ NotteAPIError,
5322
+ NotteAPIExecutionError,
5323
+ NotteAnything,
5324
+ NotteClient3 as NotteClient,
5325
+ NotteError,
3238
5326
  NotteFunction,
3239
5327
  NottePersona,
3240
5328
  NotteProxyAuthError,
5329
+ NotteSecrets,
5330
+ NotteTimeoutError,
5331
+ NotteUsage,
3241
5332
  NotteVault,
5333
+ PageFetchHTTPError,
5334
+ PageFetchResponse,
5335
+ RUN_API_KEY_HEADER,
5336
+ RemoteFileStorage,
5337
+ SESSION_START_TRIES,
5338
+ ScrapeFailedError,
3242
5339
  Session,
5340
+ SessionFiles,
5341
+ TIMEOUT_HEADER,
5342
+ actions,
3243
5343
  agentStart,
3244
5344
  agentStatus,
3245
5345
  agentStop,
3246
5346
  anythingStart,
5347
+ buildFetchScript,
5348
+ checkForLatestVersion,
3247
5349
  client,
3248
5350
  connectLinkContext,
3249
5351
  connectLinkMailboxConnect,
@@ -3252,14 +5354,11 @@ export {
3252
5354
  connectLinkStatus,
3253
5355
  connectLinkSubmit,
3254
5356
  createClient2 as createClient,
3255
- NotteClient as default,
5357
+ createOrAppendCookiesToFile,
5358
+ NotteClient3 as default,
3256
5359
  deleteSecret,
3257
- fileDownload,
3258
- fileDownloadUploadedFile,
3259
- fileListDownloads,
3260
- fileListUploads,
3261
- fileUpload,
3262
- fileUploadDownloadedFile,
5360
+ deleteSessionFile,
5361
+ downloadSessionFile,
3263
5362
  functionCreate,
3264
5363
  functionDelete,
3265
5364
  functionDownloadUrl,
@@ -3274,6 +5373,7 @@ export {
3274
5373
  functionScheduleDelete,
3275
5374
  functionScheduleSet,
3276
5375
  functionUpdate,
5376
+ getRootDomain,
3277
5377
  getScript,
3278
5378
  getSecret,
3279
5379
  getSessionScript,
@@ -3281,11 +5381,15 @@ export {
3281
5381
  getUsageLogs,
3282
5382
  healthCheck,
3283
5383
  improvePrompt,
5384
+ installServerOwnedDialogPolicy,
5385
+ isCaptchaSolveAction,
5386
+ isValidMfaSecret,
3284
5387
  listAgents,
3285
5388
  listFunctionRunsByFunctionId,
3286
5389
  listFunctions,
3287
5390
  listPersonas,
3288
5391
  listSecrets,
5392
+ listSessionFiles,
3289
5393
  listSessions,
3290
5394
  listVaults,
3291
5395
  mailboxConnectToken,
@@ -3296,16 +5400,20 @@ export {
3296
5400
  mailboxUpdate,
3297
5401
  managedAuthConnectLinkCreate,
3298
5402
  nudgePrompt,
5403
+ observeServerOwnedDialog,
3299
5404
  pageExecute,
3300
5405
  pageObserve,
3301
5406
  pageScrape,
3302
5407
  pageScreenshot,
5408
+ parseAgentStatusMessage,
3303
5409
  personaCreate,
3304
5410
  personaDelete,
3305
5411
  personaEmailsList,
3306
5412
  personaGet,
3307
5413
  personaSmsList,
3308
5414
  personaUpdate,
5415
+ prepareAgentRequest,
5416
+ processScrapeResponse,
3309
5417
  profileCookiesGet,
3310
5418
  profileCookiesSet,
3311
5419
  profileCreate,
@@ -3313,7 +5421,11 @@ export {
3313
5421
  profileDuplicate,
3314
5422
  profileGet,
3315
5423
  profileList,
5424
+ readCookiesFile,
3316
5425
  readyCheck,
5426
+ responseFromEvaluated,
5427
+ retry,
5428
+ sanitizeFilename,
3317
5429
  scrapeFromHtml,
3318
5430
  scrapeWebpage,
3319
5431
  searchWeb,
@@ -3326,7 +5438,12 @@ export {
3326
5438
  sessionStart,
3327
5439
  sessionStatus,
3328
5440
  sessionStop,
5441
+ startVersionCheck,
3329
5442
  storeSecret,
5443
+ uploadSessionFile,
5444
+ validateCredentials,
5445
+ validateMfaSecret,
5446
+ validateUrl,
3330
5447
  vaultCreate,
3331
5448
  vaultCredentialsAdd,
3332
5449
  vaultCredentialsDelete,