@bubblebrain-ai/bubble 0.0.19 → 0.0.21
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/agent/internal-reminder-sanitizer.d.ts +1 -0
- package/dist/agent/internal-reminder-sanitizer.js +46 -0
- package/dist/agent.d.ts +10 -0
- package/dist/agent.js +310 -18
- package/dist/approval/controller.d.ts +6 -0
- package/dist/approval/controller.js +104 -11
- package/dist/checkpoints.d.ts +57 -0
- package/dist/checkpoints.js +0 -0
- package/dist/debug-trace.js +4 -0
- package/dist/feishu/agent-host/run-driver.js +29 -0
- package/dist/hooks/config.d.ts +9 -0
- package/dist/hooks/config.js +278 -0
- package/dist/hooks/controller.d.ts +24 -0
- package/dist/hooks/controller.js +254 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/log.d.ts +14 -0
- package/dist/hooks/log.js +54 -0
- package/dist/hooks/runner.d.ts +5 -0
- package/dist/hooks/runner.js +225 -0
- package/dist/hooks/trust.d.ts +37 -0
- package/dist/hooks/trust.js +143 -0
- package/dist/hooks/types.d.ts +173 -0
- package/dist/hooks/types.js +46 -0
- package/dist/main.js +86 -13
- package/dist/memory/prompts.js +3 -1
- package/dist/model-catalog.js +2 -0
- package/dist/model-pricing.js +8 -0
- package/dist/network/chatgpt-transport.d.ts +0 -1
- package/dist/network/chatgpt-transport.js +40 -121
- package/dist/network/provider-transport.d.ts +32 -0
- package/dist/network/provider-transport.js +265 -0
- package/dist/network/retry.d.ts +29 -0
- package/dist/network/retry.js +88 -0
- package/dist/network/system-proxy.d.ts +18 -0
- package/dist/network/system-proxy.js +175 -0
- package/dist/provider-anthropic.d.ts +1 -0
- package/dist/provider-anthropic.js +127 -52
- package/dist/provider-openai-codex.js +19 -29
- package/dist/session-log.js +3 -3
- package/dist/session.d.ts +31 -0
- package/dist/session.js +69 -0
- package/dist/slash-commands/commands.js +164 -0
- package/dist/slash-commands/types.d.ts +6 -0
- package/dist/tools/bash.js +4 -0
- package/dist/tools/edit-apply.js +63 -3
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +6 -5
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/write.d.ts +2 -1
- package/dist/tools/write.js +2 -1
- package/dist/tui/display-history.d.ts +4 -3
- package/dist/tui/display-history.js +34 -57
- package/dist/tui/display-sanitizer.d.ts +3 -0
- package/dist/tui/display-sanitizer.js +38 -0
- package/dist/tui/image-paste.d.ts +18 -0
- package/dist/tui/image-paste.js +60 -0
- package/dist/tui/paste-placeholder.d.ts +1 -0
- package/dist/tui/paste-placeholder.js +7 -0
- package/dist/tui/run.d.ts +2 -0
- package/dist/tui/run.js +568 -223
- package/dist/tui/trace-groups.d.ts +16 -0
- package/dist/tui/trace-groups.js +82 -5
- package/dist/tui/transcript-scroll.d.ts +25 -0
- package/dist/tui/transcript-scroll.js +20 -0
- package/dist/tui/wordmark.d.ts +1 -0
- package/dist/tui/wordmark.js +56 -54
- package/dist/tui-ink/app.d.ts +4 -1
- package/dist/tui-ink/app.js +303 -248
- package/dist/tui-ink/display-history.d.ts +16 -1
- package/dist/tui-ink/display-history.js +50 -21
- package/dist/tui-ink/footer.d.ts +6 -12
- package/dist/tui-ink/footer.js +10 -29
- package/dist/tui-ink/image-paste.d.ts +59 -0
- package/dist/tui-ink/image-paste.js +277 -0
- package/dist/tui-ink/input-box.d.ts +26 -1
- package/dist/tui-ink/input-box.js +171 -41
- package/dist/tui-ink/message-list.d.ts +1 -1
- package/dist/tui-ink/message-list.js +46 -29
- package/dist/tui-ink/run.d.ts +7 -2
- package/dist/tui-ink/run.js +73 -23
- package/dist/tui-ink/terminal-mouse.d.ts +1 -0
- package/dist/tui-ink/terminal-mouse.js +4 -0
- package/dist/tui-ink/trace-groups.d.ts +16 -0
- package/dist/tui-ink/trace-groups.js +90 -6
- package/dist/tui-ink/transcript-viewport-math.d.ts +11 -0
- package/dist/tui-ink/transcript-viewport-math.js +17 -0
- package/dist/tui-ink/transcript-viewport.d.ts +24 -0
- package/dist/tui-ink/transcript-viewport.js +83 -0
- package/dist/tui-ink/welcome.d.ts +9 -7
- package/dist/tui-ink/welcome.js +7 -33
- package/dist/tui-opentui/app.js +2 -1
- package/dist/tui-opentui/trace-groups.js +40 -4
- package/dist/types.d.ts +27 -0
- 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
|
-
:
|
|
77
|
-
?
|
|
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.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
108
|
-
|
|
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
|
|
154
|
-
const
|
|
155
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return
|
|
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
|
|
213
|
-
return
|
|
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;
|