@bubblebrain-ai/bubble 0.0.15 → 0.0.17

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 (51) 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 +9 -0
  5. package/dist/agent/internal-reminder-sanitizer.js +198 -0
  6. package/dist/agent/task-classifier.js +23 -5
  7. package/dist/agent.js +215 -30
  8. package/dist/context/budget.js +15 -0
  9. package/dist/context/projector.js +4 -3
  10. package/dist/debug-trace.js +14 -0
  11. package/dist/feishu/serve.js +1 -0
  12. package/dist/main.js +2 -0
  13. package/dist/model-catalog.d.ts +3 -0
  14. package/dist/model-catalog.js +44 -0
  15. package/dist/model-config.d.ts +3 -0
  16. package/dist/model-config.js +3 -0
  17. package/dist/model-pricing.d.ts +3 -2
  18. package/dist/model-pricing.js +8 -0
  19. package/dist/network/chatgpt-transport.d.ts +16 -0
  20. package/dist/network/chatgpt-transport.js +240 -0
  21. package/dist/oauth/openai-codex.d.ts +7 -2
  22. package/dist/oauth/openai-codex.js +7 -4
  23. package/dist/orchestrator/default-hooks.js +13 -2
  24. package/dist/orchestrator/hooks.d.ts +2 -0
  25. package/dist/prompt/compose.js +1 -1
  26. package/dist/prompt/reminders.js +3 -3
  27. package/dist/prompt/runtime.js +1 -0
  28. package/dist/provider-anthropic.d.ts +77 -0
  29. package/dist/provider-anthropic.js +544 -0
  30. package/dist/provider-openai-codex.d.ts +3 -0
  31. package/dist/provider-openai-codex.js +11 -2
  32. package/dist/provider-registry.d.ts +2 -0
  33. package/dist/provider-registry.js +29 -3
  34. package/dist/provider-transform.d.ts +1 -1
  35. package/dist/provider-transform.js +23 -0
  36. package/dist/provider.d.ts +4 -1
  37. package/dist/provider.js +119 -40
  38. package/dist/reasoning-debug.js +4 -1
  39. package/dist/session-log.js +17 -2
  40. package/dist/slash-commands/commands.js +4 -2
  41. package/dist/stats/usage.d.ts +4 -0
  42. package/dist/stats/usage.js +48 -11
  43. package/dist/tools/glob.js +3 -0
  44. package/dist/tools/grep.js +7 -0
  45. package/dist/tui/run.js +22 -12
  46. package/dist/tui-ink/app.js +3 -0
  47. package/dist/tui-ink/message-list.js +6 -3
  48. package/dist/tui-opentui/app.js +3 -0
  49. package/dist/tui-opentui/message-list.js +6 -3
  50. package/dist/types.d.ts +14 -1
  51. package/package.json +2 -1
package/README.md CHANGED
@@ -62,6 +62,30 @@ Bubble stores user configuration, sessions, permissions, skills, and memory unde
62
62
 
63
63
  In the app, use `/login` or provider commands to configure model access.
64
64
 
65
+ ### ChatGPT Network Configuration
66
+
67
+ ChatGPT OAuth and GPT/Codex requests respect standard proxy variables:
68
+
69
+ ```bash
70
+ export HTTPS_PROXY=http://proxy.example.com:8080
71
+ export HTTP_PROXY=http://proxy.example.com:8080
72
+ export NO_PROXY=localhost,127.0.0.1
73
+ ```
74
+
75
+ If your network uses a corporate or custom HTTPS CA, start Bubble with:
76
+
77
+ ```bash
78
+ NODE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem bubble
79
+ ```
80
+
81
+ You can also use `BUBBLE_EXTRA_CA_CERTS` for Bubble's ChatGPT requests:
82
+
83
+ ```bash
84
+ BUBBLE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem bubble
85
+ ```
86
+
87
+ Do not disable TLS verification with `NODE_TLS_REJECT_UNAUTHORIZED=0`.
88
+
65
89
  ## Memory
66
90
 
67
91
  Bubble maintains persistent memory automatically from prior sessions. Useful commands:
@@ -0,0 +1,21 @@
1
+ import type { ContentPart, ParsedToolCall, ToolResult, ToolResultMetadata } from "../types.js";
2
+ export interface DiscoveryBarrierOptions {
3
+ cwd: string;
4
+ input: string | ContentPart[];
5
+ enabled: boolean;
6
+ }
7
+ export declare class DiscoveryBarrier {
8
+ private readonly cwd;
9
+ private readonly enabled;
10
+ private readonly knownPaths;
11
+ private readonly userMentionedPaths;
12
+ constructor(options: DiscoveryBarrierOptions);
13
+ isEnabled(): boolean;
14
+ shouldBufferStreamingToolCall(name: string): boolean;
15
+ orderToolCalls<T extends ParsedToolCall>(toolCalls: T[]): T[];
16
+ beforeToolCall(toolCall: ParsedToolCall): ToolResult | undefined;
17
+ afterToolCall(toolCall: ParsedToolCall, result: ToolResult): void;
18
+ observeMetadata(metadata: ToolResultMetadata | undefined): void;
19
+ }
20
+ export declare function isHiddenToolResult(result: ToolResult | undefined): result is ToolResult;
21
+ export declare function isHiddenToolMetadata(metadata: ToolResultMetadata | undefined): boolean;
@@ -0,0 +1,173 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import { basename, resolve } from "node:path";
3
+ import { analyzeToolIntent } from "./tool-intent.js";
4
+ import { resolveToolPath } from "../tools/path-utils.js";
5
+ export class DiscoveryBarrier {
6
+ cwd;
7
+ enabled;
8
+ knownPaths = new Set();
9
+ userMentionedPaths = new Set();
10
+ constructor(options) {
11
+ this.cwd = options.cwd;
12
+ this.enabled = options.enabled;
13
+ for (const path of extractUserMentionedPaths(options.input)) {
14
+ this.userMentionedPaths.add(canonicalPath(this.cwd, path));
15
+ }
16
+ }
17
+ isEnabled() {
18
+ return this.enabled;
19
+ }
20
+ shouldBufferStreamingToolCall(name) {
21
+ return this.enabled && (name === "read" || name === "bash" || name === "lsp");
22
+ }
23
+ orderToolCalls(toolCalls) {
24
+ if (!this.enabled || toolCalls.length < 2)
25
+ return toolCalls;
26
+ const discovery = [];
27
+ const rest = [];
28
+ for (const toolCall of toolCalls) {
29
+ if (isDiscoveryToolCall(toolCall))
30
+ discovery.push(toolCall);
31
+ else
32
+ rest.push(toolCall);
33
+ }
34
+ if (discovery.length === 0 || rest.length === 0)
35
+ return toolCalls;
36
+ return [...discovery, ...rest];
37
+ }
38
+ beforeToolCall(toolCall) {
39
+ if (!this.enabled)
40
+ return undefined;
41
+ const target = pathSensitiveReadTarget(toolCall);
42
+ if (!target)
43
+ return undefined;
44
+ const canonical = canonicalPath(this.cwd, target.path);
45
+ if (this.knownPaths.has(canonical) || this.userMentionedPaths.has(canonical))
46
+ return undefined;
47
+ return {
48
+ content: `Blocked speculative ${target.kind}. The path "${target.path}" has not been discovered in this repository context ` +
49
+ "and was not explicitly requested by the user. Run glob/grep/lsp discovery first, then read only returned paths. " +
50
+ "Do not infer from this blocked call whether the path exists.",
51
+ isError: true,
52
+ status: "blocked",
53
+ metadata: {
54
+ kind: "internal",
55
+ reason: "speculative_read_blocked",
56
+ hiddenFromTranscript: true,
57
+ path: canonical,
58
+ requestedPath: target.path,
59
+ toolName: toolCall.name,
60
+ },
61
+ };
62
+ }
63
+ afterToolCall(toolCall, result) {
64
+ if (!this.enabled || result.metadata?.hiddenFromTranscript === true)
65
+ return;
66
+ this.observeMetadata(result.metadata);
67
+ if (isSuccessfulAccessResult(result)) {
68
+ const target = pathSensitiveReadTarget(toolCall);
69
+ if (target)
70
+ this.knownPaths.add(canonicalPath(this.cwd, target.path));
71
+ }
72
+ }
73
+ observeMetadata(metadata) {
74
+ if (!metadata)
75
+ return;
76
+ for (const path of metadataPaths(metadata)) {
77
+ this.knownPaths.add(canonicalPath(this.cwd, path));
78
+ }
79
+ }
80
+ }
81
+ export function isHiddenToolResult(result) {
82
+ return result?.metadata?.hiddenFromTranscript === true;
83
+ }
84
+ export function isHiddenToolMetadata(metadata) {
85
+ return metadata?.hiddenFromTranscript === true;
86
+ }
87
+ function isDiscoveryToolCall(toolCall) {
88
+ if (toolCall.name === "glob" || toolCall.name === "grep")
89
+ return true;
90
+ return analyzeToolIntent(toolCall).family === "search";
91
+ }
92
+ function pathSensitiveReadTarget(toolCall) {
93
+ if (toolCall.name === "read") {
94
+ const path = stringArg(toolCall.parsedArgs.path ?? toolCall.parsedArgs.file);
95
+ return path ? { kind: "read", path } : undefined;
96
+ }
97
+ if (toolCall.name === "bash") {
98
+ const intent = analyzeToolIntent(toolCall);
99
+ const path = intent.read?.path;
100
+ return path ? { kind: "bash read", path } : undefined;
101
+ }
102
+ if (toolCall.name === "lsp") {
103
+ const path = stringArg(toolCall.parsedArgs.filePath);
104
+ return path ? { kind: "lsp", path } : undefined;
105
+ }
106
+ return undefined;
107
+ }
108
+ function metadataPaths(metadata) {
109
+ const paths = new Set();
110
+ addPathValue(paths, metadata.path);
111
+ addPathValue(paths, metadata.paths);
112
+ return [...paths];
113
+ }
114
+ function addPathValue(out, value) {
115
+ if (typeof value === "string" && value.trim()) {
116
+ out.add(value);
117
+ return;
118
+ }
119
+ if (!Array.isArray(value))
120
+ return;
121
+ for (const item of value) {
122
+ if (typeof item === "string" && item.trim())
123
+ out.add(item);
124
+ }
125
+ }
126
+ function isSuccessfulAccessResult(result) {
127
+ return !result.isError
128
+ && result.status !== "blocked"
129
+ && result.status !== "cancelled"
130
+ && result.status !== "command_error"
131
+ && result.status !== "timeout";
132
+ }
133
+ function canonicalPath(cwd, value) {
134
+ const absolute = resolveToolPath(cwd, value);
135
+ try {
136
+ if (existsSync(absolute))
137
+ return realpathSync.native(absolute);
138
+ }
139
+ catch {
140
+ // Fall back to lexical resolution for unreadable or racing paths.
141
+ }
142
+ return resolve(absolute);
143
+ }
144
+ function extractUserMentionedPaths(input) {
145
+ const text = typeof input === "string"
146
+ ? input
147
+ : input
148
+ .filter((part) => part.type === "text")
149
+ .map((part) => part.text)
150
+ .join("\n");
151
+ const paths = new Set();
152
+ const regex = /(?:^|[\s`'"])(~?\.{0,2}\/[^\s`'"]+|[\w@.-]+\.[A-Za-z0-9][\w.-]*)(?=$|[\s`'",.;:!?,。;:!?))])/g;
153
+ let match;
154
+ while ((match = regex.exec(text)) !== null) {
155
+ const value = match[1]?.trim();
156
+ if (value && isLikelyFilePath(value))
157
+ paths.add(stripTrailingPunctuation(value));
158
+ }
159
+ return [...paths];
160
+ }
161
+ function isLikelyFilePath(value) {
162
+ const stripped = stripTrailingPunctuation(value);
163
+ if (stripped.startsWith("./") || stripped.startsWith("../") || stripped.startsWith("~/") || stripped.startsWith("/")) {
164
+ return true;
165
+ }
166
+ return /\.[A-Za-z0-9][\w.-]*$/.test(basename(stripped));
167
+ }
168
+ function stripTrailingPunctuation(value) {
169
+ return value.replace(/[.,;:!?,。;:!?))]+$/u, "");
170
+ }
171
+ function stringArg(value) {
172
+ return typeof value === "string" ? value.trim() : "";
173
+ }
@@ -0,0 +1,9 @@
1
+ import type { AssistantProviderMetadata } from "../types.js";
2
+ export declare function formatInternalReminderBlock(kind: string, content: string): string;
3
+ export declare function formatInternalContextBlock(kind: string, content: string): string;
4
+ export declare function sanitizeInternalReminderBlocks(text: string): string;
5
+ export declare function sanitizeAssistantProviderMetadata(metadata: AssistantProviderMetadata | undefined): AssistantProviderMetadata | undefined;
6
+ export declare function createStreamingInternalReminderSanitizer(): {
7
+ push(delta: string): string;
8
+ flush(): string;
9
+ };
@@ -0,0 +1,198 @@
1
+ const INTERNAL_TAG_PREFIX = "<bubble_internal_";
2
+ const INTERNAL_TAG_NAMES = ["reminder", "context"];
3
+ const LEGACY_RUNTIME_MARKERS = [
4
+ "Runtime reminder:\n",
5
+ "Runtime context:\n",
6
+ ];
7
+ const STREAM_MARKERS = [
8
+ INTERNAL_TAG_PREFIX,
9
+ ...LEGACY_RUNTIME_MARKERS,
10
+ ];
11
+ const LEGACY_REMINDER_END_PHRASES = [
12
+ "Debugging workflow: find the failing boundary.",
13
+ "Code explanation workflow: answer directly.",
14
+ "Verify the specific failure path after the change.",
15
+ "Run a narrow verification command or explain why it cannot be run.",
16
+ "Keep summaries secondary to findings.",
17
+ "Avoid proposing changes unless the user asks for them.",
18
+ "Keep the first pass read-only unless the user asks for changes or runtime verification.",
19
+ "Avoid drifting into code changes unless the user explicitly asks to execute.",
20
+ "On rejection, remain in plan mode and iterate.",
21
+ "Do not perform destructive operations, credential exposure, or unrelated reversions just because approvals are bypassed.",
22
+ "Execute the requested change end to end; do not stop at analysis unless blocked or the user explicitly asks for discussion only.",
23
+ "If current evidence is sufficient, summarize your findings now.",
24
+ "- If you cannot determine the cause, ask the user for clarification.",
25
+ "- Skip the \"investigate the codebase\" step that applies to larger changes.",
26
+ "Do not put the final answer only in hidden reasoning.",
27
+ ];
28
+ export function formatInternalReminderBlock(kind, content) {
29
+ return formatInternalBlock("reminder", kind, content);
30
+ }
31
+ export function formatInternalContextBlock(kind, content) {
32
+ return formatInternalBlock("context", kind, content);
33
+ }
34
+ export function sanitizeInternalReminderBlocks(text) {
35
+ if (!text)
36
+ return text;
37
+ const sanitizer = createStreamingInternalReminderSanitizer();
38
+ return sanitizer.push(text) + sanitizer.flush();
39
+ }
40
+ export function sanitizeAssistantProviderMetadata(metadata) {
41
+ const anthropic = metadata?.anthropic;
42
+ const blocks = anthropic?.contentBlocks;
43
+ if (!metadata || !anthropic || !blocks?.length)
44
+ return metadata;
45
+ let changed = false;
46
+ const sanitizedBlocks = blocks.map((block) => {
47
+ if (block.type !== "text" || typeof block.text !== "string") {
48
+ return block;
49
+ }
50
+ const sanitizedText = sanitizeInternalReminderBlocks(block.text);
51
+ if (sanitizedText === block.text) {
52
+ return block;
53
+ }
54
+ changed = true;
55
+ return { ...block, text: sanitizedText };
56
+ });
57
+ if (!changed)
58
+ return metadata;
59
+ return {
60
+ ...metadata,
61
+ anthropic: {
62
+ ...anthropic,
63
+ contentBlocks: sanitizedBlocks,
64
+ },
65
+ };
66
+ }
67
+ export function createStreamingInternalReminderSanitizer() {
68
+ let pending = "";
69
+ const drain = (final) => {
70
+ let out = "";
71
+ while (pending.length > 0) {
72
+ const block = consumeInternalBlockAtStart(pending, final);
73
+ if (block?.hold)
74
+ break;
75
+ if (block?.consume !== undefined) {
76
+ pending = pending.slice(block.consume);
77
+ continue;
78
+ }
79
+ const markerIndex = findEarliestCompleteMarker(pending);
80
+ if (markerIndex >= 0) {
81
+ if (markerIndex > 0) {
82
+ out += pending.slice(0, markerIndex);
83
+ pending = pending.slice(markerIndex);
84
+ continue;
85
+ }
86
+ // A marker is present but the block is not yet complete enough to
87
+ // consume. Hold it in streaming mode; drop it on final flush.
88
+ if (final) {
89
+ pending = "";
90
+ }
91
+ break;
92
+ }
93
+ if (!final) {
94
+ const partialIndex = findEarliestMarkerPrefixSuffix(pending);
95
+ if (partialIndex >= 0) {
96
+ out += pending.slice(0, partialIndex);
97
+ pending = pending.slice(partialIndex);
98
+ break;
99
+ }
100
+ }
101
+ out += pending;
102
+ pending = "";
103
+ }
104
+ return out;
105
+ };
106
+ return {
107
+ push(delta) {
108
+ if (!delta)
109
+ return "";
110
+ pending += delta;
111
+ return drain(false);
112
+ },
113
+ flush() {
114
+ return drain(true);
115
+ },
116
+ };
117
+ }
118
+ function formatInternalBlock(type, kind, content) {
119
+ const safeKind = kind.replace(/[^a-zA-Z0-9_-]/g, "-");
120
+ return `<bubble_internal_${type} kind="${safeKind}">\n${content}\n</bubble_internal_${type}>`;
121
+ }
122
+ function consumeInternalBlockAtStart(text, final) {
123
+ if (text.startsWith(INTERNAL_TAG_PREFIX)) {
124
+ return consumeStructuredInternalBlock(text, final);
125
+ }
126
+ if (text.startsWith("Runtime reminder:\n")) {
127
+ return consumeLegacyRuntimeReminder(text, final);
128
+ }
129
+ if (text.startsWith("Runtime context:\n")) {
130
+ return final ? { consume: text.length } : { hold: true };
131
+ }
132
+ return undefined;
133
+ }
134
+ function consumeStructuredInternalBlock(text, final) {
135
+ for (const tagName of INTERNAL_TAG_NAMES) {
136
+ const openMatch = text.match(new RegExp(`^<bubble_internal_${tagName}\\b[^>]*>`));
137
+ if (!openMatch)
138
+ continue;
139
+ const closeTag = `</bubble_internal_${tagName}>`;
140
+ const closeIndex = text.indexOf(closeTag, openMatch[0].length);
141
+ if (closeIndex < 0) {
142
+ return final ? { consume: text.length } : { hold: true };
143
+ }
144
+ return { consume: consumeTrailingLineBreaks(text, closeIndex + closeTag.length) };
145
+ }
146
+ if (isPrefixOf(INTERNAL_TAG_PREFIX, text)) {
147
+ return final ? { consume: text.length } : { hold: true };
148
+ }
149
+ return undefined;
150
+ }
151
+ function consumeLegacyRuntimeReminder(text, final) {
152
+ for (const phrase of LEGACY_REMINDER_END_PHRASES) {
153
+ const endIndex = text.indexOf(phrase, LEGACY_RUNTIME_MARKERS[0].length);
154
+ if (endIndex >= 0) {
155
+ return { consume: consumeTrailingLineBreaks(text, endIndex + phrase.length) };
156
+ }
157
+ }
158
+ if (final) {
159
+ return { consume: text.length };
160
+ }
161
+ return { hold: true };
162
+ }
163
+ function consumeTrailingLineBreaks(text, index) {
164
+ let next = index;
165
+ while (text[next] === "\n")
166
+ next += 1;
167
+ return next;
168
+ }
169
+ function findEarliestCompleteMarker(text) {
170
+ let earliest = -1;
171
+ for (const marker of STREAM_MARKERS) {
172
+ const index = text.indexOf(marker);
173
+ if (index >= 0 && (earliest < 0 || index < earliest)) {
174
+ earliest = index;
175
+ }
176
+ }
177
+ return earliest;
178
+ }
179
+ function findEarliestMarkerPrefixSuffix(text) {
180
+ let earliest = -1;
181
+ for (const marker of STREAM_MARKERS) {
182
+ const max = Math.min(marker.length - 1, text.length);
183
+ for (let length = max; length > 0; length -= 1) {
184
+ const suffix = text.slice(text.length - length);
185
+ if (isPrefixOf(marker, suffix)) {
186
+ const index = text.length - length;
187
+ if (earliest < 0 || index < earliest) {
188
+ earliest = index;
189
+ }
190
+ break;
191
+ }
192
+ }
193
+ }
194
+ return earliest;
195
+ }
196
+ function isPrefixOf(value, possiblePrefix) {
197
+ return value.startsWith(possiblePrefix);
198
+ }
@@ -49,9 +49,17 @@ const EXPLANATION_PATTERNS = [
49
49
  ];
50
50
  const ORIENTATION_PATTERNS = [
51
51
  /\bwhat is this project\b/i,
52
+ /\bwhat is this (repo|repository)\b/i,
52
53
  /\borient/i,
53
54
  /\boverview\b/i,
54
- /这个项目.*(干嘛|做什么)|看下这个项目|项目.*概览/i,
55
+ /\b(repo|repository|project) overview\b/i,
56
+ /这个项目.*(干嘛|做什么)|这项目.*(干嘛|做什么)|这个\s*repo.*(干嘛|做什么)|看下(这个|这)?项目|项目.*概览|快速了解.*项目/i,
57
+ ];
58
+ const CONCRETE_ORIENTATION_EXCLUSIONS = [
59
+ /项目[中里内]\s*[,,]?\s*\S{2,}/i,
60
+ /帮我.*(实现|开发|改|加|调整|优化|修复)/i,
61
+ /(实现|开发|改一下|加一个|调整|优化|修复|支持|报错|失败|不对|有问题|bug|commit|push)/i,
62
+ /(?:\.[a-z][\w-]*|\b[A-Z]{2,}\b)/,
55
63
  ];
56
64
  const PRODUCT_PATTERNS = [
57
65
  /\bproduct\b/i,
@@ -77,15 +85,15 @@ export function classifyTask(input) {
77
85
  if (DEBUG_PATTERNS.some((pattern) => pattern.test(text))) {
78
86
  return "debugging";
79
87
  }
80
- if (ORIENTATION_PATTERNS.some((pattern) => pattern.test(text))) {
88
+ if (IMPLEMENTATION_PATTERNS.some((pattern) => pattern.test(text))) {
89
+ return "implementation";
90
+ }
91
+ if (isLikelyRepoOrientation(text)) {
81
92
  return "repo_orientation";
82
93
  }
83
94
  if (EXPLANATION_PATTERNS.some((pattern) => pattern.test(text))) {
84
95
  return "code_explanation";
85
96
  }
86
- if (IMPLEMENTATION_PATTERNS.some((pattern) => pattern.test(text))) {
87
- return "implementation";
88
- }
89
97
  if (PRODUCT_PATTERNS.some((pattern) => pattern.test(text))) {
90
98
  return "product_discussion";
91
99
  }
@@ -94,3 +102,13 @@ export function classifyTask(input) {
94
102
  }
95
103
  return "general";
96
104
  }
105
+ function isLikelyRepoOrientation(text) {
106
+ const normalized = text.trim().replace(/\s+/g, " ");
107
+ if (!ORIENTATION_PATTERNS.some((pattern) => pattern.test(normalized))) {
108
+ return false;
109
+ }
110
+ if (CONCRETE_ORIENTATION_EXCLUSIONS.some((pattern) => pattern.test(normalized))) {
111
+ return false;
112
+ }
113
+ return normalized.length <= 80;
114
+ }