@bubblebrain-ai/bubble 0.0.14 → 0.0.16

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 (39) hide show
  1. package/README.md +24 -0
  2. package/dist/agent/discovery-barrier.d.ts +21 -0
  3. package/dist/agent/discovery-barrier.js +173 -0
  4. package/dist/agent/internal-reminder-sanitizer.d.ts +7 -0
  5. package/dist/agent/internal-reminder-sanitizer.js +171 -0
  6. package/dist/agent/task-classifier.js +23 -5
  7. package/dist/agent.js +119 -26
  8. package/dist/cli.d.ts +3 -1
  9. package/dist/cli.js +12 -0
  10. package/dist/context/projector.js +4 -3
  11. package/dist/main.js +13 -0
  12. package/dist/model-catalog.js +6 -0
  13. package/dist/model-pricing.d.ts +3 -2
  14. package/dist/model-pricing.js +8 -0
  15. package/dist/network/chatgpt-transport.d.ts +16 -0
  16. package/dist/network/chatgpt-transport.js +240 -0
  17. package/dist/oauth/openai-codex.d.ts +7 -2
  18. package/dist/oauth/openai-codex.js +7 -4
  19. package/dist/orchestrator/default-hooks.js +13 -2
  20. package/dist/orchestrator/hooks.d.ts +2 -0
  21. package/dist/provider-openai-codex.d.ts +3 -0
  22. package/dist/provider-openai-codex.js +11 -2
  23. package/dist/provider-transform.js +9 -0
  24. package/dist/reasoning-debug.js +4 -1
  25. package/dist/session-log.js +4 -1
  26. package/dist/stats/usage.d.ts +4 -0
  27. package/dist/stats/usage.js +48 -11
  28. package/dist/tools/glob.js +3 -0
  29. package/dist/tools/grep.js +7 -0
  30. package/dist/tui/run.d.ts +2 -0
  31. package/dist/tui/run.js +104 -42
  32. package/dist/tui-ink/app.js +3 -0
  33. package/dist/tui-ink/message-list.js +6 -3
  34. package/dist/tui-opentui/app.js +3 -0
  35. package/dist/tui-opentui/message-list.js +6 -3
  36. package/dist/types.d.ts +1 -1
  37. package/dist/update/index.d.ts +46 -0
  38. package/dist/update/index.js +240 -0
  39. package/package.json +2 -1
package/dist/cli.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * CLI argument parsing.
3
3
  */
4
4
  import type { PermissionMode, ThinkingLevel } from "./types.js";
5
- export type CliCommand = "default" | "serve";
5
+ export type CliCommand = "default" | "serve" | "update";
6
6
  export interface CliArgs {
7
7
  command: CliCommand;
8
8
  model?: string;
@@ -22,6 +22,8 @@ export interface CliArgs {
22
22
  killOld?: boolean;
23
23
  /** `serve` subcommand: connect then exit. */
24
24
  dryRun?: boolean;
25
+ /** `update` subcommand: only report whether an update exists, don't install. */
26
+ checkOnly?: boolean;
25
27
  }
26
28
  export declare function parseArgs(argv: string[]): CliArgs;
27
29
  export declare function printHelp(): void;
package/dist/cli.js CHANGED
@@ -14,6 +14,10 @@ export function parseArgs(argv) {
14
14
  args.command = "serve";
15
15
  startIndex = 1;
16
16
  }
17
+ else if (argv[0] === "update" || argv[0] === "upgrade") {
18
+ args.command = "update";
19
+ startIndex = 1;
20
+ }
17
21
  }
18
22
  for (let i = startIndex; i < argv.length; i++) {
19
23
  const arg = argv[i];
@@ -72,6 +76,9 @@ export function parseArgs(argv) {
72
76
  case "--dry-run":
73
77
  args.dryRun = true;
74
78
  break;
79
+ case "--check":
80
+ args.checkOnly = true;
81
+ break;
75
82
  default:
76
83
  if (!arg.startsWith("-") && !args.prompt) {
77
84
  args.prompt = arg;
@@ -85,6 +92,7 @@ export function printHelp() {
85
92
  console.log(`
86
93
  Usage:
87
94
  bubble [options] [prompt] Start interactive TUI
95
+ bubble update [--check] Update to the latest version (alias: upgrade)
88
96
  bubble serve --feishu [options] Run as a Feishu bot host
89
97
 
90
98
  Options (default):
@@ -99,8 +107,12 @@ Options (default):
99
107
  --dangerously-skip-permissions
100
108
  Enable bypass mode (auto-approve EVERY tool; disables all safety prompts)
101
109
  -p, --print Non-interactive mode (single prompt)
110
+ -v, --version Print the installed version and exit
102
111
  -h, --help Show this help
103
112
 
113
+ Options (update):
114
+ --check Only report whether a newer version exists
115
+
104
116
  Options (serve --feishu):
105
117
  --setup Force the wizard (scan QR + bind first scope)
106
118
  --kill-old Kill any conflicting bubble instance for the same App ID
@@ -1,6 +1,7 @@
1
1
  import { getContextBudget } from "./budget.js";
2
2
  import { compactCurrentTurnToolGroups, compactMessages } from "./compact.js";
3
3
  import { pruneMessages } from "./prune.js";
4
+ import { formatInternalContextBlock, formatInternalReminderBlock } from "../agent/internal-reminder-sanitizer.js";
4
5
  // Prefix-cache invariant: only the leading static system prompt is promoted to
5
6
  // the first provider message. Runtime meta reminders stay in the conversational
6
7
  // body at their original relative position, so a new per-turn reminder does not
@@ -166,14 +167,14 @@ function isEmptyAssistantMessage(message) {
166
167
  function formatMetaMessage(message) {
167
168
  switch (message.kind) {
168
169
  case "system-reminder":
169
- return `Runtime reminder:\n${message.content}`;
170
+ return formatInternalReminderBlock(message.kind, message.content);
170
171
  case "runtime-context":
171
172
  default:
172
- return `Runtime context:\n${message.content}`;
173
+ return formatInternalContextBlock(message.kind, message.content);
173
174
  }
174
175
  }
175
176
  function formatRuntimeSystemMessage(message) {
176
- return `Runtime context:\n${message.content}`;
177
+ return formatInternalContextBlock("runtime-system", message.content);
177
178
  }
178
179
  function cloneMessage(message) {
179
180
  if (message.role === "assistant") {
package/dist/main.js CHANGED
@@ -34,6 +34,16 @@ async function main() {
34
34
  printHelp();
35
35
  process.exit(0);
36
36
  }
37
+ if (process.argv.includes("-v") || process.argv.includes("--version")) {
38
+ const { getCurrentVersion } = await import("./update/index.js");
39
+ console.log(`v${getCurrentVersion()}`);
40
+ process.exit(0);
41
+ }
42
+ if (args.command === "update") {
43
+ const { runUpdateCommand } = await import("./update/index.js");
44
+ const code = await runUpdateCommand({ checkOnly: args.checkOnly });
45
+ process.exit(code);
46
+ }
37
47
  if (args.command === "serve") {
38
48
  if (args.serveHost !== "feishu") {
39
49
  console.error(chalk.red("Usage: bubble serve --feishu [--setup | --kill-old | --dry-run]"));
@@ -475,6 +485,8 @@ async function main() {
475
485
  runMemorySummary,
476
486
  runMemoryRefresh,
477
487
  };
488
+ const { getStartupUpdateNotice } = await import("./update/index.js");
489
+ const updateNotice = await getStartupUpdateNotice();
478
490
  const { runTui } = await import("./tui/run.js");
479
491
  await runTui(agent, args, {
480
492
  ...commonOptions,
@@ -482,6 +494,7 @@ async function main() {
482
494
  themeOverrides: themeConfig.overrides,
483
495
  detectedTheme,
484
496
  onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
497
+ updateNotice: updateNotice ?? undefined,
485
498
  });
486
499
  if (sessionManager) {
487
500
  printOpenTuiExitSummary(sessionManager, {
@@ -9,6 +9,7 @@ export const BUILTIN_PROVIDERS = [
9
9
  { id: "zai", name: "Z.AI", baseURL: "https://api.z.ai/api/paas/v4" },
10
10
  { id: "zai-coding-plan", name: "Z.AI Coding Plan", baseURL: "https://api.z.ai/api/coding/paas/v4" },
11
11
  { id: "alibaba", name: "Alibaba DashScope", baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1" },
12
+ { id: "stepfun", name: "StepFun Step Plan", baseURL: "https://api.stepfun.com/step_plan/v1" },
12
13
  { id: "moonshot-cn", name: "Moonshot (国内 platform.moonshot.cn)", baseURL: "https://api.moonshot.cn/v1" },
13
14
  { id: "moonshot-intl", name: "Moonshot (海外 platform.moonshot.ai)", baseURL: "https://api.moonshot.ai/v1" },
14
15
  { id: "kimi-for-coding", name: "Kimi for Coding", baseURL: "https://api.kimi.com/coding/v1" },
@@ -24,6 +25,7 @@ const GPT51_CODEX_MINI_LEVELS = ["off", "medium", "high"];
24
25
  const OPENAI_CHAT_LEVELS = ["off"];
25
26
  const TOGGLE_THINKING_LEVELS = ["off", "medium"];
26
27
  const DEEPSEEK_V4_LEVELS = ["high", "max"];
28
+ const STEPFUN_REASONING_LEVELS = ["off", "low", "medium", "high"];
27
29
  export const BUILTIN_MODELS = [
28
30
  { id: "gpt-5.5", name: "gpt-5.5", providerId: "openai-codex", reasoningLevels: ALL_OPENAI_LEVELS, contextWindow: 272000, toolOutputTokenLimit: 10000 },
29
31
  { id: "gpt-5.4", name: "gpt-5.4", providerId: "openai-codex", reasoningLevels: ALL_OPENAI_LEVELS, contextWindow: 272000 },
@@ -59,6 +61,10 @@ export const BUILTIN_MODELS = [
59
61
  { id: "glm-4.6", name: "GLM-4.6", providerId: "zai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
60
62
  { id: "qwen3.6-plus", name: "Qwen3.6 Plus", providerId: "alibaba", reasoningLevels: ["off"], contextWindow: 1048576 },
61
63
  { id: "qwen3.7-max", name: "Qwen3.7 Max", providerId: "alibaba", reasoningLevels: ["off"], contextWindow: 1048576 },
64
+ { id: "step-3.7-flash", name: "Step 3.7 Flash", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS, contextWindow: 256000 },
65
+ { id: "step-3.5-flash-2603", name: "Step 3.5 Flash 2603", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
66
+ { id: "step-3.5-flash", name: "Step 3.5 Flash", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
67
+ { id: "step-router-v1", name: "Step Router V1", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
62
68
  { id: "kimi-k2.6", name: "Kimi K2.6", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
63
69
  { id: "k2.6-code-preview", name: "Kimi K2.6 Code Preview", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
64
70
  { id: "kimi-k2.5", name: "Kimi K2.5", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
@@ -1,8 +1,9 @@
1
1
  import type { TokenUsage } from "./types.js";
2
+ export type PricingCurrency = "USD" | "CNY";
2
3
  export interface ModelPricing {
3
4
  providerId: string;
4
5
  modelId: string;
5
- currency: "USD";
6
+ currency: PricingCurrency;
6
7
  inputCacheHitPerMillion: number;
7
8
  inputCacheMissPerMillion: number;
8
9
  outputPerMillion: number;
@@ -14,7 +15,7 @@ export interface ModelPricing {
14
15
  };
15
16
  }
16
17
  export interface UsageCost {
17
- currency: "USD";
18
+ currency: PricingCurrency;
18
19
  cost: number;
19
20
  estimated: boolean;
20
21
  }
@@ -21,6 +21,14 @@ export const MODEL_PRICING = [
21
21
  outputPerMillion: 3.48,
22
22
  },
23
23
  },
24
+ {
25
+ providerId: "stepfun",
26
+ modelId: "step-3.7-flash",
27
+ currency: "CNY",
28
+ inputCacheHitPerMillion: 0.27,
29
+ inputCacheMissPerMillion: 1.35,
30
+ outputPerMillion: 8.1,
31
+ },
24
32
  ];
25
33
  export function getModelPricing(providerId, modelId) {
26
34
  return MODEL_PRICING.find((item) => item.providerId === providerId && item.modelId === modelId);
@@ -0,0 +1,16 @@
1
+ import { type Dispatcher } from "undici";
2
+ export type ChatGptFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
3
+ export interface ChatGptFetchOptions {
4
+ fetch?: ChatGptFetch;
5
+ env?: NodeJS.ProcessEnv;
6
+ }
7
+ type RequestInitWithDispatcher = RequestInit & {
8
+ dispatcher?: Dispatcher;
9
+ };
10
+ export declare function chatGptFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
11
+ export declare function getChatGptFetch(env?: NodeJS.ProcessEnv): ChatGptFetch;
12
+ export declare function createChatGptFetch(options?: ChatGptFetchOptions): ChatGptFetch;
13
+ export declare function createChatGptDispatcher(env?: NodeJS.ProcessEnv, input?: RequestInfo | URL): Dispatcher | undefined;
14
+ export declare function withChatGptNetworkOptions(input: RequestInfo | URL, init: RequestInit | undefined, env?: NodeJS.ProcessEnv, dispatcher?: Dispatcher | undefined): RequestInitWithDispatcher;
15
+ export declare function normalizeChatGptNetworkError(error: unknown, env?: NodeJS.ProcessEnv): Error;
16
+ export {};
@@ -0,0 +1,240 @@
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
+ let cachedDefaultFetch;
6
+ export function chatGptFetch(input, init) {
7
+ return getChatGptFetch()(input, init);
8
+ }
9
+ export function getChatGptFetch(env = process.env) {
10
+ const signature = networkEnvSignature(env);
11
+ if (!cachedDefaultFetch || cachedDefaultFetch.signature !== signature) {
12
+ cachedDefaultFetch = {
13
+ signature,
14
+ fetch: createChatGptFetch({ env }),
15
+ };
16
+ }
17
+ return cachedDefaultFetch.fetch;
18
+ }
19
+ export function createChatGptFetch(options = {}) {
20
+ const env = options.env ?? process.env;
21
+ const fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
22
+ const dispatcher = createChatGptDispatcher(env);
23
+ return async (input, init) => {
24
+ const requestInit = withChatGptNetworkOptions(input, init, env, dispatcher);
25
+ try {
26
+ return await fetchImpl(input, requestInit);
27
+ }
28
+ catch (error) {
29
+ throw normalizeChatGptNetworkError(error, env);
30
+ }
31
+ };
32
+ }
33
+ export function createChatGptDispatcher(env = process.env, input) {
34
+ if (isBunRuntime())
35
+ return undefined;
36
+ const ca = loadExtraCaCertificates(env);
37
+ if (!hasProxyEnv(env) && ca.length === 0)
38
+ return undefined;
39
+ const proxy = input ? nodeProxyForUrl(input, env) : defaultNodeProxy(env);
40
+ const caOptions = ca.length > 0 ? { ca: [...rootCertificates, ...ca] } : undefined;
41
+ if (proxy) {
42
+ return new ProxyAgent({
43
+ uri: proxy,
44
+ ...(caOptions ? { requestTls: caOptions, proxyTls: caOptions } : {}),
45
+ });
46
+ }
47
+ return caOptions ? new Agent({ connect: caOptions }) : undefined;
48
+ }
49
+ export function withChatGptNetworkOptions(input, init, env = process.env, dispatcher = createChatGptDispatcher(env, input)) {
50
+ const next = { ...(init ?? {}) };
51
+ if (isBunRuntime()) {
52
+ const proxy = bunProxyForUrl(input, env);
53
+ if (proxy)
54
+ next.proxy = proxy;
55
+ const ca = bunExtraCaFiles(env);
56
+ if (ca.length > 0)
57
+ next.tls = { ...(next.tls ?? {}), ca };
58
+ return next;
59
+ }
60
+ if (dispatcher)
61
+ next.dispatcher = dispatcher;
62
+ return next;
63
+ }
64
+ export function normalizeChatGptNetworkError(error, env = process.env) {
65
+ const text = errorMessageChain(error).join("\n");
66
+ if (!isChatGptNetworkErrorText(text)) {
67
+ return error instanceof Error ? error : new Error(String(error));
68
+ }
69
+ const message = [
70
+ "ChatGPT connection failed before Bubble received a response.",
71
+ isCertificateErrorText(text)
72
+ ? "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."
73
+ : "This looks like a proxy or network transport failure.",
74
+ hasProxyEnv(env)
75
+ ? "Bubble is using proxy environment variables for ChatGPT requests. Make sure NO_PROXY includes localhost,127.0.0.1."
76
+ : "If your network requires a proxy, set HTTPS_PROXY or HTTP_PROXY, and set NO_PROXY=localhost,127.0.0.1.",
77
+ "Do not disable TLS verification with NODE_TLS_REJECT_UNAUTHORIZED=0.",
78
+ `Original error: ${firstMeaningfulErrorMessage(error) || "unknown network error"}`,
79
+ ].join(" ");
80
+ return new Error(message, { cause: error });
81
+ }
82
+ function hasProxyEnv(env) {
83
+ return Boolean(env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy || env.ALL_PROXY || env.all_proxy);
84
+ }
85
+ function isBunRuntime() {
86
+ return typeof globalThis.Bun !== "undefined";
87
+ }
88
+ function bunProxyForUrl(input, env) {
89
+ const url = urlFromInput(input);
90
+ if (!url || shouldBypassProxy(url, env))
91
+ return undefined;
92
+ const allProxy = env.ALL_PROXY ?? env.all_proxy;
93
+ if (url.protocol === "https:")
94
+ return env.HTTPS_PROXY ?? env.https_proxy ?? allProxy;
95
+ if (url.protocol === "http:")
96
+ return env.HTTP_PROXY ?? env.http_proxy ?? allProxy;
97
+ return undefined;
98
+ }
99
+ function nodeProxyForUrl(input, env) {
100
+ const url = urlFromInput(input);
101
+ if (!url || shouldBypassProxy(url, env))
102
+ return undefined;
103
+ if (url.protocol === "https:")
104
+ return env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy;
105
+ if (url.protocol === "http:")
106
+ return env.HTTP_PROXY ?? env.http_proxy ?? env.ALL_PROXY ?? env.all_proxy;
107
+ return defaultNodeProxy(env);
108
+ }
109
+ function defaultNodeProxy(env) {
110
+ return env.HTTPS_PROXY ?? env.https_proxy ?? env.HTTP_PROXY ?? env.http_proxy ?? env.ALL_PROXY ?? env.all_proxy;
111
+ }
112
+ function bunExtraCaFiles(env) {
113
+ const bun = globalThis.Bun;
114
+ if (!bun?.file)
115
+ return [];
116
+ return extraCaCertificatePaths(env).map((path) => bun.file(path));
117
+ }
118
+ function urlFromInput(input) {
119
+ if (input instanceof URL)
120
+ return input;
121
+ if (typeof input === "string")
122
+ return URL.canParse(input) ? new URL(input) : undefined;
123
+ const url = input.url;
124
+ return URL.canParse(url) ? new URL(url) : undefined;
125
+ }
126
+ function shouldBypassProxy(url, env) {
127
+ const noProxy = (env.NO_PROXY ?? env.no_proxy ?? "").trim();
128
+ if (!noProxy)
129
+ return false;
130
+ if (noProxy === "*")
131
+ return true;
132
+ const hostname = url.hostname.toLowerCase();
133
+ const port = url.port;
134
+ return noProxy
135
+ .split(/[,\s]+/)
136
+ .filter(Boolean)
137
+ .some((entry) => noProxyEntryMatches(entry.toLowerCase(), hostname, port));
138
+ }
139
+ function noProxyEntryMatches(entry, hostname, port) {
140
+ const [entryHost, entryPort] = entry.includes(":") ? entry.split(":") : [entry, ""];
141
+ if (entryPort && entryPort !== port)
142
+ return false;
143
+ if (entryHost === hostname)
144
+ return true;
145
+ if (entryHost.startsWith("*."))
146
+ return hostname.endsWith(entryHost.slice(1));
147
+ if (entryHost.startsWith("."))
148
+ return hostname.endsWith(entryHost);
149
+ return false;
150
+ }
151
+ function loadExtraCaCertificates(env) {
152
+ const paths = extraCaCertificatePaths(env);
153
+ return paths.map((path) => {
154
+ try {
155
+ return readFileSync(path, "utf-8");
156
+ }
157
+ catch (error) {
158
+ throw new Error(`Failed to read ChatGPT custom CA certificate at ${path}. Check NODE_EXTRA_CA_CERTS or BUBBLE_EXTRA_CA_CERTS.`, {
159
+ cause: error,
160
+ });
161
+ }
162
+ });
163
+ }
164
+ function extraCaCertificatePaths(env) {
165
+ const bubbleValue = env.BUBBLE_EXTRA_CA_CERTS?.trim();
166
+ if (bubbleValue) {
167
+ return bubbleValue.split(delimiter).map((item) => item.trim()).filter(Boolean);
168
+ }
169
+ const nodeValue = env.NODE_EXTRA_CA_CERTS?.trim();
170
+ return nodeValue ? [nodeValue] : [];
171
+ }
172
+ function networkEnvSignature(env) {
173
+ return [
174
+ env.HTTP_PROXY,
175
+ env.http_proxy,
176
+ env.HTTPS_PROXY,
177
+ env.https_proxy,
178
+ env.ALL_PROXY,
179
+ env.all_proxy,
180
+ env.NO_PROXY,
181
+ env.no_proxy,
182
+ env.NODE_EXTRA_CA_CERTS,
183
+ env.BUBBLE_EXTRA_CA_CERTS,
184
+ ].join("\0");
185
+ }
186
+ function isChatGptNetworkErrorText(text) {
187
+ return [
188
+ /fetch failed/i,
189
+ /network.*failed/i,
190
+ /socket connection was closed unexpectedly/i,
191
+ /\bConnectionClosed\b/i,
192
+ /\bECONNRESET\b/i,
193
+ /\bECONNREFUSED\b/i,
194
+ /\bETIMEDOUT\b/i,
195
+ /\bEPIPE\b/i,
196
+ /\bUND_ERR_/i,
197
+ /socket hang up/i,
198
+ /certificate/i,
199
+ /unable to verify/i,
200
+ /self[- ]signed/i,
201
+ ].some((pattern) => pattern.test(text));
202
+ }
203
+ function isCertificateErrorText(text) {
204
+ return [
205
+ /unknown certificate verification error/i,
206
+ /certificate (?:verify|verification) (?:failed|error)/i,
207
+ /unable to verify (?:the )?(?:first )?certificate/i,
208
+ /UNABLE_TO_(?:VERIFY_LEAF_SIGNATURE|GET_ISSUER_CERT_LOCALLY)/i,
209
+ /SELF_SIGNED_CERT_IN_CHAIN/i,
210
+ /DEPTH_ZERO_SELF_SIGNED_CERT/i,
211
+ /CERT_(?:HAS_EXPIRED|UNTRUSTED|INVALID)/i,
212
+ /self[- ]signed certificate/i,
213
+ ].some((pattern) => pattern.test(text));
214
+ }
215
+ function firstMeaningfulErrorMessage(error) {
216
+ return errorMessageChain(error).find((item) => item && item !== "Error");
217
+ }
218
+ function errorMessageChain(error) {
219
+ const messages = [];
220
+ let current = error;
221
+ for (let depth = 0; current && depth < 8; depth++) {
222
+ if (current instanceof Error) {
223
+ messages.push(current.name, current.message);
224
+ current = current.cause;
225
+ continue;
226
+ }
227
+ if (typeof current === "object") {
228
+ const record = current;
229
+ for (const key of ["name", "code", "message"]) {
230
+ if (typeof record[key] === "string")
231
+ messages.push(record[key]);
232
+ }
233
+ current = record.cause;
234
+ continue;
235
+ }
236
+ messages.push(String(current));
237
+ break;
238
+ }
239
+ return messages;
240
+ }
@@ -2,8 +2,13 @@
2
2
  * OpenAI Codex OAuth login (PKCE + local callback).
3
3
  */
4
4
  import type { OAuthTokens } from "./types.js";
5
+ import { type ChatGptFetch } from "../network/chatgpt-transport.js";
5
6
  export interface OpenAICodexLoginCallbacks {
6
7
  onStatus: (message: string) => void;
7
8
  }
8
- export declare function loginOpenAICodex(callbacks?: OpenAICodexLoginCallbacks): Promise<OAuthTokens>;
9
- export declare function refreshOpenAICodex(refreshToken: string): Promise<OAuthTokens>;
9
+ export declare function loginOpenAICodex(callbacks?: OpenAICodexLoginCallbacks, options?: {
10
+ fetch?: ChatGptFetch;
11
+ }): Promise<OAuthTokens>;
12
+ export declare function refreshOpenAICodex(refreshToken: string, options?: {
13
+ fetch?: ChatGptFetch;
14
+ }): Promise<OAuthTokens>;
@@ -4,6 +4,7 @@
4
4
  import { createServer } from "node:http";
5
5
  import { exec } from "node:child_process";
6
6
  import { randomBytes, createHash } from "node:crypto";
7
+ import { chatGptFetch } from "../network/chatgpt-transport.js";
7
8
  const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
8
9
  const AUTH_URL = "https://auth.openai.com/oauth/authorize";
9
10
  const TOKEN_URL = "https://auth.openai.com/oauth/token";
@@ -92,7 +93,8 @@ function extractAccountId(idToken) {
92
93
  const auth = claims?.["https://api.openai.com/auth"];
93
94
  return auth?.chatgpt_account_id || auth?.account_id || claims?.sub;
94
95
  }
95
- export async function loginOpenAICodex(callbacks) {
96
+ export async function loginOpenAICodex(callbacks, options = {}) {
97
+ const fetchImpl = options.fetch ?? chatGptFetch;
96
98
  callbacks?.onStatus("Starting OpenAI Codex OAuth login...");
97
99
  const pkce = generatePKCE();
98
100
  const state = generateState();
@@ -119,7 +121,7 @@ export async function loginOpenAICodex(callbacks) {
119
121
  throw new Error("OAuth state mismatch. Possible CSRF attack.");
120
122
  }
121
123
  callbacks?.onStatus("Exchanging authorization code for tokens...");
122
- const response = await fetch(TOKEN_URL, {
124
+ const response = await fetchImpl(TOKEN_URL, {
123
125
  method: "POST",
124
126
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
125
127
  body: new URLSearchParams({
@@ -146,8 +148,9 @@ export async function loginOpenAICodex(callbacks) {
146
148
  accountId,
147
149
  };
148
150
  }
149
- export async function refreshOpenAICodex(refreshToken) {
150
- const response = await fetch(TOKEN_URL, {
151
+ export async function refreshOpenAICodex(refreshToken, options = {}) {
152
+ const fetchImpl = options.fetch ?? chatGptFetch;
153
+ const response = await fetchImpl(TOKEN_URL, {
151
154
  method: "POST",
152
155
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
153
156
  body: new URLSearchParams({
@@ -2,6 +2,7 @@ import { classifyTask } from "../agent/task-classifier.js";
2
2
  import { classifyTaskSize } from "../agent/task-size.js";
3
3
  import { EvidenceTracker } from "../agent/evidence-tracker.js";
4
4
  import { ExecutionGovernor } from "../agent/execution-governor.js";
5
+ import { DiscoveryBarrier } from "../agent/discovery-barrier.js";
5
6
  import { arbitrateToolCall } from "../agent/tool-arbiter.js";
6
7
  import { buildEditRetryEscalationReminder, buildSmallTaskHint, buildTaskSummaryReminder, buildWorkflowPhaseReminder, } from "../prompt/reminders.js";
7
8
  import { reminderForTaskType } from "../prompt/task-reminders.js";
@@ -14,6 +15,11 @@ export function createDefaultHooks() {
14
15
  const taskType = classifyTask(ctx.input);
15
16
  ctx.state.taskType = taskType;
16
17
  ctx.state.governor = new ExecutionGovernor(taskType);
18
+ ctx.state.discoveryBarrier = new DiscoveryBarrier({
19
+ cwd: ctx.cwd,
20
+ input: ctx.input,
21
+ enabled: taskType === "repo_orientation",
22
+ });
17
23
  const taskReminder = reminderForTaskType(taskType);
18
24
  if (taskReminder) {
19
25
  ctx.queueReminder(taskReminder);
@@ -63,8 +69,12 @@ export function createDefaultHooks() {
63
69
  {
64
70
  beforeToolCall(ctx) {
65
71
  const arbitration = arbitrateToolCall(ctx.toolCall);
66
- ctx.replaceToolCall({ ...arbitration.toolCall, ...(arbitration.note ? { arbiterNote: arbitration.note } : {}) });
67
- ctx.state.governor?.beforeToolCall(ctx.toolCall);
72
+ const toolCall = { ...arbitration.toolCall, ...(arbitration.note ? { arbiterNote: arbitration.note } : {}) };
73
+ ctx.replaceToolCall(toolCall);
74
+ ctx.state.governor?.beforeToolCall(toolCall);
75
+ const blockedResult = ctx.state.discoveryBarrier?.beforeToolCall(toolCall);
76
+ if (blockedResult)
77
+ ctx.blockToolCall(blockedResult);
68
78
  },
69
79
  afterToolCall(ctx) {
70
80
  if (ctx.toolCall.arbiterNote) {
@@ -78,6 +88,7 @@ export function createDefaultHooks() {
78
88
  }
79
89
  ctx.state.evidenceTracker?.observe(ctx.toolCall, ctx.result);
80
90
  ctx.state.governor?.afterToolResult(ctx.toolCall, ctx.result);
91
+ ctx.state.discoveryBarrier?.afterToolCall(ctx.toolCall, ctx.result);
81
92
  // Edit/write retry-escalation: models can spiral on "identical content"
82
93
  // or "not found" errors. Nudge them to re-ground or switch strategy.
83
94
  if (isMutationTool(ctx.toolCall.name) && ctx.result.isError) {
@@ -3,10 +3,12 @@ import type { ContentPart, ParsedToolCall, ToolRegistryEntry, ToolResult } from
3
3
  import type { TaskType } from "../agent/task-classifier.js";
4
4
  import type { ExecutionGovernor } from "../agent/execution-governor.js";
5
5
  import type { EvidenceTracker } from "../agent/evidence-tracker.js";
6
+ import type { DiscoveryBarrier } from "../agent/discovery-barrier.js";
6
7
  import type { WorkflowPhase } from "./workflow.js";
7
8
  export interface TurnHookState {
8
9
  taskType?: TaskType;
9
10
  governor?: ExecutionGovernor;
11
+ discoveryBarrier?: DiscoveryBarrier;
10
12
  evidenceTracker?: EvidenceTracker;
11
13
  workflowPhase?: WorkflowPhase;
12
14
  workflowKey?: string;
@@ -1,5 +1,6 @@
1
1
  import type { Provider, ReasoningEffort, ThinkingLevel, TokenUsage } from "./types.js";
2
2
  import type { OAuthCredentials } from "./oauth/types.js";
3
+ import { type ChatGptFetch } from "./network/chatgpt-transport.js";
3
4
  export interface CodexModelDescriptor {
4
5
  id: string;
5
6
  displayName?: string;
@@ -25,6 +26,7 @@ export declare function createOpenAICodexProvider(options: {
25
26
  thinkingLevel?: ThinkingLevel;
26
27
  promptCacheKey?: string;
27
28
  auth?: OpenAICodexAuthAdapter;
29
+ fetch?: ChatGptFetch;
28
30
  }): Provider;
29
31
  export declare function normalizeOpenAICodexUsage(usage: any): TokenUsage;
30
32
  export declare function buildOpenAICodexPromptCacheKey(input: {
@@ -35,5 +37,6 @@ export declare function buildOpenAICodexPromptCacheKey(input: {
35
37
  export declare function fetchOpenAICodexModels(options: {
36
38
  baseURL: string;
37
39
  accessToken: string;
40
+ fetch?: ChatGptFetch;
38
41
  }): Promise<CodexModelDescriptor[]>;
39
42
  export declare function sortCodexModelDescriptors(descriptors: CodexModelDescriptor[]): CodexModelDescriptor[];
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { listBuiltinModels } from "./model-catalog.js";
3
3
  import { resolveProviderRequestConfig } from "./provider-transform.js";
4
+ import { chatGptFetch } from "./network/chatgpt-transport.js";
4
5
  const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
5
6
  const OPENAI_BETA_RESPONSES = "responses=experimental";
6
7
  const TOKEN_REFRESH_GRACE_MS = 5 * 60 * 1000;
@@ -42,6 +43,7 @@ export function extractChatGptAccountId(accessToken) {
42
43
  }
43
44
  export function createOpenAICodexProvider(options) {
44
45
  const sessionId = globalThis.crypto?.randomUUID?.() ?? `bubble_${Date.now()}`;
46
+ const fetchImpl = options.fetch ?? chatGptFetch;
45
47
  let refreshPromise;
46
48
  async function resolveRequestAuth(forceRefresh = false) {
47
49
  let credentials = await options.auth?.getCredentials();
@@ -77,7 +79,7 @@ export function createOpenAICodexProvider(options) {
77
79
  }));
78
80
  const sendRequest = async (forceRefresh = false) => {
79
81
  const { accessToken, accountId } = await resolveRequestAuth(forceRefresh);
80
- return fetch(resolveCodexUrl(options.baseURL), buildCodexRequestInit({
82
+ return fetchImpl(resolveCodexUrl(options.baseURL), buildCodexRequestInit({
81
83
  accessToken,
82
84
  accountId,
83
85
  sessionId,
@@ -278,8 +280,9 @@ export async function fetchOpenAICodexModels(options) {
278
280
  if (!accountId) {
279
281
  return [];
280
282
  }
283
+ const fetchImpl = options.fetch ?? chatGptFetch;
281
284
  for (const path of MODEL_DISCOVERY_PATHS) {
282
- const response = await fetch(resolveRelativeUrl(options.baseURL, path), {
285
+ const response = await fetchImpl(resolveRelativeUrl(options.baseURL, path), {
283
286
  method: "GET",
284
287
  headers: buildBaseHeaders(options.accessToken, accountId, globalThis.crypto?.randomUUID?.() ?? `bubble_${Date.now()}`, { accept: "application/json" }),
285
288
  }).catch(() => undefined);
@@ -451,6 +454,12 @@ function isTransientCodexTransportError(error) {
451
454
  /\bEPIPE\b/i,
452
455
  /socket hang up/i,
453
456
  /fetch failed/i,
457
+ /unknown certificate verification error/i,
458
+ /certificate (?:verify|verification) (?:failed|error)/i,
459
+ /unable to verify (?:the )?(?:first )?certificate/i,
460
+ /UNABLE_TO_(?:VERIFY_LEAF_SIGNATURE|GET_ISSUER_CERT_LOCALLY)/i,
461
+ /SELF_SIGNED_CERT_IN_CHAIN/i,
462
+ /CERT_(?:HAS_EXPIRED|UNTRUSTED|INVALID)/i,
454
463
  ].some((pattern) => pattern.test(text));
455
464
  }
456
465
  function errorMessageChain(error) {
@@ -36,6 +36,15 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
36
36
  },
37
37
  };
38
38
  }
39
+ if (providerId === "stepfun") {
40
+ return {
41
+ effectiveThinkingLevel,
42
+ reasoningContentEcho: "none",
43
+ extraBody: effectiveThinkingLevel === "off"
44
+ ? undefined
45
+ : { reasoning_effort: effectiveThinkingLevel },
46
+ };
47
+ }
39
48
  // Zhipu/Z.AI OpenAI-compatible endpoints expose reasoning via a provider-specific
40
49
  // `thinking` block rather than OpenAI's `reasoning_effort` shape.
41
50
  if (["zhipuai", "zhipuai-coding-plan", "zai", "zai-coding-plan"].includes(providerId)) {
@@ -1,8 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { appendFileSync, mkdirSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
+ import { sanitizeInternalReminderBlocks } from "./agent/internal-reminder-sanitizer.js";
4
5
  const DEBUG_PATH = process.env.BUBBLE_DEBUG_REASONING_STREAM?.trim();
5
6
  const INCLUDE_PREVIEW = process.env.BUBBLE_DEBUG_REASONING_PREVIEW !== "0";
7
+ const INCLUDE_RAW_PREVIEW = ["1", "true", "yes", "on"].includes(process.env.BUBBLE_DEBUG_REASONING_RAW?.trim().toLowerCase() ?? "");
6
8
  const PREVIEW_CHARS = 180;
7
9
  let sequence = 0;
8
10
  export function summarizeDebugText(value) {
@@ -13,7 +15,8 @@ export function summarizeDebugText(value) {
13
15
  const hash = createHash("sha256").update(value).digest("hex").slice(0, 16);
14
16
  const summary = { length: value.length, hash };
15
17
  if (INCLUDE_PREVIEW) {
16
- summary.preview = value.replace(/\s+/g, " ").slice(0, PREVIEW_CHARS);
18
+ const previewValue = INCLUDE_RAW_PREVIEW ? value : sanitizeInternalReminderBlocks(value);
19
+ summary.preview = previewValue.replace(/\s+/g, " ").slice(0, PREVIEW_CHARS);
17
20
  }
18
21
  return summary;
19
22
  }