@bubblebrain-ai/bubble 0.0.58 → 0.0.60
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/README.md +56 -6
- package/dist/agent/child-runner.js +5 -1
- package/dist/agent/internal-reminder-sanitizer.d.ts +1 -1
- package/dist/agent/internal-reminder-sanitizer.js +7 -1
- package/dist/agent/profiles.js +4 -4
- package/dist/agent/routing-catalog.d.ts +19 -1
- package/dist/agent/routing-catalog.js +32 -13
- package/dist/agent/subagent/runtime.d.ts +28 -0
- package/dist/agent/subagent/runtime.js +34 -30
- package/dist/agent/subagent-control.d.ts +7 -0
- package/dist/agent/subagent-control.js +2 -0
- package/dist/agent/subagent-lifecycle-reminder.js +2 -1
- package/dist/agent/subagent-wait-guidance.d.ts +2 -0
- package/dist/agent/subagent-wait-guidance.js +8 -0
- package/dist/agent/subtask-policy.js +4 -4
- package/dist/agent.d.ts +20 -11
- package/dist/agent.js +200 -117
- package/dist/anthropic-thinking.d.ts +4 -0
- package/dist/anthropic-thinking.js +11 -0
- package/dist/approval/controller.d.ts +20 -6
- package/dist/approval/controller.js +67 -8
- package/dist/approval/protected-paths.d.ts +1 -0
- package/dist/approval/protected-paths.js +35 -0
- package/dist/approval/session-cache.d.ts +12 -26
- package/dist/approval/session-cache.js +20 -50
- package/dist/approval/types.d.ts +27 -1
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +25 -2
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -0
- package/dist/context/checkpoint.d.ts +18 -0
- package/dist/context/checkpoint.js +117 -0
- package/dist/context/compact-llm.js +7 -0
- package/dist/context/compact.d.ts +8 -0
- package/dist/context/compact.js +40 -7
- package/dist/context/llm-compactor.d.ts +36 -2
- package/dist/context/llm-compactor.js +227 -44
- package/dist/context/session-write-lock.d.ts +20 -0
- package/dist/context/session-write-lock.js +162 -0
- package/dist/context/tool-images.d.ts +3 -0
- package/dist/context/tool-images.js +35 -0
- package/dist/context/usage.js +22 -5
- package/dist/feishu/agent-host/run-driver.js +56 -9
- package/dist/lsp/service.d.ts +13 -1
- package/dist/lsp/service.js +163 -43
- package/dist/main.js +82 -16
- package/dist/mcp/config.d.ts +4 -0
- package/dist/mcp/config.js +17 -0
- package/dist/mcp/manager.d.ts +13 -1
- package/dist/mcp/manager.js +32 -1
- package/dist/mcp/transports.js +11 -0
- package/dist/memory/db.d.ts +1 -0
- package/dist/memory/db.js +20 -4
- package/dist/memory/phase1.js +122 -16
- package/dist/memory/prompts.js +3 -0
- package/dist/model-catalog.js +19 -3
- package/dist/model-config.d.ts +1 -0
- package/dist/model-config.js +5 -5
- package/dist/model-pricing.js +26 -9
- package/dist/network/provider-transport-log.d.ts +12 -0
- package/dist/network/provider-transport-log.js +32 -0
- package/dist/network/provider-transport.js +6 -1
- package/dist/network/retry.d.ts +1 -1
- package/dist/network/retry.js +1 -1
- package/dist/oauth/grok.d.ts +1 -0
- package/dist/oauth/grok.js +20 -16
- package/dist/oauth/openai-codex.d.ts +1 -0
- package/dist/oauth/openai-codex.js +29 -25
- package/dist/oauth/refresh-control.d.ts +12 -0
- package/dist/oauth/refresh-control.js +42 -0
- package/dist/oauth/storage.d.ts +55 -1
- package/dist/oauth/storage.js +281 -16
- package/dist/permissions/rule.d.ts +6 -3
- package/dist/permissions/rule.js +71 -12
- package/dist/permissions/settings.d.ts +28 -1
- package/dist/permissions/settings.js +93 -3
- package/dist/permissions/shell-command.d.ts +36 -0
- package/dist/permissions/shell-command.js +226 -0
- package/dist/permissions/trust-prompt.d.ts +10 -0
- package/dist/permissions/trust-prompt.js +34 -0
- package/dist/permissions/trust.d.ts +26 -0
- package/dist/permissions/trust.js +113 -0
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +24 -7
- package/dist/prompt/environment.js +2 -2
- package/dist/prompt/reminders.js +1 -1
- package/dist/prompt/runtime.js +1 -1
- package/dist/provider-anthropic.d.ts +6 -2
- package/dist/provider-anthropic.js +98 -18
- package/dist/provider-error-record.d.ts +9 -0
- package/dist/provider-error-record.js +42 -14
- package/dist/provider-grok.js +5 -1
- package/dist/provider-openai-codex.d.ts +8 -0
- package/dist/provider-openai-codex.js +107 -24
- package/dist/provider-registry.d.ts +45 -2
- package/dist/provider-registry.js +300 -88
- package/dist/provider-transform.js +8 -0
- package/dist/provider-zen-errors.d.ts +9 -0
- package/dist/provider-zen-errors.js +36 -0
- package/dist/provider.js +36 -5
- package/dist/sdk/index.d.ts +69 -8
- package/dist/sdk/index.js +252 -39
- package/dist/sdk/replay-event-log.d.ts +7 -0
- package/dist/sdk/replay-event-log.js +66 -16
- package/dist/session-context-fence.d.ts +16 -0
- package/dist/session-context-fence.js +26 -0
- package/dist/session-log.d.ts +11 -2
- package/dist/session-log.js +187 -39
- package/dist/session-types.d.ts +5 -1
- package/dist/session.d.ts +39 -7
- package/dist/session.js +341 -93
- package/dist/skills/format.d.ts +31 -1
- package/dist/skills/format.js +45 -24
- package/dist/slash-commands/commands.d.ts +3 -0
- package/dist/slash-commands/commands.js +29 -14
- package/dist/system-prompt.d.ts +3 -1
- package/dist/tasks/manager.d.ts +7 -0
- package/dist/tasks/manager.js +97 -18
- package/dist/tools/agent-lifecycle.js +11 -7
- package/dist/tools/child-tools.d.ts +1 -1
- package/dist/tools/child-tools.js +4 -4
- package/dist/tools/edit-apply.d.ts +0 -3
- package/dist/tools/edit-apply.js +45 -249
- package/dist/tools/edit.js +3 -3
- package/dist/tools/exit-plan-mode.d.ts +5 -0
- package/dist/tools/exit-plan-mode.js +3 -2
- package/dist/tools/file-state.d.ts +4 -0
- package/dist/tools/file-state.js +12 -3
- package/dist/tools/grep.js +20 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/ls.d.ts +3 -0
- package/dist/tools/ls.js +85 -0
- package/dist/tools/path-utils.d.ts +6 -0
- package/dist/tools/path-utils.js +38 -1
- package/dist/tools/sensitive-paths.d.ts +8 -0
- package/dist/tools/sensitive-paths.js +85 -3
- package/dist/tools/skill.d.ts +2 -0
- package/dist/tools/skill.js +34 -2
- package/dist/tools/task-tools.js +11 -7
- package/dist/tui/app.js +22 -12
- package/dist/tui/components/approval-dialog.d.ts +5 -5
- package/dist/tui/components/approval-dialog.js +16 -51
- package/dist/tui/components/skills-panel.d.ts +4 -0
- package/dist/tui/components/skills-panel.js +15 -1
- package/dist/tui/components/streaming-message.js +7 -2
- package/dist/tui/components/tasks-pane.d.ts +38 -0
- package/dist/tui/components/tasks-pane.js +182 -32
- package/dist/tui/components/transcript.d.ts +11 -4
- package/dist/tui/components/transcript.js +158 -71
- package/dist/tui/composer-autocomplete.d.ts +17 -2
- package/dist/tui/composer-autocomplete.js +79 -2
- package/dist/tui/controller/controller.d.ts +9 -4
- package/dist/tui/controller/controller.js +87 -95
- package/dist/tui/controller/goal-runtime-controller.js +1 -1
- package/dist/tui/controller/session-transition.js +1 -1
- package/dist/tui/model/subagent-view.d.ts +11 -19
- package/dist/tui/model/subagent-view.js +56 -85
- package/dist/tui/model/task-lifecycle.d.ts +57 -0
- package/dist/tui/model/task-lifecycle.js +124 -0
- package/dist/tui/model/trace-groups.js +10 -9
- package/dist/tui/trace-groups.js +10 -9
- package/dist/types.d.ts +20 -0
- package/dist/variant/variant-resolver.js +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +6 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +8 -4
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -1
- package/package.json +11 -3
- package/dist/prompt/skills.d.ts +0 -2
- package/dist/prompt/skills.js +0 -4
- package/dist/tools/glob.d.ts +0 -5
- package/dist/tools/glob.js +0 -199
package/dist/lsp/service.js
CHANGED
|
@@ -2,11 +2,38 @@ import { execFile as execFileCallback, spawn } from "node:child_process";
|
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import { access, readFile } from "node:fs/promises";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
8
8
|
import { createMessageConnection, StreamMessageReader, StreamMessageWriter, } from "vscode-jsonrpc/node.js";
|
|
9
9
|
import { customLspServerEntries, isLspEnabled, isLspServerEnabled } from "./config.js";
|
|
10
|
+
// vscode-jsonrpc 8.x rethrows write failures inside an async Promise executor.
|
|
11
|
+
// Dispose through the lifecycle handler instead: pending requests still reject,
|
|
12
|
+
// without an additional unhandled rejection that can crash the desktop host.
|
|
13
|
+
class LspMessageWriter extends StreamMessageWriter {
|
|
14
|
+
onWriteFailure;
|
|
15
|
+
constructor(stream, onWriteFailure) {
|
|
16
|
+
super(stream);
|
|
17
|
+
this.onWriteFailure = onWriteFailure;
|
|
18
|
+
}
|
|
19
|
+
async write(message) {
|
|
20
|
+
try {
|
|
21
|
+
await super.write(message);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
this.onWriteFailure(error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Electron's executable is also a Node runtime, but only with this child-local
|
|
29
|
+
// flag. Preserve it for descendants (e.g. typescript-language-server's tsserver).
|
|
30
|
+
function spawnNodeServer(args, cwd) {
|
|
31
|
+
return spawn(process.execPath, args, {
|
|
32
|
+
cwd,
|
|
33
|
+
env: process.versions.electron ? { ...process.env, ELECTRON_RUN_AS_NODE: "1" } : process.env,
|
|
34
|
+
stdio: "pipe",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
10
37
|
const execFile = promisify(execFileCallback);
|
|
11
38
|
const services = new Map();
|
|
12
39
|
const TS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
|
|
@@ -14,9 +41,12 @@ const NODE_ROOT_MARKERS = ["package-lock.json", "bun.lockb", "bun.lock", "pnpm-l
|
|
|
14
41
|
const DENO_ROOT_MARKERS = ["deno.json", "deno.jsonc"];
|
|
15
42
|
export class ProjectLspService {
|
|
16
43
|
cwd;
|
|
44
|
+
timeouts;
|
|
17
45
|
emitter = new EventEmitter();
|
|
18
46
|
clients = new Map();
|
|
19
47
|
spawning = new Map();
|
|
48
|
+
initializing = new Map();
|
|
49
|
+
generation = 0;
|
|
20
50
|
starting = new Map();
|
|
21
51
|
broken = new Map();
|
|
22
52
|
unavailable = new Set();
|
|
@@ -24,8 +54,9 @@ export class ProjectLspService {
|
|
|
24
54
|
waiters = new Set();
|
|
25
55
|
disposed = false;
|
|
26
56
|
config;
|
|
27
|
-
constructor(cwd, config) {
|
|
57
|
+
constructor(cwd, config, timeouts = {}) {
|
|
28
58
|
this.cwd = cwd;
|
|
59
|
+
this.timeouts = timeouts;
|
|
29
60
|
this.config = config;
|
|
30
61
|
}
|
|
31
62
|
updateConfig(config) {
|
|
@@ -122,13 +153,12 @@ export class ProjectLspService {
|
|
|
122
153
|
return [];
|
|
123
154
|
const clients = await this.getClients(file);
|
|
124
155
|
await Promise.all(clients.map((client) => this.openOrChange(client, file)));
|
|
125
|
-
const results = await Promise.all(clients.map((client) => client.
|
|
126
|
-
.sendRequest("textDocument/documentSymbol", { textDocument: { uri: pathToFileURL(file).href } })
|
|
156
|
+
const results = await Promise.all(clients.map((client) => this.request(client, "textDocument/documentSymbol", { textDocument: { uri: pathToFileURL(file).href } })
|
|
127
157
|
.catch(() => [])));
|
|
128
158
|
return results.flatMap(normalizeLspResult);
|
|
129
159
|
}
|
|
130
160
|
async workspaceSymbol(query) {
|
|
131
|
-
const results = await Promise.all([...this.clients.values()].map((client) =>
|
|
161
|
+
const results = await Promise.all([...this.clients.values()].map((client) => this.request(client, "workspace/symbol", { query }).catch(() => [])));
|
|
132
162
|
return results.flatMap(normalizeLspResult).slice(0, 50);
|
|
133
163
|
}
|
|
134
164
|
async prepareCallHierarchy(input) {
|
|
@@ -157,12 +187,11 @@ export class ProjectLspService {
|
|
|
157
187
|
for (const waiter of this.waiters)
|
|
158
188
|
waiter.resolve();
|
|
159
189
|
this.waiters.clear();
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
this.clients.clear();
|
|
190
|
+
this.disposed = true;
|
|
191
|
+
this.generation += 1;
|
|
192
|
+
for (const client of [...this.initializing.values(), ...this.clients.values()])
|
|
193
|
+
this.stopClient(client);
|
|
194
|
+
this.starting.clear();
|
|
166
195
|
}
|
|
167
196
|
async runRequest(input, method, params) {
|
|
168
197
|
const file = this.resolveInsideCwd(input.file);
|
|
@@ -170,7 +199,7 @@ export class ProjectLspService {
|
|
|
170
199
|
return [];
|
|
171
200
|
const clients = await this.getClients(file);
|
|
172
201
|
await Promise.all(clients.map((client) => this.openOrChange(client, file)));
|
|
173
|
-
const results = await Promise.all(clients.map((client) =>
|
|
202
|
+
const results = await Promise.all(clients.map((client) => this.request(client, method, params).catch(() => [])));
|
|
174
203
|
return results.flatMap(normalizeLspResult);
|
|
175
204
|
}
|
|
176
205
|
async callHierarchyRequest(input, method) {
|
|
@@ -180,8 +209,7 @@ export class ProjectLspService {
|
|
|
180
209
|
const clients = await this.getClients(file);
|
|
181
210
|
const results = await Promise.all(clients.map(async (client) => {
|
|
182
211
|
await this.openOrChange(client, file);
|
|
183
|
-
const items = await client
|
|
184
|
-
.sendRequest("textDocument/prepareCallHierarchy", {
|
|
212
|
+
const items = await this.request(client, "textDocument/prepareCallHierarchy", {
|
|
185
213
|
textDocument: { uri: pathToFileURL(file).href },
|
|
186
214
|
position: { line: input.line, character: input.character },
|
|
187
215
|
})
|
|
@@ -189,16 +217,20 @@ export class ProjectLspService {
|
|
|
189
217
|
const first = normalizeLspResult(items)[0];
|
|
190
218
|
if (!first)
|
|
191
219
|
return [];
|
|
192
|
-
return
|
|
220
|
+
return this.request(client, method, { item: first }).catch(() => []);
|
|
193
221
|
}));
|
|
194
222
|
return results.flatMap(normalizeLspResult);
|
|
195
223
|
}
|
|
196
224
|
async getClients(file) {
|
|
225
|
+
if (this.disposed)
|
|
226
|
+
return [];
|
|
197
227
|
const matches = await this.matchingServers(file);
|
|
198
228
|
const clients = await Promise.all(matches.map(({ server, root }) => this.getClient(server, root)));
|
|
199
229
|
return clients.filter((client) => !!client);
|
|
200
230
|
}
|
|
201
231
|
async getClient(server, root) {
|
|
232
|
+
if (this.disposed)
|
|
233
|
+
return undefined;
|
|
202
234
|
const key = `${root}:${server.id}`;
|
|
203
235
|
const existing = this.clients.get(key);
|
|
204
236
|
if (existing)
|
|
@@ -221,27 +253,36 @@ export class ProjectLspService {
|
|
|
221
253
|
const task = this.spawnClient(server, root, key);
|
|
222
254
|
this.spawning.set(key, task);
|
|
223
255
|
task.finally(() => {
|
|
224
|
-
if (this.spawning.get(key) === task)
|
|
256
|
+
if (this.spawning.get(key) === task) {
|
|
225
257
|
this.spawning.delete(key);
|
|
226
|
-
|
|
227
|
-
|
|
258
|
+
if (this.starting.delete(key))
|
|
259
|
+
this.emitStatus();
|
|
260
|
+
}
|
|
228
261
|
});
|
|
229
262
|
return task;
|
|
230
263
|
}
|
|
231
264
|
async spawnClient(server, root, key) {
|
|
265
|
+
const generation = this.generation;
|
|
266
|
+
let client;
|
|
232
267
|
try {
|
|
233
268
|
const handle = await server.spawn(root, { cwd: this.cwd });
|
|
234
269
|
if (!handle) {
|
|
235
270
|
this.unavailable.add(key);
|
|
236
271
|
return undefined;
|
|
237
272
|
}
|
|
238
|
-
|
|
273
|
+
if (this.disposed || generation !== this.generation || !isLspServerEnabled(this.config, server.id)) {
|
|
274
|
+
// Install an error handler even when shutdown won the spawn race.
|
|
275
|
+
handle.process.on("error", () => { });
|
|
276
|
+
handle.process.kill();
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
const connection = createMessageConnection(new StreamMessageReader(handle.process.stdout), new LspMessageWriter(handle.process.stdin, (error) => client?.fail(`server write failed: ${String(error)}`)), {
|
|
239
280
|
error: () => { },
|
|
240
281
|
warn: () => { },
|
|
241
282
|
info: () => { },
|
|
242
283
|
log: () => { },
|
|
243
284
|
});
|
|
244
|
-
|
|
285
|
+
client = {
|
|
245
286
|
key,
|
|
246
287
|
id: server.id,
|
|
247
288
|
name: server.name,
|
|
@@ -251,7 +292,22 @@ export class ProjectLspService {
|
|
|
251
292
|
connection,
|
|
252
293
|
documents: new Map(),
|
|
253
294
|
stopping: false,
|
|
295
|
+
fail: (message) => {
|
|
296
|
+
if (client.stopping)
|
|
297
|
+
return;
|
|
298
|
+
this.stopClient(client);
|
|
299
|
+
if (!this.disposed && generation === this.generation)
|
|
300
|
+
this.markBroken(key, server, root, message);
|
|
301
|
+
},
|
|
254
302
|
};
|
|
303
|
+
const activeClient = client;
|
|
304
|
+
this.initializing.set(key, client);
|
|
305
|
+
// Drain stderr: an unread pipe can block an otherwise healthy server.
|
|
306
|
+
handle.process.stderr.resume();
|
|
307
|
+
handle.process.once("error", (error) => activeClient.fail(`server error: ${error.message}`));
|
|
308
|
+
handle.process.once("exit", (code, signal) => activeClient.fail(`server exited${code === null ? "" : ` with code ${code}`}${signal ? ` (${signal})` : ""}`));
|
|
309
|
+
connection.onClose(() => activeClient.fail("server connection closed"));
|
|
310
|
+
connection.onError(([error]) => activeClient.fail(`server connection error: ${error.message}`));
|
|
255
311
|
connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
|
256
312
|
if (!params?.uri)
|
|
257
313
|
return;
|
|
@@ -265,14 +321,8 @@ export class ProjectLspService {
|
|
|
265
321
|
connection.onRequest("workspace/workspaceFolders", () => [{ uri: pathToFileURL(root).href, name: basename(root) }]);
|
|
266
322
|
connection.onRequest("client/registerCapability", () => null);
|
|
267
323
|
connection.onRequest("client/unregisterCapability", () => null);
|
|
268
|
-
handle.process.once("exit", (code, signal) => {
|
|
269
|
-
this.clients.delete(key);
|
|
270
|
-
if (!this.disposed && !client.stopping) {
|
|
271
|
-
this.markBroken(key, server, root, `server exited${code === null ? "" : ` with code ${code}`}${signal ? ` (${signal})` : ""}`);
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
324
|
connection.listen();
|
|
275
|
-
await
|
|
325
|
+
await this.request(client, "initialize", {
|
|
276
326
|
processId: process.pid,
|
|
277
327
|
rootPath: root,
|
|
278
328
|
rootUri: pathToFileURL(root).href,
|
|
@@ -296,16 +346,74 @@ export class ProjectLspService {
|
|
|
296
346
|
},
|
|
297
347
|
window: { workDoneProgress: false },
|
|
298
348
|
},
|
|
299
|
-
});
|
|
300
|
-
|
|
349
|
+
}, this.timeouts.initializeMs ?? 10_000);
|
|
350
|
+
if (client.stopping || this.disposed || generation !== this.generation)
|
|
351
|
+
return undefined;
|
|
352
|
+
await this.notify(client, "initialized", {});
|
|
353
|
+
if (client.stopping || this.disposed || generation !== this.generation)
|
|
354
|
+
return undefined;
|
|
301
355
|
this.clients.set(key, client);
|
|
302
356
|
this.emitStatus();
|
|
303
357
|
return client;
|
|
304
358
|
}
|
|
305
359
|
catch (error) {
|
|
306
|
-
|
|
360
|
+
if (client)
|
|
361
|
+
client.fail(error instanceof Error ? error.message : String(error));
|
|
362
|
+
else if (!this.disposed && generation === this.generation) {
|
|
363
|
+
this.markBroken(key, server, root, error instanceof Error ? error.message : String(error));
|
|
364
|
+
}
|
|
307
365
|
return undefined;
|
|
308
366
|
}
|
|
367
|
+
finally {
|
|
368
|
+
if (this.initializing.get(key) === client)
|
|
369
|
+
this.initializing.delete(key);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async request(client, method, params, timeoutMs = this.timeouts.requestMs ?? 10_000) {
|
|
373
|
+
return this.withDeadline(client, method, () => client.connection.sendRequest(method, params), timeoutMs);
|
|
374
|
+
}
|
|
375
|
+
async notify(client, method, params) {
|
|
376
|
+
await this.withDeadline(client, method, () => client.connection.sendNotification(method, params));
|
|
377
|
+
}
|
|
378
|
+
async withDeadline(client, method, operation, timeoutMs = this.timeouts.requestMs ?? 10_000) {
|
|
379
|
+
let timer;
|
|
380
|
+
try {
|
|
381
|
+
return await Promise.race([
|
|
382
|
+
operation(),
|
|
383
|
+
new Promise((_, reject) => {
|
|
384
|
+
timer = setTimeout(() => {
|
|
385
|
+
const message = `${method} timed out after ${timeoutMs}ms`;
|
|
386
|
+
reject(new Error(message));
|
|
387
|
+
client.fail(message);
|
|
388
|
+
}, timeoutMs);
|
|
389
|
+
}),
|
|
390
|
+
]);
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
clearTimeout(timer);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
stopClient(client) {
|
|
397
|
+
client.stopping = true;
|
|
398
|
+
if (this.clients.get(client.key) === client)
|
|
399
|
+
this.clients.delete(client.key);
|
|
400
|
+
if (this.initializing.get(client.key) === client)
|
|
401
|
+
this.initializing.delete(client.key);
|
|
402
|
+
// JSON-RPC close alone does not reject pending requests; dispose does.
|
|
403
|
+
client.connection.dispose();
|
|
404
|
+
if (client.process.exitCode === null && client.process.signalCode === null) {
|
|
405
|
+
client.process.kill();
|
|
406
|
+
const forceKill = setTimeout(() => {
|
|
407
|
+
if (client.process.exitCode === null && client.process.signalCode === null)
|
|
408
|
+
client.process.kill("SIGKILL");
|
|
409
|
+
}, 1_000);
|
|
410
|
+
forceKill.unref();
|
|
411
|
+
client.process.once("exit", () => clearTimeout(forceKill));
|
|
412
|
+
}
|
|
413
|
+
for (const file of client.documents.keys()) {
|
|
414
|
+
this.diagnosticsByFile.get(file)?.delete(client.id);
|
|
415
|
+
this.resolveDiagnosticWaiters(file);
|
|
416
|
+
}
|
|
309
417
|
}
|
|
310
418
|
async openOrChange(client, file) {
|
|
311
419
|
const text = await readFile(file, "utf-8");
|
|
@@ -314,13 +422,13 @@ export class ProjectLspService {
|
|
|
314
422
|
const existing = client.documents.get(file);
|
|
315
423
|
if (!existing) {
|
|
316
424
|
client.documents.set(file, { languageId, version: 1 });
|
|
317
|
-
|
|
425
|
+
await this.notify(client, "textDocument/didOpen", {
|
|
318
426
|
textDocument: { uri, languageId, version: 1, text },
|
|
319
427
|
});
|
|
320
428
|
return;
|
|
321
429
|
}
|
|
322
430
|
existing.version += 1;
|
|
323
|
-
|
|
431
|
+
await this.notify(client, "textDocument/didChange", {
|
|
324
432
|
textDocument: { uri, version: existing.version },
|
|
325
433
|
contentChanges: [{ text }],
|
|
326
434
|
});
|
|
@@ -392,29 +500,41 @@ export class ProjectLspService {
|
|
|
392
500
|
resolveInsideCwd(filePath) {
|
|
393
501
|
const file = normalize(isAbsolute(filePath) ? filePath : resolve(this.cwd, filePath));
|
|
394
502
|
const rel = relative(this.cwd, file);
|
|
395
|
-
|
|
503
|
+
// Only a whole `..` segment climbs out; `..notes.md` is a file in the project.
|
|
504
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
396
505
|
return undefined;
|
|
397
506
|
return file;
|
|
398
507
|
}
|
|
399
508
|
async shutdownClient(client) {
|
|
400
509
|
client.stopping = true;
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
510
|
+
try {
|
|
511
|
+
await this.request(client, "shutdown", undefined, this.timeouts.shutdownMs ?? 1_000);
|
|
512
|
+
await this.withDeadline(client, "exit", () => client.connection.sendNotification("exit"), this.timeouts.shutdownMs ?? 1_000);
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
// Shutdown must finish even if the server no longer answers.
|
|
516
|
+
}
|
|
517
|
+
finally {
|
|
518
|
+
this.stopClient(client);
|
|
519
|
+
}
|
|
405
520
|
}
|
|
406
521
|
async shutdownClients() {
|
|
522
|
+
this.generation += 1;
|
|
523
|
+
for (const client of [...this.initializing.values()])
|
|
524
|
+
this.stopClient(client);
|
|
525
|
+
this.spawning.clear();
|
|
407
526
|
await Promise.all([...this.clients.values()].map((client) => this.shutdownClient(client)));
|
|
408
527
|
this.clients.clear();
|
|
409
528
|
this.starting.clear();
|
|
410
529
|
this.diagnosticsByFile.clear();
|
|
411
530
|
}
|
|
412
531
|
async shutdownDisabledClients() {
|
|
532
|
+
for (const client of [...this.initializing.values()]) {
|
|
533
|
+
if (!isLspServerEnabled(this.config, client.id))
|
|
534
|
+
this.stopClient(client);
|
|
535
|
+
}
|
|
413
536
|
const disabled = [...this.clients.values()].filter((client) => !isLspServerEnabled(this.config, client.id));
|
|
414
537
|
await Promise.all(disabled.map((client) => this.shutdownClient(client)));
|
|
415
|
-
for (const client of disabled) {
|
|
416
|
-
this.clients.delete(client.key);
|
|
417
|
-
}
|
|
418
538
|
}
|
|
419
539
|
}
|
|
420
540
|
export const TypeScriptLspService = ProjectLspService;
|
|
@@ -446,7 +566,7 @@ const TypeScriptServer = {
|
|
|
446
566
|
if (!tsserverPath || !serverPath)
|
|
447
567
|
return undefined;
|
|
448
568
|
return {
|
|
449
|
-
process:
|
|
569
|
+
process: spawnNodeServer([serverPath, "--stdio"], root),
|
|
450
570
|
initializationOptions: { tsserver: { path: tsserverPath } },
|
|
451
571
|
};
|
|
452
572
|
},
|
|
@@ -485,7 +605,7 @@ const VueServer = {
|
|
|
485
605
|
return undefined;
|
|
486
606
|
const args = tsdk ? [serverPath, "--stdio", "--tsdk", tsdk] : [serverPath, "--stdio"];
|
|
487
607
|
return {
|
|
488
|
-
process:
|
|
608
|
+
process: spawnNodeServer(args, root),
|
|
489
609
|
};
|
|
490
610
|
},
|
|
491
611
|
};
|
|
@@ -506,7 +626,7 @@ const ESLintServer = {
|
|
|
506
626
|
if (!serverPath)
|
|
507
627
|
return undefined;
|
|
508
628
|
return {
|
|
509
|
-
process:
|
|
629
|
+
process: spawnNodeServer([serverPath, "--stdio"], root),
|
|
510
630
|
};
|
|
511
631
|
},
|
|
512
632
|
configuration: (root, items) => items.map(() => eslintConfiguration(root)),
|
package/dist/main.js
CHANGED
|
@@ -5,17 +5,20 @@
|
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { Agent } from "./agent.js";
|
|
7
7
|
import { BudgetLedger } from "./agent/budget-ledger.js";
|
|
8
|
-
import { parseArgs, printHelp } from "./cli.js";
|
|
8
|
+
import { enterWorkingDirectory, parseArgs, printHelp } from "./cli.js";
|
|
9
9
|
import { effectiveThemeModeForTerminal, shouldProbeTerminalTheme, UserConfig } from "./config.js";
|
|
10
10
|
import { createProviderInstance, createUnavailableProvider } from "./provider.js";
|
|
11
11
|
import { resolveConfiguredModel } from "./model-selection.js";
|
|
12
12
|
import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "./provider-transform.js";
|
|
13
13
|
import { ProviderRegistry, displayModel, encodeModel, decodeModel } from "./provider-registry.js";
|
|
14
14
|
import { SessionManager } from "./session.js";
|
|
15
|
+
import { SessionContextFence } from "./session-context-fence.js";
|
|
15
16
|
import { createSessionTitleUpdater } from "./session-title.js";
|
|
16
17
|
import { buildSystemPrompt } from "./system-prompt.js";
|
|
17
18
|
import { createRoutableModelIndex, createRoutingSnapshotAccessor } from "./agent/routing-catalog.js";
|
|
18
19
|
import { buildModelRoutingPrompt } from "./prompt/routing.js";
|
|
20
|
+
/** Upper bound on blocking startup for OAuth catalog discovery (one HTTP round trip normally). */
|
|
21
|
+
const MODEL_DISCOVERY_STARTUP_WAIT_MS = 3_000;
|
|
19
22
|
import { SkillRegistry } from "./skills/registry.js";
|
|
20
23
|
import { buildToolPromptOptions, createAllTools } from "./tools/index.js";
|
|
21
24
|
import { getProcessManager } from "./tasks/manager.js";
|
|
@@ -30,7 +33,8 @@ import { SettingsManager } from "./permissions/settings.js";
|
|
|
30
33
|
import { ExternalHookController } from "./hooks/index.js";
|
|
31
34
|
import { getLspService } from "./lsp/index.js";
|
|
32
35
|
import { loadMcpConfig } from "./mcp/config.js";
|
|
33
|
-
import {
|
|
36
|
+
import { promptRepoTrust } from "./permissions/trust-prompt.js";
|
|
37
|
+
import { McpManager, gateMcpTools } from "./mcp/manager.js";
|
|
34
38
|
import { normalizeInheritedThinkingLevel } from "./variant/variant-resolver.js";
|
|
35
39
|
import { QuestionController } from "./question/index.js";
|
|
36
40
|
import { buildMemoryPrompt, formatMemoryStartupResult, purgeUnsafeMemorySources, recordMemoryCitations, runMemoryPhase2, runMemoryStartupPipeline, startMemoryStartupTask, } from "./memory/index.js";
|
|
@@ -52,6 +56,13 @@ async function main() {
|
|
|
52
56
|
console.log(`v${getCurrentVersion()}`);
|
|
53
57
|
process.exit(0);
|
|
54
58
|
}
|
|
59
|
+
// --cwd means "run as if launched there" (like git -C): every layer, including
|
|
60
|
+
// the TUI, shell children and relative paths, then sees the same directory.
|
|
61
|
+
const cwdError = enterWorkingDirectory(args.cwd);
|
|
62
|
+
if (cwdError) {
|
|
63
|
+
console.error(`bubble: ${cwdError}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
55
66
|
if (args.command === "update") {
|
|
56
67
|
const { runUpdateCommand } = await import("./update/index.js");
|
|
57
68
|
const code = await runUpdateCommand({ checkOnly: args.checkOnly });
|
|
@@ -86,10 +97,10 @@ async function main() {
|
|
|
86
97
|
console.error(chalk.red("Error: No provider configured. Start interactive mode and use /login or /provider --add <id>."));
|
|
87
98
|
process.exit(1);
|
|
88
99
|
}
|
|
89
|
-
console.log(chalk.dim("No provider configured yet. Start with /login for ChatGPT or /provider --add <id> for an API key."));
|
|
100
|
+
console.log(chalk.dim("No provider configured yet. Start with /login for ChatGPT or Grok, or /provider --add <id> for an API key."));
|
|
90
101
|
}
|
|
91
102
|
const defaultProvider = registry.getDefault();
|
|
92
|
-
const unavailableProviderMessage = "No provider configured. Use /login for ChatGPT or /provider --add <id> before sending a prompt.";
|
|
103
|
+
const unavailableProviderMessage = "No provider configured. Use /login for ChatGPT or Grok, or /provider --add <id> before sending a prompt.";
|
|
93
104
|
let sessionPromptCacheKey;
|
|
94
105
|
const provider = defaultProvider
|
|
95
106
|
? createProviderInstance({
|
|
@@ -149,6 +160,11 @@ async function main() {
|
|
|
149
160
|
const approvalHandlerRef = {};
|
|
150
161
|
const questionController = new QuestionController();
|
|
151
162
|
const bashAllowlist = new BashAllowlist();
|
|
163
|
+
// Folder trust (Kimi Code style): the repository's .bubble settings may
|
|
164
|
+
// enable allow rules, MCP servers and LSP servers; ask before loading them.
|
|
165
|
+
if (!printMode && process.stdin.isTTY && process.stdout.isTTY) {
|
|
166
|
+
await promptRepoTrust(args.cwd);
|
|
167
|
+
}
|
|
152
168
|
const settingsManager = new SettingsManager(args.cwd);
|
|
153
169
|
for (const d of settingsManager.getMerged().diagnostics) {
|
|
154
170
|
console.error(chalk.yellow(`[settings:${d.scope}] ${d.path}: ${d.message}`));
|
|
@@ -161,6 +177,7 @@ async function main() {
|
|
|
161
177
|
getMode: () => agentRef?.mode ?? "default",
|
|
162
178
|
handlerRef: approvalHandlerRef,
|
|
163
179
|
bashAllowlist,
|
|
180
|
+
sessionGrants: new Set(),
|
|
164
181
|
cwd: args.cwd,
|
|
165
182
|
getRuleSet: () => settingsManager.getMerged().ruleSet,
|
|
166
183
|
externalHooks: hookController,
|
|
@@ -209,7 +226,7 @@ async function main() {
|
|
|
209
226
|
let externalRuntime;
|
|
210
227
|
if (mcpLoaded.servers.length > 0) {
|
|
211
228
|
await mcpManager.start();
|
|
212
|
-
tools.push(...mcpManager.getToolEntries());
|
|
229
|
+
tools.push(...gateMcpTools(mcpManager.getToolEntries(), approvalController));
|
|
213
230
|
}
|
|
214
231
|
// Expose MCP prompts as slash commands. Queried live at each lookup so
|
|
215
232
|
// /mcp reconnect picks up new prompts without restarting the process.
|
|
@@ -320,8 +337,21 @@ async function main() {
|
|
|
320
337
|
? decodeModel(normalizedConfiguredModel)
|
|
321
338
|
: { providerId: undefined, modelId: "" };
|
|
322
339
|
let activeProviderId = effectiveProviderId || fallbackProviderId;
|
|
323
|
-
|
|
324
|
-
|
|
340
|
+
// getConfigured() resolves the legacy openai-codex auth alias; the raw
|
|
341
|
+
// auth key would skip token refresh and discovery for such logins.
|
|
342
|
+
if (registry.getConfigured().find((item) => item.id === activeProviderId)?.authType === "oauth") {
|
|
343
|
+
try {
|
|
344
|
+
await registry.prepareProvider(activeProviderId);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
// A dead login must not lock the user out: /login only exists inside the
|
|
348
|
+
// session, so start anyway and let the first request surface the error.
|
|
349
|
+
console.error(chalk.yellow(`[auth] ${error instanceof Error ? error.message : String(error)}`));
|
|
350
|
+
}
|
|
351
|
+
// The routing prompt below is composed once; give the account catalog a
|
|
352
|
+
// bounded chance to land first so the menu the model reads matches what a
|
|
353
|
+
// spawn will actually get (design §1.5).
|
|
354
|
+
await registry.waitForModelDiscovery(activeProviderId, MODEL_DISCOVERY_STARTUP_WAIT_MS);
|
|
325
355
|
}
|
|
326
356
|
const activeProvider = registry.getConfigured().find((p) => p.id === activeProviderId) || defaultProvider;
|
|
327
357
|
const activeModel = activeProvider && effectiveModelId
|
|
@@ -356,6 +386,8 @@ async function main() {
|
|
|
356
386
|
: undefined;
|
|
357
387
|
const systemPrompt = buildSystemPrompt({
|
|
358
388
|
agentName: "Bubble",
|
|
389
|
+
skills: skillSummaries,
|
|
390
|
+
skillCatalogChars: userConfig.getSkillCatalogChars(),
|
|
359
391
|
configuredProvider: activeProviderId || "none",
|
|
360
392
|
configuredModel: activeModel ? displayModel(activeModel) : "none",
|
|
361
393
|
configuredModelId: activeModel || "none",
|
|
@@ -389,6 +421,30 @@ async function main() {
|
|
|
389
421
|
}
|
|
390
422
|
const budgetLedger = new BudgetLedger();
|
|
391
423
|
let sessionTitleUpdater;
|
|
424
|
+
let contextFence = new SessionContextFence(sessionManager);
|
|
425
|
+
// A fence rejection means the durable log diverged from this host's resident
|
|
426
|
+
// history (a foreign append/clear/rewind landed while a turn was in flight).
|
|
427
|
+
// The write is refused — and the resident transcript must roll back to the
|
|
428
|
+
// file's truth, or the rejected message lingers in the TUI while every later
|
|
429
|
+
// fenced write keeps failing on the stale revision until a manual reload.
|
|
430
|
+
const persistFenced = (write) => {
|
|
431
|
+
try {
|
|
432
|
+
write();
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
// Any refused write (fence rejection, busy lock, I/O) leaves the agent's
|
|
436
|
+
// already-pushed message unpersisted, so always restore the file's truth.
|
|
437
|
+
try {
|
|
438
|
+
const history = contextFence.reloadHistory();
|
|
439
|
+
const head = splitLeadingContext(agent.messages).leading;
|
|
440
|
+
agent.messages = [...head, ...history];
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
// Surface the original write failure; the next write retries the reload.
|
|
444
|
+
}
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
};
|
|
392
448
|
const agent = new Agent({
|
|
393
449
|
provider: activeProvider
|
|
394
450
|
? createProvider(activeProviderId, activeProvider.apiKey, activeProvider.baseURL)
|
|
@@ -410,7 +466,8 @@ async function main() {
|
|
|
410
466
|
// they will be re-injected as needed on resume based on the current mode.
|
|
411
467
|
if (message.role === "meta")
|
|
412
468
|
return;
|
|
413
|
-
sessionManager
|
|
469
|
+
const manager = sessionManager;
|
|
470
|
+
persistFenced(() => manager.appendMessage(message, contextFence.getRevision()));
|
|
414
471
|
traceEvent("session_message_persisted", {
|
|
415
472
|
message: summarizeTraceMessage(message),
|
|
416
473
|
});
|
|
@@ -420,12 +477,14 @@ async function main() {
|
|
|
420
477
|
}
|
|
421
478
|
},
|
|
422
479
|
onProviderError: (error) => {
|
|
423
|
-
sessionManager?.appendProviderError(error);
|
|
480
|
+
persistFenced(() => sessionManager?.appendProviderError(error, contextFence.getRevision()));
|
|
424
481
|
},
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
482
|
+
getContextRevision: () => contextFence.getRevision(),
|
|
483
|
+
onContextCheckpoint: (checkpoint) => {
|
|
484
|
+
if (!sessionManager)
|
|
485
|
+
throw new Error("No session available for context commit");
|
|
486
|
+
const manager = sessionManager;
|
|
487
|
+
persistFenced(() => manager.commitContextCheckpoint(checkpoint, contextFence.getRevision()));
|
|
429
488
|
},
|
|
430
489
|
onToolResult: (toolName, result) => {
|
|
431
490
|
if (!sessionManager)
|
|
@@ -434,14 +493,16 @@ async function main() {
|
|
|
434
493
|
return;
|
|
435
494
|
const match = result.content.match(/^Skill:\s+([^\n]+)$/m);
|
|
436
495
|
if (match?.[1]) {
|
|
437
|
-
sessionManager
|
|
496
|
+
const manager = sessionManager;
|
|
497
|
+
persistFenced(() => manager.appendMarker("skill_activated", match[1].trim(), contextFence.getRevision()));
|
|
438
498
|
}
|
|
439
499
|
},
|
|
440
500
|
onModeUpdate: (mode) => {
|
|
441
|
-
sessionManager?.appendMarker("mode_switch", mode);
|
|
501
|
+
persistFenced(() => sessionManager?.appendMarker("mode_switch", mode, contextFence.getRevision()));
|
|
442
502
|
},
|
|
443
503
|
budgetLedger,
|
|
444
504
|
skills: skillSummaries,
|
|
505
|
+
skillCatalogChars: userConfig.getSkillCatalogChars(),
|
|
445
506
|
memoryPrompt,
|
|
446
507
|
fileStateTracker,
|
|
447
508
|
agentCategories: userConfig.getAgentCategories(),
|
|
@@ -550,7 +611,7 @@ async function main() {
|
|
|
550
611
|
}
|
|
551
612
|
// Restore session if requested
|
|
552
613
|
if (resumedExistingSession && sessionManager) {
|
|
553
|
-
const history =
|
|
614
|
+
const history = contextFence.reloadHistory();
|
|
554
615
|
if (history.length > 0) {
|
|
555
616
|
agent.messages = [{ role: "system", content: systemPrompt }, ...history];
|
|
556
617
|
// Reassigning agent.messages drops any runtime meta reminder injected during
|
|
@@ -699,6 +760,7 @@ async function main() {
|
|
|
699
760
|
const activateSession = (next) => {
|
|
700
761
|
try {
|
|
701
762
|
const history = next.getMessages();
|
|
763
|
+
const historyRevision = next.getRevision();
|
|
702
764
|
const nextPromptCacheKey = next.getOrCreatePromptCacheKey();
|
|
703
765
|
const nextTitleUpdater = createSessionTitleUpdater({
|
|
704
766
|
sessionManager: next,
|
|
@@ -717,6 +779,10 @@ async function main() {
|
|
|
717
779
|
// Commit only after every file read/write and reconstruction step has
|
|
718
780
|
// succeeded. Callers can safely prepare candidate sessions without a
|
|
719
781
|
// failed switch rebinding persistence or replacing the live history.
|
|
782
|
+
if (next.getRevision() !== historyRevision)
|
|
783
|
+
throw new Error("Session changed while switching; retry");
|
|
784
|
+
contextFence.dispose();
|
|
785
|
+
contextFence = new SessionContextFence(next);
|
|
720
786
|
sessionManager = next;
|
|
721
787
|
sessionPromptCacheKey = nextPromptCacheKey;
|
|
722
788
|
sessionTitleUpdater = nextTitleUpdater;
|
package/dist/mcp/config.d.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* identified by its key in mcpServers; if the same name appears in multiple
|
|
6
6
|
* scopes, the higher-precedence scope overwrites, and we record a diagnostic.
|
|
7
7
|
*
|
|
8
|
+
* Servers from the project and local files arrive with the repository and
|
|
9
|
+
* Bubble would spawn their commands at startup, so they are skipped until the
|
|
10
|
+
* user trusts the folder (asked at startup, trust.ts).
|
|
11
|
+
*
|
|
8
12
|
* Env expansion: ${VAR} in command, args, env values, url, or header values
|
|
9
13
|
* is replaced with the matching process.env entry. Missing vars become empty
|
|
10
14
|
* strings and yield a diagnostic (non-fatal).
|
package/dist/mcp/config.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* identified by its key in mcpServers; if the same name appears in multiple
|
|
6
6
|
* scopes, the higher-precedence scope overwrites, and we record a diagnostic.
|
|
7
7
|
*
|
|
8
|
+
* Servers from the project and local files arrive with the repository and
|
|
9
|
+
* Bubble would spawn their commands at startup, so they are skipped until the
|
|
10
|
+
* user trusts the folder (asked at startup, trust.ts).
|
|
11
|
+
*
|
|
8
12
|
* Env expansion: ${VAR} in command, args, env values, url, or header values
|
|
9
13
|
* is replaced with the matching process.env entry. Missing vars become empty
|
|
10
14
|
* strings and yield a diagnostic (non-fatal).
|
|
@@ -12,6 +16,7 @@
|
|
|
12
16
|
import { existsSync, readFileSync } from "node:fs";
|
|
13
17
|
import { join } from "node:path";
|
|
14
18
|
import { getBubbleHome } from "../bubble-home.js";
|
|
19
|
+
import { isRepoConfigTrusted, readRepoSettings } from "../permissions/trust.js";
|
|
15
20
|
export function loadMcpConfig(options) {
|
|
16
21
|
const bubbleHome = options.bubbleHome ?? getBubbleHome();
|
|
17
22
|
const paths = {
|
|
@@ -21,6 +26,7 @@ export function loadMcpConfig(options) {
|
|
|
21
26
|
};
|
|
22
27
|
const diagnostics = [];
|
|
23
28
|
const merged = new Map();
|
|
29
|
+
const repoTrusted = isRepoConfigTrusted(options.cwd, readRepoSettings(options.cwd), { bubbleHome });
|
|
24
30
|
for (const scope of ["user", "project", "local"]) {
|
|
25
31
|
const path = paths[scope];
|
|
26
32
|
if (!existsSync(path))
|
|
@@ -38,6 +44,17 @@ export function loadMcpConfig(options) {
|
|
|
38
44
|
const mcpServers = raw.mcpServers;
|
|
39
45
|
if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers))
|
|
40
46
|
continue;
|
|
47
|
+
if (scope !== "user" && !repoTrusted) {
|
|
48
|
+
const names = Object.keys(mcpServers);
|
|
49
|
+
if (names.length > 0) {
|
|
50
|
+
diagnostics.push({
|
|
51
|
+
scope,
|
|
52
|
+
path,
|
|
53
|
+
message: `Skipped MCP server${names.length === 1 ? "" : "s"} ${names.join(", ")} from this repository: the folder is not trusted (Bubble asks at startup).`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
41
58
|
for (const [name, value] of Object.entries(mcpServers)) {
|
|
42
59
|
const validated = validateServerConfig(value, (msg) => {
|
|
43
60
|
diagnostics.push({ scope, path, message: `mcpServers.${name}: ${msg}` });
|