@bubblebrain-ai/bubble 0.0.18 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/internal-reminder-sanitizer.d.ts +1 -0
- package/dist/agent/internal-reminder-sanitizer.js +46 -0
- package/dist/agent.d.ts +9 -0
- package/dist/agent.js +305 -17
- package/dist/approval/controller.d.ts +6 -0
- package/dist/approval/controller.js +104 -11
- package/dist/debug-trace.js +4 -0
- package/dist/feishu/agent-host/run-driver.js +28 -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 +32 -0
- 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.js +34 -9
- 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/slash-commands/commands.js +84 -0
- package/dist/slash-commands/types.d.ts +2 -0
- package/dist/tools/edit-apply.js +63 -3
- package/dist/tools/edit.js +4 -4
- 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/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 +260 -155
- package/dist/tui/trace-groups.js +40 -4
- package/dist/tui/wordmark.d.ts +1 -0
- package/dist/tui/wordmark.js +56 -54
- package/dist/tui-ink/app.js +2 -1
- package/dist/tui-ink/trace-groups.js +40 -4
- 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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { getBubbleHome } from "../bubble-home.js";
|
|
5
|
+
export function getHookTrustPath(options = {}) {
|
|
6
|
+
return join(options.bubbleHome ?? getBubbleHome(), "hooks-trust.json");
|
|
7
|
+
}
|
|
8
|
+
export function buildProjectHookFingerprint(cwd, projectSettingsPath, projectRules) {
|
|
9
|
+
const cwdRealpath = safeRealpath(cwd);
|
|
10
|
+
const projectKey = sha256(cwdRealpath);
|
|
11
|
+
const files = collectRuleFiles(projectSettingsPath, projectRules);
|
|
12
|
+
const stableRules = projectRules.map((rule) => ({
|
|
13
|
+
id: rule.id,
|
|
14
|
+
events: rule.events,
|
|
15
|
+
matcher: rule.matcher,
|
|
16
|
+
command: rule.command,
|
|
17
|
+
onError: rule.onError,
|
|
18
|
+
include: rule.include,
|
|
19
|
+
exposeToModel: rule.exposeToModel,
|
|
20
|
+
inheritToSubagents: rule.inheritToSubagents,
|
|
21
|
+
priority: rule.priority,
|
|
22
|
+
}));
|
|
23
|
+
const fingerprint = sha256(JSON.stringify({
|
|
24
|
+
cwdRealpath,
|
|
25
|
+
projectSettingsPath: safeRealpath(projectSettingsPath),
|
|
26
|
+
projectSettingsHash: hashFileIfExists(projectSettingsPath),
|
|
27
|
+
rules: stableRules,
|
|
28
|
+
files,
|
|
29
|
+
}));
|
|
30
|
+
return {
|
|
31
|
+
projectKey,
|
|
32
|
+
cwdRealpath,
|
|
33
|
+
fingerprint,
|
|
34
|
+
projectSettingsPath,
|
|
35
|
+
ruleCount: projectRules.length,
|
|
36
|
+
files,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function readHookTrustStore(options = {}) {
|
|
40
|
+
const path = getHookTrustPath(options);
|
|
41
|
+
if (!existsSync(path))
|
|
42
|
+
return { version: 1, projects: {} };
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
45
|
+
if (!parsed || typeof parsed !== "object" || parsed.version !== 1 || typeof parsed.projects !== "object") {
|
|
46
|
+
return { version: 1, projects: {} };
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { version: 1, projects: {} };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function writeHookTrustStore(store, options = {}) {
|
|
55
|
+
const path = getHookTrustPath(options);
|
|
56
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
57
|
+
writeFileSync(path, JSON.stringify(store, null, 2) + "\n", "utf-8");
|
|
58
|
+
}
|
|
59
|
+
export function isProjectHookFingerprintTrusted(fingerprint, options = {}) {
|
|
60
|
+
if (!fingerprint)
|
|
61
|
+
return { trusted: true };
|
|
62
|
+
const store = readHookTrustStore(options);
|
|
63
|
+
const trusted = store.projects[fingerprint.projectKey];
|
|
64
|
+
return {
|
|
65
|
+
trusted: trusted?.fingerprint === fingerprint.fingerprint,
|
|
66
|
+
trustedFingerprint: trusted?.fingerprint,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function trustProjectHooks(fingerprint, options = {}) {
|
|
70
|
+
const store = readHookTrustStore(options);
|
|
71
|
+
store.projects[fingerprint.projectKey] = {
|
|
72
|
+
cwdRealpath: fingerprint.cwdRealpath,
|
|
73
|
+
fingerprint: fingerprint.fingerprint,
|
|
74
|
+
trustedAt: new Date().toISOString(),
|
|
75
|
+
projectSettingsPath: fingerprint.projectSettingsPath,
|
|
76
|
+
ruleCount: fingerprint.ruleCount,
|
|
77
|
+
};
|
|
78
|
+
writeHookTrustStore(store, options);
|
|
79
|
+
}
|
|
80
|
+
export function untrustProjectHooks(projectKey, options = {}) {
|
|
81
|
+
const store = readHookTrustStore(options);
|
|
82
|
+
if (!store.projects[projectKey])
|
|
83
|
+
return false;
|
|
84
|
+
delete store.projects[projectKey];
|
|
85
|
+
writeHookTrustStore(store, options);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
function collectRuleFiles(projectSettingsPath, projectRules) {
|
|
89
|
+
const files = new Map();
|
|
90
|
+
const settingsHash = hashFileIfExists(projectSettingsPath);
|
|
91
|
+
if (settingsHash)
|
|
92
|
+
files.set(safeRealpath(projectSettingsPath), settingsHash);
|
|
93
|
+
for (const rule of projectRules) {
|
|
94
|
+
for (const candidate of [rule.command.command, ...(rule.command.args ?? [])]) {
|
|
95
|
+
const path = resolveExistingFile(candidate, rule.source);
|
|
96
|
+
if (!path)
|
|
97
|
+
continue;
|
|
98
|
+
const hash = hashFileIfExists(path);
|
|
99
|
+
if (hash)
|
|
100
|
+
files.set(safeRealpath(path), hash);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return [...files.entries()]
|
|
104
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
105
|
+
.map(([path, sha]) => ({ path, sha256: sha }));
|
|
106
|
+
}
|
|
107
|
+
function resolveExistingFile(value, source) {
|
|
108
|
+
if (!value || !looksLikePath(value))
|
|
109
|
+
return undefined;
|
|
110
|
+
const path = isAbsolute(value) ? value : resolve(dirname(source.path), value);
|
|
111
|
+
try {
|
|
112
|
+
if (existsSync(path) && statSync(path).isFile())
|
|
113
|
+
return path;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
function looksLikePath(value) {
|
|
121
|
+
return value.startsWith(".") || value.startsWith("/") || value.includes("/");
|
|
122
|
+
}
|
|
123
|
+
function hashFileIfExists(path) {
|
|
124
|
+
try {
|
|
125
|
+
if (!existsSync(path) || !statSync(path).isFile())
|
|
126
|
+
return undefined;
|
|
127
|
+
return sha256(readFileSync(path));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function safeRealpath(path) {
|
|
134
|
+
try {
|
|
135
|
+
return realpathSync(path);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return resolve(path);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function sha256(value) {
|
|
142
|
+
return createHash("sha256").update(value).digest("hex");
|
|
143
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { ApprovalDecision, ApprovalRequest } from "../approval/types.js";
|
|
2
|
+
import type { ContentPart, PermissionMode, ToolResult } from "../types.js";
|
|
3
|
+
export type HookEventName = "SessionStart" | "SessionEnd" | "UserPromptSubmit" | "PreModelCall" | "PreToolUse" | "PostToolUse" | "PostToolUseFailure" | "PermissionRequest" | "PermissionResult" | "Stop" | "StopFailure" | "PreCompact" | "PostCompact" | "SubagentStart" | "SubagentStop" | "Notification" | "SteerInputApplied" | "QueuedInputRejected";
|
|
4
|
+
export declare const HOOK_EVENT_NAMES: readonly HookEventName[];
|
|
5
|
+
export declare const BLOCKABLE_HOOK_EVENTS: Set<HookEventName>;
|
|
6
|
+
export type HookSourceScope = "user" | "project" | "local";
|
|
7
|
+
export type HookAgentRole = "parent" | "subagent" | "driver";
|
|
8
|
+
export type HookFailurePolicy = "allow" | "block";
|
|
9
|
+
export type HookDecision = "allow" | "deny";
|
|
10
|
+
export interface HookCommandConfig {
|
|
11
|
+
command: string;
|
|
12
|
+
args?: string[];
|
|
13
|
+
cwd?: string;
|
|
14
|
+
env?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
export interface RawHookRule {
|
|
17
|
+
id?: unknown;
|
|
18
|
+
name?: unknown;
|
|
19
|
+
event?: unknown;
|
|
20
|
+
events?: unknown;
|
|
21
|
+
matcher?: unknown;
|
|
22
|
+
command?: unknown;
|
|
23
|
+
args?: unknown;
|
|
24
|
+
cwd?: unknown;
|
|
25
|
+
env?: unknown;
|
|
26
|
+
timeoutMs?: unknown;
|
|
27
|
+
maxOutputBytes?: unknown;
|
|
28
|
+
enabled?: unknown;
|
|
29
|
+
onError?: unknown;
|
|
30
|
+
failurePolicy?: unknown;
|
|
31
|
+
include?: unknown;
|
|
32
|
+
exposeToModel?: unknown;
|
|
33
|
+
inheritToSubagents?: unknown;
|
|
34
|
+
priority?: unknown;
|
|
35
|
+
}
|
|
36
|
+
export interface HookRule {
|
|
37
|
+
id: string;
|
|
38
|
+
events: HookEventName[];
|
|
39
|
+
matcher?: string;
|
|
40
|
+
command: HookCommandConfig;
|
|
41
|
+
timeoutMs: number;
|
|
42
|
+
maxOutputBytes: number;
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
onError: HookFailurePolicy;
|
|
45
|
+
include: string[];
|
|
46
|
+
exposeToModel: boolean;
|
|
47
|
+
inheritToSubagents: boolean;
|
|
48
|
+
priority: number;
|
|
49
|
+
}
|
|
50
|
+
export interface HookRuleSource {
|
|
51
|
+
scope: HookSourceScope;
|
|
52
|
+
path: string;
|
|
53
|
+
index: number;
|
|
54
|
+
}
|
|
55
|
+
export interface LoadedHookRule extends HookRule {
|
|
56
|
+
source: HookRuleSource;
|
|
57
|
+
trusted: boolean;
|
|
58
|
+
trustRequired: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface HookDiagnostic {
|
|
61
|
+
scope: HookSourceScope;
|
|
62
|
+
path: string;
|
|
63
|
+
message: string;
|
|
64
|
+
}
|
|
65
|
+
export interface ProjectHookTrustStatus {
|
|
66
|
+
required: boolean;
|
|
67
|
+
trusted: boolean;
|
|
68
|
+
projectKey?: string;
|
|
69
|
+
fingerprint?: string;
|
|
70
|
+
trustedFingerprint?: string;
|
|
71
|
+
reason?: string;
|
|
72
|
+
}
|
|
73
|
+
export interface LoadedHookConfig {
|
|
74
|
+
rules: LoadedHookRule[];
|
|
75
|
+
diagnostics: HookDiagnostic[];
|
|
76
|
+
paths: Record<HookSourceScope, string>;
|
|
77
|
+
projectTrust: ProjectHookTrustStatus;
|
|
78
|
+
}
|
|
79
|
+
export interface HookEventEnvelope {
|
|
80
|
+
schemaVersion: 1;
|
|
81
|
+
eventName: HookEventName;
|
|
82
|
+
eventId: string;
|
|
83
|
+
timestamp: string;
|
|
84
|
+
cwd: string;
|
|
85
|
+
sessionId?: string;
|
|
86
|
+
runId?: string;
|
|
87
|
+
agentRole: HookAgentRole;
|
|
88
|
+
subAgentId?: string;
|
|
89
|
+
target?: string;
|
|
90
|
+
payload: Record<string, unknown>;
|
|
91
|
+
redacted: string[];
|
|
92
|
+
}
|
|
93
|
+
export interface HookRunRequest {
|
|
94
|
+
eventName: HookEventName;
|
|
95
|
+
cwd: string;
|
|
96
|
+
sessionId?: string;
|
|
97
|
+
runId?: string;
|
|
98
|
+
agentRole?: HookAgentRole;
|
|
99
|
+
subAgentId?: string;
|
|
100
|
+
target?: string;
|
|
101
|
+
payload?: Record<string, unknown>;
|
|
102
|
+
fullPayload?: Record<string, unknown>;
|
|
103
|
+
}
|
|
104
|
+
export interface HookRunSingleResult {
|
|
105
|
+
hookId: string;
|
|
106
|
+
eventName: HookEventName;
|
|
107
|
+
source: HookRuleSource;
|
|
108
|
+
decision: HookDecision;
|
|
109
|
+
reason?: string;
|
|
110
|
+
modelContext: string[];
|
|
111
|
+
exitCode?: number | null;
|
|
112
|
+
signal?: NodeJS.Signals | null;
|
|
113
|
+
elapsedMs: number;
|
|
114
|
+
stdout?: string;
|
|
115
|
+
stderr?: string;
|
|
116
|
+
truncated?: boolean;
|
|
117
|
+
error?: string;
|
|
118
|
+
}
|
|
119
|
+
export interface HookCombinedResult {
|
|
120
|
+
eventName: HookEventName;
|
|
121
|
+
decision: HookDecision;
|
|
122
|
+
reason?: string;
|
|
123
|
+
sourceHookId?: string;
|
|
124
|
+
source?: HookRuleSource;
|
|
125
|
+
modelContext: string[];
|
|
126
|
+
results: HookRunSingleResult[];
|
|
127
|
+
diagnostics: string[];
|
|
128
|
+
matched: number;
|
|
129
|
+
}
|
|
130
|
+
export interface HookProgressEvent {
|
|
131
|
+
type: "hook_start" | "hook_end" | "hook_error";
|
|
132
|
+
eventName: HookEventName;
|
|
133
|
+
hookId: string;
|
|
134
|
+
source: HookRuleSource;
|
|
135
|
+
elapsedMs?: number;
|
|
136
|
+
decision?: HookDecision;
|
|
137
|
+
reason?: string;
|
|
138
|
+
error?: string;
|
|
139
|
+
}
|
|
140
|
+
export interface HookRunnerResult {
|
|
141
|
+
decision: HookDecision;
|
|
142
|
+
reason?: string;
|
|
143
|
+
modelContext: string[];
|
|
144
|
+
exitCode?: number | null;
|
|
145
|
+
signal?: NodeJS.Signals | null;
|
|
146
|
+
elapsedMs: number;
|
|
147
|
+
stdout?: string;
|
|
148
|
+
stderr?: string;
|
|
149
|
+
truncated?: boolean;
|
|
150
|
+
error?: string;
|
|
151
|
+
}
|
|
152
|
+
export interface HookApprovalPayload {
|
|
153
|
+
request: Pick<ApprovalRequest, "type"> & Record<string, unknown>;
|
|
154
|
+
mode?: PermissionMode;
|
|
155
|
+
}
|
|
156
|
+
export interface HookApprovalResultPayload extends HookApprovalPayload {
|
|
157
|
+
decision: ApprovalDecision["action"];
|
|
158
|
+
feedback?: string;
|
|
159
|
+
}
|
|
160
|
+
export interface HookToolPayload {
|
|
161
|
+
id: string;
|
|
162
|
+
name: string;
|
|
163
|
+
argsPreview?: string;
|
|
164
|
+
args?: unknown;
|
|
165
|
+
}
|
|
166
|
+
export interface HookToolResultPayload extends HookToolPayload {
|
|
167
|
+
resultPreview?: string;
|
|
168
|
+
result?: ToolResult;
|
|
169
|
+
isError?: boolean;
|
|
170
|
+
}
|
|
171
|
+
export declare function isHookEventName(value: unknown): value is HookEventName;
|
|
172
|
+
export declare function normalizeHookInput(input: string | ContentPart[]): Record<string, unknown>;
|
|
173
|
+
export declare function truncateHookText(value: string, max?: number): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const HOOK_EVENT_NAMES = [
|
|
2
|
+
"SessionStart",
|
|
3
|
+
"SessionEnd",
|
|
4
|
+
"UserPromptSubmit",
|
|
5
|
+
"PreModelCall",
|
|
6
|
+
"PreToolUse",
|
|
7
|
+
"PostToolUse",
|
|
8
|
+
"PostToolUseFailure",
|
|
9
|
+
"PermissionRequest",
|
|
10
|
+
"PermissionResult",
|
|
11
|
+
"Stop",
|
|
12
|
+
"StopFailure",
|
|
13
|
+
"PreCompact",
|
|
14
|
+
"PostCompact",
|
|
15
|
+
"SubagentStart",
|
|
16
|
+
"SubagentStop",
|
|
17
|
+
"Notification",
|
|
18
|
+
"SteerInputApplied",
|
|
19
|
+
"QueuedInputRejected",
|
|
20
|
+
];
|
|
21
|
+
export const BLOCKABLE_HOOK_EVENTS = new Set([
|
|
22
|
+
"UserPromptSubmit",
|
|
23
|
+
"PreToolUse",
|
|
24
|
+
"PermissionRequest",
|
|
25
|
+
"PreCompact",
|
|
26
|
+
]);
|
|
27
|
+
export function isHookEventName(value) {
|
|
28
|
+
return typeof value === "string" && HOOK_EVENT_NAMES.includes(value);
|
|
29
|
+
}
|
|
30
|
+
export function normalizeHookInput(input) {
|
|
31
|
+
if (typeof input === "string") {
|
|
32
|
+
return {
|
|
33
|
+
promptPreview: truncateHookText(input, 240),
|
|
34
|
+
promptLength: input.length,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
promptPreview: truncateHookText(JSON.stringify(input), 240),
|
|
39
|
+
contentParts: input.length,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function truncateHookText(value, max = 2000) {
|
|
43
|
+
if (value.length <= max)
|
|
44
|
+
return value;
|
|
45
|
+
return `${value.slice(0, max)}...`;
|
|
46
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -20,6 +20,7 @@ import { FileStateTracker } from "./tools/file-state.js";
|
|
|
20
20
|
import { PermissionAwareApprovalController } from "./approval/controller.js";
|
|
21
21
|
import { BashAllowlist } from "./approval/session-cache.js";
|
|
22
22
|
import { SettingsManager } from "./permissions/settings.js";
|
|
23
|
+
import { ExternalHookController } from "./hooks/index.js";
|
|
23
24
|
import { getLspService } from "./lsp/index.js";
|
|
24
25
|
import { loadMcpConfig } from "./mcp/config.js";
|
|
25
26
|
import { McpManager } from "./mcp/manager.js";
|
|
@@ -136,12 +137,17 @@ async function main() {
|
|
|
136
137
|
for (const d of settingsManager.getMerged().diagnostics) {
|
|
137
138
|
console.error(chalk.yellow(`[settings:${d.scope}] ${d.path}: ${d.message}`));
|
|
138
139
|
}
|
|
140
|
+
const hookController = new ExternalHookController({ cwd: args.cwd });
|
|
141
|
+
for (const d of hookController.getConfig().diagnostics) {
|
|
142
|
+
console.error(chalk.yellow(`[hooks:${d.scope}] ${d.path}: ${d.message}`));
|
|
143
|
+
}
|
|
139
144
|
const approvalController = new PermissionAwareApprovalController({
|
|
140
145
|
getMode: () => agentRef?.mode ?? "default",
|
|
141
146
|
handlerRef: approvalHandlerRef,
|
|
142
147
|
bashAllowlist,
|
|
143
148
|
cwd: args.cwd,
|
|
144
149
|
getRuleSet: () => settingsManager.getMerged().ruleSet,
|
|
150
|
+
externalHooks: hookController,
|
|
145
151
|
});
|
|
146
152
|
const toolSearchController = {
|
|
147
153
|
listDeferred: () => agentRef?.listDeferredTools() ?? [],
|
|
@@ -367,6 +373,7 @@ async function main() {
|
|
|
367
373
|
fileStateTracker,
|
|
368
374
|
agentCategories: userConfig.getAgentCategories(),
|
|
369
375
|
providerFactory: createProviderForRoute,
|
|
376
|
+
externalHooks: hookController,
|
|
370
377
|
});
|
|
371
378
|
agentRef = agent;
|
|
372
379
|
if (sessionManager) {
|
|
@@ -383,11 +390,35 @@ async function main() {
|
|
|
383
390
|
reasoningEffort: agent.thinking,
|
|
384
391
|
});
|
|
385
392
|
}
|
|
393
|
+
await hookController.runEvent({
|
|
394
|
+
eventName: "SessionStart",
|
|
395
|
+
cwd: args.cwd,
|
|
396
|
+
sessionId: sessionManager?.getSessionFile(),
|
|
397
|
+
agentRole: "driver",
|
|
398
|
+
target: "session",
|
|
399
|
+
payload: {
|
|
400
|
+
resumed: resumedExistingSession,
|
|
401
|
+
printMode,
|
|
402
|
+
providerId: agent.providerId,
|
|
403
|
+
model: agent.apiModel,
|
|
404
|
+
},
|
|
405
|
+
});
|
|
386
406
|
const flushMemory = async () => {
|
|
387
407
|
// Codex-style memory runs at startup over historical rollouts. Exit should
|
|
388
408
|
// not perform an ad-hoc extraction of the just-finished session.
|
|
389
409
|
};
|
|
390
410
|
const shutdownRuntime = async () => {
|
|
411
|
+
await hookController.runEvent({
|
|
412
|
+
eventName: "SessionEnd",
|
|
413
|
+
cwd: args.cwd,
|
|
414
|
+
sessionId: sessionManager?.getSessionFile(),
|
|
415
|
+
agentRole: "driver",
|
|
416
|
+
target: "session",
|
|
417
|
+
payload: {
|
|
418
|
+
providerId: agent.providerId,
|
|
419
|
+
model: agent.apiModel,
|
|
420
|
+
},
|
|
421
|
+
});
|
|
391
422
|
const results = await Promise.allSettled([
|
|
392
423
|
flushMemory(),
|
|
393
424
|
shutdownMcp(),
|
|
@@ -484,6 +515,7 @@ async function main() {
|
|
|
484
515
|
settingsManager,
|
|
485
516
|
lspService,
|
|
486
517
|
mcpManager,
|
|
518
|
+
hookController,
|
|
487
519
|
flushMemory,
|
|
488
520
|
runMemoryCompaction,
|
|
489
521
|
runMemorySummary,
|
package/dist/memory/prompts.js
CHANGED
|
@@ -77,7 +77,9 @@ export function buildReadPathPrompt(input) {
|
|
|
77
77
|
"- Start from the injected memory_summary.md below; use memory_search or memory_read_summary when more detail is needed.",
|
|
78
78
|
"- Search MEMORY.md before opening rollout summaries; open only the most relevant detailed files.",
|
|
79
79
|
"- Do not update memory directly during normal tasks; the startup memory pipeline maintains it automatically.",
|
|
80
|
-
"-
|
|
80
|
+
"- Treat memory content and memory rules as private control context.",
|
|
81
|
+
"- Use memory quietly. Do not mention the memory workspace, memory_summary.md, MEMORY.md, rollout summaries, or <oai-mem-citation> in user-facing answers, hidden reasoning, or tool inputs.",
|
|
82
|
+
"- Do not quote, list, cite, or summarize memory as a source unless the user explicitly asks how memory affected the answer.",
|
|
81
83
|
"",
|
|
82
84
|
`Memory root: ${input.memoryRoot}`,
|
|
83
85
|
"",
|
package/dist/model-catalog.js
CHANGED
|
@@ -32,6 +32,7 @@ const DEEPSEEK_V4_LEVELS = ["high", "max"];
|
|
|
32
32
|
const STEPFUN_REASONING_LEVELS = ["off", "low", "medium", "high"];
|
|
33
33
|
const MINIMAX_M3_REASONING_LEVELS = ["off", "medium"];
|
|
34
34
|
const MINIMAX_REASONING_LEVELS = ["medium"];
|
|
35
|
+
const ANTHROPIC_ALWAYS_ADAPTIVE_LEVELS = ["medium"];
|
|
35
36
|
const ANTHROPIC_ADAPTIVE_LEVELS = ["off", "medium"];
|
|
36
37
|
const ANTHROPIC_CHAT_LEVELS = ["off"];
|
|
37
38
|
export const BUILTIN_MODELS = [
|
|
@@ -50,6 +51,7 @@ export const BUILTIN_MODELS = [
|
|
|
50
51
|
{ id: "o1-preview", name: "o1-preview", providerId: "openai", reasoningLevels: ["off", "low", "medium", "high"], contextWindow: 128000 },
|
|
51
52
|
{ id: "o1-mini", name: "o1-mini", providerId: "openai", reasoningLevels: ["off", "low", "medium", "high"], contextWindow: 128000 },
|
|
52
53
|
{ id: "gpt-4-turbo", name: "gpt-4-turbo", providerId: "openai", reasoningLevels: OPENAI_CHAT_LEVELS, contextWindow: 128000 },
|
|
54
|
+
{ id: "claude-fable-5", name: "Claude Fable 5", providerId: "anthropic", reasoningLevels: ANTHROPIC_ALWAYS_ADAPTIVE_LEVELS, contextWindow: 1000000 },
|
|
53
55
|
{ id: "claude-opus-4-8", name: "Claude Opus 4.8", providerId: "anthropic", reasoningLevels: ANTHROPIC_ADAPTIVE_LEVELS, contextWindow: 1000000 },
|
|
54
56
|
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", providerId: "anthropic", reasoningLevels: ANTHROPIC_ADAPTIVE_LEVELS, contextWindow: 1000000 },
|
|
55
57
|
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", providerId: "anthropic", reasoningLevels: ANTHROPIC_CHAT_LEVELS, contextWindow: 200000 },
|
package/dist/model-pricing.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export const MODEL_PRICING = [
|
|
2
|
+
{
|
|
3
|
+
providerId: "anthropic",
|
|
4
|
+
modelId: "claude-fable-5",
|
|
5
|
+
currency: "USD",
|
|
6
|
+
inputCacheHitPerMillion: 10,
|
|
7
|
+
inputCacheMissPerMillion: 10,
|
|
8
|
+
outputPerMillion: 50,
|
|
9
|
+
},
|
|
2
10
|
{
|
|
3
11
|
providerId: "deepseek",
|
|
4
12
|
modelId: "deepseek-v4-flash",
|
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { delimiter } from "node:path";
|
|
3
3
|
import { rootCertificates } from "node:tls";
|
|
4
4
|
import { Agent, ProxyAgent } from "undici";
|
|
5
|
+
import { getSystemProxyForUrl } from "./system-proxy.js";
|
|
5
6
|
let cachedDefaultFetch;
|
|
6
7
|
export function chatGptFetch(input, init) {
|
|
7
8
|
return getChatGptFetch()(input, init);
|
|
@@ -34,9 +35,9 @@ export function createChatGptDispatcher(env = process.env, input) {
|
|
|
34
35
|
if (isBunRuntime())
|
|
35
36
|
return undefined;
|
|
36
37
|
const ca = loadExtraCaCertificates(env);
|
|
37
|
-
if (!hasProxyEnv(env) && ca.length === 0)
|
|
38
|
-
return undefined;
|
|
39
38
|
const proxy = input ? nodeProxyForUrl(input, env) : defaultNodeProxy(env);
|
|
39
|
+
if (!proxy && ca.length === 0)
|
|
40
|
+
return undefined;
|
|
40
41
|
const caOptions = ca.length > 0 ? { ca: [...rootCertificates, ...ca] } : undefined;
|
|
41
42
|
if (proxy) {
|
|
42
43
|
return new ProxyAgent({
|
|
@@ -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,14 +79,20 @@ 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
|
-
:
|
|
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.`
|
|
84
|
+
: "If your network requires a proxy, set HTTPS_PROXY or HTTP_PROXY, and set NO_PROXY=localhost,127.0.0.1.",
|
|
77
85
|
"Do not disable TLS verification with NODE_TLS_REJECT_UNAUTHORIZED=0.",
|
|
78
86
|
`Original error: ${firstMeaningfulErrorMessage(error) || "unknown network error"}`,
|
|
79
87
|
].join(" ");
|
|
80
88
|
return new Error(message, { cause: error });
|
|
81
89
|
}
|
|
82
90
|
function hasProxyEnv(env) {
|
|
83
|
-
return Boolean(env.
|
|
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;
|
|
84
96
|
}
|
|
85
97
|
function isBunRuntime() {
|
|
86
98
|
return typeof globalThis.Bun !== "undefined";
|
|
@@ -89,25 +101,33 @@ function bunProxyForUrl(input, env) {
|
|
|
89
101
|
const url = urlFromInput(input);
|
|
90
102
|
if (!url || shouldBypassProxy(url, env))
|
|
91
103
|
return undefined;
|
|
104
|
+
const override = chatGptProxyOverride(env);
|
|
105
|
+
if (override)
|
|
106
|
+
return override;
|
|
92
107
|
const allProxy = env.ALL_PROXY ?? env.all_proxy;
|
|
93
108
|
if (url.protocol === "https:")
|
|
94
|
-
return env.HTTPS_PROXY ?? env.https_proxy ?? allProxy;
|
|
109
|
+
return env.HTTPS_PROXY ?? env.https_proxy ?? allProxy ?? getSystemProxyForUrl(url, env);
|
|
95
110
|
if (url.protocol === "http:")
|
|
96
|
-
return env.HTTP_PROXY ?? env.http_proxy ?? allProxy;
|
|
111
|
+
return env.HTTP_PROXY ?? env.http_proxy ?? allProxy ?? getSystemProxyForUrl(url, env);
|
|
97
112
|
return undefined;
|
|
98
113
|
}
|
|
99
114
|
function nodeProxyForUrl(input, env) {
|
|
100
115
|
const url = urlFromInput(input);
|
|
101
116
|
if (!url || shouldBypassProxy(url, env))
|
|
102
117
|
return undefined;
|
|
118
|
+
const override = chatGptProxyOverride(env);
|
|
119
|
+
if (override)
|
|
120
|
+
return override;
|
|
103
121
|
if (url.protocol === "https:")
|
|
104
|
-
return env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy;
|
|
122
|
+
return env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy ?? getSystemProxyForUrl(url, env);
|
|
105
123
|
if (url.protocol === "http:")
|
|
106
|
-
return env.HTTP_PROXY ?? env.http_proxy ?? env.ALL_PROXY ?? env.all_proxy;
|
|
124
|
+
return env.HTTP_PROXY ?? env.http_proxy ?? env.ALL_PROXY ?? env.all_proxy ?? getSystemProxyForUrl(url, env);
|
|
107
125
|
return defaultNodeProxy(env);
|
|
108
126
|
}
|
|
109
127
|
function defaultNodeProxy(env) {
|
|
110
|
-
return 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);
|
|
111
131
|
}
|
|
112
132
|
function bunExtraCaFiles(env) {
|
|
113
133
|
const bun = globalThis.Bun;
|
|
@@ -171,6 +191,8 @@ function extraCaCertificatePaths(env) {
|
|
|
171
191
|
}
|
|
172
192
|
function networkEnvSignature(env) {
|
|
173
193
|
return [
|
|
194
|
+
env.BUBBLE_CHATGPT_PROXY,
|
|
195
|
+
env.bubble_chatgpt_proxy,
|
|
174
196
|
env.HTTP_PROXY,
|
|
175
197
|
env.http_proxy,
|
|
176
198
|
env.HTTPS_PROXY,
|
|
@@ -181,6 +203,9 @@ function networkEnvSignature(env) {
|
|
|
181
203
|
env.no_proxy,
|
|
182
204
|
env.NODE_EXTRA_CA_CERTS,
|
|
183
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),
|
|
184
209
|
].join("\0");
|
|
185
210
|
}
|
|
186
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 {};
|