@bubblebrain-ai/bubble 0.0.17 → 0.0.19

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 (46) hide show
  1. package/dist/agent/tool-intent.js +0 -1
  2. package/dist/agent.d.ts +1 -0
  3. package/dist/agent.js +54 -21
  4. package/dist/context/prune.d.ts +1 -0
  5. package/dist/context/prune.js +32 -0
  6. package/dist/feishu/agent-host/run-driver.js +2 -2
  7. package/dist/feishu/card/run-state.js +1 -0
  8. package/dist/main.js +11 -9
  9. package/dist/model-pricing.js +2 -1
  10. package/dist/model-selection.d.ts +7 -0
  11. package/dist/model-selection.js +9 -0
  12. package/dist/network/chatgpt-transport.d.ts +1 -0
  13. package/dist/network/chatgpt-transport.js +123 -16
  14. package/dist/orchestrator/default-hooks.js +1 -1
  15. package/dist/prompt/environment.js +1 -3
  16. package/dist/prompt/runtime.js +1 -1
  17. package/dist/provider-anthropic.d.ts +15 -3
  18. package/dist/provider-anthropic.js +55 -2
  19. package/dist/provider-openai-codex.js +3 -1
  20. package/dist/provider.js +1 -1
  21. package/dist/session-title.js +3 -6
  22. package/dist/slash-commands/commands.js +4 -0
  23. package/dist/stats/usage.d.ts +1 -0
  24. package/dist/stats/usage.js +28 -3
  25. package/dist/tools/edit.js +75 -1
  26. package/dist/tools/glob.js +77 -12
  27. package/dist/tools/index.d.ts +1 -1
  28. package/dist/tools/index.js +1 -3
  29. package/dist/tools/prompt-metadata.d.ts +3 -0
  30. package/dist/tools/prompt-metadata.js +17 -0
  31. package/dist/tools/write.js +14 -0
  32. package/dist/tui/paste-placeholder.d.ts +10 -0
  33. package/dist/tui/paste-placeholder.js +45 -0
  34. package/dist/tui/run.js +23 -0
  35. package/dist/tui-ink/app.js +2 -0
  36. package/dist/tui-ink/input-box.d.ts +1 -8
  37. package/dist/tui-ink/input-box.js +8 -38
  38. package/dist/tui-opentui/app.js +2 -0
  39. package/dist/tui-opentui/input-box.d.ts +1 -3
  40. package/dist/tui-opentui/input-box.js +17 -26
  41. package/dist/types.d.ts +9 -0
  42. package/package.json +7 -3
  43. package/dist/tools/apply-patch.d.ts +0 -9
  44. package/dist/tools/apply-patch.js +0 -330
  45. package/dist/tools/patch-apply.d.ts +0 -41
  46. package/dist/tools/patch-apply.js +0 -312
@@ -55,7 +55,6 @@ export function analyzeToolIntent(toolCall) {
55
55
  case "write":
56
56
  return { family: "write" };
57
57
  case "edit":
58
- case "apply_patch":
59
58
  return { family: "edit" };
60
59
  case "web_search":
61
60
  case "web_fetch":
package/dist/agent.d.ts CHANGED
@@ -87,6 +87,7 @@ export declare class Agent {
87
87
  unlockDeferredTools(names: string[]): void;
88
88
  /** All deferred tools in this session (for tool_search to inspect). */
89
89
  listDeferredTools(): ToolRegistryEntry[];
90
+ getSystemPromptToolOptions(): Pick<import("./system-prompt.js").SystemPromptOptions, "tools" | "toolSnippets" | "guidelines">;
90
91
  getContextUsageSnapshot(): ContextUsageSnapshot;
91
92
  resetContextUsageAnchor(): void;
92
93
  /** Whether a given tool is deferred and not yet unlocked. */
package/dist/agent.js CHANGED
@@ -9,9 +9,9 @@ import { estimateContextTokens, getContextBudget } from "./context/budget.js";
9
9
  import { buildContextUsageSnapshot } from "./context/usage.js";
10
10
  import { isContextOverflowError } from "./context/overflow.js";
11
11
  import { projectMessages } from "./context/projector.js";
12
- import { aggressivePruneMessages } from "./context/prune.js";
12
+ import { aggressivePruneMessages, markStableCurrentToolResultsForCache } from "./context/prune.js";
13
13
  import { truncateToolOutputForModel } from "./context/tool-output-truncate.js";
14
- import { buildDeferredToolsReminder, buildToolFreezeReminder, isPermissionModeReminder, reminderForMode } from "./prompt/reminders.js";
14
+ import { buildDeferredToolsReminder, buildToolFreezeReminder, reminderForMode } from "./prompt/reminders.js";
15
15
  import { HookBus } from "./orchestrator/hooks.js";
16
16
  import { createDefaultHooks } from "./orchestrator/default-hooks.js";
17
17
  import { resolveModelRoute, resolveSubagentRoute } from "./agent/categories.js";
@@ -24,6 +24,7 @@ import { createStreamingInternalReminderSanitizer, sanitizeAssistantProviderMeta
24
24
  import { buildSystemPrompt } from "./system-prompt.js";
25
25
  import { isOnlyProviderProtocolArtifacts, stripProviderProtocolArtifacts } from "./provider-artifacts.js";
26
26
  import { debugReasoningStream, summarizeDebugText } from "./reasoning-debug.js";
27
+ import { buildToolPromptOptions } from "./tools/prompt-metadata.js";
27
28
  import { stopAutoServersForSession } from "./tools/server-manager.js";
28
29
  import { summarizeAgentEventForTrace, summarizeTraceError, summarizeTraceMessage, summarizeTraceToolResult, summarizeTraceValue, traceEvent, } from "./debug-trace.js";
29
30
  const MAX_CONSECUTIVE_OVERFLOW_RECOVERIES = 3;
@@ -31,7 +32,6 @@ const RESIDENT_HISTORY_KEEP_RECENT_TURNS = 3;
31
32
  const RESIDENT_HISTORY_MESSAGE_LIMIT = 160;
32
33
  const RESIDENT_HISTORY_CHAR_SOFT_LIMIT = 256 * 1024;
33
34
  const RESIDENT_HISTORY_CHAR_HARD_LIMIT = 512 * 1024;
34
- const RESIDENT_HISTORY_HEAP_SOFT_LIMIT = 512 * 1024 * 1024;
35
35
  const RESIDENT_HISTORY_HEAP_HARD_LIMIT = 768 * 1024 * 1024;
36
36
  const MAX_EMPTY_ASSISTANT_RECOVERIES = 1;
37
37
  const EMPTY_ASSISTANT_RECOVERY_REMINDER = "The previous model response contained no user-visible assistant content and no tool calls. " +
@@ -131,6 +131,9 @@ export class Agent {
131
131
  listDeferredTools() {
132
132
  return [...this.tools.values()].filter((t) => t.deferred);
133
133
  }
134
+ getSystemPromptToolOptions() {
135
+ return buildToolPromptOptions(this.getActiveToolEntries());
136
+ }
134
137
  getContextUsageSnapshot() {
135
138
  return buildContextUsageSnapshot({
136
139
  providerId: this.providerId,
@@ -153,17 +156,20 @@ export class Agent {
153
156
  }
154
157
  getActiveToolEntries() {
155
158
  return [...this.tools.values()]
156
- .filter((tool) => !tool.deferred || this.unlockedDeferred.has(tool.name))
157
- .filter((tool) => this._mode === "plan" || tool.name !== "exit_plan_mode");
159
+ .filter((tool) => !tool.deferred || this.unlockedDeferred.has(tool.name));
158
160
  }
159
161
  injectSystemReminder(content) {
160
162
  this.appendMessage({ role: "meta", kind: "system-reminder", content });
161
163
  }
162
164
  injectModeReminder() {
163
- this.messages = this.messages.filter((message) => !(message.role === "meta"
164
- && message.kind === "system-reminder"
165
- && isPermissionModeReminder(message.content)));
166
- this.injectSystemReminder(reminderForMode(this._mode));
165
+ const reminder = reminderForMode(this._mode);
166
+ const last = this.messages.at(-1);
167
+ if (last?.role === "meta"
168
+ && last.kind === "system-reminder"
169
+ && last.content === reminder) {
170
+ return;
171
+ }
172
+ this.injectSystemReminder(reminder);
167
173
  }
168
174
  get model() {
169
175
  return this._model;
@@ -398,11 +404,9 @@ export class Agent {
398
404
  };
399
405
  await hookBus.runBeforeModelCall(beforeModelCallCtx);
400
406
  toolEntries = beforeModelCallCtx.toolEntries;
401
- if (this._mode !== "plan") {
402
- toolEntries = toolEntries.filter((t) => t.name !== "exit_plan_mode");
403
- }
404
407
  flushGovernorReminders();
405
- const toolDefinitions = ((hookState.forceTextOnlyReason ? [] : toolEntries))
408
+ const textOnly = !!hookState.forceTextOnlyReason;
409
+ const toolDefinitions = toolEntries
406
410
  .map((t) => ({
407
411
  name: t.name,
408
412
  description: t.description,
@@ -417,6 +421,7 @@ export class Agent {
417
421
  const bufferedStreamingToolCallIds = new Set();
418
422
  const discoveryBarrier = hookState.discoveryBarrier;
419
423
  try {
424
+ markStableCurrentToolResultsForCache(this.messages);
420
425
  const projectedMessages = projectMessages(this.messages, {
421
426
  mode: "budgeted",
422
427
  providerId: this.providerId,
@@ -434,11 +439,12 @@ export class Agent {
434
439
  toolCount: toolDefinitions.length,
435
440
  thinkingLevel: this.thinkingLevel,
436
441
  mode: this._mode,
437
- requestFingerprint: buildProviderRequestFingerprint(projectedMessages, toolDefinitions, this.providerId),
442
+ requestFingerprint: buildProviderRequestFingerprint(projectedMessages, toolDefinitions, this.providerId, toolDefinitions.length > 0 ? (textOnly ? "none" : "auto") : undefined),
438
443
  }, traceContext);
439
444
  const stream = this.provider.streamChat(projectedMessages, {
440
445
  model: this.apiModel,
441
446
  tools: toolDefinitions,
447
+ toolChoice: toolDefinitions.length > 0 ? (textOnly ? "none" : "auto") : undefined,
442
448
  temperature: this.temperature,
443
449
  thinkingLevel: this.thinkingLevel,
444
450
  abortSignal,
@@ -1366,7 +1372,7 @@ export class Agent {
1366
1372
  thinkingLevel: route.thinkingLevel,
1367
1373
  mode: "plan",
1368
1374
  workingDir: cwd,
1369
- tools: childToolNames,
1375
+ ...buildToolPromptOptions(tools),
1370
1376
  memoryPrompt: childToolNames.some((name) => name === "memory_search" || name === "memory_read_summary")
1371
1377
  ? this.memoryPrompt
1372
1378
  : undefined,
@@ -1518,8 +1524,7 @@ export class Agent {
1518
1524
  || heapUsed >= RESIDENT_HISTORY_HEAP_HARD_LIMIT;
1519
1525
  const shouldCompact = !!budget?.shouldCompact
1520
1526
  || candidate.length >= RESIDENT_HISTORY_MESSAGE_LIMIT
1521
- || residentChars >= RESIDENT_HISTORY_CHAR_SOFT_LIMIT
1522
- || heapUsed >= RESIDENT_HISTORY_HEAP_SOFT_LIMIT;
1527
+ || residentChars >= RESIDENT_HISTORY_CHAR_SOFT_LIMIT;
1523
1528
  if (shouldAggressivelyPrune) {
1524
1529
  candidate = aggressivePruneMessages(candidate);
1525
1530
  }
@@ -1627,7 +1632,22 @@ export class Agent {
1627
1632
  metadata: { kind: "security", reason: "args_corrupt" },
1628
1633
  };
1629
1634
  }
1630
- const missingRequired = findMissingRequiredArgs(tool.parameters, toolCall.parsedArgs);
1635
+ let preparedArgs = toolCall.parsedArgs;
1636
+ if (tool.prepareArguments) {
1637
+ try {
1638
+ preparedArgs = tool.prepareArguments(preparedArgs);
1639
+ }
1640
+ catch (err) {
1641
+ return {
1642
+ content: `Error: Tool "${toolCall.name}" arguments could not be normalized before execution: ` +
1643
+ `${err instanceof Error ? err.message : String(err)}. Re-issue the call with valid arguments.`,
1644
+ isError: true,
1645
+ status: "blocked",
1646
+ metadata: { kind: "security", reason: "args_prepare_failed" },
1647
+ };
1648
+ }
1649
+ }
1650
+ const missingRequired = findMissingRequiredArgs(tool.parameters, preparedArgs);
1631
1651
  if (missingRequired.length > 0) {
1632
1652
  return {
1633
1653
  content: `Error: Tool "${toolCall.name}" was called without required argument${missingRequired.length === 1 ? "" : "s"}: ${missingRequired.map((name) => `"${name}"`).join(", ")}. ` +
@@ -1638,7 +1658,7 @@ export class Agent {
1638
1658
  };
1639
1659
  }
1640
1660
  try {
1641
- return await tool.execute(toolCall.parsedArgs, {
1661
+ return await tool.execute(preparedArgs, {
1642
1662
  cwd,
1643
1663
  sessionID: this.sessionID,
1644
1664
  abortSignal,
@@ -1716,7 +1736,7 @@ function appendProviderContentBlock(message, provider, block) {
1716
1736
  },
1717
1737
  };
1718
1738
  }
1719
- function buildProviderRequestFingerprint(messages, tools, providerId) {
1739
+ function buildProviderRequestFingerprint(messages, tools, providerId, toolChoice) {
1720
1740
  const roleCounts = {};
1721
1741
  let contentChars = 0;
1722
1742
  let reasoningChars = 0;
@@ -1756,11 +1776,24 @@ function buildProviderRequestFingerprint(messages, tools, providerId) {
1756
1776
  contentChars += message.content.length;
1757
1777
  }
1758
1778
  }
1779
+ const systemMessages = messages.filter((message) => message.role === "system");
1780
+ const bodyMessages = messages.filter((message) => message.role !== "system");
1781
+ const systemJsonBytes = Buffer.byteLength(JSON.stringify(systemMessages), "utf8");
1782
+ const bodyJsonBytes = Buffer.byteLength(JSON.stringify(bodyMessages), "utf8");
1783
+ const toolSchemaJsonBytes = Buffer.byteLength(JSON.stringify(tools), "utf8");
1759
1784
  return {
1760
1785
  roleCounts,
1761
1786
  estimatedTokens: estimateContextTokens(messages, providerId),
1762
1787
  projectedJsonBytes: Buffer.byteLength(JSON.stringify(messages), "utf8"),
1763
- toolSchemaJsonBytes: Buffer.byteLength(JSON.stringify(tools), "utf8"),
1788
+ systemJsonBytes,
1789
+ bodyJsonBytes,
1790
+ toolSchemaJsonBytes,
1791
+ staticPrefixJsonBytes: Buffer.byteLength(JSON.stringify({
1792
+ system: systemMessages,
1793
+ tools,
1794
+ tool_choice: toolChoice,
1795
+ }), "utf8"),
1796
+ toolChoice,
1764
1797
  contentChars,
1765
1798
  reasoningChars,
1766
1799
  toolResultChars,
@@ -1,5 +1,6 @@
1
1
  import type { Message } from "../types.js";
2
2
  export declare function pruneMessages<T extends Message>(messages: T[]): T[];
3
+ export declare function markStableCurrentToolResultsForCache(messages: Message[]): void;
3
4
  /**
4
5
  * Aggressive variant of pruneMessages: drops the content of every prunable
5
6
  * tool output except the latest unresolved tool turn that the model still
@@ -3,6 +3,8 @@ const PRUNEABLE_TOOLS = new Set([
3
3
  ]);
4
4
  const TOOL_RESULT_KEEP_COUNT = 2;
5
5
  const MIN_PRUNE_LENGTH = 240;
6
+ const CACHE_STABLE_PROJECTION_KEY = "cacheStableProjection";
7
+ const CACHE_STABLE_FULL_PROJECTION = "full";
6
8
  export function pruneMessages(messages) {
7
9
  const toolNameByCallId = new Map();
8
10
  const pruneCandidates = [];
@@ -19,6 +21,9 @@ export function pruneMessages(messages) {
19
21
  if (message.role !== "tool") {
20
22
  continue;
21
23
  }
24
+ if (isCacheStableFullToolResult(message)) {
25
+ continue;
26
+ }
22
27
  if (protectedToolCallIds.has(message.toolCallId)) {
23
28
  const toolName = toolNameByCallId.get(message.toolCallId);
24
29
  if (toolName && shouldPruneToolResult(toolName, message.content)) {
@@ -49,6 +54,30 @@ export function pruneMessages(messages) {
49
54
  };
50
55
  });
51
56
  }
57
+ export function markStableCurrentToolResultsForCache(messages) {
58
+ const protectedToolCallIds = collectProtectedToolCallIds(messages);
59
+ if (protectedToolCallIds.size === 0)
60
+ return;
61
+ const toolNameByCallId = new Map();
62
+ for (const message of messages) {
63
+ if (message.role !== "assistant" || !message.toolCalls)
64
+ continue;
65
+ for (const toolCall of message.toolCalls) {
66
+ toolNameByCallId.set(toolCall.id, toolCall.name);
67
+ }
68
+ }
69
+ for (const message of messages) {
70
+ if (message.role !== "tool" || !protectedToolCallIds.has(message.toolCallId))
71
+ continue;
72
+ const toolName = toolNameByCallId.get(message.toolCallId);
73
+ if (!toolName || !shouldPruneToolResult(toolName, message.content))
74
+ continue;
75
+ message.metadata = {
76
+ ...message.metadata,
77
+ [CACHE_STABLE_PROJECTION_KEY]: CACHE_STABLE_FULL_PROJECTION,
78
+ };
79
+ }
80
+ }
52
81
  function shouldPruneToolResult(toolName, content) {
53
82
  if (!PRUNEABLE_TOOLS.has(toolName)) {
54
83
  return false;
@@ -64,6 +93,9 @@ function shouldPruneToolResult(toolName, content) {
64
93
  function summarizePrunedToolResult(toolName, content) {
65
94
  return `[${toolName} output omitted to control context size; original length ${content.length} chars]`;
66
95
  }
96
+ function isCacheStableFullToolResult(message) {
97
+ return message.metadata?.[CACHE_STABLE_PROJECTION_KEY] === CACHE_STABLE_FULL_PROJECTION;
98
+ }
67
99
  /**
68
100
  * Aggressive variant of pruneMessages: drops the content of every prunable
69
101
  * tool output except the latest unresolved tool turn that the model still
@@ -19,7 +19,7 @@ import { BashAllowlist } from "../../approval/session-cache.js";
19
19
  import { getLspService } from "../../lsp/index.js";
20
20
  import { buildSystemPrompt } from "../../system-prompt.js";
21
21
  import { FileStateTracker } from "../../tools/file-state.js";
22
- import { createAllTools } from "../../tools/index.js";
22
+ import { buildToolPromptOptions, createAllTools } from "../../tools/index.js";
23
23
  import { displayModel, encodeModel, decodeModel } from "../../provider-registry.js";
24
24
  import { buildMemoryPrompt, recordMemoryCitations } from "../../memory/index.js";
25
25
  import { getDefaultThinkingLevel } from "../../provider-transform.js";
@@ -94,7 +94,7 @@ export class RunDriver {
94
94
  thinkingLevel,
95
95
  mode: initialMode,
96
96
  workingDir: session.cwd,
97
- tools: tools.map((t) => t.name),
97
+ ...buildToolPromptOptions(tools.filter((tool) => !tool.deferred)),
98
98
  memoryPrompt,
99
99
  });
100
100
  const budgetLedger = new BudgetLedger();
@@ -211,6 +211,7 @@ function mergeUsage(prev, next) {
211
211
  completionTokens: prev.completionTokens + (next.completionTokens ?? 0),
212
212
  promptCacheHitTokens: (prev.promptCacheHitTokens ?? 0) + (next.promptCacheHitTokens ?? 0),
213
213
  promptCacheMissTokens: (prev.promptCacheMissTokens ?? 0) + (next.promptCacheMissTokens ?? 0),
214
+ cacheCreationTokens: (prev.cacheCreationTokens ?? 0) + (next.cacheCreationTokens ?? 0),
214
215
  reasoningTokens: (prev.reasoningTokens ?? 0) + (next.reasoningTokens ?? 0),
215
216
  totalTokens: (prev.totalTokens ?? 0) + (next.totalTokens ?? 0),
216
217
  };
package/dist/main.js CHANGED
@@ -8,13 +8,14 @@ import { BudgetLedger } from "./agent/budget-ledger.js";
8
8
  import { parseArgs, printHelp } from "./cli.js";
9
9
  import { UserConfig } from "./config.js";
10
10
  import { createProviderInstance, createUnavailableProvider } from "./provider.js";
11
+ import { resolveConfiguredModel } from "./model-selection.js";
11
12
  import { getDefaultThinkingLevel } from "./provider-transform.js";
12
13
  import { ProviderRegistry, displayModel, encodeModel, decodeModel } from "./provider-registry.js";
13
14
  import { SessionManager } from "./session.js";
14
15
  import { createSessionTitleUpdater } from "./session-title.js";
15
16
  import { buildSystemPrompt } from "./system-prompt.js";
16
17
  import { SkillRegistry } from "./skills/registry.js";
17
- import { createAllTools } from "./tools/index.js";
18
+ import { buildToolPromptOptions, createAllTools } from "./tools/index.js";
18
19
  import { FileStateTracker } from "./tools/file-state.js";
19
20
  import { PermissionAwareApprovalController } from "./approval/controller.js";
20
21
  import { BashAllowlist } from "./approval/session-cache.js";
@@ -244,18 +245,19 @@ async function main() {
244
245
  }
245
246
  sessionPromptCacheKey = sessionManager.getOrCreatePromptCacheKey();
246
247
  // Model resolution:
247
- // 1. Session metadata 2. User-configured default model 3. CLI flag
248
+ // 1. CLI flag 2. Session metadata 3. User-configured default model
248
249
  // No implicit built-in model fallback.
249
250
  const fallbackProviderId = defaultProvider?.id || "";
250
251
  const sessionModel = sessionManager?.getMetadata().model;
251
- const configuredModel = sessionModel ?? userConfig.getDefaultModel() ?? args.model;
252
+ const defaultModel = userConfig.getDefaultModel();
252
253
  const sessionThinkingLevel = sessionManager?.getMetadata().thinkingLevel;
253
254
  const configuredThinkingLevel = userConfig.getDefaultThinkingLevel();
254
- const normalizedConfiguredModel = configuredModel
255
- ? (configuredModel.includes(":")
256
- ? configuredModel
257
- : (fallbackProviderId ? encodeModel(fallbackProviderId, configuredModel) : ""))
258
- : "";
255
+ const normalizedConfiguredModel = resolveConfiguredModel({
256
+ cliModel: args.model,
257
+ sessionModel,
258
+ defaultModel,
259
+ fallbackProviderId,
260
+ });
259
261
  const { providerId: effectiveProviderId, modelId: effectiveModelId } = normalizedConfiguredModel
260
262
  ? decodeModel(normalizedConfiguredModel)
261
263
  : { providerId: undefined, modelId: "" };
@@ -288,7 +290,7 @@ async function main() {
288
290
  thinkingLevel: initialThinkingLevel,
289
291
  mode: initialMode,
290
292
  workingDir: args.cwd,
291
- tools: tools.map((tool) => tool.name),
293
+ ...buildToolPromptOptions(tools.filter((tool) => !tool.deferred)),
292
294
  memoryPrompt,
293
295
  });
294
296
  const traceInfo = configureDebugTrace({
@@ -38,7 +38,8 @@ export function calculateUsageCost(providerId, modelId, usage) {
38
38
  if (!pricing)
39
39
  return undefined;
40
40
  const hasCacheBreakdown = typeof usage.promptCacheHitTokens === "number"
41
- || typeof usage.promptCacheMissTokens === "number";
41
+ || typeof usage.promptCacheMissTokens === "number"
42
+ || typeof usage.cacheCreationTokens === "number";
42
43
  const hit = usage.promptCacheHitTokens ?? 0;
43
44
  const miss = hasCacheBreakdown
44
45
  ? usage.promptCacheMissTokens ?? Math.max(0, usage.promptTokens - hit)
@@ -0,0 +1,7 @@
1
+ export interface ResolveConfiguredModelInput {
2
+ cliModel?: string;
3
+ sessionModel?: string;
4
+ defaultModel?: string;
5
+ fallbackProviderId?: string;
6
+ }
7
+ export declare function resolveConfiguredModel(input: ResolveConfiguredModelInput): string;
@@ -0,0 +1,9 @@
1
+ import { encodeModel } from "./provider-registry.js";
2
+ export function resolveConfiguredModel(input) {
3
+ const selected = input.cliModel ?? input.sessionModel ?? input.defaultModel;
4
+ if (!selected)
5
+ return "";
6
+ if (selected.includes(":"))
7
+ return selected;
8
+ return input.fallbackProviderId ? encodeModel(input.fallbackProviderId, selected) : "";
9
+ }
@@ -13,4 +13,5 @@ export declare function createChatGptFetch(options?: ChatGptFetchOptions): ChatG
13
13
  export declare function createChatGptDispatcher(env?: NodeJS.ProcessEnv, input?: RequestInfo | URL): Dispatcher | undefined;
14
14
  export declare function withChatGptNetworkOptions(input: RequestInfo | URL, init: RequestInit | undefined, env?: NodeJS.ProcessEnv, dispatcher?: Dispatcher | undefined): RequestInitWithDispatcher;
15
15
  export declare function normalizeChatGptNetworkError(error: unknown, env?: NodeJS.ProcessEnv): Error;
16
+ export declare function parseMacSystemProxyForUrl(output: string, url: URL): string | undefined;
16
17
  export {};
@@ -1,3 +1,4 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import { readFileSync } from "node:fs";
2
3
  import { delimiter } from "node:path";
3
4
  import { rootCertificates } from "node:tls";
@@ -19,9 +20,8 @@ export function getChatGptFetch(env = process.env) {
19
20
  export function createChatGptFetch(options = {}) {
20
21
  const env = options.env ?? process.env;
21
22
  const fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
22
- const dispatcher = createChatGptDispatcher(env);
23
23
  return async (input, init) => {
24
- const requestInit = withChatGptNetworkOptions(input, init, env, dispatcher);
24
+ const requestInit = withChatGptNetworkOptions(input, init, env);
25
25
  try {
26
26
  return await fetchImpl(input, requestInit);
27
27
  }
@@ -34,9 +34,9 @@ export function createChatGptDispatcher(env = process.env, input) {
34
34
  if (isBunRuntime())
35
35
  return undefined;
36
36
  const ca = loadExtraCaCertificates(env);
37
- if (!hasProxyEnv(env) && ca.length === 0)
38
- return undefined;
39
37
  const proxy = input ? nodeProxyForUrl(input, env) : defaultNodeProxy(env);
38
+ if (!proxy && ca.length === 0)
39
+ return undefined;
40
40
  const caOptions = ca.length > 0 ? { ca: [...rootCertificates, ...ca] } : undefined;
41
41
  if (proxy) {
42
42
  return new ProxyAgent({
@@ -73,41 +73,144 @@ export function normalizeChatGptNetworkError(error, env = process.env) {
73
73
  : "This looks like a proxy or network transport failure.",
74
74
  hasProxyEnv(env)
75
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.",
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."
78
+ : "If your network requires a proxy, set HTTPS_PROXY or HTTP_PROXY, and set NO_PROXY=localhost,127.0.0.1.",
77
79
  "Do not disable TLS verification with NODE_TLS_REJECT_UNAUTHORIZED=0.",
78
80
  `Original error: ${firstMeaningfulErrorMessage(error) || "unknown network error"}`,
79
81
  ].join(" ");
80
82
  return new Error(message, { cause: error });
81
83
  }
82
84
  function hasProxyEnv(env) {
83
- return Boolean(env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy || env.ALL_PROXY || env.all_proxy);
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);
84
93
  }
85
94
  function isBunRuntime() {
86
95
  return typeof globalThis.Bun !== "undefined";
87
96
  }
88
97
  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) {
89
104
  const url = urlFromInput(input);
90
105
  if (!url || shouldBypassProxy(url, env))
91
106
  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;
92
125
  const allProxy = env.ALL_PROXY ?? env.all_proxy;
93
126
  if (url.protocol === "https:")
94
127
  return env.HTTPS_PROXY ?? env.https_proxy ?? allProxy;
95
128
  if (url.protocol === "http:")
96
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
+ }
97
151
  return undefined;
98
152
  }
99
- function nodeProxyForUrl(input, env) {
100
- const url = urlFromInput(input);
101
- if (!url || shouldBypassProxy(url, env))
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)
102
164
  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);
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}`;
108
169
  }
109
- function defaultNodeProxy(env) {
110
- return env.HTTPS_PROXY ?? env.https_proxy ?? env.HTTP_PROXY ?? env.http_proxy ?? env.ALL_PROXY ?? env.all_proxy;
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;
211
+ }
212
+ function truthyEnv(value) {
213
+ return /^(1|true|yes|on)$/i.test(value?.trim() ?? "");
111
214
  }
112
215
  function bunExtraCaFiles(env) {
113
216
  const bun = globalThis.Bun;
@@ -177,6 +280,9 @@ function networkEnvSignature(env) {
177
280
  env.https_proxy,
178
281
  env.ALL_PROXY,
179
282
  env.all_proxy,
283
+ env.BUBBLE_CHATGPT_PROXY,
284
+ env.bubble_chatgpt_proxy,
285
+ env.BUBBLE_DISABLE_SYSTEM_PROXY,
180
286
  env.NO_PROXY,
181
287
  env.no_proxy,
182
288
  env.NODE_EXTRA_CA_CERTS,
@@ -195,6 +301,7 @@ function isChatGptNetworkErrorText(text) {
195
301
  /\bEPIPE\b/i,
196
302
  /\bUND_ERR_/i,
197
303
  /socket hang up/i,
304
+ /Unable to connect\. Is the computer able to access the url\?/i,
198
305
  /certificate/i,
199
306
  /unable to verify/i,
200
307
  /self[- ]signed/i,
@@ -176,7 +176,7 @@ function isCodeWriteResult(_toolCall, result) {
176
176
  return result.metadata?.kind === "write" || result.metadata?.kind === "edit" || result.metadata?.kind === "patch";
177
177
  }
178
178
  function isMutationTool(name) {
179
- return name === "edit" || name === "write" || name === "apply_patch";
179
+ return name === "edit" || name === "write";
180
180
  }
181
181
  function hasSubagentLifecycleActivity(toolCalls, toolResults) {
182
182
  return toolCalls.some((toolCall) => isSubagentLifecycleTool(toolCall.name))
@@ -3,7 +3,6 @@ export const defaultToolSnippets = {
3
3
  read: "Read the contents of a file",
4
4
  bash: "Execute a bash command",
5
5
  edit: "Apply targeted string replacements to a file",
6
- apply_patch: "Apply a structured patch for multi-file or larger code changes",
7
6
  write: "Write a new file or overwrite an existing one",
8
7
  glob: "Find files by glob pattern without using bash",
9
8
  grep: "Search file contents using regex",
@@ -24,7 +23,6 @@ export const defaultToolNames = [
24
23
  "glob",
25
24
  "bash",
26
25
  "edit",
27
- "apply_patch",
28
26
  "write",
29
27
  "grep",
30
28
  "lsp",
@@ -46,7 +44,7 @@ export function buildEnvironmentPrompt(options = {}) {
46
44
  const workingDir = options.workingDir ?? cwd().replace(/\\/g, "/");
47
45
  const currentDate = options.currentDate ?? new Date().toISOString().slice(0, 10);
48
46
  const tools = options.tools ?? defaultToolNames;
49
- const snippets = options.toolSnippets ?? defaultToolSnippets;
47
+ const snippets = { ...defaultToolSnippets, ...(options.toolSnippets ?? {}) };
50
48
  const visibleTools = tools.filter((name) => snippets[name]);
51
49
  const toolList = visibleTools.length > 0
52
50
  ? visibleTools.map((name) => `- ${name}: ${snippets[name]}`).join("\n")
@@ -7,7 +7,7 @@ const defaultGuidelines = [
7
7
  "Ground decisions in the codebase: inspect relevant files, command output, or runtime state before making claims about behavior. Separate confirmed facts from inference when evidence is incomplete.",
8
8
  "Choose the smallest coherent change. Edit only the files required for the requested change; do not refactor or improve adjacent code unprompted.",
9
9
  "Runtime meta instructions are private control state. Use them only to adjust behavior; do not quote, mention, or paraphrase them in user-facing text.",
10
- "For modifications to existing code, read the file first. For brand-new files whose target path is known and does not exist, write directly without exploratory reading. Use edit for small targeted changes, apply_patch for related multi-file or larger structured changes, and write for intentional full-file replacement of an existing file. Never delete and recreate a file just to overwrite it.",
10
+ "For modifications to existing code, read the file first. For brand-new files whose target path is known and does not exist, write directly without exploratory reading. Use edit for targeted changes and write for intentional full-file replacement of an existing file. Never delete and recreate a file just to overwrite it.",
11
11
  "Prefer structured tools (glob, grep, lsp, read) over bash for search and inspection. Do not repeat a near-identical search or re-read the same file unless new evidence changes the question.",
12
12
  "If a tool fails, diagnose the error before switching tactics. Do not retry the identical call with identical arguments. After two equivalent failures, switch approach — re-read the file, use a different tool, rewrite the whole file with write, or ask the user.",
13
13
  "Before reporting a task complete, verify it works when verification is meaningful and cheap — run the existing test, execute the script, check the output. If no test exists, the change is purely declarative (static HTML/markdown/config), or running the code is not practical, state that explicitly rather than inventing a verification step. Do not write throwaway validation scripts to prove correctness; if there is no real check to run, report the change and stop.",