@bubblebrain-ai/bubble 0.0.19 → 0.0.20

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.
Files changed (59) hide show
  1. package/dist/agent/internal-reminder-sanitizer.d.ts +1 -0
  2. package/dist/agent/internal-reminder-sanitizer.js +46 -0
  3. package/dist/agent.d.ts +9 -0
  4. package/dist/agent.js +305 -17
  5. package/dist/approval/controller.d.ts +6 -0
  6. package/dist/approval/controller.js +104 -11
  7. package/dist/debug-trace.js +4 -0
  8. package/dist/feishu/agent-host/run-driver.js +28 -0
  9. package/dist/hooks/config.d.ts +9 -0
  10. package/dist/hooks/config.js +278 -0
  11. package/dist/hooks/controller.d.ts +24 -0
  12. package/dist/hooks/controller.js +254 -0
  13. package/dist/hooks/index.d.ts +6 -0
  14. package/dist/hooks/index.js +4 -0
  15. package/dist/hooks/log.d.ts +14 -0
  16. package/dist/hooks/log.js +54 -0
  17. package/dist/hooks/runner.d.ts +5 -0
  18. package/dist/hooks/runner.js +225 -0
  19. package/dist/hooks/trust.d.ts +37 -0
  20. package/dist/hooks/trust.js +143 -0
  21. package/dist/hooks/types.d.ts +173 -0
  22. package/dist/hooks/types.js +46 -0
  23. package/dist/main.js +32 -0
  24. package/dist/memory/prompts.js +3 -1
  25. package/dist/model-catalog.js +2 -0
  26. package/dist/model-pricing.js +8 -0
  27. package/dist/network/chatgpt-transport.d.ts +0 -1
  28. package/dist/network/chatgpt-transport.js +40 -121
  29. package/dist/network/provider-transport.d.ts +32 -0
  30. package/dist/network/provider-transport.js +265 -0
  31. package/dist/network/retry.d.ts +29 -0
  32. package/dist/network/retry.js +88 -0
  33. package/dist/network/system-proxy.d.ts +18 -0
  34. package/dist/network/system-proxy.js +175 -0
  35. package/dist/provider-anthropic.d.ts +1 -0
  36. package/dist/provider-anthropic.js +127 -52
  37. package/dist/provider-openai-codex.js +19 -29
  38. package/dist/session-log.js +3 -3
  39. package/dist/slash-commands/commands.js +84 -0
  40. package/dist/slash-commands/types.d.ts +2 -0
  41. package/dist/tools/edit-apply.js +63 -3
  42. package/dist/tools/edit.js +4 -4
  43. package/dist/tui/display-history.d.ts +4 -3
  44. package/dist/tui/display-history.js +34 -57
  45. package/dist/tui/display-sanitizer.d.ts +3 -0
  46. package/dist/tui/display-sanitizer.js +38 -0
  47. package/dist/tui/paste-placeholder.d.ts +1 -0
  48. package/dist/tui/paste-placeholder.js +7 -0
  49. package/dist/tui/run.d.ts +2 -0
  50. package/dist/tui/run.js +260 -155
  51. package/dist/tui/trace-groups.js +40 -4
  52. package/dist/tui/wordmark.d.ts +1 -0
  53. package/dist/tui/wordmark.js +56 -54
  54. package/dist/tui-ink/app.js +2 -1
  55. package/dist/tui-ink/trace-groups.js +40 -4
  56. package/dist/tui-opentui/app.js +2 -1
  57. package/dist/tui-opentui/trace-groups.js +40 -4
  58. package/dist/types.d.ts +27 -0
  59. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
- import { execFileSync } from "node:child_process";
2
1
  import { readFileSync } from "node:fs";
3
2
  import { delimiter } from "node:path";
4
3
  import { rootCertificates } from "node:tls";
5
4
  import { Agent, ProxyAgent } from "undici";
5
+ import { getSystemProxyForUrl } from "./system-proxy.js";
6
6
  let cachedDefaultFetch;
7
7
  export function chatGptFetch(input, init) {
8
8
  return getChatGptFetch()(input, init);
@@ -20,8 +20,9 @@ export function getChatGptFetch(env = process.env) {
20
20
  export function createChatGptFetch(options = {}) {
21
21
  const env = options.env ?? process.env;
22
22
  const fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
23
+ const dispatcher = createChatGptDispatcher(env);
23
24
  return async (input, init) => {
24
- const requestInit = withChatGptNetworkOptions(input, init, env);
25
+ const requestInit = withChatGptNetworkOptions(input, init, env, dispatcher);
25
26
  try {
26
27
  return await fetchImpl(input, requestInit);
27
28
  }
@@ -62,10 +63,15 @@ export function withChatGptNetworkOptions(input, init, env = process.env, dispat
62
63
  return next;
63
64
  }
64
65
  export function normalizeChatGptNetworkError(error, env = process.env) {
66
+ // Already normalized — wrapping again would nest "Original error:" messages.
67
+ if (error instanceof Error && error.message.includes("connection failed before Bubble received a response")) {
68
+ return error;
69
+ }
65
70
  const text = errorMessageChain(error).join("\n");
66
71
  if (!isChatGptNetworkErrorText(text)) {
67
72
  return error instanceof Error ? error : new Error(String(error));
68
73
  }
74
+ const systemProxy = hasProxyEnv(env) ? undefined : getSystemProxyForUrl(new URL("https://chatgpt.com/"), env);
69
75
  const message = [
70
76
  "ChatGPT connection failed before Bubble received a response.",
71
77
  isCertificateErrorText(text)
@@ -73,8 +79,8 @@ export function normalizeChatGptNetworkError(error, env = process.env) {
73
79
  : "This looks like a proxy or network transport failure.",
74
80
  hasProxyEnv(env)
75
81
  ? "Bubble is using proxy environment variables for ChatGPT requests. Make sure NO_PROXY includes localhost,127.0.0.1."
76
- : hasMacSystemProxy(env)
77
- ? "Bubble detected the macOS system proxy for ChatGPT requests. If it is stale, restart your proxy client or set BUBBLE_DISABLE_SYSTEM_PROXY=1 and configure HTTPS_PROXY explicitly."
82
+ : systemProxy
83
+ ? `Bubble is routing this request through the OS system proxy at ${systemProxy} (detected automatically). Check that the proxy app is running and healthy, or set BUBBLE_SYSTEM_PROXY=0 to disable system proxy detection.`
78
84
  : "If your network requires a proxy, set HTTPS_PROXY or HTTP_PROXY, and set NO_PROXY=localhost,127.0.0.1.",
79
85
  "Do not disable TLS verification with NODE_TLS_REJECT_UNAUTHORIZED=0.",
80
86
  `Original error: ${firstMeaningfulErrorMessage(error) || "unknown network error"}`,
@@ -82,135 +88,46 @@ export function normalizeChatGptNetworkError(error, env = process.env) {
82
88
  return new Error(message, { cause: error });
83
89
  }
84
90
  function hasProxyEnv(env) {
85
- return Boolean(env.BUBBLE_CHATGPT_PROXY ||
86
- env.bubble_chatgpt_proxy ||
87
- env.HTTPS_PROXY ||
88
- env.https_proxy ||
89
- env.HTTP_PROXY ||
90
- env.http_proxy ||
91
- env.ALL_PROXY ||
92
- env.all_proxy);
91
+ return Boolean(env.BUBBLE_CHATGPT_PROXY || env.bubble_chatgpt_proxy
92
+ || env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy || env.ALL_PROXY || env.all_proxy);
93
+ }
94
+ function chatGptProxyOverride(env) {
95
+ return env.BUBBLE_CHATGPT_PROXY ?? env.bubble_chatgpt_proxy;
93
96
  }
94
97
  function isBunRuntime() {
95
98
  return typeof globalThis.Bun !== "undefined";
96
99
  }
97
100
  function bunProxyForUrl(input, env) {
98
- return proxyForUrl(input, env);
99
- }
100
- function nodeProxyForUrl(input, env) {
101
- return proxyForUrl(input, env);
102
- }
103
- function proxyForUrl(input, env) {
104
101
  const url = urlFromInput(input);
105
102
  if (!url || shouldBypassProxy(url, env))
106
103
  return undefined;
107
- const explicit = explicitProxyForUrl(url, env);
108
- return explicit ?? macSystemProxyForUrl(url, env);
109
- }
110
- function defaultNodeProxy(env) {
111
- return (env.BUBBLE_CHATGPT_PROXY ??
112
- env.bubble_chatgpt_proxy ??
113
- env.HTTPS_PROXY ??
114
- env.https_proxy ??
115
- env.HTTP_PROXY ??
116
- env.http_proxy ??
117
- env.ALL_PROXY ??
118
- env.all_proxy ??
119
- macSystemProxyForUrl(new URL("https://chatgpt.com/"), env));
120
- }
121
- function explicitProxyForUrl(url, env) {
122
- const chatGptProxy = env.BUBBLE_CHATGPT_PROXY ?? env.bubble_chatgpt_proxy;
123
- if (chatGptProxy)
124
- return chatGptProxy;
104
+ const override = chatGptProxyOverride(env);
105
+ if (override)
106
+ return override;
125
107
  const allProxy = env.ALL_PROXY ?? env.all_proxy;
126
108
  if (url.protocol === "https:")
127
- return env.HTTPS_PROXY ?? env.https_proxy ?? allProxy;
109
+ return env.HTTPS_PROXY ?? env.https_proxy ?? allProxy ?? getSystemProxyForUrl(url, env);
128
110
  if (url.protocol === "http:")
129
- return env.HTTP_PROXY ?? env.http_proxy ?? allProxy;
130
- return allProxy;
131
- }
132
- function hasMacSystemProxy(env) {
133
- return Boolean(macSystemProxyForUrl(new URL("https://chatgpt.com/"), env));
134
- }
135
- function macSystemProxyForUrl(url, env) {
136
- if (process.platform !== "darwin" || truthyEnv(env.BUBBLE_DISABLE_SYSTEM_PROXY))
137
- return undefined;
138
- const output = readMacSystemProxy();
139
- return output ? parseMacSystemProxyForUrl(output, url) : undefined;
140
- }
141
- export function parseMacSystemProxyForUrl(output, url) {
142
- if (macProxyExceptionMatches(output, url.hostname, url.port))
143
- return undefined;
144
- const values = parseScutilProxyValues(output);
145
- if (url.protocol === "https:") {
146
- return formatMacProxy(values.HTTPSEnable, values.HTTPSProxy, values.HTTPSPort);
147
- }
148
- if (url.protocol === "http:") {
149
- return formatMacProxy(values.HTTPEnable, values.HTTPProxy, values.HTTPPort);
150
- }
111
+ return env.HTTP_PROXY ?? env.http_proxy ?? allProxy ?? getSystemProxyForUrl(url, env);
151
112
  return undefined;
152
113
  }
153
- function parseScutilProxyValues(output) {
154
- const values = {};
155
- for (const line of output.split(/\r?\n/)) {
156
- const match = line.match(/^\s*([A-Za-z]+(?:Enable|Proxy|Port))\s*:\s*(.+?)\s*$/);
157
- if (match)
158
- values[match[1]] = match[2];
159
- }
160
- return values;
161
- }
162
- function formatMacProxy(enabled, host, port) {
163
- if (enabled !== "1" || !host || !port)
114
+ function nodeProxyForUrl(input, env) {
115
+ const url = urlFromInput(input);
116
+ if (!url || shouldBypassProxy(url, env))
164
117
  return undefined;
165
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(host))
166
- return host;
167
- const normalizedHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
168
- return `http://${normalizedHost}:${port}`;
169
- }
170
- function macProxyExceptionMatches(output, hostname, port) {
171
- const exceptions = parseMacProxyExceptions(output);
172
- return exceptions.some((entry) => noProxyEntryMatches(entry.toLowerCase(), hostname.toLowerCase(), port));
173
- }
174
- function parseMacProxyExceptions(output) {
175
- const exceptions = [];
176
- let inExceptions = false;
177
- for (const line of output.split(/\r?\n/)) {
178
- if (line.includes("ExceptionsList")) {
179
- inExceptions = true;
180
- continue;
181
- }
182
- if (!inExceptions)
183
- continue;
184
- if (/^\s*}\s*$/.test(line))
185
- break;
186
- const match = line.match(/^\s*\d+\s*:\s*(.+?)\s*$/);
187
- if (match)
188
- exceptions.push(match[1].trim());
189
- }
190
- return exceptions;
191
- }
192
- let cachedMacSystemProxy;
193
- function readMacSystemProxy() {
194
- const now = Date.now();
195
- if (cachedMacSystemProxy && now - cachedMacSystemProxy.checkedAtMs < 5_000) {
196
- return cachedMacSystemProxy.output;
197
- }
198
- let output;
199
- try {
200
- output = execFileSync("scutil", ["--proxy"], {
201
- encoding: "utf-8",
202
- stdio: ["ignore", "pipe", "ignore"],
203
- timeout: 1_000,
204
- });
205
- }
206
- catch {
207
- output = undefined;
208
- }
209
- cachedMacSystemProxy = { checkedAtMs: now, output };
210
- return output;
118
+ const override = chatGptProxyOverride(env);
119
+ if (override)
120
+ return override;
121
+ if (url.protocol === "https:")
122
+ return env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy ?? getSystemProxyForUrl(url, env);
123
+ if (url.protocol === "http:")
124
+ return env.HTTP_PROXY ?? env.http_proxy ?? env.ALL_PROXY ?? env.all_proxy ?? getSystemProxyForUrl(url, env);
125
+ return defaultNodeProxy(env);
211
126
  }
212
- function truthyEnv(value) {
213
- return /^(1|true|yes|on)$/i.test(value?.trim() ?? "");
127
+ function defaultNodeProxy(env) {
128
+ return chatGptProxyOverride(env)
129
+ ?? env.HTTPS_PROXY ?? env.https_proxy ?? env.HTTP_PROXY ?? env.http_proxy ?? env.ALL_PROXY ?? env.all_proxy
130
+ ?? getSystemProxyForUrl(new URL("https://system-proxy-default.invalid/"), env);
214
131
  }
215
132
  function bunExtraCaFiles(env) {
216
133
  const bun = globalThis.Bun;
@@ -274,19 +191,21 @@ function extraCaCertificatePaths(env) {
274
191
  }
275
192
  function networkEnvSignature(env) {
276
193
  return [
194
+ env.BUBBLE_CHATGPT_PROXY,
195
+ env.bubble_chatgpt_proxy,
277
196
  env.HTTP_PROXY,
278
197
  env.http_proxy,
279
198
  env.HTTPS_PROXY,
280
199
  env.https_proxy,
281
200
  env.ALL_PROXY,
282
201
  env.all_proxy,
283
- env.BUBBLE_CHATGPT_PROXY,
284
- env.bubble_chatgpt_proxy,
285
- env.BUBBLE_DISABLE_SYSTEM_PROXY,
286
202
  env.NO_PROXY,
287
203
  env.no_proxy,
288
204
  env.NODE_EXTRA_CA_CERTS,
289
205
  env.BUBBLE_EXTRA_CA_CERTS,
206
+ // Invalidate the cached fetch when the user toggles the OS proxy
207
+ // (e.g. turning Clash system proxy on/off mid-session).
208
+ getSystemProxyForUrl(new URL("https://chatgpt.com/"), env),
290
209
  ].join("\0");
291
210
  }
292
211
  function isChatGptNetworkErrorText(text) {
@@ -0,0 +1,32 @@
1
+ import { type Dispatcher } from "undici";
2
+ export type ProviderFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
3
+ export interface ProviderFetchOptions {
4
+ providerName: string;
5
+ fetch?: ProviderFetch;
6
+ env?: NodeJS.ProcessEnv;
7
+ verboseEnvVar?: string;
8
+ }
9
+ type RequestInitWithProviderOptions = RequestInit & {
10
+ dispatcher?: Dispatcher;
11
+ proxy?: string;
12
+ tls?: {
13
+ ca?: unknown[];
14
+ };
15
+ verbose?: boolean;
16
+ };
17
+ export declare function providerFetch(input: RequestInfo | URL, init: RequestInit | undefined, options: ProviderFetchOptions): Promise<Response>;
18
+ export declare function createProviderFetch(options: ProviderFetchOptions): ProviderFetch;
19
+ export declare function createProviderDispatcher(env?: NodeJS.ProcessEnv, input?: RequestInfo | URL, providerName?: string): Dispatcher | undefined;
20
+ export declare function withProviderNetworkOptions(input: RequestInfo | URL, init: RequestInit | undefined, options?: {
21
+ env?: NodeJS.ProcessEnv;
22
+ providerName?: string;
23
+ verboseEnvVar?: string;
24
+ }): RequestInitWithProviderOptions;
25
+ export declare function normalizeProviderNetworkError(error: unknown, options: {
26
+ providerName: string;
27
+ input?: RequestInfo | URL;
28
+ env?: NodeJS.ProcessEnv;
29
+ }): Error;
30
+ export declare function isProviderTransportError(error: unknown): boolean;
31
+ export declare function shouldEnableFetchVerbose(env?: NodeJS.ProcessEnv, providerVerboseEnvVar?: string): boolean;
32
+ export {};
@@ -0,0 +1,265 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { delimiter } from "node:path";
3
+ import { rootCertificates } from "node:tls";
4
+ import { Agent, ProxyAgent } from "undici";
5
+ import { getSystemProxyForUrl } from "./system-proxy.js";
6
+ export function providerFetch(input, init, options) {
7
+ return createProviderFetch(options)(input, init);
8
+ }
9
+ export function createProviderFetch(options) {
10
+ const env = options.env ?? process.env;
11
+ const fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
12
+ return async (input, init) => {
13
+ try {
14
+ const requestInit = withProviderNetworkOptions(input, init, {
15
+ env,
16
+ providerName: options.providerName,
17
+ verboseEnvVar: options.verboseEnvVar,
18
+ });
19
+ return await fetchImpl(input, requestInit);
20
+ }
21
+ catch (error) {
22
+ throw normalizeProviderNetworkError(error, {
23
+ providerName: options.providerName,
24
+ input,
25
+ env,
26
+ });
27
+ }
28
+ };
29
+ }
30
+ export function createProviderDispatcher(env = process.env, input, providerName = "provider") {
31
+ if (isBunRuntime())
32
+ return undefined;
33
+ const ca = loadExtraCaCertificates(env, providerName);
34
+ const proxy = input ? nodeProxyForUrl(input, env) : defaultNodeProxy(env);
35
+ if (!proxy && ca.length === 0)
36
+ return undefined;
37
+ const caOptions = ca.length > 0 ? { ca: [...rootCertificates, ...ca] } : undefined;
38
+ if (proxy) {
39
+ return new ProxyAgent({
40
+ uri: proxy,
41
+ ...(caOptions ? { requestTls: caOptions, proxyTls: caOptions } : {}),
42
+ });
43
+ }
44
+ return caOptions ? new Agent({ connect: caOptions }) : undefined;
45
+ }
46
+ export function withProviderNetworkOptions(input, init, options = {}) {
47
+ const env = options.env ?? process.env;
48
+ const providerName = options.providerName ?? "provider";
49
+ const next = { ...(init ?? {}) };
50
+ if (isBunRuntime()) {
51
+ const proxy = bunProxyForUrl(input, env);
52
+ if (proxy)
53
+ next.proxy = proxy;
54
+ const ca = bunExtraCaFiles(env);
55
+ if (ca.length > 0)
56
+ next.tls = { ...(next.tls ?? {}), ca };
57
+ }
58
+ else {
59
+ const dispatcher = createProviderDispatcher(env, input, providerName);
60
+ if (dispatcher)
61
+ next.dispatcher = dispatcher;
62
+ }
63
+ if (shouldEnableFetchVerbose(env, options.verboseEnvVar)) {
64
+ next.verbose = true;
65
+ }
66
+ return next;
67
+ }
68
+ export function normalizeProviderNetworkError(error, options) {
69
+ const env = options.env ?? process.env;
70
+ // Already normalized (e.g. by createProviderFetch) — wrapping again would
71
+ // nest "Original error:" messages.
72
+ if (error instanceof Error && error.message.includes("connection failed before Bubble received a response")) {
73
+ return error;
74
+ }
75
+ const text = errorMessageChain(error).join("\n");
76
+ if (!isProviderNetworkErrorText(text)) {
77
+ return error instanceof Error ? error : new Error(String(error));
78
+ }
79
+ const origin = originFromInput(options.input);
80
+ const providerLabel = options.providerName || "provider";
81
+ const systemProxy = hasProxyEnv(env) ? undefined : getSystemProxyForUrl(options.input ? urlFromInput(options.input) : undefined, env);
82
+ const message = [
83
+ `${providerLabel} connection failed before Bubble received a response.`,
84
+ origin ? `Target origin: ${origin}.` : undefined,
85
+ isCertificateErrorText(text)
86
+ ? "TLS certificate verification failed. If you are on a corporate proxy, VPN, or HTTPS inspection network, start Bubble with NODE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem or BUBBLE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem."
87
+ : "This looks like a proxy or network transport failure.",
88
+ hasProxyEnv(env)
89
+ ? "Bubble is using proxy environment variables for provider requests. Make sure NO_PROXY includes localhost,127.0.0.1 and any direct-connect hosts."
90
+ : systemProxy
91
+ ? `Bubble is routing this request through the OS system proxy at ${systemProxy} (detected automatically). Check that the proxy app is running and healthy, or set BUBBLE_SYSTEM_PROXY=0 to disable system proxy detection.`
92
+ : "If your network requires a proxy, set HTTPS_PROXY or HTTP_PROXY, and set NO_PROXY=localhost,127.0.0.1.",
93
+ hasCustomCaEnv(env)
94
+ ? "A custom CA environment variable is configured."
95
+ : "No custom CA environment variable is configured.",
96
+ "Do not disable TLS verification with NODE_TLS_REJECT_UNAUTHORIZED=0.",
97
+ `Original error: ${firstMeaningfulErrorMessage(error) || "unknown network error"}`,
98
+ ].filter(Boolean).join(" ");
99
+ return new Error(message, { cause: error });
100
+ }
101
+ export function isProviderTransportError(error) {
102
+ return isProviderNetworkErrorText(errorMessageChain(error).join("\n"));
103
+ }
104
+ export function shouldEnableFetchVerbose(env = process.env, providerVerboseEnvVar) {
105
+ const providerValue = providerVerboseEnvVar ? env[providerVerboseEnvVar] : undefined;
106
+ return isTruthyEnv(providerValue) || isTruthyEnv(env.BUBBLE_PROVIDER_FETCH_VERBOSE);
107
+ }
108
+ function hasProxyEnv(env) {
109
+ return Boolean(env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy || env.ALL_PROXY || env.all_proxy);
110
+ }
111
+ function hasCustomCaEnv(env) {
112
+ return Boolean(env.NODE_EXTRA_CA_CERTS?.trim() || env.BUBBLE_EXTRA_CA_CERTS?.trim());
113
+ }
114
+ function isBunRuntime() {
115
+ return typeof globalThis.Bun !== "undefined";
116
+ }
117
+ function bunProxyForUrl(input, env) {
118
+ const url = urlFromInput(input);
119
+ if (!url || shouldBypassProxy(url, env))
120
+ return undefined;
121
+ const allProxy = env.ALL_PROXY ?? env.all_proxy;
122
+ if (url.protocol === "https:")
123
+ return env.HTTPS_PROXY ?? env.https_proxy ?? allProxy ?? getSystemProxyForUrl(url, env);
124
+ if (url.protocol === "http:")
125
+ return env.HTTP_PROXY ?? env.http_proxy ?? allProxy ?? getSystemProxyForUrl(url, env);
126
+ return undefined;
127
+ }
128
+ function nodeProxyForUrl(input, env) {
129
+ const url = urlFromInput(input);
130
+ if (!url || shouldBypassProxy(url, env))
131
+ return undefined;
132
+ if (url.protocol === "https:")
133
+ return env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy ?? getSystemProxyForUrl(url, env);
134
+ if (url.protocol === "http:")
135
+ return env.HTTP_PROXY ?? env.http_proxy ?? env.ALL_PROXY ?? env.all_proxy ?? getSystemProxyForUrl(url, env);
136
+ return undefined;
137
+ }
138
+ function defaultNodeProxy(env) {
139
+ return env.HTTPS_PROXY ?? env.https_proxy ?? env.HTTP_PROXY ?? env.http_proxy ?? env.ALL_PROXY ?? env.all_proxy
140
+ ?? getSystemProxyForUrl(new URL("https://system-proxy-default.invalid/"), env);
141
+ }
142
+ function bunExtraCaFiles(env) {
143
+ const bun = globalThis.Bun;
144
+ if (!bun?.file)
145
+ return [];
146
+ return extraCaCertificatePaths(env).map((path) => bun.file(path));
147
+ }
148
+ function urlFromInput(input) {
149
+ if (input instanceof URL)
150
+ return input;
151
+ if (typeof input === "string")
152
+ return URL.canParse(input) ? new URL(input) : undefined;
153
+ const url = input.url;
154
+ return URL.canParse(url) ? new URL(url) : undefined;
155
+ }
156
+ function originFromInput(input) {
157
+ if (!input)
158
+ return undefined;
159
+ return urlFromInput(input)?.origin;
160
+ }
161
+ function shouldBypassProxy(url, env) {
162
+ const noProxy = (env.NO_PROXY ?? env.no_proxy ?? "").trim();
163
+ if (!noProxy)
164
+ return false;
165
+ if (noProxy === "*")
166
+ return true;
167
+ const hostname = url.hostname.toLowerCase();
168
+ const port = url.port;
169
+ return noProxy
170
+ .split(/[,\s]+/)
171
+ .filter(Boolean)
172
+ .some((entry) => noProxyEntryMatches(entry.toLowerCase(), hostname, port));
173
+ }
174
+ function noProxyEntryMatches(entry, hostname, port) {
175
+ const [entryHost, entryPort] = entry.includes(":") ? entry.split(":") : [entry, ""];
176
+ if (entryPort && entryPort !== port)
177
+ return false;
178
+ if (entryHost === hostname)
179
+ return true;
180
+ if (entryHost.startsWith("*."))
181
+ return hostname.endsWith(entryHost.slice(1));
182
+ if (entryHost.startsWith("."))
183
+ return hostname.endsWith(entryHost);
184
+ return false;
185
+ }
186
+ function loadExtraCaCertificates(env, providerName) {
187
+ const paths = extraCaCertificatePaths(env);
188
+ return paths.map((path) => {
189
+ try {
190
+ return readFileSync(path, "utf-8");
191
+ }
192
+ catch (error) {
193
+ throw new Error(`Failed to read ${providerName} custom CA certificate at ${path}. Check NODE_EXTRA_CA_CERTS or BUBBLE_EXTRA_CA_CERTS.`, {
194
+ cause: error,
195
+ });
196
+ }
197
+ });
198
+ }
199
+ function extraCaCertificatePaths(env) {
200
+ const bubbleValue = env.BUBBLE_EXTRA_CA_CERTS?.trim();
201
+ if (bubbleValue) {
202
+ return bubbleValue.split(delimiter).map((item) => item.trim()).filter(Boolean);
203
+ }
204
+ const nodeValue = env.NODE_EXTRA_CA_CERTS?.trim();
205
+ return nodeValue ? [nodeValue] : [];
206
+ }
207
+ function isProviderNetworkErrorText(text) {
208
+ return [
209
+ /fetch failed/i,
210
+ /network.*failed/i,
211
+ /socket connection was closed unexpectedly/i,
212
+ /\bConnectionClosed\b/i,
213
+ /\bECONNRESET\b/i,
214
+ /\bECONNREFUSED\b/i,
215
+ /\bETIMEDOUT\b/i,
216
+ /\bEPIPE\b/i,
217
+ /\bUND_ERR_/i,
218
+ /socket hang up/i,
219
+ /Unable to connect\. Is the computer able to access the url\?/i,
220
+ /certificate/i,
221
+ /unable to verify/i,
222
+ /self[- ]signed/i,
223
+ ].some((pattern) => pattern.test(text));
224
+ }
225
+ function isCertificateErrorText(text) {
226
+ return [
227
+ /unknown certificate verification error/i,
228
+ /certificate (?:verify|verification) (?:failed|error)/i,
229
+ /unable to verify (?:the )?(?:first )?certificate/i,
230
+ /UNABLE_TO_(?:VERIFY_LEAF_SIGNATURE|GET_ISSUER_CERT_LOCALLY)/i,
231
+ /SELF_SIGNED_CERT_IN_CHAIN/i,
232
+ /DEPTH_ZERO_SELF_SIGNED_CERT/i,
233
+ /CERT_(?:HAS_EXPIRED|UNTRUSTED|INVALID)/i,
234
+ /self[- ]signed certificate/i,
235
+ ].some((pattern) => pattern.test(text));
236
+ }
237
+ function firstMeaningfulErrorMessage(error) {
238
+ return errorMessageChain(error).find((item) => item && item !== "Error");
239
+ }
240
+ function errorMessageChain(error) {
241
+ const messages = [];
242
+ let current = error;
243
+ for (let depth = 0; current && depth < 8; depth++) {
244
+ if (current instanceof Error) {
245
+ messages.push(current.name, current.message);
246
+ current = current.cause;
247
+ continue;
248
+ }
249
+ if (typeof current === "object") {
250
+ const record = current;
251
+ for (const key of ["name", "code", "message"]) {
252
+ if (typeof record[key] === "string")
253
+ messages.push(record[key]);
254
+ }
255
+ current = record.cause;
256
+ continue;
257
+ }
258
+ messages.push(String(current));
259
+ break;
260
+ }
261
+ return messages;
262
+ }
263
+ function isTruthyEnv(value) {
264
+ return /^(1|true|yes)$/i.test(value ?? "");
265
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared retry policy for provider transports.
3
+ *
4
+ * Connection-level failures (nothing received yet) and retryable HTTP
5
+ * statuses are retried inside the provider with exponential backoff.
6
+ * Mid-stream interruptions (content already surfaced to the UI) are
7
+ * signalled with ProviderStreamInterruptedError so the agent loop can
8
+ * discard the partial assistant message and re-issue the whole request.
9
+ */
10
+ export declare const MAX_STREAM_INTERRUPTION_RETRIES = 2;
11
+ export declare class ProviderStreamInterruptedError extends Error {
12
+ readonly isProviderStreamInterruption = true;
13
+ constructor(message: string, options?: {
14
+ cause?: unknown;
15
+ });
16
+ }
17
+ export declare function isProviderStreamInterruption(error: unknown): boolean;
18
+ export declare function getProviderMaxRetries(env?: NodeJS.ProcessEnv): number;
19
+ export declare function isRetryableHttpStatus(status: number): boolean;
20
+ /**
21
+ * Equal-jitter exponential backoff: attempt 1 → 0.5-1s, 2 → 1-2s, 3 → 2-4s,
22
+ * 4 → 4-8s, capped at 32s. A retry-after hint from the server wins (capped
23
+ * at 60s) since it reflects actual load shedding.
24
+ */
25
+ export declare function computeRetryDelayMs(attempt: number, options?: {
26
+ retryAfterMs?: number;
27
+ }): number;
28
+ export declare function retryAfterMsFromResponse(response: Response): number | undefined;
29
+ export declare function sleepBeforeRetry(ms: number, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Shared retry policy for provider transports.
3
+ *
4
+ * Connection-level failures (nothing received yet) and retryable HTTP
5
+ * statuses are retried inside the provider with exponential backoff.
6
+ * Mid-stream interruptions (content already surfaced to the UI) are
7
+ * signalled with ProviderStreamInterruptedError so the agent loop can
8
+ * discard the partial assistant message and re-issue the whole request.
9
+ */
10
+ const DEFAULT_MAX_RETRIES = 4;
11
+ const MAX_CONFIGURABLE_RETRIES = 10;
12
+ const BASE_DELAY_MS = 1000;
13
+ const MAX_DELAY_MS = 32_000;
14
+ const MAX_RETRY_AFTER_MS = 60_000;
15
+ export const MAX_STREAM_INTERRUPTION_RETRIES = 2;
16
+ const RETRYABLE_HTTP_STATUSES = new Set([408, 429, 500, 502, 503, 504, 529]);
17
+ export class ProviderStreamInterruptedError extends Error {
18
+ isProviderStreamInterruption = true;
19
+ constructor(message, options) {
20
+ super(message, options);
21
+ this.name = "ProviderStreamInterruptedError";
22
+ }
23
+ }
24
+ export function isProviderStreamInterruption(error) {
25
+ return !!error
26
+ && typeof error === "object"
27
+ && error.isProviderStreamInterruption === true;
28
+ }
29
+ export function getProviderMaxRetries(env = process.env) {
30
+ const raw = env.BUBBLE_PROVIDER_MAX_RETRIES?.trim();
31
+ if (!raw)
32
+ return DEFAULT_MAX_RETRIES;
33
+ const value = Number(raw);
34
+ if (!Number.isInteger(value) || value < 0)
35
+ return DEFAULT_MAX_RETRIES;
36
+ return Math.min(value, MAX_CONFIGURABLE_RETRIES);
37
+ }
38
+ export function isRetryableHttpStatus(status) {
39
+ return RETRYABLE_HTTP_STATUSES.has(status);
40
+ }
41
+ /**
42
+ * Equal-jitter exponential backoff: attempt 1 → 0.5-1s, 2 → 1-2s, 3 → 2-4s,
43
+ * 4 → 4-8s, capped at 32s. A retry-after hint from the server wins (capped
44
+ * at 60s) since it reflects actual load shedding.
45
+ */
46
+ export function computeRetryDelayMs(attempt, options) {
47
+ if (process.env.NODE_ENV === "test")
48
+ return 0;
49
+ if (options?.retryAfterMs !== undefined && options.retryAfterMs >= 0) {
50
+ return Math.min(options.retryAfterMs, MAX_RETRY_AFTER_MS);
51
+ }
52
+ const cap = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** Math.max(0, attempt - 1));
53
+ return Math.floor(cap / 2 + Math.random() * (cap / 2));
54
+ }
55
+ export function retryAfterMsFromResponse(response) {
56
+ const header = response.headers.get("retry-after")?.trim();
57
+ if (!header)
58
+ return undefined;
59
+ const seconds = Number(header);
60
+ if (Number.isFinite(seconds) && seconds >= 0)
61
+ return Math.round(seconds * 1000);
62
+ const date = Date.parse(header);
63
+ if (!Number.isNaN(date))
64
+ return Math.max(0, date - Date.now());
65
+ return undefined;
66
+ }
67
+ export function sleepBeforeRetry(ms, signal) {
68
+ if (signal?.aborted)
69
+ return Promise.reject(toAbortError(signal));
70
+ return new Promise((resolve, reject) => {
71
+ const onAbort = () => {
72
+ clearTimeout(timeout);
73
+ reject(toAbortError(signal));
74
+ };
75
+ const timeout = setTimeout(() => {
76
+ signal?.removeEventListener("abort", onAbort);
77
+ resolve();
78
+ }, ms);
79
+ signal?.addEventListener("abort", onAbort, { once: true });
80
+ });
81
+ }
82
+ function toAbortError(signal) {
83
+ if (signal?.reason instanceof Error)
84
+ return signal.reason;
85
+ const error = new Error(typeof signal?.reason === "string" ? signal.reason : "Request retry aborted.");
86
+ error.name = "AbortError";
87
+ return error;
88
+ }
@@ -0,0 +1,18 @@
1
+ export interface SystemProxySettings {
2
+ httpProxy?: string;
3
+ httpsProxy?: string;
4
+ exceptions: string[];
5
+ }
6
+ /**
7
+ * Resolves the OS-level proxy for a request URL. Used as a fallback when no
8
+ * proxy environment variables are set, so Bubble follows the same proxy that
9
+ * browsers and other GUI apps use (e.g. Clash/Surge "system proxy" mode).
10
+ * Reads `scutil --proxy` on macOS and the Internet Settings registry key on
11
+ * Windows. Returns undefined on other platforms, when disabled via
12
+ * BUBBLE_SYSTEM_PROXY=0, or when the URL matches the OS proxy bypass list.
13
+ */
14
+ export declare function getSystemProxyForUrl(url: URL | undefined, env?: NodeJS.ProcessEnv): string | undefined;
15
+ export declare function systemProxyForUrl(url: URL, settings: SystemProxySettings): string | undefined;
16
+ export declare function parseScutilProxyOutput(output: string): SystemProxySettings | undefined;
17
+ export declare function parseWindowsProxyOutput(output: string): SystemProxySettings | undefined;
18
+ export declare function resetSystemProxyCacheForTests(): void;