diff --git a/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx b/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx
index 7dff45cb33..ba54eb2796 100644
--- a/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx
+++ b/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx
@@ -28,6 +28,7 @@ vi.mock("@/hooks/queries/system-queries", () => ({
data: {
experiments: {
claudeCodeMockCliTraffic: false,
+ cloudAi: false,
newOnboarding: false,
toolsHub: true,
},
diff --git a/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx b/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx
index b2f91fa6c8..98e8cebdf2 100644
--- a/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx
+++ b/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx
@@ -24,6 +24,7 @@ vi.mock("@/hooks/queries/system-queries", () => ({
data: {
experiments: {
claudeCodeMockCliTraffic: false,
+ cloudAi: false,
newOnboarding: false,
toolsHub: true,
},
diff --git a/apps/app/src/components/plugin/PluginSettings.test.tsx b/apps/app/src/components/plugin/PluginSettings.test.tsx
index 68d1ddbdaa..60973357d7 100644
--- a/apps/app/src/components/plugin/PluginSettings.test.tsx
+++ b/apps/app/src/components/plugin/PluginSettings.test.tsx
@@ -1,6 +1,7 @@
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { MemoryRouter } from "react-router-dom";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createQueryClientTestHarness } from "@/test/queryClientTestHarness";
import {
@@ -283,14 +284,16 @@ describe("PluginSettingsDetail settings gating", () => {
});
const { wrapper } = createQueryClientTestHarness();
render(
-
--server https://.getbb.app` pairs this bb
- server for browser access at `.getbb.app` (get the code from
+- `bb connect --code --server https://.getbb.app` connects this
+ bb server to bb Cloud (getbb.app): browser access at `.getbb.app`,
+ port shares, and AI features through the account (get the code from
https://getbb.app). Pairing returns immediately — the
server itself holds the tunnel and reconnects on restart, so there is no
foreground process.
- `bb connect status` / `bb connect off` report and clear the pairing.
+ `bb connect status` reports the connection; `bb connect off` unlinks this bb
+ from Cloud and clears the pairing.
+ After `bb settings experiment cloudAi true`, AI features (thread titles,
+ commit messages, voice transcription) can route through bb Cloud with
+ automatic fallback to locally configured providers. `bb connect ai
+ [on|off]` shows or sets the plugin's inner preference; the experiment and
+ preference must both be on.
Port sharing works from a thread on any enrolled host. `bb connect expose
` resolves that thread's environment host and returns its public URL;
outside a thread it defaults to the server host. Pass `--host
@@ -175,10 +186,19 @@ isolated|reuse`, or anchor with `--source-seq-end`. Permission mode inherits
name, url, live) so callers can discover siblings; `--json` includes
`selfHandle` for deduping this server. When you start a local server the user
should open remotely, expose the port and give them the share URL. Remote
- access is owned by the builtin `connect` plugin: `bb plugin disable connect`
- cuts it off entirely; with bb connect still enabled, `bb plugin enable
- connect` restores the command. Plugins → Connect shows the current URL, QR
- code, shared ports, re-pair form, and disconnect control.
+ bb Cloud is provided by the builtin Cloud plugin (internal id `connect`):
+ `bb plugin disable connect` cuts it off entirely (remote access and AI
+ features alike);
+ `bb plugin enable connect` restores it. Settings → Cloud always contains
+ Remote access (URL, QR code, shared ports, pairing, and disconnect) and adds
+ a separate Cloud AI section when the `cloudAi` experiment is enabled.
+ `BB_CONNECT_BASE_URL` overrides the Cloud account and pairing-code redemption
+ origin for alternate deployments and local QA; leave it unset for
+ `https://getbb.app`. Cloud returns the authoritative tunnel-gate URL, so it
+ may differ from this account origin.
+ `BB_CONNECT_LOOPBACK_URL` is a source-QA-only loopback HTTP origin served by
+ the bare handle URL. Leave it unset outside source development;
+ `pnpm cloud:dev` manages it automatically.
- Add remote execution machines from Settings → Machines. Its one-line
installer stores the bb connect machine credential locally and configures
both the daemon protocol and agent-launched `bb` CLI to traverse the account
diff --git a/apps/server/src/services/skills/builtin-skills/bb-cli/references/app-settings.md b/apps/server/src/services/skills/builtin-skills/bb-cli/references/app-settings.md
index bab61c7147..d17a61fb5a 100644
--- a/apps/server/src/services/skills/builtin-skills/bb-cli/references/app-settings.md
+++ b/apps/server/src/services/skills/builtin-skills/bb-cli/references/app-settings.md
@@ -55,3 +55,14 @@ every window and client sees the same value.
- Enable it with `bb settings experiment newOnboarding true`.
- Use `bb settings replay-onboarding` to enable the experiment and show the
agent and project setup guide again.
+
+## Cloud AI
+
+- The `cloudAi` experiment defaults to false. Enable it with
+ `bb settings experiment cloudAi true`.
+- It is the outer product gate for cloud-backed thread titles, commit messages,
+ and voice transcription. While off, Cloud AI is hidden in the Cloud plugin
+ and the server does not send requests to its registered provider.
+- Remote access, pairing, and port sharing remain available while it is off.
+- Once enabled, `bb connect ai [on|off]` controls the plugin's separate inner
+ routing preference. Both the experiment and preference must be on.
diff --git a/apps/server/src/services/skills/builtin-skills/bb-plugin-authoring/SKILL.md b/apps/server/src/services/skills/builtin-skills/bb-plugin-authoring/SKILL.md
index 7cf0a97c30..be751ce158 100644
--- a/apps/server/src/services/skills/builtin-skills/bb-plugin-authoring/SKILL.md
+++ b/apps/server/src/services/skills/builtin-skills/bb-plugin-authoring/SKILL.md
@@ -731,6 +731,22 @@ the same cleanup without a replacement. A
captured `bb` from a previous load throws `PluginContextStaleError` on use
— never stash the API object in module-level state that outlives a load.
+### bb.experimental_registerCloudAiProvider
+
+`bb.experimental_registerCloudAiProvider(provider)` (EXPERIMENTAL — see
+docs/api_to_audit.md) registers a cloud route for bb's small AI tasks:
+thread-title inference, commit-message inference, and voice transcription.
+The provider is `{ isAvailable(): boolean; complete({prompt, schema, signal});
+transcribe({file, prompt?, signal}) }` where the async methods resolve to
+`{ ok: true, value }` or `{ ok: false, code, message }` (codes: `unauthorized`,
+`quota_exhausted`, `unavailable`) — result-shaped, never thrown classes. When
+the server's `cloudAi` experiment is enabled and `isAvailable()` is true, the
+host tries the provider before the locally configured AI providers and falls
+back on `ok: false`. While the experiment is off, the provider may remain
+registered but is never called. Single host-wide slot; the most recent
+registration wins and is unregistered automatically with the plugin's dispose
+hooks. Used by the builtin Cloud plugin (internal id `connect`) for bb Cloud.
+
## Frontend (`bb.app` entry)
`app.tsx` default-exports `definePluginApp` from `@bb/plugin-sdk/app`.
@@ -821,11 +837,16 @@ export default definePluginApp((app) => {
id: "credentials",
component: CredentialForm,
});
+ app.slots.settingsSection({
+ id: "remote-access",
+ title: "Remote access",
+ component: RemoteAccessSettings,
+ });
app.slots.sidebarFooterAction({
- id: "remote",
+ id: "remote-access",
title: "Remote access",
icon: "Smartphone",
- run: ({ openSettings }) => openSettings(),
+ run: ({ openSettings }) => openSettings({ sectionId: "remote-access" }),
});
app.slots.messageDirective({ id: "inline-vis", component: InlineVis });
app.slots.experimental_threadList({
@@ -1079,8 +1100,10 @@ Slot props contracts (versioned, additive-only):
the chrome so icons stay consistent. Registration:
`{ id, title, icon, run }`. Activating it calls
`run({ openSettings })` — use `openSettings()` to open this plugin's
- detail page in Tools, or do anything else (rpc, toast). Errors from `run`
- (sync or async) are contained and logged,
+ detail page in Tools, or `openSettings({ sectionId: "my-section" })` to
+ target one of its registered settings sections. The action's `icon` is its
+ own glyph rather than the plugin branding icon. The callback can also do
+ anything else (rpc, toast). Errors from `run` (sync or async) are contained and logged,
never breaking the sidebar. `title` is the tooltip + accessible label;
`icon` is a BB icon-name hint (unknown names fall back to a generic bolt).
- `fileOpener` → `{ path: string, source }` — register as a viewer/editor
diff --git a/apps/server/test/ai/inference.test.ts b/apps/server/test/ai/inference.test.ts
index 6e8c89a4b3..6c8f3668b0 100644
--- a/apps/server/test/ai/inference.test.ts
+++ b/apps/server/test/ai/inference.test.ts
@@ -1,5 +1,12 @@
+import { setExperiments, type DbConnection } from "@bb/db";
+import { defaultExperiments } from "@bb/domain";
+import type { CloudAiProvider, CloudAiResult, JsonValue } from "@bb/plugin-sdk";
import { Type } from "@earendil-works/pi-ai";
-import { describe, expect, it } from "vitest";
+import { afterEach, describe, expect, it } from "vitest";
+import {
+ registerCloudAiProvider,
+ resetCloudAiProviderForTests,
+} from "../../src/services/ai/cloud-ai-provider.js";
import {
InferenceTimeoutError,
inferenceComplete,
@@ -16,6 +23,10 @@ const titleSchema = Type.Object({
title: Type.String(),
});
+function enableCloudAiExperiment(db: DbConnection): void {
+ setExperiments(db, { ...defaultExperiments, cloudAi: true });
+}
+
describe("inferenceComplete", () => {
it("surfaces missing host for codex inference", async () => {
await withTestHarness({
@@ -127,3 +138,178 @@ describe("inferenceComplete", () => {
});
});
});
+
+describe("inferenceComplete cloud provider routing", () => {
+ afterEach(() => {
+ resetCloudAiProviderForTests();
+ });
+
+ function stubProvider(over: {
+ available?: boolean;
+ complete?: CloudAiProvider["complete"];
+ }): CloudAiProvider & { completeCalls: number } {
+ const provider = {
+ completeCalls: 0,
+ isAvailable: () => over.available ?? true,
+ async complete(
+ args: Parameters[0],
+ ): Promise> {
+ provider.completeCalls += 1;
+ if (over.complete) return over.complete(args);
+ return { ok: true, value: { title: "Cloud title" } };
+ },
+ async transcribe(): Promise> {
+ throw new Error("not under test");
+ },
+ };
+ return provider;
+ }
+
+ it("does not use a registered provider while the experiment is off", async () => {
+ await withTestHarness(async (harness) => {
+ const provider = stubProvider({});
+ registerCloudAiProvider("connect", provider);
+
+ await expect(
+ inferenceComplete(harness.deps, {
+ prompt: "Generate a title",
+ schema: titleSchema,
+ }),
+ ).resolves.toBeNull();
+ expect(provider.completeCalls).toBe(0);
+ });
+ });
+
+ it("routes through an available provider instead of the codex daemon", async () => {
+ // codex model configured and NO host connected: success proves the daemon
+ // path was never consulted (it would throw host_unavailable).
+ await withTestHarness({
+ inferenceModel: "codex/gpt-5.4-mini",
+ }, async (harness) => {
+ enableCloudAiExperiment(harness.db);
+ const provider = stubProvider({});
+ registerCloudAiProvider("connect", provider);
+ let sentSchema: JsonValue | null = null;
+ provider.complete = async (args) => {
+ provider.completeCalls += 1;
+ sentSchema = args.schema;
+ return { ok: true, value: { title: "Cloud title" } };
+ };
+
+ await expect(
+ inferenceComplete(harness.deps, {
+ prompt: "Generate a title",
+ schema: titleSchema,
+ timeoutMs: 5000,
+ }),
+ ).resolves.toEqual({ title: "Cloud title" });
+ expect(sentSchema).toMatchObject({ type: "object" });
+ });
+ });
+
+ it("skips an unavailable provider and returns null on invalid results", async () => {
+ await withTestHarness(async (harness) => {
+ enableCloudAiExperiment(harness.db);
+ const unavailable = stubProvider({ available: false });
+ registerCloudAiProvider("connect", unavailable);
+ // Local config is test/mock-model (unsupported) → local path yields null.
+ await expect(
+ inferenceComplete(harness.deps, {
+ prompt: "Generate a title",
+ schema: titleSchema,
+ }),
+ ).resolves.toBeNull();
+ expect(unavailable.completeCalls).toBe(0);
+
+ const invalidResult = stubProvider({
+ complete: async () => ({ ok: true, value: { wrong: 1 } }),
+ });
+ registerCloudAiProvider("connect", invalidResult);
+ await expect(
+ inferenceComplete(harness.deps, {
+ prompt: "Generate a title",
+ schema: titleSchema,
+ }),
+ ).resolves.toBeNull();
+ });
+ });
+
+ it("falls through to local on provider failure or a provider bug", async () => {
+ await withTestHarness(async (harness) => {
+ enableCloudAiExperiment(harness.db);
+ const failing = stubProvider({
+ complete: async () => ({
+ ok: false,
+ code: "quota_exhausted",
+ message: "budget reached",
+ }),
+ });
+ registerCloudAiProvider("connect", failing);
+ await expect(
+ inferenceComplete(harness.deps, {
+ prompt: "Generate a title",
+ schema: titleSchema,
+ }),
+ ).resolves.toBeNull();
+ expect(failing.completeCalls).toBe(1);
+
+ const throwing = stubProvider({
+ complete: async () => {
+ throw new Error("plugin bug");
+ },
+ });
+ registerCloudAiProvider("connect", throwing);
+ await expect(
+ inferenceComplete(harness.deps, {
+ prompt: "Generate a title",
+ schema: titleSchema,
+ }),
+ ).resolves.toBeNull();
+ });
+ });
+
+ it("turns a provider timeout into InferenceTimeoutError without local fallback", async () => {
+ await withTestHarness(async (harness) => {
+ enableCloudAiExperiment(harness.db);
+ registerCloudAiProvider(
+ "connect",
+ stubProvider({
+ complete: (args) =>
+ new Promise((_, reject) => {
+ args.signal.addEventListener("abort", () => {
+ reject(new DOMException("aborted", "AbortError"));
+ });
+ }),
+ }),
+ );
+
+ await expect(
+ inferenceComplete(harness.deps, {
+ prompt: "Generate a title",
+ schema: titleSchema,
+ timeoutMs: 50,
+ }),
+ ).rejects.toBeInstanceOf(InferenceTimeoutError);
+ });
+ });
+
+ it("stale unregister hooks cannot tear down a newer registration", async () => {
+ await withTestHarness(async (harness) => {
+ enableCloudAiExperiment(harness.db);
+ const first = stubProvider({});
+ const unregisterFirst = registerCloudAiProvider("connect", first);
+ const second = stubProvider({});
+ registerCloudAiProvider("connect", second);
+ unregisterFirst();
+
+ await expect(
+ inferenceComplete(harness.deps, {
+ prompt: "Generate a title",
+ schema: titleSchema,
+ }),
+ ).resolves.toEqual({ title: "Cloud title" });
+ expect(second.completeCalls).toBe(1);
+ expect(first.completeCalls).toBe(0);
+ });
+ });
+});
diff --git a/apps/server/test/ai/voice-transcription.test.ts b/apps/server/test/ai/voice-transcription.test.ts
index 930d4915d4..cb1da547f0 100644
--- a/apps/server/test/ai/voice-transcription.test.ts
+++ b/apps/server/test/ai/voice-transcription.test.ts
@@ -1,8 +1,18 @@
import { Buffer } from "node:buffer";
+import { setExperiments, type DbConnection } from "@bb/db";
+import { defaultExperiments } from "@bb/domain";
import type { HostDaemonOnlineRpcRequestMessage } from "@bb/host-daemon-contract";
-import { describe, expect, it, vi } from "vitest";
+import type { CloudAiProvider, CloudAiResult } from "@bb/plugin-sdk";
+import { afterEach, describe, expect, it, vi } from "vitest";
import { ApiError } from "../../src/errors.js";
-import { transcribeVoiceInput } from "../../src/services/ai/voice-transcription.js";
+import {
+ registerCloudAiProvider,
+ resetCloudAiProviderForTests,
+} from "../../src/services/ai/cloud-ai-provider.js";
+import {
+ resolveVoiceTranscriptionEnabled,
+ transcribeVoiceInput,
+} from "../../src/services/ai/voice-transcription.js";
import {
registerHostRpcResponder,
type HostRpcResponder,
@@ -44,6 +54,10 @@ function emptyVoiceFile(): File {
return new File([], "prompt.webm", { type: "audio/webm" });
}
+function enableCloudAiExperiment(db: DbConnection): void {
+ setExperiments(db, { ...defaultExperiments, cloudAi: true });
+}
+
function requireCodexVoiceTranscribeCommand(
request: HostDaemonOnlineRpcRequestMessage,
): CodexVoiceTranscribeCommand {
@@ -271,3 +285,162 @@ describe("voice transcription", () => {
}
});
});
+
+describe("voice transcription cloud provider routing", () => {
+ afterEach(() => {
+ resetCloudAiProviderForTests();
+ });
+
+ function stubProvider(over: {
+ available?: boolean;
+ transcribe?: CloudAiProvider["transcribe"];
+ }): CloudAiProvider {
+ return {
+ isAvailable: () => over.available ?? true,
+ async complete(): Promise> {
+ throw new Error("not under test");
+ },
+ async transcribe(
+ args: Parameters[0],
+ ): Promise> {
+ if (over.transcribe) return over.transcribe(args);
+ return { ok: true, value: "hello cloud" };
+ },
+ };
+ }
+
+ it("transcribes through an available provider with the trimmed prompt", async () => {
+ const harness = await createTestAppHarness({});
+ try {
+ enableCloudAiExperiment(harness.deps.db);
+ let sentPrompt: string | undefined;
+ registerCloudAiProvider(
+ "connect",
+ stubProvider({
+ transcribe: async (args) => {
+ sentPrompt = args.prompt;
+ return { ok: true, value: "hello cloud" };
+ },
+ }),
+ );
+ await expect(
+ transcribeVoiceInput(harness.deps, {
+ file: voiceFile(),
+ prompt: " context ",
+ }),
+ ).resolves.toBe("hello cloud");
+ expect(sentPrompt).toBe("context");
+ } finally {
+ await harness.cleanup();
+ }
+ });
+
+ it("falls back to the codex daemon when the provider fails", async () => {
+ const harness = await createCodexTranscriptionHarness({
+ handle(request) {
+ const command = requireCodexVoiceTranscribeCommand(request);
+ return {
+ ok: true,
+ result: { model: command.model, text: "hello daemon" },
+ };
+ },
+ });
+ try {
+ enableCloudAiExperiment(harness.deps.db);
+ registerCloudAiProvider(
+ "connect",
+ stubProvider({
+ transcribe: async () => ({
+ ok: false,
+ code: "quota_exhausted",
+ message: "budget reached",
+ }),
+ }),
+ );
+ await expect(
+ transcribeVoiceInput(harness.deps, { file: voiceFile() }),
+ ).resolves.toBe("hello daemon");
+ expect(harness.requests).toHaveLength(1);
+ } finally {
+ await harness.cleanup();
+ }
+ });
+
+ it("maps quota exhaustion to a retryable 503 when no local provider exists", async () => {
+ const harness = await createTestAppHarness({});
+ try {
+ enableCloudAiExperiment(harness.deps.db);
+ registerCloudAiProvider(
+ "connect",
+ stubProvider({
+ transcribe: async () => ({
+ ok: false,
+ code: "quota_exhausted",
+ message: "budget reached",
+ }),
+ }),
+ );
+ await transcribeVoiceInput(harness.deps, { file: voiceFile() }).then(
+ () => {
+ throw new Error("Expected transcription to fail");
+ },
+ (error: unknown) => {
+ expectRetryableApiError(error, {
+ code: "transcription_unavailable",
+ status: 503,
+ });
+ expect((error as ApiError).body.message).toContain("budget");
+ },
+ );
+ } finally {
+ await harness.cleanup();
+ }
+ });
+
+ it("maps a provider timeout to the standard 504 without a local attempt", async () => {
+ const harness = await createTestAppHarness({});
+ try {
+ enableCloudAiExperiment(harness.deps.db);
+ registerCloudAiProvider(
+ "connect",
+ stubProvider({
+ transcribe: (args) =>
+ new Promise((_, reject) => {
+ args.signal.addEventListener("abort", () => {
+ reject(new DOMException("aborted", "AbortError"));
+ });
+ }),
+ }),
+ );
+ vi.useFakeTimers();
+ const attempt = transcribeVoiceInput(harness.deps, {
+ file: voiceFile(),
+ });
+ const expectation = expect(attempt).rejects.toMatchObject({
+ status: 504,
+ body: { code: "transcription_timeout" },
+ });
+ await vi.advanceTimersByTimeAsync(20_000);
+ await expectation;
+ } finally {
+ vi.useRealTimers();
+ await harness.cleanup();
+ }
+ });
+
+ it("reports voice transcription enabled only when the experiment and provider are available", async () => {
+ const harness = await createTestAppHarness({});
+ try {
+ // Default harness config: unsupported local transcription provider.
+ expect(resolveVoiceTranscriptionEnabled(harness.deps)).toBe(false);
+ registerCloudAiProvider("connect", stubProvider({ available: false }));
+ expect(resolveVoiceTranscriptionEnabled(harness.deps)).toBe(false);
+ registerCloudAiProvider("connect", stubProvider({}));
+ expect(resolveVoiceTranscriptionEnabled(harness.deps)).toBe(false);
+ enableCloudAiExperiment(harness.deps.db);
+ expect(resolveVoiceTranscriptionEnabled(harness.deps)).toBe(true);
+ } finally {
+ await harness.cleanup();
+ }
+ });
+});
diff --git a/apps/server/test/services/plugins/builtin-plugins.test.ts b/apps/server/test/services/plugins/builtin-plugins.test.ts
index 9ceef11992..73580ee6f0 100644
--- a/apps/server/test/services/plugins/builtin-plugins.test.ts
+++ b/apps/server/test/services/plugins/builtin-plugins.test.ts
@@ -198,7 +198,7 @@ describe("builtin plugin reconciliation", () => {
const expectedIcons = new Map([
["ask-user-question", "MessageQuestion"],
["automations", "Clock"],
- ["connect", "Smartphone"],
+ ["connect", "Cloud"],
["custom-instructions", "EditFile"],
["inline-vis", "AppWindow"],
["provider-retry", "ArrowReloadHorizontal"],
@@ -218,6 +218,17 @@ describe("builtin plugin reconciliation", () => {
}
});
+ it("presents the compatibility-id connect plugin as Cloud", async () => {
+ const manifest = await readPluginManifest(
+ resolveBuiltinPluginRootPath("connect"),
+ );
+ expect(manifest).toMatchObject({
+ id: "connect",
+ name: "Cloud",
+ branding: { icon: "Cloud" },
+ });
+ });
+
afterEach(async () => {
await service?.stop();
db.$client.close();
diff --git a/apps/server/test/services/plugins/plugin-authoring-docs.test.ts b/apps/server/test/services/plugins/plugin-authoring-docs.test.ts
index 00bf8aba10..4d96599791 100644
--- a/apps/server/test/services/plugins/plugin-authoring-docs.test.ts
+++ b/apps/server/test/services/plugins/plugin-authoring-docs.test.ts
@@ -67,6 +67,7 @@ const BB_PLUGIN_API_KEYS = [
"hosts",
"sdk",
"onDispose",
+ "experimental_registerCloudAiProvider",
] as const satisfies readonly (keyof BbPluginApi)[];
type MissingApiKey = Exclude<
diff --git a/apps/server/test/system/experiments.test.ts b/apps/server/test/system/experiments.test.ts
index f6806306cf..00504041dc 100644
--- a/apps/server/test/system/experiments.test.ts
+++ b/apps/server/test/system/experiments.test.ts
@@ -13,6 +13,7 @@ describe("experiments settings", () => {
const body = systemConfigResponseSchema.parse(await readJson(response));
expect(body.experiments).toEqual({
claudeCodeMockCliTraffic: false,
+ cloudAi: false,
newOnboarding: false,
toolsHub: false,
});
@@ -26,6 +27,7 @@ describe("experiments settings", () => {
headers: { "content-type": "application/json" },
body: JSON.stringify({
claudeCodeMockCliTraffic: true,
+ cloudAi: true,
newOnboarding: true,
toolsHub: true,
}),
@@ -33,11 +35,13 @@ describe("experiments settings", () => {
expect(put.status).toBe(200);
expect(experimentsSchema.parse(await readJson(put))).toEqual({
claudeCodeMockCliTraffic: true,
+ cloudAi: true,
newOnboarding: true,
toolsHub: true,
});
expect(getExperiments(harness.db)).toEqual({
claudeCodeMockCliTraffic: true,
+ cloudAi: true,
newOnboarding: true,
toolsHub: true,
});
@@ -47,6 +51,7 @@ describe("experiments settings", () => {
systemConfigResponseSchema.parse(await readJson(config)).experiments,
).toEqual({
claudeCodeMockCliTraffic: true,
+ cloudAi: true,
newOnboarding: true,
toolsHub: true,
});
@@ -63,6 +68,7 @@ describe("experiments settings", () => {
headers: { "content-type": "application/json" },
body: JSON.stringify({
claudeCodeMockCliTraffic: false,
+ cloudAi: false,
newOnboarding: false,
toolsHub: false,
}),
diff --git a/apps/server/test/threads/thread-runtime-config.test.ts b/apps/server/test/threads/thread-runtime-config.test.ts
index 9d8cf245c5..af436ba495 100644
--- a/apps/server/test/threads/thread-runtime-config.test.ts
+++ b/apps/server/test/threads/thread-runtime-config.test.ts
@@ -849,6 +849,7 @@ describe("thread runtime config", () => {
setExperiments(harness.db, {
claudeCodeMockCliTraffic: true,
+ cloudAi: false,
newOnboarding: false,
toolsHub: false,
});
diff --git a/apps/web/.dev.vars.example b/apps/web/.dev.vars.example
new file mode 100644
index 0000000000..3d1ced7a45
--- /dev/null
+++ b/apps/web/.dev.vars.example
@@ -0,0 +1,5 @@
+# Optional: only needed for `pnpm cloud:dev -- --github-auth`.
+# Create a separate local GitHub OAuth App with this callback URL:
+# http://127.0.0.1/api/auth/callback/github
+GITHUB_CLIENT_ID=replace-with-local-oauth-client-id
+GITHUB_CLIENT_SECRET=replace-with-local-oauth-client-secret
diff --git a/apps/web/src/routes/dashboard.tsx b/apps/web/src/routes/dashboard.tsx
index ce966c5347..b3055a19e1 100644
--- a/apps/web/src/routes/dashboard.tsx
+++ b/apps/web/src/routes/dashboard.tsx
@@ -328,7 +328,8 @@ function Home() {
}, [data.authed, search.returnTo]);
if (!data.authed) return ;
- if (!data.handle) return ;
+ if (!data.handle)
+ return ;
return ;
}
@@ -361,7 +362,7 @@ function SignInView({ returnTo }: { returnTo: string | undefined }) {
/* ── shared claim field (W2 handle + M2 label) ────────────────────── */
function ClaimField({
- baseDomain,
+ serverUrlTemplate,
initial = "",
autoFocus,
previewLead = "Your bb will live at",
@@ -371,7 +372,7 @@ function ClaimField({
cancelLabel = "Cancel",
layout,
}: {
- baseDomain: string;
+ serverUrlTemplate: string;
initial?: string;
autoFocus?: boolean;
previewLead?: string;
@@ -409,7 +410,8 @@ function ClaimField({
const error = submitError ?? (avail ? availabilityCopy(avail) : null);
const canSubmit = !busy && !!label && (avail?.available ?? false);
- const preview = `https://${label || "you"}.${baseDomain}`;
+ const preview = serverUrlTemplate.replace("{label}", label || "you");
+ const addressSuffix = serverUrlTemplate.split("{label}")[1] ?? "";
async function submit() {
if (!canSubmit) return;
@@ -448,7 +450,7 @@ function ClaimField({
aria-label="Address"
/>
- .{baseDomain}
+ {addressSuffix}
@@ -474,7 +476,7 @@ function ClaimField({
/* ── W2: claim handle ─────────────────────────────────────────────── */
-function ClaimView({ baseDomain }: { baseDomain: string }) {
+function ClaimView({ serverUrlTemplate }: { serverUrlTemplate: string }) {
const router = useRouter();
return (
@@ -488,9 +490,11 @@ function ClaimView({ baseDomain }: { baseDomain: string }) {
- l ? `Claim ${l}.${baseDomain}` : "Claim your address"
+ l
+ ? `Claim ${serverUrlTemplate.replace("{label}", l).replace(/^https?:\/\//u, "")}`
+ : "Claim your address"
}
onClaim={async (label) => {
const r = await claimHandleFn({ data: label });
@@ -541,7 +545,7 @@ function SetupCodePanel({
}, [code, fetchCode]);
const cli = code
- ? `npx -p bb-app@latest bb connect --code ${code.code} --server ${code.serverUrl}`
+ ? `npx -p bb-app@latest bb connect --code ${code.code} --server ${code.serverUrl} --base-url ${code.baseUrl}`
: "";
return (
@@ -726,11 +730,9 @@ function RowMenu({
function ServerRow({
server,
- baseDomain,
autoPair,
}: {
server: ServerSummary;
- baseDomain: string;
/** First-run: the sole never-paired bb opens its pair panel by default. */
autoPair?: boolean;
}) {
@@ -780,10 +782,7 @@ function ServerRow({
- {server.subdomain}
-
- .{baseDomain}
-
+ {server.serverUrl.replace(/^https?:\/\//u, "")}
{server.online ? (
@@ -919,7 +918,7 @@ function ConnectAnotherDialog({
`Claim ${l || "…"}`}
@@ -940,7 +939,7 @@ function ConnectAnotherDialog({
Pair the new bb
- {server.subdomain}.{state.baseDomain}
+ {server.serverUrl.replace(/^https?:\/\//u, "")}
{" "}
is reserved for it.
@@ -1067,12 +1066,7 @@ function AccountDashboard({ state }: { state: ServerState }) {
{state.servers.map((s: ServerSummary) => (
-
+
))}
@@ -1115,10 +1109,9 @@ function AccountDashboard({ state }: { state: ServerState }) {
{machine.subdomain !== null ? (
- {machine.subdomain}
-
- .{state.baseDomain}
-
+ {state.serverUrlTemplate
+ .replace("{label}", machine.subdomain)
+ .replace(/^https?:\/\//u, "")}
) : (
diff --git a/apps/web/src/server/api.test.ts b/apps/web/src/server/api.test.ts
index 0e0a8c6ca8..d6f5f74de5 100644
--- a/apps/web/src/server/api.test.ts
+++ b/apps/web/src/server/api.test.ts
@@ -26,8 +26,10 @@ import {
getAccountState,
redeemConnectCode,
redeemMachineCode,
+ resolveServerUrlTemplate,
revokeMachineForServerCredential,
revokeMachine,
+ serverUrlForLabel,
} from "./api.js";
import { sha256Hex } from "./tokens.js";
@@ -55,6 +57,7 @@ beforeEach(() => {
db,
baseDomain: "getbb.app",
appUrl: "https://getbb.app",
+ serverUrlTemplate: "https://{label}.getbb.app",
closeTunnel,
};
});
@@ -78,6 +81,37 @@ function seedUser(id: string, githubLogin?: string): void {
.run();
}
+describe("server URL template boundary", () => {
+ it("defaults production and accepts a per-label loopback port", () => {
+ expect(resolveServerUrlTemplate(undefined, "getbb.app")).toBe(
+ "https://{label}.getbb.app",
+ );
+ const local = resolveServerUrlTemplate(
+ "http://{label}.localhost:8791",
+ "localhost",
+ );
+ expect(local).toBe("http://{label}.localhost:8791");
+ expect(serverUrlForLabel("sawyer", local)).toBe(
+ "http://sawyer.localhost:8791",
+ );
+ });
+
+ it("rejects fixed, foreign, and non-origin templates", () => {
+ expect(() =>
+ resolveServerUrlTemplate("http://127.0.0.1:8791", "localhost"),
+ ).toThrow("must contain {label} exactly once");
+ expect(() =>
+ resolveServerUrlTemplate("http://{label}.example.com:8791", "localhost"),
+ ).toThrow("form {label}.BASE_DOMAIN");
+ expect(() =>
+ resolveServerUrlTemplate(
+ "http://{label}.localhost:8791/path",
+ "localhost",
+ ),
+ ).toThrow("form {label}.BASE_DOMAIN");
+ });
+});
+
describe("claimHandle", () => {
it("creates the profile and the primary server (subdomain = handle)", async () => {
seedUser("u1");
@@ -197,6 +231,7 @@ describe("createConnectCode (per-server minting + reuse)", () => {
serverId: desktop.server.id,
});
if ("error" in r) throw new Error(r.error);
+ expect(r.baseUrl).toBe("https://getbb.app");
expect(r.serverUrl).toBe("https://sawyer-desktop.getbb.app");
expect(r.serverId).toBe(desktop.server.id);
@@ -267,6 +302,7 @@ describe("redeemConnectCode (multi-server routing label)", () => {
expect(result.handle).toBe("sawyer-desktop");
expect(result.serverId).toBe(desktop.server.id);
// (b) tunnelUrl is keyed by that subdomain.
+ expect(result.serverUrl).toBe("https://sawyer-desktop.getbb.app");
expect(result.tunnelUrl).toBe("wss://sawyer-desktop.getbb.app/__tunnel");
expect(result.credential.startsWith("bbcred_")).toBe(true);
@@ -306,8 +342,35 @@ describe("redeemConnectCode (multi-server routing label)", () => {
// Primary server: subdomain === account handle — byte-identical pre-fix behavior.
expect(result.handle).toBe("sawyer");
+ expect(result.serverUrl).toBe("https://sawyer.getbb.app");
expect(result.tunnelUrl).toBe("wss://sawyer.getbb.app/__tunnel");
});
+
+ it("returns the configured per-label Connect gate URL as authoritative", async () => {
+ seedUser("u1");
+ await claimHandle(deps, "u1", "sawyer");
+ const primary = db
+ .select()
+ .from(server)
+ .where(eq(server.subdomain, "sawyer"))
+ .get();
+ const minted = await createConnectCode(deps, "u1", {
+ serverId: primary!.id,
+ });
+ if ("error" in minted) throw new Error(minted.error);
+
+ const result = await redeemConnectCode(
+ {
+ ...deps,
+ serverUrlTemplate: "http://{label}.localhost:8791",
+ },
+ minted.code,
+ );
+ if ("error" in result) throw new Error(result.error);
+
+ expect(result.serverUrl).toBe("http://sawyer.localhost:8791");
+ expect(result.tunnelUrl).toBe("ws://sawyer.localhost:8791/__tunnel");
+ });
});
describe("disconnectServer (server-scoped)", () => {
diff --git a/apps/web/src/server/api.ts b/apps/web/src/server/api.ts
index abdcd3ea6d..3d73d0b2d2 100644
--- a/apps/web/src/server/api.ts
+++ b/apps/web/src/server/api.ts
@@ -28,14 +28,53 @@ export interface Deps {
db: ConnectDb;
baseDomain: string;
appUrl: string;
+ /** Authoritative gate origin template; `{label}` is the routing label. */
+ serverUrlTemplate: string;
closeTunnel?: (routingKey: string) => Promise;
}
+export function resolveServerUrlTemplate(
+ value: string | undefined,
+ baseDomain: string,
+): string {
+ const configured = value?.trim();
+ if (!configured) return `https://{label}.${baseDomain}`;
+ if (configured.split("{label}").length !== 2) {
+ throw new Error(
+ "CONNECT_SERVER_URL_TEMPLATE must contain {label} exactly once",
+ );
+ }
+ const probe = "bb-label-probe";
+ const url = new URL(configured.replace("{label}", probe));
+ if (
+ (url.protocol !== "http:" && url.protocol !== "https:") ||
+ url.username !== "" ||
+ url.password !== "" ||
+ url.pathname !== "/" ||
+ url.search !== "" ||
+ url.hash !== "" ||
+ url.hostname !== `${probe}.${baseDomain}`
+ ) {
+ throw new Error(
+ "CONNECT_SERVER_URL_TEMPLATE must be an HTTP(S) origin in the form {label}.BASE_DOMAIN",
+ );
+ }
+ return `${url.protocol}//{label}.${baseDomain}${url.port ? `:${url.port}` : ""}`;
+}
+
+export function serverUrlForLabel(label: string, template: string): string {
+ return template.replace("{label}", label);
+}
+
export function depsFromEnv(env: Env): Deps {
return {
db: drizzle(env.DB),
baseDomain: env.BASE_DOMAIN,
appUrl: env.APP_URL,
+ serverUrlTemplate: resolveServerUrlTemplate(
+ env.CONNECT_SERVER_URL_TEMPLATE,
+ env.BASE_DOMAIN,
+ ),
closeTunnel: async (routingKey) => {
const stub = env.TUNNEL_DO.get(env.TUNNEL_DO.idFromName(routingKey));
const response = await stub.fetch("https://tunnel/__control/close");
@@ -80,6 +119,8 @@ export interface AccountState {
servers: ServerSummary[];
appUrl: string;
baseDomain: string;
+ /** Same template used for previews and the authoritative pairing response. */
+ serverUrlTemplate: string;
/** GitHub login for the account footer link; null for pre-column rows. */
githubLogin: string | null;
/** Per-account server ceiling, surfaced in the footer as "N of MAX bbs". */
@@ -103,7 +144,7 @@ type ServerRow = typeof server.$inferSelect;
function toServerSummary(
srv: ServerRow,
handle: string,
- baseDomain: string,
+ serverUrlTemplate: string,
now: number,
): ServerSummary {
const lastSeenMs = srv.lastSeenAt?.getTime() ?? null;
@@ -121,7 +162,7 @@ function toServerSummary(
lastSeenAt: lastSeenMs,
version: srv.version,
createdAt: srv.createdAt.getTime(),
- serverUrl: `https://${srv.subdomain}.${baseDomain}`,
+ serverUrl: serverUrlForLabel(srv.subdomain, serverUrlTemplate),
};
}
@@ -182,6 +223,7 @@ export async function getAccountState(
const base = {
appUrl: deps.appUrl,
baseDomain,
+ serverUrlTemplate: deps.serverUrlTemplate,
githubLogin: userRow?.githubLogin ?? null,
maxServers: MAX_SERVERS_PER_ACCOUNT,
};
@@ -204,7 +246,8 @@ export async function getAccountState(
id: row.id,
name: row.name,
subdomain: row.subdomain,
- online: lastSeenMs != null && now - lastSeenMs < SERVER_OFFLINE_AFTER_MS,
+ online:
+ lastSeenMs != null && now - lastSeenMs < SERVER_OFFLINE_AFTER_MS,
lastSeenAt: lastSeenMs,
createdAt: row.createdAt.getTime(),
};
@@ -222,7 +265,9 @@ export async function getAccountState(
.all();
const servers = serverRows
- .map((srv) => toServerSummary(srv, prof.handle, baseDomain, now))
+ .map((srv) =>
+ toServerSummary(srv, prof.handle, deps.serverUrlTemplate, now),
+ )
.sort((a, b) =>
a.isPrimary !== b.isPrimary
? a.isPrimary
@@ -385,7 +430,7 @@ export async function createServer(
userId: string,
rawLabel: string,
): Promise<{ ok: true; server: ServerSummary } | { error: CreateServerError }> {
- const { db, baseDomain } = deps;
+ const { db } = deps;
const prof = await db
.select()
.from(profile)
@@ -436,13 +481,20 @@ export async function createServer(
}
return {
ok: true,
- server: toServerSummary(created, prof.handle, baseDomain, Date.now()),
+ server: toServerSummary(
+ created,
+ prof.handle,
+ deps.serverUrlTemplate,
+ Date.now(),
+ ),
};
}
export interface IssuedCode {
code: string;
expiresInMs: number;
+ /** Account/redeem origin; distinct from serverUrl in local development. */
+ baseUrl: string;
serverUrl: string;
serverId: string;
}
@@ -457,10 +509,10 @@ export async function createConnectCode(
userId: string,
opts: { serverId?: string; reuse?: boolean } = {},
): Promise {
- const { db, baseDomain } = deps;
+ const { db, serverUrlTemplate } = deps;
const srv = await resolveServer(db, userId, opts.serverId);
if (!srv) return { error: "no-server" };
- const serverUrl = `https://${srv.subdomain}.${baseDomain}`;
+ const serverUrl = serverUrlForLabel(srv.subdomain, serverUrlTemplate);
const now = Date.now();
if (opts.reuse) {
@@ -482,6 +534,7 @@ export async function createConnectCode(
return {
code: valid.code,
expiresInMs: valid.expiresAt.getTime() - now,
+ baseUrl: deps.appUrl,
serverUrl,
serverId: srv.id,
};
@@ -504,6 +557,7 @@ export async function createConnectCode(
return {
code,
expiresInMs: CONNECT_CODE_TTL_MS,
+ baseUrl: deps.appUrl,
serverUrl,
serverId: srv.id,
};
@@ -517,7 +571,7 @@ export async function createMachineCode(
): Promise<
{ code: string; expiresInMs: number; serverUrl: string } | { error: string }
> {
- const { db, baseDomain } = deps;
+ const { db, serverUrlTemplate } = deps;
const prof = await db
.select()
.from(profile)
@@ -559,7 +613,7 @@ export async function createMachineCode(
return {
code,
expiresInMs: CONNECT_CODE_TTL_MS,
- serverUrl: `https://${srv.subdomain}.${baseDomain}`,
+ serverUrl: serverUrlForLabel(srv.subdomain, serverUrlTemplate),
};
}
@@ -711,24 +765,27 @@ function rowsChanged(result: unknown): number {
* `handle` is the redeemed server's routing label (its subdomain). For the
* primary server this equals the account handle, so primary pairing is
* byte-identical to the pre-multi-server behavior. The tunnel client uses
- * this field to build serverUrl / share URLs — it must not be the account's
- * primary handle when a non-primary server was paired.
+ * this field to build share URLs — it must not be the account's primary handle
+ * when a non-primary server was paired. `serverUrl` is the authoritative gate
+ * origin; production and local development both derive it from the label via
+ * the boundary-validated server URL template.
*
* Accepts `Deps` (D1 in the worker via `depsFromEnv`, better-sqlite3 in tests).
*/
export async function redeemConnectCode(
- deps: Pick,
+ deps: Pick,
code: string,
): Promise<
| {
credential: string;
serverId: string;
- handle: string | null;
- tunnelUrl: string | null;
+ handle: string;
+ serverUrl: string;
+ tunnelUrl: string;
}
| { error: string; status: number }
> {
- const { db, baseDomain } = deps;
+ const { db, serverUrlTemplate } = deps;
const normalized = code.trim().toUpperCase();
if (!normalized) return { error: "missing-code", status: 400 };
@@ -765,19 +822,17 @@ export async function redeemConnectCode(
.from(server)
.where(eq(server.id, row.serverId))
.get();
- const prof = await db
- .select()
- .from(profile)
- .where(eq(profile.userId, row.userId))
- .get();
+ if (!srv) return { error: "invalid-code", status: 404 };
// Routing label of the redeemed server (not necessarily the account handle).
- const handle = srv?.subdomain ?? prof?.handle ?? null;
+ const handle = srv.subdomain;
+ const serverUrl = serverUrlForLabel(handle, serverUrlTemplate);
return {
credential,
serverId: row.serverId,
handle,
+ serverUrl,
// Keyed by this server's subdomain (which may be non-primary), not the account handle.
- tunnelUrl: srv ? `wss://${srv.subdomain}.${baseDomain}/__tunnel` : null,
+ tunnelUrl: `${serverUrl.replace(/^http/u, "ws")}/__tunnel`,
};
}
@@ -786,7 +841,7 @@ export async function redeemConnectCode(
* creates a machine row, and returns the durable machine credential once.
*/
export async function redeemMachineCode(
- deps: Pick,
+ deps: Pick,
code: string,
): Promise<
| {
@@ -797,7 +852,7 @@ export async function redeemMachineCode(
}
| { error: string; status: number }
> {
- const { db, baseDomain } = deps;
+ const { db, serverUrlTemplate } = deps;
const normalized = code.trim().toUpperCase();
if (!normalized) return { error: "missing-code", status: 400 };
@@ -866,6 +921,6 @@ export async function redeemMachineCode(
credential,
machineId,
handle: prof?.handle ?? null,
- serverUrl: label ? `https://${label}.${baseDomain}` : null,
+ serverUrl: label ? serverUrlForLabel(label, serverUrlTemplate) : null,
};
}
diff --git a/apps/web/src/server/auth-runtime.test.ts b/apps/web/src/server/auth-runtime.test.ts
new file mode 100644
index 0000000000..f5a708286a
--- /dev/null
+++ b/apps/web/src/server/auth-runtime.test.ts
@@ -0,0 +1,78 @@
+import { describe, expect, it } from "vitest";
+import {
+ crossSubdomainCookieConfig,
+ isLoopbackHostname,
+ resolveDevAuthUserId,
+} from "./auth-runtime.js";
+
+describe("local web authentication", () => {
+ it.each(["127.0.0.1", "127.1.2.3", "localhost", "[::1]"])(
+ "recognizes the loopback hostname %s",
+ (hostname) => {
+ expect(isLoopbackHostname(hostname)).toBe(true);
+ },
+ );
+
+ it.each(["getbb.app", "127.0.0.256", "128.0.0.1", "localhost.example"])(
+ "rejects the non-loopback hostname %s",
+ (hostname) => {
+ expect(isLoopbackHostname(hostname)).toBe(false);
+ },
+ );
+
+ it("returns the seeded user for an HTTP loopback app", () => {
+ expect(
+ resolveDevAuthUserId({
+ APP_URL: "http://127.0.0.1:8792",
+ DEV_AUTH_USER_ID: " usr_cloud_dev ",
+ }),
+ ).toBe("usr_cloud_dev");
+ });
+
+ it("uses Better Auth when no seeded user is configured", () => {
+ expect(
+ resolveDevAuthUserId({
+ APP_URL: "https://getbb.app",
+ }),
+ ).toBeNull();
+ });
+
+ it.each([
+ "https://getbb.app",
+ "https://127.0.0.1:8792",
+ "http://dev.getbb.app:8792",
+ ])("fails closed when seeded auth is configured for %s", (APP_URL) => {
+ expect(() =>
+ resolveDevAuthUserId({ APP_URL, DEV_AUTH_USER_ID: "usr_cloud_dev" }),
+ ).toThrow(/only allowed/u);
+ });
+});
+
+describe("Better Auth cookie scope", () => {
+ it("shares production cookies with Connect subdomains", () => {
+ expect(
+ crossSubdomainCookieConfig({
+ APP_URL: "https://getbb.app",
+ BASE_DOMAIN: "getbb.app",
+ }),
+ ).toEqual({ enabled: true, domain: ".getbb.app" });
+ });
+
+ it("shares staging cookies with its Connect subdomains", () => {
+ expect(
+ crossSubdomainCookieConfig({
+ APP_URL: "https://auth.vibecodethis.site",
+ BASE_DOMAIN: "vibecodethis.site",
+ }),
+ ).toEqual({ enabled: true, domain: ".vibecodethis.site" });
+ });
+
+ it("uses a host-only cookie for the local web worker", () => {
+ expect(
+ crossSubdomainCookieConfig({
+ APP_URL: "http://127.0.0.1:8792",
+ BASE_DOMAIN: "getbb.app",
+ }),
+ ).toEqual({ enabled: false });
+ });
+});
diff --git a/apps/web/src/server/auth-runtime.ts b/apps/web/src/server/auth-runtime.ts
new file mode 100644
index 0000000000..9d6fcfc917
--- /dev/null
+++ b/apps/web/src/server/auth-runtime.ts
@@ -0,0 +1,80 @@
+import type { Env } from "./env.js";
+
+const LOOPBACK_IPV4_PREFIX = "127.";
+
+function normalizeHostname(hostname: string): string {
+ const normalized = hostname.trim().toLowerCase();
+ return normalized.startsWith("[") && normalized.endsWith("]")
+ ? normalized.slice(1, -1)
+ : normalized;
+}
+
+export function isLoopbackHostname(hostname: string): boolean {
+ const normalized = normalizeHostname(hostname);
+ if (normalized === "localhost" || normalized === "::1") return true;
+ if (!normalized.startsWith(LOOPBACK_IPV4_PREFIX)) return false;
+
+ const octets = normalized.split(".");
+ return (
+ octets.length === 4 &&
+ octets.every((octet) => {
+ if (!/^\d{1,3}$/u.test(octet)) return false;
+ const value = Number(octet);
+ return value >= 0 && value <= 255;
+ })
+ );
+}
+
+function appUrl(env: Pick): URL {
+ try {
+ return new URL(env.APP_URL);
+ } catch {
+ throw new Error("APP_URL must be an absolute URL");
+ }
+}
+
+/**
+ * The launcher may opt into a seeded account, but only for a loopback web app.
+ * Throwing on a non-loopback origin makes an accidental production binding
+ * fail closed instead of silently bypassing Better Auth.
+ */
+export function resolveDevAuthUserId(
+ env: Pick,
+): string | null {
+ if (env.DEV_AUTH_USER_ID === undefined) return null;
+
+ const userId = env.DEV_AUTH_USER_ID.trim();
+ if (userId.length === 0) {
+ throw new Error("DEV_AUTH_USER_ID must be a non-empty user id");
+ }
+
+ const url = appUrl(env);
+ if (url.protocol !== "http:" || !isLoopbackHostname(url.hostname)) {
+ throw new Error(
+ "DEV_AUTH_USER_ID is only allowed when APP_URL is an HTTP loopback origin",
+ );
+ }
+ return userId;
+}
+
+/**
+ * Production/staging share the Better Auth session with label subdomains.
+ * Loopback and preview origins instead receive a host-only cookie; a Domain
+ * attribute for `.getbb.app` would be rejected by a browser on 127.0.0.1.
+ */
+export function crossSubdomainCookieConfig(
+ env: Pick,
+): { enabled: false } | { enabled: true; domain: string } {
+ const url = appUrl(env);
+ const hostname = normalizeHostname(url.hostname);
+ const baseDomain = normalizeHostname(env.BASE_DOMAIN).replace(/^\./u, "");
+ if (baseDomain.length === 0) {
+ throw new Error("BASE_DOMAIN must be a non-empty hostname");
+ }
+
+ const belongsToBaseDomain =
+ hostname === baseDomain || hostname.endsWith(`.${baseDomain}`);
+ return belongsToBaseDomain
+ ? { enabled: true, domain: `.${baseDomain}` }
+ : { enabled: false };
+}
diff --git a/apps/web/src/server/auth.ts b/apps/web/src/server/auth.ts
index 0b417223ff..7841401998 100644
--- a/apps/web/src/server/auth.ts
+++ b/apps/web/src/server/auth.ts
@@ -3,13 +3,16 @@ import { drizzleAdapter } from "@better-auth/drizzle-adapter";
import { drizzle } from "drizzle-orm/d1";
import { account, session, user, verification } from "@bb/connect-db";
import type { Env } from "./env.js";
+import { crossSubdomainCookieConfig } from "./auth-runtime.js";
export type Auth = ReturnType;
/**
- * better-auth bound to the staging D1 via drizzle. GitHub is the only provider.
- * Cookies are scoped to `.${BASE_DOMAIN}` so the tunnel gate on
- * `.${BASE_DOMAIN}` can validate the same session.
+ * better-auth bound to the account D1 via drizzle. GitHub is the only provider.
+ * Deployed cookies are scoped to `.${BASE_DOMAIN}` so the tunnel gate on
+ * `.${BASE_DOMAIN}` can validate the same session. Loopback development
+ * uses a host-only cookie because browsers reject a getbb.app Domain attribute
+ * on 127.0.0.1.
*/
export function createAuth(env: Env) {
const db = drizzle(env.DB);
@@ -41,7 +44,7 @@ export function createAuth(env: Env) {
},
},
advanced: {
- crossSubDomainCookies: { enabled: true, domain: `.${env.BASE_DOMAIN}` },
+ crossSubDomainCookies: crossSubdomainCookieConfig(env),
},
});
}
diff --git a/apps/web/src/server/current-user.server.ts b/apps/web/src/server/current-user.server.ts
index ad1bf45da3..f903802c4e 100644
--- a/apps/web/src/server/current-user.server.ts
+++ b/apps/web/src/server/current-user.server.ts
@@ -1,14 +1,19 @@
import { getRequest } from "@tanstack/react-start/server";
import { createAuth } from "./auth.js";
import { getEnv } from "./env.js";
+import { resolveDevAuthUserId } from "./auth-runtime.js";
// `.server.ts`: server-only. Never import from client code — only from server
// functions / route handlers. Holds the request-bound session lookup.
/** The authenticated user id for the current request, or null. */
export async function getSessionUserId(): Promise {
+ const env = getEnv();
+ const devUserId = resolveDevAuthUserId(env);
+ if (devUserId !== null) return devUserId;
+
const request = getRequest();
- const auth = createAuth(getEnv());
+ const auth = createAuth(env);
const session = await auth.api.getSession({ headers: request.headers });
return session?.user?.id ?? null;
}
diff --git a/apps/web/src/server/env.ts b/apps/web/src/server/env.ts
index ed3d8d8cf4..19319b30f2 100644
--- a/apps/web/src/server/env.ts
+++ b/apps/web/src/server/env.ts
@@ -5,6 +5,17 @@ export interface Env {
TUNNEL_DO: DurableObjectNamespace;
BASE_DOMAIN: string;
APP_URL: string;
+ /**
+ * Loopback-only account override used by the local Cloud launcher. Omitted
+ * everywhere that should authenticate through Better Auth.
+ */
+ DEV_AUTH_USER_ID?: string;
+ /**
+ * Optional authoritative Connect gate URL template. `{label}` is replaced
+ * with the claimed routing label. Production derives the equivalent template
+ * from BASE_DOMAIN; local development supplies its worktree-specific port.
+ */
+ CONNECT_SERVER_URL_TEMPLATE?: string;
GITHUB_CLIENT_ID: string;
GITHUB_CLIENT_SECRET: string;
BETTER_AUTH_SECRET: string;
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index 984700df00..c08757d28a 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -5,6 +5,34 @@ import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
+const cloudDevStatePath = process.env.BB_CLOUD_DEV_STATE_PATH?.trim();
+const cloudDevAppUrl = process.env.BB_CLOUD_DEV_APP_URL?.trim();
+const cloudDevBaseDomain = process.env.BB_CLOUD_DEV_BASE_DOMAIN?.trim();
+const cloudDevConnectServerUrlTemplate =
+ process.env.BB_CLOUD_DEV_CONNECT_SERVER_URL_TEMPLATE?.trim();
+const cloudDevAuthUserId = process.env.DEV_AUTH_USER_ID?.trim();
+
+const cloudDevConfig =
+ cloudDevStatePath &&
+ cloudDevAppUrl &&
+ cloudDevBaseDomain &&
+ cloudDevConnectServerUrlTemplate
+ ? {
+ persistState: { path: cloudDevStatePath },
+ config: (config: { vars?: Record }) => ({
+ vars: {
+ ...config.vars,
+ APP_URL: cloudDevAppUrl,
+ BASE_DOMAIN: cloudDevBaseDomain,
+ CONNECT_SERVER_URL_TEMPLATE: cloudDevConnectServerUrlTemplate,
+ ...(cloudDevAuthUserId
+ ? { DEV_AUTH_USER_ID: cloudDevAuthUserId }
+ : {}),
+ },
+ }),
+ }
+ : {};
+
export default defineConfig({
resolve: {
alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) },
@@ -15,7 +43,10 @@ export default defineConfig({
allowedHosts: [".ts.net"],
},
plugins: [
- cloudflare({ viteEnvironment: { name: "ssr" } }),
+ cloudflare({
+ viteEnvironment: { name: "ssr" },
+ ...cloudDevConfig,
+ }),
tailwindcss(),
tanstackStart(),
viteReact(),
diff --git a/docs/api_to_audit.md b/docs/api_to_audit.md
index 17b8fc7f01..cdd3c1398d 100644
--- a/docs/api_to_audit.md
+++ b/docs/api_to_audit.md
@@ -44,7 +44,6 @@ Each label is capped at 80 characters and rendered as a truncating segment.
only for non-MCP native plugin tools. Confirm that distinction stays sound
as provider adapters and dynamic-tool provenance evolve.
-
## `experimental_NewThreadComposer` (`@bb/plugin-sdk/app`)
**What it does.** The host-owned new-thread compose surface, the create-side
@@ -213,6 +212,7 @@ reimplementing it, and `indicatorLabel` carries the matching accessible string.
a sidebar of many distinct worktrees does not stampede the git host; and
returning `null` for "lookup failed" (rather than an error) is the right
failure for a row that should simply show nothing.
+
## `app.slots.experimental_threadHeaderAction` (`@bb/plugin-sdk/app`)
**What it does.** Renders a plugin component in the thread header's action row.
@@ -253,3 +253,35 @@ other pane's copy (or release its owned state). The thread-list slot omits it
deliberately: it mounts once, and a crash there should disable it everywhere.
Confirm that split before stabilizing, and decide whether other multi-mount
slots need the same treatment.
+
+## `bb.experimental_registerCloudAiProvider`
+
+**What it does.** Registers a cloud route for bb's small AI tasks (thread-title
+inference, commit-message inference, voice transcription). Per call, when the
+server's `cloudAi` experiment is enabled and the provider's synchronous
+`isAvailable()` is true, the host tries it before the locally configured
+`BB_INFERENCE`/`BB_TRANSCRIPTION` providers; an `ok: false` result falls
+through to those, and host-side timeouts keep the host's existing timeout
+semantics. While the experiment is off, a provider may remain registered but
+is never called. Single slot host-wide: the most recent registration wins,
+and the registration is torn down with the plugin's dispose hooks. The
+contract is result-shaped (no thrown error classes) so it survives plugin
+bundling. Shipped for the builtin Cloud plugin's bb Cloud routing (the
+plugin's compatibility id remains `connect`).
+
+**Audit before stabilizing.**
+
+1. **Registration policy.** Any plugin may currently claim the slot, which
+ reroutes prompt/diff/audio content to plugin-chosen infrastructure. Decide
+ whether the slot needs a permission gate, builtin-only restriction, or a
+ user-visible indicator of which plugin holds it.
+2. **Single-slot semantics.** Last-registration-wins is the simplest rule;
+ confirm it against multiple plugins wanting the slot (priority? explicit
+ user choice?) before freezing.
+3. **Failure vocabulary.** `unauthorized | quota_exhausted | unavailable`
+ drives fallback routing. Confirm the set is sufficient (rate limiting?
+ partial availability per capability?) — codes are additive-only after
+ stabilization.
+4. **Capability granularity.** One provider currently covers completion and
+ transcription together; decide whether providers should be able to offer
+ one without the other.
diff --git a/docs/configuration.md b/docs/configuration.md
index 5c4b340c62..60d00c517d 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -94,14 +94,16 @@ signal it, so a stale file left by a crash cannot stop an unrelated process.
## Common Keys
-| Key | Command | When to set | Used for |
-| ------------------ | --------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
-| `BB_APP_URL` | `bb-app config` | Optional for remote use | Human-facing app URL used for generated links and allowed browser origins. Leave empty for local-only use. |
-| `BB_INFERENCE` | `bb-app config` | Optional | Server-side helper model in `provider/model` format. Defaults to `codex/gpt-5.6-luna`; the Codex helper route uses no reasoning. |
-| `BB_TRANSCRIPTION` | `bb-app config` | Optional | Voice transcription model in `provider/model` format. Defaults to `codex/gpt-transcribe`. |
-| `BB_SERVER_URL` | `bb-app config` | Remote CLI/host use | Server URL for standalone `bb` CLI and `host-daemon` commands on the current machine. The CLI defaults to `http://127.0.0.1:38886` when unset. |
-| `BB_LOG_LEVEL` | `bb-app config` | Debugging | Log level for the next bb start: `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. |
-| `OPENAI_API_KEY` | `bb-app env` | OpenAI opt-in routes | Required only when selecting explicit OpenAI provider routes such as `openai/gpt-4o-mini` or `openai/gpt-transcribe`. |
+| Key | Command | When to set | Used for |
+| ------------------------- | --------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
+| `BB_APP_URL` | `bb-app config` | Optional for remote use | Human-facing app URL used for generated links and allowed browser origins. Leave empty for local-only use. |
+| `BB_INFERENCE` | `bb-app config` | Optional | Server-side helper model in `provider/model` format. Defaults to `codex/gpt-5.6-luna`; the Codex helper route uses no reasoning. |
+| `BB_TRANSCRIPTION` | `bb-app config` | Optional | Voice transcription model in `provider/model` format. Defaults to `codex/gpt-transcribe`. |
+| `BB_SERVER_URL` | `bb-app config` | Remote CLI/host use | Server URL for standalone `bb` CLI and `host-daemon` commands on the current machine. The CLI defaults to `http://127.0.0.1:38886` when unset. |
+| `BB_CONNECT_BASE_URL` | `bb-app env` | Alternate Cloud | HTTP(S) origin used for Cloud account links and pairing-code redemption. Leave unset for `https://getbb.app`. |
+| `BB_CONNECT_LOOPBACK_URL` | `bb-app env` | Source QA only | Loopback HTTP origin served by the bare Cloud handle. `pnpm cloud:dev` manages this automatically. |
+| `BB_LOG_LEVEL` | `bb-app config` | Debugging | Log level for the next bb start: `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. |
+| `OPENAI_API_KEY` | `bb-app env` | OpenAI opt-in routes | Required only when selecting explicit OpenAI provider routes such as `openai/gpt-4o-mini` or `openai/gpt-transcribe`. |
By default, helper inference and voice transcription use Codex credentials from
the host daemon. Run `codex login` on the host for the default path. Set
@@ -428,12 +430,15 @@ SDK request, plus ephemeral maximize/restore delivery through
target thread is already open in a multi-pane app window; the response reports
how many connected clients received the broadcast.
-## bb connect
+## bb Cloud (bb connect)
-`bb connect --code --server https://.getbb.app` pairs this bb
-server for browser access at `.getbb.app` (claim a handle and copy the
-command at https://getbb.app). Remote access is owned by the builtin
-**connect plugin** (`plugins/connect/`): pairing redeems the code and stores
+`bb connect --code --server https://.getbb.app` connects this
+bb server to **bb Cloud** — the getbb.app account relationship (claim a handle
+and copy the command at https://getbb.app). Once connected: the bb is
+reachable from any browser at `.getbb.app`, port shares and machine
+pairing work through the account, and AI features run through it (below).
+bb Cloud is provided by the builtin **Cloud plugin** (internal id `connect`,
+implemented in `plugins/connect/`): pairing redeems the code and stores
the durable credential in the plugin's kv storage (in `bb.db`), and the
plugin's background service holds the connect tunnel — dialing the gate,
proxying relayed requests to the server's own loopback (which serves the SPA
@@ -443,9 +448,9 @@ proxying relayed requests to the server's own loopback (which serves the SPA
re-establishes on restart; there is no foreground client. Pair from a machine
without an installed bb via `npx -p bb-app@latest bb connect …`.
`bb connect status` shows the connect state and every share's host and URL;
- `bb connect off` disconnects and clears the pairing. After pairing,
- `bb connect expose ` run from a thread shares that thread environment's
- enrolled host. Server-host URLs remain
+ `bb connect off` unlinks this bb from Cloud and clears the pairing. After
+ pairing, `bb connect expose ` run from a thread shares that thread
+ environment's enrolled host. Server-host URLs remain
`https://--.getbb.app`; other machines use
`https://--.getbb.app` and proxy directly through the
owning daemon. Outside a thread the command defaults to the server host;
@@ -456,18 +461,56 @@ proxying relayed requests to the server's own loopback (which serves the SPA
`url`; `shares --json` also includes the resolved `host`. A machine without
a live Connect enrollment fails fast with instructions to remove and re-add
it in Settings → Machines. Disabling the plugin
- (`bb plugin disable connect`) cuts off all remote access;
- `bb plugin enable connect` restores it.
-
-The tunnel client lives in `plugins/connect/`; the CLI command is proxied to
-the plugin, and Settings → Connect drives the plugin's rpc (including shared
-ports).
+ (`bb plugin disable connect`) cuts off all bb Cloud capabilities — remote
+ access and AI features alike; `bb plugin enable connect` restores them.
+
+When the default-off `cloudAi` experiment is enabled, **AI features** —
+thread-title inference, commit-message inference, and voice transcription —
+can route through the account's bb Cloud AI proxy while connected, with no
+local AI credentials needed. Enable the experiment in Settings → Experiments
+or with `bb settings experiment cloudAi true`. The cloud picks the models;
+`BB_INFERENCE`/`BB_TRANSCRIPTION` govern only the local fallback, which bb uses
+automatically when a cloud call fails. Usage is capped by a generous
+per-account daily cost budget (an abuse ceiling, not a usage limit — normal
+use never approaches it).
+
+The experiment is the outer product gate: while it is off, Cloud AI is hidden
+from the Cloud plugin settings and the server never sends it inference or
+voice requests. Once enabled, Settings → Cloud → Cloud AI and
+`bb connect ai [on|off]` control the paired plugin's inner routing preference;
+both switches must be on for Cloud AI to be used. `bb connect status` includes
+the inner preference. With both enabled, prompts, commit diffs, and voice audio
+transit getbb.app and its upstream model provider; they are processed for the
+request and not stored.
+
+The tunnel client lives in `plugins/connect/`, and the `bb connect` CLI command
+is proxied to the plugin. Settings → Cloud always contains Remote access
+(pairing, URL, QR code, and shared ports) and adds a separate Cloud AI section
+when its experiment is enabled, preserving one account connection underneath.
+
+`BB_CONNECT_BASE_URL` changes the Cloud account and pairing-code redemption
+origin. It must be an HTTP(S) origin with no path, query, credentials, or
+fragment. The redemption response supplies the authoritative tunnel-gate URL,
+so account and gate services may use different origins. Leave the setting
+unset in production to use `https://getbb.app`; source development may put it
+in `.env.development.local`. The `pnpm cloud:dev` launcher applies its local
+value to the current checkout's dev server while running and restores the
+previous managed environment value when it exits.
+
+`BB_CONNECT_LOOPBACK_URL` is a source-development escape hatch for selecting
+the loopback HTTP origin served by the bare Cloud handle. It accepts only an
+HTTP origin on `127.0.0.1`, `localhost`, or `::1`. Leave it unset in packaged
+and production use. `pnpm cloud:dev` manages it automatically so the handle
+serves the current worktree's Vite app rather than its API-only server port.
## Experiments
Experimental surfaces are off by default and can be changed in Settings →
Experiments or with `bb settings experiment `. The
`newOnboarding` experiment exposes the first-run agent and project setup guide.
+The `cloudAi` experiment permits server AI tasks to use a registered cloud
+provider and reveals the Cloud AI section of the builtin Cloud plugin. It does
+not affect Remote access, pairing, or port sharing.
The `toolsHub` experiment exposes Extensions for managing skills and plugins,
while Automations stays in the Plugins section beside threads. The `toolsHub`
gate only controls the UI. Installed skills, automation execution, plugin
diff --git a/docs/debugging-and-qa.md b/docs/debugging-and-qa.md
index 095bf3dc2a..bd90add77b 100644
--- a/docs/debugging-and-qa.md
+++ b/docs/debugging-and-qa.md
@@ -30,3 +30,73 @@ Test agents with:
eval "$(scripts/bb-dev-app env)"
pnpm bb:dev thread spawn --project proj_personal --provider codex --permission-mode accept-edits --title "Smoke test" --prompt "Reply only with ok." --json
```
+
+## Local bb Cloud
+
+Run the Cloud account service and tunnel/AI gate locally with one command:
+
+```bash
+pnpm cloud:dev
+```
+
+Before the first run, copy `apps/connect/.dev.vars.example` to the ignored
+`apps/connect/.dev.vars`, add an OpenAI API key, and generate the local auth
+secret with `openssl rand -hex 32`. The launcher applies the Connect migrations
+to an ignored, checkout-local D1 under `.wrangler/cloud-dev`, starts `apps/web`
+and `apps/connect` on deterministic, worktree-specific ports against that same
+database, creates only a local authenticated user identity, and configures the
+current checkout's dev server to use the local account worker. Open the
+dashboard URL printed by the launcher, choose a handle, create the first bb,
+and generate its pairing code through the same UI as production. A handle such
+as `michael` gets the real worktree-local gate URL printed by the UI,
+`http://michael.localhost:`; local development never displays or
+dials a `getbb.app` address. It skips GitHub authentication but does not seed a
+handle, server, or pairing code. It never reads or writes remote Cloudflare
+resources.
+
+Keep ordinary `pnpm dev` running in another terminal. The launcher applies
+`BB_CONNECT_BASE_URL` and `BB_CONNECT_LOOPBACK_URL` through the dev server's
+managed environment reload, so the settings flow needs no URL field and no
+server restart. The latter targets the current worktree's Vite app port, which
+means the bare local handle serves the same UI as `pnpm dev`. The launcher
+restores both previous values when stopped. After generating a code in the
+Cloud dashboard, open the printed Settings → Cloud URL and paste it. Use the
+printed commands to enable the `cloudAi` experiment and AI preference before
+testing thread-title inference, commit-message inference, or voice
+transcription. Press Ctrl-C in the Cloud terminal to stop both Workers.
+
+The dashboard's terminal disclosure is also self-contained: its `bb connect`
+command includes both the per-label local gate (`--server`) and the separate
+account/redeem worker (`--base-url`). It does not depend on production's
+single-origin deployment topology.
+
+The printed setup unsets `BB_CLI` after selecting the dev server. Agent shells
+can inherit that variable from another running bb installation; leaving it set
+would make `pnpm bb:dev` re-exec that installation's CLI instead of the CLI in
+this checkout.
+
+The defaults use the same checkout hash as `pnpm dev`, in separate port ranges,
+so multiple worktrees can run concurrently without manual configuration. Flags
+remain available for a one-off override:
+
+```bash
+pnpm cloud:dev -- --connect-port 8891 --web-port 8892
+```
+
+To exercise the real GitHub OAuth flow instead of seeded authentication, create
+a separate local GitHub OAuth App. Configure its homepage as
+`http://127.0.0.1` and its authorization callback as
+`http://127.0.0.1/api/auth/callback/github`. GitHub permits the redirect URI for
+a loopback callback to select the actual listening port, so the same local OAuth
+App works across worktrees. Copy `apps/web/.dev.vars.example` to the ignored
+`apps/web/.dev.vars`, add that app's client id and secret, then run:
+
+```bash
+pnpm cloud:dev -- --github-auth
+```
+
+The launcher does not enable seeded authentication or mint a seeded pairing
+code in this mode. Continue with GitHub in `apps/web`, then claim a handle and
+create/pair a bb from the dashboard. The local Better Auth cookie is host-only
+and uses its HTTP name; deployed staging and production retain the shared,
+secure domain cookie.
diff --git a/package.json b/package.json
index 7b24c19717..4925c02370 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"cli:prepare": "pnpm exec turbo run build --filter=@bb/scripts --filter=@bb/cli --output-logs=none --log-prefix=none --summarize=false",
"ensure-native-modules": "node scripts/ensure-native-modules.mjs",
"dev": "node scripts/ensure-native-modules.mjs && cross-env NODE_ENV=development dotenv -c development -- node --conditions=source --import tsx packages/scripts/src/commands/run-dev.ts",
+ "cloud:dev": "node --conditions=source --import tsx scripts/bb-cloud-dev.mjs",
"dev:desktop": "scripts/bb-dev-app current --desktop",
"dev:status": "scripts/bb-dev-app status",
"dev:restart": "cross-env NODE_ENV=development node --conditions=source --import tsx packages/scripts/src/commands/request-dev-restart.ts both",
diff --git a/packages/config/src/runtime.ts b/packages/config/src/runtime.ts
index f0481cfdb0..cbbb368395 100644
--- a/packages/config/src/runtime.ts
+++ b/packages/config/src/runtime.ts
@@ -6,6 +6,8 @@ export type BbRuntimeMode = "dev" | "prod";
export interface DevPortSet {
appPort: number;
+ cloudConnectPort: number;
+ cloudWebPort: number;
hostDaemonPort: number;
serverPort: number;
}
@@ -88,6 +90,10 @@ const DEV_PORT_BUCKETS = 8_000;
const DEV_APP_PORT_BASE = 11_000;
const DEV_SERVER_PORT_BASE = 19_000;
const DEV_HOST_DAEMON_PORT_BASE = 27_000;
+const DEV_CLOUD_CONNECT_PORT_BASE = 35_000;
+// The Connect range skips the packaged app's 38886/38887 pair, ending at
+// 43001. Start web immediately after it so all five dev ranges stay disjoint.
+const DEV_CLOUD_WEB_PORT_BASE = 43_002;
const DEV_PROCESS_STRIPPED_ENV_KEYS: readonly string[] = [
"BB_ENVIRONMENT_ID",
"BB_THREAD_ID",
@@ -134,10 +140,22 @@ function resolvePortOffset(repoRootPath: string): number {
return Number.parseInt(hash.slice(0, 8), 16) % DEV_PORT_BUCKETS;
}
+function skipPackagedAppPorts(port: number): number {
+ let availablePort = port;
+ for (const reservedPort of [BB_PROD_SERVER_PORT, BB_PROD_HOST_DAEMON_PORT]) {
+ if (availablePort >= reservedPort) availablePort += 1;
+ }
+ return availablePort;
+}
+
function resolvePorts(repoRootPath: string): DevPortSet {
const offset = resolvePortOffset(repoRootPath);
return {
appPort: DEV_APP_PORT_BASE + offset,
+ cloudConnectPort: skipPackagedAppPorts(
+ DEV_CLOUD_CONNECT_PORT_BASE + offset,
+ ),
+ cloudWebPort: DEV_CLOUD_WEB_PORT_BASE + offset,
hostDaemonPort: DEV_HOST_DAEMON_PORT_BASE + offset,
serverPort: DEV_SERVER_PORT_BASE + offset,
};
diff --git a/packages/connect-client/src/ai.ts b/packages/connect-client/src/ai.ts
new file mode 100644
index 0000000000..3a553e0673
--- /dev/null
+++ b/packages/connect-client/src/ai.ts
@@ -0,0 +1,152 @@
+import { z } from "zod";
+import type { ConnectCredential } from "./credential.js";
+
+// bb Cloud AI proxy calls for a paired bb (`/api/connect/ai/*` on the gate).
+// The client sends a prompt+schema or an audio file — never a model name;
+// models are a gate-side choice so they can change without a client release.
+
+export type ConnectAiErrorCode =
+ | "unauthorized"
+ | "quota_exhausted"
+ | "server_error"
+ | "network"
+ | "invalid_response";
+
+/**
+ * Typed AI-proxy failure. `code` is stable for routing decisions (fall back to
+ * a local provider, latch a dead credential); `message` carries detail for
+ * logs. Aborts are re-thrown untouched so callers keep their own timeout
+ * semantics.
+ */
+export class ConnectAiError extends Error {
+ constructor(
+ readonly code: ConnectAiErrorCode,
+ message: string,
+ readonly status?: number,
+ ) {
+ super(message);
+ this.name = "ConnectAiError";
+ }
+}
+
+const aiInferenceResponseSchema = z.object({
+ value: z.record(z.string(), z.unknown()),
+});
+const aiTranscriptionResponseSchema = z.object({ text: z.string() });
+
+function isAbortError(error: unknown): boolean {
+ return error instanceof DOMException && error.name === "AbortError";
+}
+
+async function postAi(
+ credential: ConnectCredential,
+ path: string,
+ init: RequestInit,
+ fetchImpl: typeof fetch,
+): Promise {
+ const url = `${credential.serverUrl.replace(/\/$/, "")}${path}`;
+ let response: Response;
+ try {
+ response = await fetchImpl(url, {
+ ...init,
+ method: "POST",
+ headers: {
+ ...init.headers,
+ "x-bb-connect-machine": credential.credential,
+ },
+ });
+ } catch (error) {
+ if (isAbortError(error)) throw error;
+ throw new ConnectAiError(
+ "network",
+ error instanceof Error ? error.message : String(error),
+ );
+ }
+ if (response.status === 401 || response.status === 403) {
+ throw new ConnectAiError(
+ "unauthorized",
+ "bb Cloud AI request not authorized",
+ response.status,
+ );
+ }
+ if (response.status === 429) {
+ throw new ConnectAiError(
+ "quota_exhausted",
+ "Daily bb Cloud AI budget reached",
+ response.status,
+ );
+ }
+ if (!response.ok) {
+ throw new ConnectAiError(
+ "server_error",
+ `bb Cloud AI request failed (${response.status})`,
+ response.status,
+ );
+ }
+ return response;
+}
+
+/**
+ * Structured completion through the gate. Returns the raw value for
+ * caller-side schema validation (bb validates against its TypeBox schema).
+ */
+export async function fetchAiInference(
+ credential: ConnectCredential,
+ args: {
+ prompt: string;
+ schema: Record;
+ signal?: AbortSignal;
+ },
+ fetchImpl: typeof fetch = globalThis.fetch,
+): Promise {
+ const response = await postAi(
+ credential,
+ "/api/connect/ai/inference",
+ {
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ prompt: args.prompt, schema: args.schema }),
+ ...(args.signal ? { signal: args.signal } : {}),
+ },
+ fetchImpl,
+ );
+ const parsed = aiInferenceResponseSchema.safeParse(
+ await response.json().catch(() => null),
+ );
+ if (!parsed.success) {
+ throw new ConnectAiError(
+ "invalid_response",
+ "bb Cloud AI inference response failed schema validation",
+ );
+ }
+ return parsed.data.value;
+}
+
+/** Voice transcription through the gate (multipart file + optional prompt). */
+export async function fetchAiTranscription(
+ credential: ConnectCredential,
+ args: { file: File; prompt?: string; signal?: AbortSignal },
+ fetchImpl: typeof fetch = globalThis.fetch,
+): Promise {
+ const form = new FormData();
+ form.set("file", args.file, args.file.name);
+ if (args.prompt !== undefined) form.set("prompt", args.prompt);
+ const response = await postAi(
+ credential,
+ "/api/connect/ai/transcription",
+ {
+ body: form,
+ ...(args.signal ? { signal: args.signal } : {}),
+ },
+ fetchImpl,
+ );
+ const parsed = aiTranscriptionResponseSchema.safeParse(
+ await response.json().catch(() => null),
+ );
+ if (!parsed.success) {
+ throw new ConnectAiError(
+ "invalid_response",
+ "bb Cloud AI transcription response failed schema validation",
+ );
+ }
+ return parsed.data.text;
+}
diff --git a/packages/connect-client/src/index.ts b/packages/connect-client/src/index.ts
index 3248e4d174..34f1b20f22 100644
--- a/packages/connect-client/src/index.ts
+++ b/packages/connect-client/src/index.ts
@@ -9,6 +9,12 @@ export {
isRevokedCredentialError,
type ConnectListErrorCode,
} from "./errors.js";
+export {
+ ConnectAiError,
+ fetchAiInference,
+ fetchAiTranscription,
+ type ConnectAiErrorCode,
+} from "./ai.js";
export {
accountServerSchema,
accountServersResponseSchema,
diff --git a/packages/connect-client/test/connect-client.test.ts b/packages/connect-client/test/connect-client.test.ts
index e3e51866e1..218104da01 100644
--- a/packages/connect-client/test/connect-client.test.ts
+++ b/packages/connect-client/test/connect-client.test.ts
@@ -3,6 +3,8 @@ import {
ConnectListError,
ConnectMachineRedeemError,
deriveConnectBaseUrl,
+ fetchAiInference,
+ fetchAiTranscription,
listAccountServers,
redeemMachineCredential,
serverUrlForHandle,
@@ -183,3 +185,108 @@ describe("redeemMachineCredential", () => {
).resolves.toMatchObject({ handle: "laptop" });
});
});
+
+describe("fetchAiInference", () => {
+ it("posts prompt+schema with the credential header and no model name", async () => {
+ const fetchImpl = vi.fn(
+ async (_url: string, _init: RequestInit) =>
+ new Response(JSON.stringify({ value: { title: "Hi" } })),
+ );
+ await expect(
+ fetchAiInference(
+ CREDENTIAL,
+ {
+ prompt: "Summarize",
+ schema: { type: "object", properties: {} },
+ },
+ fetchImpl as unknown as typeof fetch,
+ ),
+ ).resolves.toEqual({ title: "Hi" });
+
+ const [url, init] = fetchImpl.mock.calls[0];
+ expect(url).toBe("https://laptop.getbb.app/api/connect/ai/inference");
+ expect(init.method).toBe("POST");
+ expect(
+ (init.headers as Record)["x-bb-connect-machine"],
+ ).toBe("bbcm_desktop");
+ expect(JSON.parse(String(init.body))).toEqual({
+ prompt: "Summarize",
+ schema: { type: "object", properties: {} },
+ });
+ });
+
+ it("maps statuses to stable codes", async () => {
+ const cases: Array<[number, string]> = [
+ [401, "unauthorized"],
+ [403, "unauthorized"],
+ [429, "quota_exhausted"],
+ [500, "server_error"],
+ ];
+ for (const [status, code] of cases) {
+ await expect(
+ fetchAiInference(
+ CREDENTIAL,
+ { prompt: "p", schema: {} },
+ async () => new Response("{}", { status }),
+ ),
+ ).rejects.toMatchObject({ name: "ConnectAiError", code, status });
+ }
+ });
+
+ it("rejects malformed bodies and network failures with typed codes", async () => {
+ await expect(
+ fetchAiInference(
+ CREDENTIAL,
+ { prompt: "p", schema: {} },
+ async () => new Response(JSON.stringify({ value: "not-an-object" })),
+ ),
+ ).rejects.toMatchObject({ code: "invalid_response" });
+ await expect(
+ fetchAiInference(CREDENTIAL, { prompt: "p", schema: {} }, async () => {
+ throw new Error("socket hang up");
+ }),
+ ).rejects.toMatchObject({ code: "network" });
+ });
+
+ it("re-throws aborts untouched so callers keep timeout semantics", async () => {
+ await expect(
+ fetchAiInference(CREDENTIAL, { prompt: "p", schema: {} }, async () => {
+ throw new DOMException("aborted", "AbortError");
+ }),
+ ).rejects.toMatchObject({ name: "AbortError" });
+ });
+});
+
+describe("fetchAiTranscription", () => {
+ it("posts multipart with filename and optional prompt", async () => {
+ const fetchImpl = vi.fn(
+ async (_url: string, _init: RequestInit) =>
+ new Response(JSON.stringify({ text: "hello" })),
+ );
+ const file = new File(["audio"], "recording.webm", { type: "audio/webm" });
+ await expect(
+ fetchAiTranscription(
+ CREDENTIAL,
+ { file, prompt: "ctx" },
+ fetchImpl as unknown as typeof fetch,
+ ),
+ ).resolves.toBe("hello");
+
+ const [url, init] = fetchImpl.mock.calls[0];
+ expect(url).toBe("https://laptop.getbb.app/api/connect/ai/transcription");
+ const form = init.body as FormData;
+ expect((form.get("file") as File).name).toBe("recording.webm");
+ expect(form.get("prompt")).toBe("ctx");
+ });
+
+ it("maps a 429 to quota_exhausted", async () => {
+ const file = new File(["audio"], "recording.webm", { type: "audio/webm" });
+ await expect(
+ fetchAiTranscription(
+ CREDENTIAL,
+ { file },
+ async () => new Response("{}", { status: 429 }),
+ ),
+ ).rejects.toMatchObject({ code: "quota_exhausted" });
+ });
+});
diff --git a/packages/connect-db/migrations/0006_ai_usage.sql b/packages/connect-db/migrations/0006_ai_usage.sql
new file mode 100644
index 0000000000..83a403ab15
--- /dev/null
+++ b/packages/connect-db/migrations/0006_ai_usage.sql
@@ -0,0 +1,7 @@
+CREATE TABLE `ai_usage` (
+ `user_id` text NOT NULL,
+ `day` text NOT NULL,
+ `cost_micro_usd` integer DEFAULT 0 NOT NULL,
+ PRIMARY KEY(`user_id`, `day`),
+ FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
+);
diff --git a/packages/connect-db/migrations/meta/0006_snapshot.json b/packages/connect-db/migrations/meta/0006_snapshot.json
new file mode 100644
index 0000000000..826087bc60
--- /dev/null
+++ b/packages/connect-db/migrations/meta/0006_snapshot.json
@@ -0,0 +1,920 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "863cc45a-b627-4837-9f67-071eceb5a2c3",
+ "prevId": "7ff3fbcf-840d-4547-8d1f-94d571998114",
+ "tables": {
+ "account": {
+ "name": "account",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "account_user_id_idx": {
+ "name": "account_user_id_idx",
+ "columns": [
+ "user_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "ai_usage": {
+ "name": "ai_usage",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "day": {
+ "name": "day",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "cost_micro_usd": {
+ "name": "cost_micro_usd",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "ai_usage_user_id_user_id_fk": {
+ "name": "ai_usage_user_id_user_id_fk",
+ "tableFrom": "ai_usage",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "ai_usage_user_id_day_pk": {
+ "columns": [
+ "user_id",
+ "day"
+ ],
+ "name": "ai_usage_user_id_day_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "audit_log": {
+ "name": "audit_log",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "detail": {
+ "name": "detail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "audit_log_user_id_idx": {
+ "name": "audit_log_user_id_idx",
+ "columns": [
+ "user_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "audit_log_user_id_user_id_fk": {
+ "name": "audit_log_user_id_user_id_fk",
+ "tableFrom": "audit_log",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "connect_code": {
+ "name": "connect_code",
+ "columns": {
+ "code": {
+ "name": "code",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "server_id": {
+ "name": "server_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "purpose": {
+ "name": "purpose",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "consumed_at": {
+ "name": "consumed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "connect_code_user_id_idx": {
+ "name": "connect_code_user_id_idx",
+ "columns": [
+ "user_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "connect_code_user_id_user_id_fk": {
+ "name": "connect_code_user_id_user_id_fk",
+ "tableFrom": "connect_code",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "connect_code_server_id_server_id_fk": {
+ "name": "connect_code_server_id_server_id_fk",
+ "tableFrom": "connect_code",
+ "tableTo": "server",
+ "columnsFrom": [
+ "server_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "label_claim": {
+ "name": "label_claim",
+ "columns": {
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "generation": {
+ "name": "generation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "label_claim_user_id_idx": {
+ "name": "label_claim_user_id_idx",
+ "columns": [
+ "user_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "label_claim_user_id_user_id_fk": {
+ "name": "label_claim_user_id_user_id_fk",
+ "tableFrom": "label_claim",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "machine": {
+ "name": "machine",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "subdomain": {
+ "name": "subdomain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "credential_hash": {
+ "name": "credential_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "machine_subdomain_unique": {
+ "name": "machine_subdomain_unique",
+ "columns": [
+ "subdomain"
+ ],
+ "isUnique": true
+ },
+ "machine_user_id_idx": {
+ "name": "machine_user_id_idx",
+ "columns": [
+ "user_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "machine_user_id_user_id_fk": {
+ "name": "machine_user_id_user_id_fk",
+ "tableFrom": "machine",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "profile": {
+ "name": "profile",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "handle": {
+ "name": "handle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "profile_handle_unique": {
+ "name": "profile_handle_unique",
+ "columns": [
+ "handle"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "profile_user_id_user_id_fk": {
+ "name": "profile_user_id_user_id_fk",
+ "tableFrom": "profile",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "server": {
+ "name": "server",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'default'"
+ },
+ "subdomain": {
+ "name": "subdomain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "credential_hash": {
+ "name": "credential_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "version": {
+ "name": "version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "server_subdomain_unique": {
+ "name": "server_subdomain_unique",
+ "columns": [
+ "subdomain"
+ ],
+ "isUnique": true
+ },
+ "server_user_name_idx": {
+ "name": "server_user_name_idx",
+ "columns": [
+ "user_id",
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "server_user_id_user_id_fk": {
+ "name": "server_user_id_user_id_fk",
+ "tableFrom": "server",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "session": {
+ "name": "session",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "columns": [
+ "token"
+ ],
+ "isUnique": true
+ },
+ "session_user_id_idx": {
+ "name": "session_user_id_idx",
+ "columns": [
+ "user_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "github_login": {
+ "name": "github_login",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "verification": {
+ "name": "verification",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "verification_identifier_idx": {
+ "name": "verification_identifier_idx",
+ "columns": [
+ "identifier"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/packages/connect-db/migrations/meta/_journal.json b/packages/connect-db/migrations/meta/_journal.json
index 1b9234becf..4c9416b652 100644
--- a/packages/connect-db/migrations/meta/_journal.json
+++ b/packages/connect-db/migrations/meta/_journal.json
@@ -43,6 +43,13 @@
"when": 1783989596863,
"tag": "0005_label_claim_triggers",
"breakpoints": true
+ },
+ {
+ "idx": 6,
+ "version": "6",
+ "when": 1785967822801,
+ "tag": "0006_ai_usage",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/connect-db/src/constants.ts b/packages/connect-db/src/constants.ts
index 7e8c168844..6d9ae9caa6 100644
--- a/packages/connect-db/src/constants.ts
+++ b/packages/connect-db/src/constants.ts
@@ -77,6 +77,21 @@ export const SERVER_OFFLINE_AFTER_MS = 90 * 1000;
/** Token prefixes (mirrors bb's host-key convention; distinct namespaces). */
export const CLOUD_PAT_PREFIX = "bbc_";
+/**
+ * bb Cloud AI proxy policy. The daily budget is an abuse ceiling, not a usage
+ * limit: it is sized an order of magnitude above heavy legitimate use (a big
+ * day of titles, commit messages, and voice notes costs well under $0.50) so
+ * no real user ever hits it, while bounding worst-case spend from a leaked
+ * credential or runaway loop. Usage is metered as estimated upstream cost in
+ * micro-USD; the per-request estimate rates live in the gate worker beside the
+ * model choices.
+ */
+export const AI_DAILY_BUDGET_MICROUSD = 2_000_000;
+export const AI_INFERENCE_MAX_PROMPT_BYTES = 64 * 1024;
+export const AI_INFERENCE_MAX_SCHEMA_BYTES = 16 * 1024;
+export const AI_TRANSCRIPTION_MAX_FILE_BYTES = 25 * 1024 * 1024;
+export const AI_TRANSCRIPTION_MAX_PROMPT_BYTES = 4 * 1024;
+
export type HandleValidationError =
| "too-short"
| "too-long"
@@ -127,8 +142,8 @@ export interface VisitorHost {
/**
* Resolve a visitor host to its handle and optional share target.
*
- * - `. ` → `{ handle, target: null }`
- * - `--. ` → `{ handle, target: port }` when port is a
+ * - `. [:listener-port]` → `{ handle, target: null }`
+ * - `--. [:listener-port]` → `{ handle, target: port }` when port is a
* valid decimal 1–65535 with no leading zeros
* - apex, multi-level labels, foreign domains, or invalid share labels → null
*
@@ -136,10 +151,29 @@ export interface VisitorHost {
* (prefix = handle, suffix = target). An invalid target makes the whole
* host unroutable (null), not a bare-handle fallback.
*/
-export function parseVisitorHost(host: string, baseDomain: string): VisitorHost | null {
- const suffix = `.${baseDomain}`;
- if (!host.endsWith(suffix)) return null;
- const label = host.slice(0, -suffix.length);
+export function parseVisitorHost(
+ host: string,
+ baseDomain: string,
+): VisitorHost | null {
+ let hostname: string;
+ try {
+ const parsed = new URL(`http://${host}`);
+ if (
+ parsed.username !== "" ||
+ parsed.password !== "" ||
+ parsed.pathname !== "/" ||
+ parsed.search !== "" ||
+ parsed.hash !== ""
+ ) {
+ return null;
+ }
+ hostname = parsed.hostname.toLowerCase();
+ } catch {
+ return null;
+ }
+ const suffix = `.${baseDomain.toLowerCase()}`;
+ if (!hostname.endsWith(suffix)) return null;
+ const label = hostname.slice(0, -suffix.length);
if (!label || label.includes(".")) return null;
const sep = label.indexOf("--");
diff --git a/packages/connect-db/src/schema.ts b/packages/connect-db/src/schema.ts
index b350ca3a7f..d82ba07466 100644
--- a/packages/connect-db/src/schema.ts
+++ b/packages/connect-db/src/schema.ts
@@ -14,6 +14,7 @@ import { sql } from "drizzle-orm";
import {
index,
integer,
+ primaryKey,
sqliteTable,
text,
uniqueIndex,
@@ -221,6 +222,25 @@ export const machine = sqliteTable(
(table) => [index("machine_user_id_idx").on(table.userId)],
);
+/**
+ * Daily bb Cloud AI usage per account, metered as estimated upstream cost in
+ * micro-USD (the abuse ceiling is dollars, so dollars are what we count). One
+ * row per user per UTC day (`day` is "YYYY-MM-DD"); rollover is implicit via
+ * the primary key, no sweeper. Metering only — request/response content is
+ * never stored.
+ */
+export const aiUsage = sqliteTable(
+ "ai_usage",
+ {
+ userId: text("user_id")
+ .notNull()
+ .references(() => user.id, { onDelete: "cascade" }),
+ day: text("day").notNull(),
+ costMicroUsd: integer("cost_micro_usd").notNull().default(0),
+ },
+ (table) => [primaryKey({ columns: [table.userId, table.day] })],
+);
+
/** Append-only audit of security-relevant events (connect, revoke, pair). */
export const auditLog = sqliteTable(
"audit_log",
@@ -254,5 +274,6 @@ export const schema = {
server,
machine,
connectCode,
+ aiUsage,
auditLog,
};
diff --git a/packages/connect-db/test/schema.test.ts b/packages/connect-db/test/schema.test.ts
index 0c524c3eaf..fc47626767 100644
--- a/packages/connect-db/test/schema.test.ts
+++ b/packages/connect-db/test/schema.test.ts
@@ -506,6 +506,10 @@ describe("parseVisitorHost", () => {
handle: "sawyer",
target: null,
});
+ expect(parseVisitorHost("sawyer.localhost:8791", "localhost")).toEqual({
+ handle: "sawyer",
+ target: null,
+ });
});
it("extracts handle--port share hosts", () => {
@@ -517,6 +521,9 @@ describe("parseVisitorHost", () => {
handle: "sawyer",
target: "5173",
});
+ expect(
+ parseVisitorHost("sawyer--5173.localhost:8791", "localhost"),
+ ).toEqual({ handle: "sawyer", target: "5173" });
});
it("rejects invalid share targets as unroutable", () => {
@@ -533,6 +540,9 @@ describe("parseVisitorHost", () => {
expect(parseVisitorHost("a.b.getbb.app", "getbb.app")).toBeNull();
expect(parseVisitorHost("evil.com", "getbb.app")).toBeNull();
expect(parseVisitorHost("getbb.app.evil.com", "getbb.app")).toBeNull();
+ expect(
+ parseVisitorHost("sawyer.localhost:not-a-port", "localhost"),
+ ).toBeNull();
});
});
diff --git a/packages/db/drizzle/0088_minor_juggernaut.sql b/packages/db/drizzle/0088_minor_juggernaut.sql
new file mode 100644
index 0000000000..db86b945e6
--- /dev/null
+++ b/packages/db/drizzle/0088_minor_juggernaut.sql
@@ -0,0 +1 @@
+ALTER TABLE `system_experiments` ADD `cloud_ai` integer DEFAULT false NOT NULL;
\ No newline at end of file
diff --git a/packages/db/drizzle/meta/0088_snapshot.json b/packages/db/drizzle/meta/0088_snapshot.json
new file mode 100644
index 0000000000..34395c263c
--- /dev/null
+++ b/packages/db/drizzle/meta/0088_snapshot.json
@@ -0,0 +1,3436 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "293c24ad-bd5d-41ad-bb55-0c114528b4be",
+ "prevId": "3a348e16-0592-4d7b-8f90-b6afcafffe48",
+ "tables": {
+ "app_settings": {
+ "name": "app_settings",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "caffeinate": {
+ "name": "caffeinate",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "show_keyboard_hints": {
+ "name": "show_keyboard_hints",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "steer_active_thread_on_enter": {
+ "name": "steer_active_thread_on_enter",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "show_unhandled_provider_events": {
+ "name": "show_unhandled_provider_events",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "codex_memory_enabled": {
+ "name": "codex_memory_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "claude_code_memory_enabled": {
+ "name": "claude_code_memory_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "codex_subagents_disabled": {
+ "name": "codex_subagents_disabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "claude_code_subagents_disabled": {
+ "name": "claude_code_subagents_disabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "claude_code_workflows_disabled": {
+ "name": "claude_code_workflows_disabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "keybinding_overrides": {
+ "name": "keybinding_overrides",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'[]'"
+ },
+ "onboarding_completed_at": {
+ "name": "onboarding_completed_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "app_theme": {
+ "name": "app_theme",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "theme_id": {
+ "name": "theme_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "favicon_color": {
+ "name": "favicon_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'default'"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "apikey": {
+ "name": "apikey",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "start": {
+ "name": "start",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "prefix": {
+ "name": "prefix",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "referenceId": {
+ "name": "referenceId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "refillInterval": {
+ "name": "refillInterval",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "refillAmount": {
+ "name": "refillAmount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lastRefillAt": {
+ "name": "lastRefillAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "rateLimitEnabled": {
+ "name": "rateLimitEnabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "rateLimitTimeWindow": {
+ "name": "rateLimitTimeWindow",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "rateLimitMax": {
+ "name": "rateLimitMax",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "requestCount": {
+ "name": "requestCount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "remaining": {
+ "name": "remaining",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lastRequest": {
+ "name": "lastRequest",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expiresAt": {
+ "name": "expiresAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permissions": {
+ "name": "permissions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "configId": {
+ "name": "configId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "apikey_key_unique": {
+ "name": "apikey_key_unique",
+ "columns": [
+ "key"
+ ],
+ "isUnique": true
+ },
+ "apikey_reference_id_idx": {
+ "name": "apikey_reference_id_idx",
+ "columns": [
+ "referenceId"
+ ],
+ "isUnique": false
+ },
+ "apikey_config_id_idx": {
+ "name": "apikey_config_id_idx",
+ "columns": [
+ "configId"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "apikey_referenceId_user_id_fk": {
+ "name": "apikey_referenceId_user_id_fk",
+ "tableFrom": "apikey",
+ "tableTo": "user",
+ "columnsFrom": [
+ "referenceId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "emailVerified": {
+ "name": "emailVerified",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "environments": {
+ "name": "environments",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "host_id": {
+ "name": "host_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "managed": {
+ "name": "managed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "is_git_repo": {
+ "name": "is_git_repo",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "is_worktree": {
+ "name": "is_worktree",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "branch_name": {
+ "name": "branch_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "base_branch": {
+ "name": "base_branch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "default_branch": {
+ "name": "default_branch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "merge_base_branch": {
+ "name": "merge_base_branch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "destroy_attempt_id": {
+ "name": "destroy_attempt_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "workspace_provision_type": {
+ "name": "workspace_provision_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'provisioning'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "environments_project_host_path_idx": {
+ "name": "environments_project_host_path_idx",
+ "columns": [
+ "project_id",
+ "host_id",
+ "path"
+ ],
+ "isUnique": true
+ },
+ "environments_host_path_lookup_idx": {
+ "name": "environments_host_path_lookup_idx",
+ "columns": [
+ "host_id",
+ "path"
+ ],
+ "isUnique": false
+ },
+ "environments_project_idx": {
+ "name": "environments_project_idx",
+ "columns": [
+ "project_id"
+ ],
+ "isUnique": false
+ },
+ "environments_status_idx": {
+ "name": "environments_status_idx",
+ "columns": [
+ "status"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "environments_project_id_projects_id_fk": {
+ "name": "environments_project_id_projects_id_fk",
+ "tableFrom": "environments",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "environments_host_id_hosts_id_fk": {
+ "name": "environments_host_id_hosts_id_fk",
+ "tableFrom": "environments",
+ "tableTo": "hosts",
+ "columnsFrom": [
+ "host_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "events": {
+ "name": "events",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "thread_id": {
+ "name": "thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "environment_id": {
+ "name": "environment_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "scope_kind": {
+ "name": "scope_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "turn_id": {
+ "name": "turn_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "provider_thread_id": {
+ "name": "provider_thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sequence": {
+ "name": "sequence",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "item_kind": {
+ "name": "item_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "data": {
+ "name": "data",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "events_thread_sequence_idx": {
+ "name": "events_thread_sequence_idx",
+ "columns": [
+ "thread_id",
+ "sequence"
+ ],
+ "isUnique": true
+ },
+ "events_thread_type_item_kind_sequence_idx": {
+ "name": "events_thread_type_item_kind_sequence_idx",
+ "columns": [
+ "thread_id",
+ "type",
+ "item_kind",
+ "sequence"
+ ],
+ "isUnique": false
+ },
+ "events_thread_type_sequence_idx": {
+ "name": "events_thread_type_sequence_idx",
+ "columns": [
+ "thread_id",
+ "type",
+ "sequence"
+ ],
+ "isUnique": false
+ },
+ "events_thread_turn_type_item_sequence_idx": {
+ "name": "events_thread_turn_type_item_sequence_idx",
+ "columns": [
+ "thread_id",
+ "turn_id",
+ "type",
+ "item_id",
+ "sequence"
+ ],
+ "isUnique": false
+ },
+ "events_environment_idx": {
+ "name": "events_environment_idx",
+ "columns": [
+ "environment_id"
+ ],
+ "isUnique": false
+ },
+ "events_completed_item_truncation_idx": {
+ "name": "events_completed_item_truncation_idx",
+ "columns": [
+ "item_kind",
+ "created_at",
+ "id"
+ ],
+ "isUnique": false,
+ "where": "\"events\".\"type\" = 'item/completed'"
+ }
+ },
+ "foreignKeys": {
+ "events_thread_id_threads_id_fk": {
+ "name": "events_thread_id_threads_id_fk",
+ "tableFrom": "events",
+ "tableTo": "threads",
+ "columnsFrom": [
+ "thread_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "events_environment_id_environments_id_fk": {
+ "name": "events_environment_id_environments_id_fk",
+ "tableFrom": "events",
+ "tableTo": "environments",
+ "columnsFrom": [
+ "environment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {
+ "events_scope_shape_check": {
+ "name": "events_scope_shape_check",
+ "value": "(\n (\"events\".\"scope_kind\" = 'turn' AND \"events\".\"turn_id\" IS NOT NULL)\n OR\n (\"events\".\"scope_kind\" = 'thread' AND \"events\".\"turn_id\" IS NULL)\n )"
+ }
+ }
+ },
+ "host_daemon_sessions": {
+ "name": "host_daemon_sessions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "host_id": {
+ "name": "host_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "instance_id": {
+ "name": "instance_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "host_name": {
+ "name": "host_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "host_type": {
+ "name": "host_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "data_dir": {
+ "name": "data_dir",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "protocol_version": {
+ "name": "protocol_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "heartbeat_interval_ms": {
+ "name": "heartbeat_interval_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "lease_timeout_ms": {
+ "name": "lease_timeout_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "lease_expires_at": {
+ "name": "lease_expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "closed_at": {
+ "name": "closed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "close_reason": {
+ "name": "close_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "host_daemon_sessions_host_status_idx": {
+ "name": "host_daemon_sessions_host_status_idx",
+ "columns": [
+ "host_id",
+ "status"
+ ],
+ "isUnique": false
+ },
+ "host_daemon_sessions_host_latest_idx": {
+ "name": "host_daemon_sessions_host_latest_idx",
+ "columns": [
+ "host_id",
+ "updated_at",
+ "created_at",
+ "id"
+ ],
+ "isUnique": false
+ },
+ "host_daemon_sessions_closed_prune_idx": {
+ "name": "host_daemon_sessions_closed_prune_idx",
+ "columns": [
+ "status",
+ "closed_at",
+ "id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "host_daemon_sessions_host_id_hosts_id_fk": {
+ "name": "host_daemon_sessions_host_id_hosts_id_fk",
+ "tableFrom": "host_daemon_sessions",
+ "tableTo": "hosts",
+ "columnsFrom": [
+ "host_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "hosts": {
+ "name": "hosts",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "connect_machine_id": {
+ "name": "connect_machine_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "max_permission_mode": {
+ "name": "max_permission_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'full'"
+ },
+ "destroyed_at": {
+ "name": "destroyed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_rejected_protocol_version": {
+ "name": "last_rejected_protocol_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "hosts_last_seen_idx": {
+ "name": "hosts_last_seen_idx",
+ "columns": [
+ "last_seen_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "plugins": {
+ "name": "plugins",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provenance": {
+ "name": "provenance",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'direct'"
+ },
+ "catalog_entry_id": {
+ "name": "catalog_entry_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_kind": {
+ "name": "source_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'path'"
+ },
+ "source_path": {
+ "name": "source_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_builtin_name": {
+ "name": "source_builtin_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_npm_package": {
+ "name": "source_npm_package",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_npm_registry": {
+ "name": "source_npm_registry",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_npm_requested_spec": {
+ "name": "source_npm_requested_spec",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_npm_spec_kind": {
+ "name": "source_npm_spec_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_git_url": {
+ "name": "source_git_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_git_subdirectory": {
+ "name": "source_git_subdirectory",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_git_requested_ref": {
+ "name": "source_git_requested_ref",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_git_ref_kind": {
+ "name": "source_git_ref_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "npm_resolved_version": {
+ "name": "npm_resolved_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "npm_integrity": {
+ "name": "npm_integrity",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "git_resolved_commit": {
+ "name": "git_resolved_commit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_update_check_at": {
+ "name": "last_update_check_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "available_compatible_version": {
+ "name": "available_compatible_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "newest_incompatible_version": {
+ "name": "newest_incompatible_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "update_status_detail": {
+ "name": "update_status_detail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_failure_version": {
+ "name": "last_failure_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_failure_at": {
+ "name": "last_failure_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_failure_detail": {
+ "name": "last_failure_detail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "active_artifact_id": {
+ "name": "active_artifact_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "normalization_version": {
+ "name": "normalization_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "root_dir": {
+ "name": "root_dir",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "version": {
+ "name": "version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "removed_at": {
+ "name": "removed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "installed_at": {
+ "name": "installed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "plugins_active_artifact_id_plugin_artifacts_id_fk": {
+ "name": "plugins_active_artifact_id_plugin_artifacts_id_fk",
+ "tableFrom": "plugins",
+ "tableTo": "plugin_artifacts",
+ "columnsFrom": [
+ "active_artifact_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "maintenance_scan_cursors": {
+ "name": "maintenance_scan_cursors",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "policy": {
+ "name": "policy",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "item_kind": {
+ "name": "item_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "output_path": {
+ "name": "output_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "last_created_at": {
+ "name": "last_created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "last_event_id": {
+ "name": "last_event_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "maintenance_scan_cursors_path_idx": {
+ "name": "maintenance_scan_cursors_path_idx",
+ "columns": [
+ "policy",
+ "version",
+ "item_kind",
+ "output_path"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "pending_interactions": {
+ "name": "pending_interactions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "thread_id": {
+ "name": "thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "origin_kind": {
+ "name": "origin_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'provider'"
+ },
+ "turn_id": {
+ "name": "turn_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "provider_thread_id": {
+ "name": "provider_thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "provider_request_id": {
+ "name": "provider_request_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "renderer_id": {
+ "name": "renderer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "payload": {
+ "name": "payload",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "resolution": {
+ "name": "resolution",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status_reason": {
+ "name": "status_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "resolved_at": {
+ "name": "resolved_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "pending_interactions_provider_request_idx": {
+ "name": "pending_interactions_provider_request_idx",
+ "columns": [
+ "provider_id",
+ "provider_thread_id",
+ "provider_request_id"
+ ],
+ "isUnique": true
+ },
+ "pending_interactions_thread_created_idx": {
+ "name": "pending_interactions_thread_created_idx",
+ "columns": [
+ "thread_id",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "pending_interactions_thread_status_created_idx": {
+ "name": "pending_interactions_thread_status_created_idx",
+ "columns": [
+ "thread_id",
+ "status",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "pending_interactions_status_created_idx": {
+ "name": "pending_interactions_status_created_idx",
+ "columns": [
+ "status",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "pending_interactions_plugin_status_created_idx": {
+ "name": "pending_interactions_plugin_status_created_idx",
+ "columns": [
+ "plugin_id",
+ "status",
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "pending_interactions_thread_id_threads_id_fk": {
+ "name": "pending_interactions_thread_id_threads_id_fk",
+ "tableFrom": "pending_interactions",
+ "tableTo": "threads",
+ "columnsFrom": [
+ "thread_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "plugin_artifacts": {
+ "name": "plugin_artifacts",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_kind": {
+ "name": "source_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "npm_resolved_version": {
+ "name": "npm_resolved_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "git_resolved_commit": {
+ "name": "git_resolved_commit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "integrity": {
+ "name": "integrity",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "content_hash": {
+ "name": "content_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "validation_result": {
+ "name": "validation_result",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "validated_at": {
+ "name": "validated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "plugin_artifacts_plugin_idx": {
+ "name": "plugin_artifacts_plugin_idx",
+ "columns": [
+ "plugin_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "plugin_kv": {
+ "name": "plugin_kv",
+ "columns": {
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "plugin_kv_plugin_id_key_pk": {
+ "columns": [
+ "plugin_id",
+ "key"
+ ],
+ "name": "plugin_kv_plugin_id_key_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "plugin_schedules": {
+ "name": "plugin_schedules",
+ "columns": {
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "cron": {
+ "name": "cron",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "next_run_at": {
+ "name": "next_run_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "last_run_at": {
+ "name": "last_run_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_status": {
+ "name": "last_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "plugin_schedules_plugin_id_name_pk": {
+ "columns": [
+ "plugin_id",
+ "name"
+ ],
+ "name": "plugin_schedules_plugin_id_name_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "plugin_settings": {
+ "name": "plugin_settings",
+ "columns": {
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "plugin_settings_plugin_id_key_pk": {
+ "columns": [
+ "plugin_id",
+ "key"
+ ],
+ "name": "plugin_settings_plugin_id_key_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "plugin_state_snapshots": {
+ "name": "plugin_state_snapshots",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "from_artifact_id": {
+ "name": "from_artifact_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "to_artifact_id": {
+ "name": "to_artifact_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "snapshot_path": {
+ "name": "snapshot_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "database_path": {
+ "name": "database_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "state_path": {
+ "name": "state_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "secrets_path": {
+ "name": "secrets_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "registration_path": {
+ "name": "registration_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "rollback_candidate_version": {
+ "name": "rollback_candidate_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "rollback_source_fingerprint": {
+ "name": "rollback_source_fingerprint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "rollback_bb_version": {
+ "name": "rollback_bb_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "rollback_sdk_version": {
+ "name": "rollback_sdk_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "rollback_detail": {
+ "name": "rollback_detail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "retained_until": {
+ "name": "retained_until",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "plugin_state_snapshots_plugin_idx": {
+ "name": "plugin_state_snapshots_plugin_idx",
+ "columns": [
+ "plugin_id"
+ ],
+ "isUnique": false
+ },
+ "plugin_state_snapshots_retention_idx": {
+ "name": "plugin_state_snapshots_retention_idx",
+ "columns": [
+ "retained_until"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "project_execution_defaults": {
+ "name": "project_execution_defaults",
+ "columns": {
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "service_tier": {
+ "name": "service_tier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "reasoning_level": {
+ "name": "reasoning_level",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permission_mode": {
+ "name": "permission_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "project_execution_defaults_project_idx": {
+ "name": "project_execution_defaults_project_idx",
+ "columns": [
+ "project_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "project_execution_defaults_project_id_projects_id_fk": {
+ "name": "project_execution_defaults_project_id_projects_id_fk",
+ "tableFrom": "project_execution_defaults",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "project_sources": {
+ "name": "project_sources",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "host_id": {
+ "name": "host_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "project_sources_project_idx": {
+ "name": "project_sources_project_idx",
+ "columns": [
+ "project_id"
+ ],
+ "isUnique": false
+ },
+ "project_sources_host_idx": {
+ "name": "project_sources_host_idx",
+ "columns": [
+ "host_id"
+ ],
+ "isUnique": false
+ },
+ "project_sources_project_host_idx": {
+ "name": "project_sources_project_host_idx",
+ "columns": [
+ "project_id",
+ "host_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "project_sources_project_id_projects_id_fk": {
+ "name": "project_sources_project_id_projects_id_fk",
+ "tableFrom": "project_sources",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "project_sources_host_id_hosts_id_fk": {
+ "name": "project_sources_host_id_hosts_id_fk",
+ "tableFrom": "project_sources",
+ "tableTo": "hosts",
+ "columnsFrom": [
+ "host_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {
+ "project_sources_shape_check": {
+ "name": "project_sources_shape_check",
+ "value": "(\n \"project_sources\".\"type\" = 'local_path' AND \"project_sources\".\"host_id\" IS NOT NULL AND \"project_sources\".\"path\" IS NOT NULL\n )"
+ }
+ }
+ },
+ "projects": {
+ "name": "projects",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'standard'"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "git_remote_url": {
+ "name": "git_remote_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sort_key": {
+ "name": "sort_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'V'"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "projects_updated_idx": {
+ "name": "projects_updated_idx",
+ "columns": [
+ "updated_at"
+ ],
+ "isUnique": false
+ },
+ "projects_deleted_idx": {
+ "name": "projects_deleted_idx",
+ "columns": [
+ "deleted_at"
+ ],
+ "isUnique": false
+ },
+ "projects_sort_idx": {
+ "name": "projects_sort_idx",
+ "columns": [
+ "sort_key",
+ "id"
+ ],
+ "isUnique": false
+ },
+ "projects_personal_singleton_idx": {
+ "name": "projects_personal_singleton_idx",
+ "columns": [
+ "kind"
+ ],
+ "isUnique": true,
+ "where": "\"projects\".\"kind\" = 'personal'"
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "prompt_history_entries": {
+ "name": "prompt_history_entries",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "thread_id": {
+ "name": "thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "request_sequence": {
+ "name": "request_sequence",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "input": {
+ "name": "input",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "prompt_history_entries_thread_request_idx": {
+ "name": "prompt_history_entries_thread_request_idx",
+ "columns": [
+ "thread_id",
+ "request_sequence"
+ ],
+ "isUnique": true
+ },
+ "prompt_history_entries_project_scope_created_idx": {
+ "name": "prompt_history_entries_project_scope_created_idx",
+ "columns": [
+ "project_id",
+ "scope",
+ "created_at",
+ "request_sequence",
+ "id"
+ ],
+ "isUnique": false
+ },
+ "prompt_history_entries_thread_scope_created_idx": {
+ "name": "prompt_history_entries_thread_scope_created_idx",
+ "columns": [
+ "thread_id",
+ "scope",
+ "created_at",
+ "request_sequence",
+ "id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "prompt_history_entries_project_id_projects_id_fk": {
+ "name": "prompt_history_entries_project_id_projects_id_fk",
+ "tableFrom": "prompt_history_entries",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "prompt_history_entries_thread_id_threads_id_fk": {
+ "name": "prompt_history_entries_thread_id_threads_id_fk",
+ "tableFrom": "prompt_history_entries",
+ "tableTo": "threads",
+ "columnsFrom": [
+ "thread_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "queued_thread_messages": {
+ "name": "queued_thread_messages",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "thread_id": {
+ "name": "thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "sender_thread_id": {
+ "name": "sender_thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "reasoning_level": {
+ "name": "reasoning_level",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permission_mode": {
+ "name": "permission_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "service_tier": {
+ "name": "service_tier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "group_with_next": {
+ "name": "group_with_next",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "claimed_at": {
+ "name": "claimed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "claim_token": {
+ "name": "claim_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sort_key": {
+ "name": "sort_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "queued_thread_messages_thread_created_idx": {
+ "name": "queued_thread_messages_thread_created_idx",
+ "columns": [
+ "thread_id",
+ "created_at",
+ "id"
+ ],
+ "isUnique": false
+ },
+ "queued_thread_messages_thread_sort_idx": {
+ "name": "queued_thread_messages_thread_sort_idx",
+ "columns": [
+ "thread_id",
+ "sort_key",
+ "id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "queued_thread_messages_thread_id_threads_id_fk": {
+ "name": "queued_thread_messages_thread_id_threads_id_fk",
+ "tableFrom": "queued_thread_messages",
+ "tableTo": "threads",
+ "columnsFrom": [
+ "thread_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "system_experiments": {
+ "name": "system_experiments",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "claude_code_mock_cli_traffic": {
+ "name": "claude_code_mock_cli_traffic",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "cloud_ai": {
+ "name": "cloud_ai",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "new_onboarding": {
+ "name": "new_onboarding",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "tools_hub": {
+ "name": "tools_hub",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "terminal_sessions": {
+ "name": "terminal_sessions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "thread_id": {
+ "name": "thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "environment_id": {
+ "name": "environment_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "host_id": {
+ "name": "host_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "daemon_session_id": {
+ "name": "daemon_session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "initial_cwd": {
+ "name": "initial_cwd",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "cols": {
+ "name": "cols",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "rows": {
+ "name": "rows",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "exit_code": {
+ "name": "exit_code",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "close_reason": {
+ "name": "close_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "last_user_input_at": {
+ "name": "last_user_input_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "terminal_sessions_thread_status_updated_idx": {
+ "name": "terminal_sessions_thread_status_updated_idx",
+ "columns": [
+ "thread_id",
+ "status",
+ "updated_at"
+ ],
+ "isUnique": false
+ },
+ "terminal_sessions_environment_status_idx": {
+ "name": "terminal_sessions_environment_status_idx",
+ "columns": [
+ "environment_id",
+ "status"
+ ],
+ "isUnique": false
+ },
+ "terminal_sessions_host_status_idx": {
+ "name": "terminal_sessions_host_status_idx",
+ "columns": [
+ "host_id",
+ "status"
+ ],
+ "isUnique": false
+ },
+ "terminal_sessions_daemon_session_idx": {
+ "name": "terminal_sessions_daemon_session_idx",
+ "columns": [
+ "daemon_session_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "terminal_sessions_thread_id_threads_id_fk": {
+ "name": "terminal_sessions_thread_id_threads_id_fk",
+ "tableFrom": "terminal_sessions",
+ "tableTo": "threads",
+ "columnsFrom": [
+ "thread_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "terminal_sessions_environment_id_environments_id_fk": {
+ "name": "terminal_sessions_environment_id_environments_id_fk",
+ "tableFrom": "terminal_sessions",
+ "tableTo": "environments",
+ "columnsFrom": [
+ "environment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "terminal_sessions_host_id_hosts_id_fk": {
+ "name": "terminal_sessions_host_id_hosts_id_fk",
+ "tableFrom": "terminal_sessions",
+ "tableTo": "hosts",
+ "columnsFrom": [
+ "host_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "terminal_sessions_daemon_session_id_host_daemon_sessions_id_fk": {
+ "name": "terminal_sessions_daemon_session_id_host_daemon_sessions_id_fk",
+ "tableFrom": "terminal_sessions",
+ "tableTo": "host_daemon_sessions",
+ "columnsFrom": [
+ "daemon_session_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "thread_dynamic_context_file_states": {
+ "name": "thread_dynamic_context_file_states",
+ "columns": {
+ "thread_id": {
+ "name": "thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "file_key": {
+ "name": "file_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content_status": {
+ "name": "content_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content_hash": {
+ "name": "content_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "shown_at": {
+ "name": "shown_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "thread_dynamic_context_file_states_thread_file_idx": {
+ "name": "thread_dynamic_context_file_states_thread_file_idx",
+ "columns": [
+ "thread_id",
+ "file_key"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "thread_dynamic_context_file_states_thread_id_threads_id_fk": {
+ "name": "thread_dynamic_context_file_states_thread_id_threads_id_fk",
+ "tableFrom": "thread_dynamic_context_file_states",
+ "tableTo": "threads",
+ "columnsFrom": [
+ "thread_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "thread_search_segments": {
+ "name": "thread_search_segments",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "thread_id": {
+ "name": "thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_kind": {
+ "name": "source_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_key": {
+ "name": "source_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_seq": {
+ "name": "source_seq",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "text": {
+ "name": "text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "thread_search_segments_source_idx": {
+ "name": "thread_search_segments_source_idx",
+ "columns": [
+ "thread_id",
+ "source_kind",
+ "source_key"
+ ],
+ "isUnique": true
+ },
+ "thread_search_segments_thread_idx": {
+ "name": "thread_search_segments_thread_idx",
+ "columns": [
+ "thread_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "thread_search_segments_thread_id_threads_id_fk": {
+ "name": "thread_search_segments_thread_id_threads_id_fk",
+ "tableFrom": "thread_search_segments",
+ "tableTo": "threads",
+ "columnsFrom": [
+ "thread_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "thread_sections": {
+ "name": "thread_sections",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "thread_sections_name_idx": {
+ "name": "thread_sections_name_idx",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "thread_tabs": {
+ "name": "thread_tabs",
+ "columns": {
+ "thread_id": {
+ "name": "thread_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "tabs_json": {
+ "name": "tabs_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "thread_tabs_thread_id_threads_id_fk": {
+ "name": "thread_tabs_thread_id_threads_id_fk",
+ "tableFrom": "thread_tabs",
+ "tableTo": "threads",
+ "columnsFrom": [
+ "thread_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "threads": {
+ "name": "threads",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "environment_id": {
+ "name": "environment_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "model_override": {
+ "name": "model_override",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reasoning_level_override": {
+ "name": "reasoning_level_override",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "title_fallback": {
+ "name": "title_fallback",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "section_id": {
+ "name": "section_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'starting'"
+ },
+ "parent_thread_id": {
+ "name": "parent_thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_thread_id": {
+ "name": "source_thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "origin_kind": {
+ "name": "origin_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "child_origin": {
+ "name": "child_origin",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "origin_plugin_id": {
+ "name": "origin_plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "visibility": {
+ "name": "visibility",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'visible'"
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "pinned_at": {
+ "name": "pinned_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "pin_sort_key": {
+ "name": "pin_sort_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_read_at": {
+ "name": "last_read_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "latest_attention_at": {
+ "name": "latest_attention_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "threads_project_updated_idx": {
+ "name": "threads_project_updated_idx",
+ "columns": [
+ "project_id",
+ "updated_at"
+ ],
+ "isUnique": false
+ },
+ "threads_project_archived_deleted_idx": {
+ "name": "threads_project_archived_deleted_idx",
+ "columns": [
+ "project_id",
+ "archived_at",
+ "deleted_at",
+ "id"
+ ],
+ "isUnique": false
+ },
+ "threads_pin_sort_idx": {
+ "name": "threads_pin_sort_idx",
+ "columns": [
+ "archived_at",
+ "deleted_at",
+ "pin_sort_key",
+ "id"
+ ],
+ "isUnique": false,
+ "where": "\"threads\".\"pinned_at\" IS NOT NULL"
+ },
+ "threads_environment_idx": {
+ "name": "threads_environment_idx",
+ "columns": [
+ "environment_id"
+ ],
+ "isUnique": false
+ },
+ "threads_parent_idx": {
+ "name": "threads_parent_idx",
+ "columns": [
+ "parent_thread_id"
+ ],
+ "isUnique": false
+ },
+ "threads_source_origin_idx": {
+ "name": "threads_source_origin_idx",
+ "columns": [
+ "source_thread_id",
+ "origin_kind"
+ ],
+ "isUnique": false
+ },
+ "threads_origin_plugin_archived_idx": {
+ "name": "threads_origin_plugin_archived_idx",
+ "columns": [
+ "origin_plugin_id",
+ "archived_at"
+ ],
+ "isUnique": false
+ },
+ "threads_section_archived_deleted_idx": {
+ "name": "threads_section_archived_deleted_idx",
+ "columns": [
+ "section_id",
+ "archived_at",
+ "deleted_at",
+ "id"
+ ],
+ "isUnique": false
+ },
+ "threads_archived_status_idx": {
+ "name": "threads_archived_status_idx",
+ "columns": [
+ "archived_at",
+ "status"
+ ],
+ "isUnique": false
+ },
+ "threads_environment_archived_deleted_idx": {
+ "name": "threads_environment_archived_deleted_idx",
+ "columns": [
+ "environment_id",
+ "archived_at",
+ "deleted_at"
+ ],
+ "isUnique": false
+ },
+ "threads_active_maintenance_idx": {
+ "name": "threads_active_maintenance_idx",
+ "columns": [
+ "status"
+ ],
+ "isUnique": false,
+ "where": "\"threads\".\"deleted_at\" IS NULL"
+ }
+ },
+ "foreignKeys": {
+ "threads_project_id_projects_id_fk": {
+ "name": "threads_project_id_projects_id_fk",
+ "tableFrom": "threads",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "threads_environment_id_environments_id_fk": {
+ "name": "threads_environment_id_environments_id_fk",
+ "tableFrom": "threads",
+ "tableTo": "environments",
+ "columnsFrom": [
+ "environment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "threads_section_id_thread_sections_id_fk": {
+ "name": "threads_section_id_thread_sections_id_fk",
+ "tableFrom": "threads",
+ "tableTo": "thread_sections",
+ "columnsFrom": [
+ "section_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "threads_parent_thread_id_threads_id_fk": {
+ "name": "threads_parent_thread_id_threads_id_fk",
+ "tableFrom": "threads",
+ "tableTo": "threads",
+ "columnsFrom": [
+ "parent_thread_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "threads_source_thread_id_threads_id_fk": {
+ "name": "threads_source_thread_id_threads_id_fk",
+ "tableFrom": "threads",
+ "tableTo": "threads",
+ "columnsFrom": [
+ "source_thread_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json
index fb688482de..b3119e72ba 100644
--- a/packages/db/drizzle/meta/_journal.json
+++ b/packages/db/drizzle/meta/_journal.json
@@ -617,6 +617,13 @@
"when": 1786043536668,
"tag": "0087_brief_khan",
"breakpoints": true
+ },
+ {
+ "idx": 88,
+ "version": "6",
+ "when": 1786124160418,
+ "tag": "0088_minor_juggernaut",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/db/src/data/experiments.ts b/packages/db/src/data/experiments.ts
index 13a3262ff6..5f21bbfa27 100644
--- a/packages/db/src/data/experiments.ts
+++ b/packages/db/src/data/experiments.ts
@@ -9,6 +9,7 @@ export function getExperiments(db: DbConnection): Experiments {
const row = db
.select({
claudeCodeMockCliTraffic: systemExperiments.claudeCodeMockCliTraffic,
+ cloudAi: systemExperiments.cloudAi,
newOnboarding: systemExperiments.newOnboarding,
toolsHub: systemExperiments.toolsHub,
})
@@ -28,6 +29,7 @@ export function setExperiments(
.values({
id: SYSTEM_EXPERIMENTS_ROW_ID,
claudeCodeMockCliTraffic: experiments.claudeCodeMockCliTraffic,
+ cloudAi: experiments.cloudAi,
newOnboarding: experiments.newOnboarding,
toolsHub: experiments.toolsHub,
updatedAt,
@@ -36,6 +38,7 @@ export function setExperiments(
target: systemExperiments.id,
set: {
claudeCodeMockCliTraffic: experiments.claudeCodeMockCliTraffic,
+ cloudAi: experiments.cloudAi,
newOnboarding: experiments.newOnboarding,
toolsHub: experiments.toolsHub,
updatedAt,
diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts
index 6f066ccff3..37152da36e 100644
--- a/packages/db/src/schema.ts
+++ b/packages/db/src/schema.ts
@@ -152,6 +152,7 @@ export const systemExperiments = sqliteTable("system_experiments", {
claudeCodeMockCliTraffic: integer("claude_code_mock_cli_traffic", {
mode: "boolean",
}).notNull(),
+ cloudAi: integer("cloud_ai", { mode: "boolean" }).notNull().default(false),
newOnboarding: integer("new_onboarding", { mode: "boolean" })
.notNull()
.default(false),
diff --git a/packages/db/test/migrate.test.ts b/packages/db/test/migrate.test.ts
index b615bbe667..49ac9dc962 100644
--- a/packages/db/test/migrate.test.ts
+++ b/packages/db/test/migrate.test.ts
@@ -294,6 +294,7 @@ function dropRewindAddedTables(db: DbConnection): void {
dropSideChatPluginExperimentColumn(db);
dropToolsHubExperimentColumn(db);
dropNewOnboardingExperimentColumn(db);
+ dropCloudAiExperimentColumn(db);
dropSteerActiveThreadOnEnterColumn(db);
dropOnboardingCompletedAtColumn(db);
// Thread visibility was added after the legacy checkpoints these tests
@@ -475,6 +476,19 @@ function dropNewOnboardingExperimentColumn(db: DbConnection): void {
}
}
+// Migration 0088 adds the Cloud AI experiment column. Rewind scenarios that
+// clear its migration row must drop the column before replay.
+function dropCloudAiExperimentColumn(db: DbConnection): void {
+ const columns = db.$client
+ .prepare<[], TableInfoRow>("PRAGMA table_info(system_experiments)")
+ .all();
+ if (columns.some((column) => column.name === "cloud_ai")) {
+ db.$client
+ .prepare("ALTER TABLE system_experiments DROP COLUMN cloud_ai")
+ .run();
+ }
+}
+
// Migration 0083 adds the machine permission ceiling. Rewind scenarios that
// clear its migration row must drop the column before replay.
function dropHostMaxPermissionModeColumn(db: DbConnection): void {
@@ -1224,6 +1238,7 @@ describe("migrate", () => {
// exactly what an upgrading user's database looks like.
dropOnboardingCompletedAtColumn(db);
dropNewOnboardingExperimentColumn(db);
+ dropCloudAiExperimentColumn(db);
// Delete by the journal timestamp, not a hash substring: migration hashes
// are hex and can contain "0085" by coincidence.
db.$client
@@ -1510,6 +1525,7 @@ describe("migrate", () => {
dropSteerActiveThreadOnEnterColumn(db);
dropOnboardingCompletedAtColumn(db);
dropNewOnboardingExperimentColumn(db);
+ dropCloudAiExperimentColumn(db);
dropHostMaxPermissionModeColumn(db);
migrate(db);
@@ -1907,6 +1923,7 @@ describe("migrate", () => {
dropSteerActiveThreadOnEnterColumn(db);
dropOnboardingCompletedAtColumn(db);
dropNewOnboardingExperimentColumn(db);
+ dropCloudAiExperimentColumn(db);
dropHostMaxPermissionModeColumn(db);
expect(
@@ -2001,6 +2018,7 @@ describe("migrate", () => {
dropSteerActiveThreadOnEnterColumn(db);
dropOnboardingCompletedAtColumn(db);
dropNewOnboardingExperimentColumn(db);
+ dropCloudAiExperimentColumn(db);
dropHostMaxPermissionModeColumn(db);
expect(() => migrate(db)).not.toThrow();
diff --git a/packages/domain/src/experiments.ts b/packages/domain/src/experiments.ts
index 33711fc0cb..b5e4cc0f1f 100644
--- a/packages/domain/src/experiments.ts
+++ b/packages/domain/src/experiments.ts
@@ -14,6 +14,11 @@ export const experimentsSchema = z.object({
* local proxy so forwarded requests use CLI-shaped traffic.
*/
claudeCodeMockCliTraffic: z.boolean(),
+ /**
+ * Cloud AI: permits paired cloud providers to serve server-owned AI
+ * features such as metadata inference and voice transcription.
+ */
+ cloudAi: z.boolean(),
/**
* New onboarding: shows the first-run agent and project setup guide.
*/
@@ -29,6 +34,7 @@ export type Experiments = z.infer;
export const defaultExperiments: Experiments = {
claudeCodeMockCliTraffic: false,
+ cloudAi: false,
newOnboarding: false,
toolsHub: false,
};
diff --git a/packages/plugin-registry/r/icon.json b/packages/plugin-registry/r/icon.json
index 02e5c11d5c..92f40c68cc 100644
--- a/packages/plugin-registry/r/icon.json
+++ b/packages/plugin-registry/r/icon.json
@@ -14,7 +14,7 @@
"files": [
{
"path": "registry/components/ui/icon.tsx",
- "content": "import { HugeiconsIcon, type IconSvgElement } from \"@hugeicons/react\";\nimport {\n AiContentGenerator01Icon,\n Alert02Icon,\n AlertCircleIcon,\n Archive03Icon,\n ArrowDown01Icon,\n ArrowDown02Icon,\n ArrowDownDoubleIcon,\n ArrowLeft01Icon,\n ArrowMoveDownLeftIcon,\n ArrowMoveDownRightIcon,\n ArrowRight01Icon,\n ArrowRight02Icon,\n ArrowReloadHorizontalIcon,\n ArrowUp01Icon,\n ArrowUp02Icon,\n ArrowUpDoubleIcon,\n ArrowUpDownIcon,\n ArrowTurnBackwardIcon,\n ArrowTurnForwardIcon,\n ArrowUpRight01Icon,\n AttachmentIcon,\n AudioWaveIcon,\n BrowserIcon,\n BubbleChatAddIcon,\n BubbleChatIcon,\n BubbleChatQuestionIcon,\n Calendar03Icon,\n CalendarCheckOut02Icon,\n CalendarSyncIcon,\n Cancel01Icon,\n CancelCircleIcon,\n ChatFeedback01Icon,\n Bug01Icon,\n ChartColumnIcon,\n CheckListIcon,\n CheckmarkCircle02Icon,\n CircleArrowShrink01Icon,\n CircleIcon,\n Clock01Icon,\n CloudIcon,\n CollapseIcon,\n Book02Icon,\n BrainIcon,\n ComputerTerminal01Icon,\n Copy01Icon,\n DashedLine02Icon,\n DashedLineCircleIcon,\n DateTimeIcon,\n Delete02Icon,\n Download01Icon,\n DiscordIcon,\n GithubIcon,\n DragDropHorizontalIcon,\n DragDropVerticalIcon,\n Edit02Icon,\n Edit04Icon,\n ElectricPlugsIcon,\n ExpandIcon,\n ViewIcon,\n ViewOffIcon,\n File01Icon,\n FileAttachmentIcon,\n FileEmpty02Icon,\n FileQuestionMarkIcon,\n FileXIcon,\n Folder02Icon,\n FolderAddIcon,\n FolderGitTwoIcon,\n FolderIcon,\n FolderRemoveIcon,\n GitBranchIcon,\n GitForkIcon,\n GitMergeIcon,\n GitPullRequestArrow,\n GitPullRequestClosedIcon,\n GitPullRequestDraftIcon,\n GitPullRequestIcon,\n GridViewIcon,\n HelpCircleIcon,\n InformationCircleIcon,\n InternetIcon,\n LaptopIcon,\n Layers01Icon,\n ListViewIcon,\n LockIcon,\n Loading03Icon,\n LayoutThreeRowIcon,\n LayoutTwoColumnIcon,\n LayoutTwoRowIcon,\n LinkSquare02Icon,\n Mail02Icon,\n MailOpen01Icon,\n Menu02Icon,\n MessageAdd02Icon,\n MessageQuestionIcon,\n Mic02Icon,\n MoreHorizontalIcon,\n PackageReceiveIcon,\n PauseIcon,\n PinIcon,\n PinOffIcon,\n PlayIcon,\n PlusMinusSquare01Icon,\n PlusSignIcon,\n PuzzleIcon,\n Refresh01Icon,\n RepeatIcon,\n Search01Icon,\n SentIcon,\n Settings01Icon,\n SidebarBottomIcon,\n SidebarLeftIcon,\n SidebarRightIcon,\n SlidersHorizontalIcon,\n SmartPhone01Icon,\n Sorting01Icon,\n SourceCodeIcon,\n SquareIcon,\n StarIcon,\n Target02Icon,\n TestTube01Icon,\n TextWrapIcon,\n Tick02Icon,\n TimeScheduleIcon,\n ToolboxIcon,\n Unarchive03Icon,\n UserAdd01Icon,\n UserIcon,\n WorkflowCircle03Icon,\n ZapIcon,\n ZoomInAreaIcon,\n ZoomOutAreaIcon,\n} from \"@hugeicons/core-free-icons\";\nimport { cn } from \"../../lib/utils\";\n\n// The free hugeicons set ships no artist-palette glyph (its `Palette` export\n// is a pen nib), so this inlines the stroke-rounded palette artwork in the\n// same element format the set uses.\nconst PaletteStrokeRoundedIcon: IconSvgElement = [\n [\n \"path\",\n {\n d: \"M21.8205 10.4127C22.062 11.8519 22.1827 12.5715 21.2423 13.9326C21.1459 14.0722 20.8966 14.3713 20.777 14.4911C19.6103 15.6586 18.4308 15.6586 16.0716 15.6586H14.1392C13.5085 15.6586 13.1931 15.6586 12.9639 15.7142C11.9586 15.9581 11.3031 16.9391 11.453 17.9755C11.4872 18.2118 11.6043 18.5085 11.8386 19.102C11.9345 19.3449 11.9824 19.4664 12.0136 19.7304C12.1292 20.7084 11.0869 21.9508 10.1158 21.9926C9.85358 22.0039 9.83681 22.0002 9.80326 21.9926C7.66174 21.51 5.66204 20.3123 4.18389 18.4421C0.736789 14.0808 1.43146 7.71364 5.73548 4.22064C10.0395 0.727643 16.323 1.43156 19.7701 5.79289C20.868 7.1819 21.5457 8.77438 21.8205 10.4127Z\",\n fill: \"none\",\n fillRule: \"evenodd\",\n clipRule: \"evenodd\",\n stroke: \"currentColor\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"0\",\n },\n ],\n [\n \"path\",\n {\n d: \"M7.36719 7.74976H7.24219M7.49219 7.74976C7.49219 7.88783 7.38026 7.99976 7.24219 7.99976C7.10412 7.99976 6.99219 7.88783 6.99219 7.74976C6.99219 7.61169 7.10412 7.49976 7.24219 7.49976C7.38026 7.49976 7.49219 7.61169 7.49219 7.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"1\",\n },\n ],\n [\n \"path\",\n {\n d: \"M7.36719 15.7498H7.24219M7.49219 15.7498C7.49219 15.8878 7.38026 15.9998 7.24219 15.9998C7.10412 15.9998 6.99219 15.8878 6.99219 15.7498C6.99219 15.6117 7.10412 15.4998 7.24219 15.4998C7.38026 15.4998 7.49219 15.6117 7.49219 15.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"2\",\n },\n ],\n [\n \"path\",\n {\n d: \"M11.8672 5.74976H11.7422M11.9922 5.74976C11.9922 5.88783 11.8803 5.99976 11.7422 5.99976C11.6041 5.99976 11.4922 5.88783 11.4922 5.74976C11.4922 5.61169 11.6041 5.49976 11.7422 5.49976C11.8803 5.49976 11.9922 5.61169 11.9922 5.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"3\",\n },\n ],\n [\n \"path\",\n {\n d: \"M16.3672 7.74976H16.2422M16.4922 7.74976C16.4922 7.88783 16.3803 7.99976 16.2422 7.99976C16.1041 7.99976 15.9922 7.88783 15.9922 7.74976C15.9922 7.61169 16.1041 7.49976 16.2422 7.49976C16.3803 7.49976 16.4922 7.61169 16.4922 7.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"4\",\n },\n ],\n [\n \"path\",\n {\n d: \"M18.3672 11.7498H18.2422M18.4922 11.7498C18.4922 11.8878 18.3803 11.9998 18.2422 11.9998C18.1041 11.9998 17.9922 11.8878 17.9922 11.7498C17.9922 11.6117 18.1041 11.4998 18.2422 11.4998C18.3803 11.4998 18.4922 11.6117 18.4922 11.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"5\",\n },\n ],\n [\n \"path\",\n {\n d: \"M5.86719 11.7498H5.74219M5.99219 11.7498C5.99219 11.8878 5.88026 11.9998 5.74219 11.9998C5.60412 11.9998 5.49219 11.8878 5.49219 11.7498C5.49219 11.6117 5.60412 11.4998 5.74219 11.4998C5.88026 11.4998 5.99219 11.6117 5.99219 11.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"6\",\n },\n ],\n];\n\n// Custom \"new section\" glyph: the set's ListView rows with the middle and\n// bottom rows shortened so the plus owns the lower-right quadrant, matching\n// FolderAdd's non-overlapping plus placement (same plus geometry). Hugeicons\n// has no list-with-plus variant that keeps the ListView row shape, so this\n// inlines the artwork in the same element format the set uses.\nconst SectionAddStrokeRoundedIcon: IconSvgElement = [\n [\n \"path\",\n {\n d: \"M2 3.4C2 2.24173 2.24173 2 3.4 2H20.6C21.7583 2 22 2.24173 22 3.4V4.6C22 5.75827 21.7583 6 20.6 6H3.4C2.24173 6 2 5.75827 2 4.6V3.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"0\",\n },\n ],\n [\n \"path\",\n {\n d: \"M2 11.4C2 10.2417 2.24173 10 3.4 10H10.6C11.7583 10 12 10.2417 12 11.4V12.6C12 13.7583 11.7583 14 10.6 14H3.4C2.24173 14 2 13.7583 2 12.6V11.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"1\",\n },\n ],\n [\n \"path\",\n {\n d: \"M2 19.4C2 18.2417 2.24173 18 3.4 18H10.6C11.7583 18 12 18.2417 12 19.4V20.6C12 21.7583 11.7583 22 10.6 22H3.4C2.24173 22 2 21.7583 2 20.6V19.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"2\",\n },\n ],\n [\n \"path\",\n {\n d: \"M18 13V21M22 17H14\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"3\",\n },\n ],\n];\n\nconst ICON_MAP = {\n AiContentGenerator01: AiContentGenerator01Icon,\n AlertCircle: AlertCircleIcon,\n AlertTriangle: Alert02Icon,\n AlignLeft: Menu02Icon,\n AppWindow: BrowserIcon,\n Archive: Archive03Icon,\n ArchiveRestore: Unarchive03Icon,\n ArrowDown: ArrowDown02Icon,\n ArrowRight: ArrowRight02Icon,\n ArrowReloadHorizontal: ArrowReloadHorizontalIcon,\n ArrowUp: ArrowUp02Icon,\n ArrowUpDown: ArrowUpDownIcon,\n ArrowTurnBackward: ArrowTurnBackwardIcon,\n ArrowTurnForward: ArrowTurnForwardIcon,\n ArrowUpRight: ArrowUpRight01Icon,\n AudioLines: AudioWaveIcon,\n Beaker: TestTube01Icon,\n BubbleChatQuestion: BubbleChatQuestionIcon,\n Browser: BrowserIcon,\n Brain: BrainIcon,\n Bug: Bug01Icon,\n Calendar: Calendar03Icon,\n CalendarCheckOut02: CalendarCheckOut02Icon,\n CalendarSync: CalendarSyncIcon,\n ChatFeedback: ChatFeedback01Icon,\n ChartColumn: ChartColumnIcon,\n Check: Tick02Icon,\n ChevronDown: ArrowDown01Icon,\n ChevronLeft: ArrowLeft01Icon,\n ChevronRight: ArrowRight01Icon,\n ChevronUp: ArrowUp01Icon,\n ChevronsDown: ArrowDownDoubleIcon,\n ChevronsUp: ArrowUpDoubleIcon,\n Circle: CircleIcon,\n CircleArrowShrink: CircleArrowShrink01Icon,\n CircleCheck: CheckmarkCircle02Icon,\n CircleDashed: DashedLineCircleIcon,\n CircleQuestion: HelpCircleIcon,\n CircleX: CancelCircleIcon,\n Clock: Clock01Icon,\n Code: SourceCodeIcon,\n ComputerTerminal01: ComputerTerminal01Icon,\n Columns2: LayoutTwoColumnIcon,\n Container: CloudIcon,\n Copy: Copy01Icon,\n CornerDownLeft: ArrowMoveDownLeftIcon,\n CornerDownRight: ArrowMoveDownRightIcon,\n Discord: DiscordIcon,\n DateTime: DateTimeIcon,\n Github: GithubIcon,\n DragDropHorizontal: DragDropHorizontalIcon,\n DragDropVertical: DragDropVerticalIcon,\n Download: Download01Icon,\n Edit: Edit02Icon,\n EditFile: Edit04Icon,\n ElectricPlugs: ElectricPlugsIcon,\n Eye: ViewIcon,\n EyeOff: ViewOffIcon,\n Explore: Book02Icon,\n ExternalLink: LinkSquare02Icon,\n FileDiff: PlusMinusSquare01Icon,\n File: FileEmpty02Icon,\n FileAttachment: FileAttachmentIcon,\n FileQuestion: FileQuestionMarkIcon,\n FileText: File01Icon,\n FileX2: FileXIcon,\n Folder: FolderIcon,\n FolderGit: FolderGitTwoIcon,\n FolderOpen: Folder02Icon,\n FolderMinus: FolderRemoveIcon,\n FolderPlus: FolderAddIcon,\n Fork: GitForkIcon,\n GitBranch: GitBranchIcon,\n GitMerge: GitMergeIcon,\n GitPullRequest: GitPullRequestIcon,\n GitPullRequestArrow: GitPullRequestArrow,\n GitPullRequestClosed: GitPullRequestClosedIcon,\n GitPullRequestDraft: GitPullRequestDraftIcon,\n Globe: InternetIcon,\n GridView: GridViewIcon,\n Info: InformationCircleIcon,\n Laptop: LaptopIcon,\n Layers: Layers01Icon,\n ListView: ListViewIcon,\n SectionAdd: SectionAddStrokeRoundedIcon,\n ListTodo: CheckListIcon,\n Loading: Loading03Icon,\n Lock: LockIcon,\n Mail: Mail02Icon,\n MailOpen: MailOpen01Icon,\n Maximize2: ExpandIcon,\n MessageQuestion: MessageQuestionIcon,\n MessageCirclePlus: BubbleChatAddIcon,\n MessageSquarePlus: BubbleChatAddIcon,\n MessageSquare: BubbleChatIcon,\n Mic: Mic02Icon,\n Minimize2: CollapseIcon,\n MoreHorizontal: MoreHorizontalIcon,\n NewTab: DashedLine02Icon,\n PackageReceive: PackageReceiveIcon,\n Palette: PaletteStrokeRoundedIcon,\n PanelBottom: SidebarBottomIcon,\n PanelLeft: SidebarLeftIcon,\n PanelRight: SidebarRightIcon,\n Paperclip: AttachmentIcon,\n Pause: PauseIcon,\n Pin: PinIcon,\n PinOff: PinOffIcon,\n Play: PlayIcon,\n Plus: PlusSignIcon,\n Puzzle: PuzzleIcon,\n Repeat: RepeatIcon,\n RotateCcw: Refresh01Icon,\n Rows2: LayoutTwoRowIcon,\n Rows3: LayoutThreeRowIcon,\n Search: Search01Icon,\n Sent: SentIcon,\n Settings: Settings01Icon,\n SideChat: MessageAdd02Icon,\n ClosePluginPane: Cancel01Icon,\n CloseThreadPane: Cancel01Icon,\n SlidersHorizontal: SlidersHorizontalIcon,\n Smartphone: SmartPhone01Icon,\n Sort: Sorting01Icon,\n Spinner: DashedLineCircleIcon,\n Square: SquareIcon,\n Star: StarIcon,\n Target: Target02Icon,\n Terminal: ComputerTerminal01Icon,\n TextWrap: TextWrapIcon,\n TimeSchedule: TimeScheduleIcon,\n Toolbox: ToolboxIcon,\n Trash2: Delete02Icon,\n UserRound: UserIcon,\n UserRoundPlus: UserAdd01Icon,\n Workflow: WorkflowCircle03Icon,\n X: Cancel01Icon,\n Zap: ZapIcon,\n ZoomIn: ZoomInAreaIcon,\n ZoomOut: ZoomOutAreaIcon,\n} as const satisfies Record;\n\nexport type IconName = keyof typeof ICON_MAP;\n\nexport const ICON_NAMES = Object.keys(ICON_MAP) as readonly IconName[];\n\nexport interface IconProps {\n name: IconName;\n className?: string;\n \"aria-hidden\"?: boolean | \"true\" | \"false\";\n \"aria-label\"?: string;\n}\n\nexport function Icon({\n name,\n className,\n \"aria-hidden\": ariaHidden,\n \"aria-label\": ariaLabel,\n}: IconProps) {\n return (\n \n );\n}\n",
+ "content": "import { HugeiconsIcon, type IconSvgElement } from \"@hugeicons/react\";\nimport {\n AiContentGenerator01Icon,\n Alert02Icon,\n AlertCircleIcon,\n Archive03Icon,\n ArrowDown01Icon,\n ArrowDown02Icon,\n ArrowDownDoubleIcon,\n ArrowLeft01Icon,\n ArrowMoveDownLeftIcon,\n ArrowMoveDownRightIcon,\n ArrowRight01Icon,\n ArrowRight02Icon,\n ArrowReloadHorizontalIcon,\n ArrowUp01Icon,\n ArrowUp02Icon,\n ArrowUpDoubleIcon,\n ArrowUpDownIcon,\n ArrowTurnBackwardIcon,\n ArrowTurnForwardIcon,\n ArrowUpRight01Icon,\n AttachmentIcon,\n AudioWaveIcon,\n BrowserIcon,\n BubbleChatAddIcon,\n BubbleChatIcon,\n BubbleChatQuestionIcon,\n Calendar03Icon,\n CalendarCheckOut02Icon,\n CalendarSyncIcon,\n Cancel01Icon,\n CancelCircleIcon,\n ChatFeedback01Icon,\n Bug01Icon,\n ChartColumnIcon,\n CheckListIcon,\n CheckmarkCircle02Icon,\n CircleArrowShrink01Icon,\n CircleIcon,\n Clock01Icon,\n CloudIcon,\n CollapseIcon,\n Book02Icon,\n BrainIcon,\n ComputerTerminal01Icon,\n Copy01Icon,\n DashedLine02Icon,\n DashedLineCircleIcon,\n DateTimeIcon,\n Delete02Icon,\n Download01Icon,\n DiscordIcon,\n GithubIcon,\n DragDropHorizontalIcon,\n DragDropVerticalIcon,\n Edit02Icon,\n Edit04Icon,\n ElectricPlugsIcon,\n ExpandIcon,\n ViewIcon,\n ViewOffIcon,\n File01Icon,\n FileAttachmentIcon,\n FileEmpty02Icon,\n FileQuestionMarkIcon,\n FileXIcon,\n Folder02Icon,\n FolderAddIcon,\n FolderGitTwoIcon,\n FolderIcon,\n FolderRemoveIcon,\n GitBranchIcon,\n GitForkIcon,\n GitMergeIcon,\n GitPullRequestArrow,\n GitPullRequestClosedIcon,\n GitPullRequestDraftIcon,\n GitPullRequestIcon,\n GridViewIcon,\n HelpCircleIcon,\n InformationCircleIcon,\n InternetIcon,\n LaptopIcon,\n Layers01Icon,\n ListViewIcon,\n LockIcon,\n Loading03Icon,\n LayoutThreeRowIcon,\n LayoutTwoColumnIcon,\n LayoutTwoRowIcon,\n LinkSquare02Icon,\n Mail02Icon,\n MailOpen01Icon,\n Menu02Icon,\n MessageAdd02Icon,\n MessageQuestionIcon,\n Mic02Icon,\n MoreHorizontalIcon,\n PackageReceiveIcon,\n PauseIcon,\n PinIcon,\n PinOffIcon,\n PlayIcon,\n PlusMinusSquare01Icon,\n PlusSignIcon,\n PuzzleIcon,\n Refresh01Icon,\n RepeatIcon,\n Search01Icon,\n SentIcon,\n Settings01Icon,\n SidebarBottomIcon,\n SidebarLeftIcon,\n SidebarRightIcon,\n SlidersHorizontalIcon,\n SmartPhone01Icon,\n Sorting01Icon,\n SourceCodeIcon,\n SquareIcon,\n StarIcon,\n Target02Icon,\n TestTube01Icon,\n TextWrapIcon,\n Tick02Icon,\n TimeScheduleIcon,\n ToolboxIcon,\n Unarchive03Icon,\n UserAdd01Icon,\n UserIcon,\n WorkflowCircle03Icon,\n ZapIcon,\n ZoomInAreaIcon,\n ZoomOutAreaIcon,\n} from \"@hugeicons/core-free-icons\";\nimport { cn } from \"../../lib/utils\";\n\n// The free hugeicons set ships no artist-palette glyph (its `Palette` export\n// is a pen nib), so this inlines the stroke-rounded palette artwork in the\n// same element format the set uses.\nconst PaletteStrokeRoundedIcon: IconSvgElement = [\n [\n \"path\",\n {\n d: \"M21.8205 10.4127C22.062 11.8519 22.1827 12.5715 21.2423 13.9326C21.1459 14.0722 20.8966 14.3713 20.777 14.4911C19.6103 15.6586 18.4308 15.6586 16.0716 15.6586H14.1392C13.5085 15.6586 13.1931 15.6586 12.9639 15.7142C11.9586 15.9581 11.3031 16.9391 11.453 17.9755C11.4872 18.2118 11.6043 18.5085 11.8386 19.102C11.9345 19.3449 11.9824 19.4664 12.0136 19.7304C12.1292 20.7084 11.0869 21.9508 10.1158 21.9926C9.85358 22.0039 9.83681 22.0002 9.80326 21.9926C7.66174 21.51 5.66204 20.3123 4.18389 18.4421C0.736789 14.0808 1.43146 7.71364 5.73548 4.22064C10.0395 0.727643 16.323 1.43156 19.7701 5.79289C20.868 7.1819 21.5457 8.77438 21.8205 10.4127Z\",\n fill: \"none\",\n fillRule: \"evenodd\",\n clipRule: \"evenodd\",\n stroke: \"currentColor\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"0\",\n },\n ],\n [\n \"path\",\n {\n d: \"M7.36719 7.74976H7.24219M7.49219 7.74976C7.49219 7.88783 7.38026 7.99976 7.24219 7.99976C7.10412 7.99976 6.99219 7.88783 6.99219 7.74976C6.99219 7.61169 7.10412 7.49976 7.24219 7.49976C7.38026 7.49976 7.49219 7.61169 7.49219 7.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"1\",\n },\n ],\n [\n \"path\",\n {\n d: \"M7.36719 15.7498H7.24219M7.49219 15.7498C7.49219 15.8878 7.38026 15.9998 7.24219 15.9998C7.10412 15.9998 6.99219 15.8878 6.99219 15.7498C6.99219 15.6117 7.10412 15.4998 7.24219 15.4998C7.38026 15.4998 7.49219 15.6117 7.49219 15.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"2\",\n },\n ],\n [\n \"path\",\n {\n d: \"M11.8672 5.74976H11.7422M11.9922 5.74976C11.9922 5.88783 11.8803 5.99976 11.7422 5.99976C11.6041 5.99976 11.4922 5.88783 11.4922 5.74976C11.4922 5.61169 11.6041 5.49976 11.7422 5.49976C11.8803 5.49976 11.9922 5.61169 11.9922 5.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"3\",\n },\n ],\n [\n \"path\",\n {\n d: \"M16.3672 7.74976H16.2422M16.4922 7.74976C16.4922 7.88783 16.3803 7.99976 16.2422 7.99976C16.1041 7.99976 15.9922 7.88783 15.9922 7.74976C15.9922 7.61169 16.1041 7.49976 16.2422 7.49976C16.3803 7.49976 16.4922 7.61169 16.4922 7.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"4\",\n },\n ],\n [\n \"path\",\n {\n d: \"M18.3672 11.7498H18.2422M18.4922 11.7498C18.4922 11.8878 18.3803 11.9998 18.2422 11.9998C18.1041 11.9998 17.9922 11.8878 17.9922 11.7498C17.9922 11.6117 18.1041 11.4998 18.2422 11.4998C18.3803 11.4998 18.4922 11.6117 18.4922 11.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"5\",\n },\n ],\n [\n \"path\",\n {\n d: \"M5.86719 11.7498H5.74219M5.99219 11.7498C5.99219 11.8878 5.88026 11.9998 5.74219 11.9998C5.60412 11.9998 5.49219 11.8878 5.49219 11.7498C5.49219 11.6117 5.60412 11.4998 5.74219 11.4998C5.88026 11.4998 5.99219 11.6117 5.99219 11.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"6\",\n },\n ],\n];\n\n// Custom \"new section\" glyph: the set's ListView rows with the middle and\n// bottom rows shortened so the plus owns the lower-right quadrant, matching\n// FolderAdd's non-overlapping plus placement (same plus geometry). Hugeicons\n// has no list-with-plus variant that keeps the ListView row shape, so this\n// inlines the artwork in the same element format the set uses.\nconst SectionAddStrokeRoundedIcon: IconSvgElement = [\n [\n \"path\",\n {\n d: \"M2 3.4C2 2.24173 2.24173 2 3.4 2H20.6C21.7583 2 22 2.24173 22 3.4V4.6C22 5.75827 21.7583 6 20.6 6H3.4C2.24173 6 2 5.75827 2 4.6V3.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"0\",\n },\n ],\n [\n \"path\",\n {\n d: \"M2 11.4C2 10.2417 2.24173 10 3.4 10H10.6C11.7583 10 12 10.2417 12 11.4V12.6C12 13.7583 11.7583 14 10.6 14H3.4C2.24173 14 2 13.7583 2 12.6V11.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"1\",\n },\n ],\n [\n \"path\",\n {\n d: \"M2 19.4C2 18.2417 2.24173 18 3.4 18H10.6C11.7583 18 12 18.2417 12 19.4V20.6C12 21.7583 11.7583 22 10.6 22H3.4C2.24173 22 2 21.7583 2 20.6V19.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"2\",\n },\n ],\n [\n \"path\",\n {\n d: \"M18 13V21M22 17H14\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"3\",\n },\n ],\n];\n\nconst ICON_MAP = {\n AiContentGenerator01: AiContentGenerator01Icon,\n AlertCircle: AlertCircleIcon,\n AlertTriangle: Alert02Icon,\n AlignLeft: Menu02Icon,\n AppWindow: BrowserIcon,\n Archive: Archive03Icon,\n ArchiveRestore: Unarchive03Icon,\n ArrowDown: ArrowDown02Icon,\n ArrowRight: ArrowRight02Icon,\n ArrowReloadHorizontal: ArrowReloadHorizontalIcon,\n ArrowUp: ArrowUp02Icon,\n ArrowUpDown: ArrowUpDownIcon,\n ArrowTurnBackward: ArrowTurnBackwardIcon,\n ArrowTurnForward: ArrowTurnForwardIcon,\n ArrowUpRight: ArrowUpRight01Icon,\n AudioLines: AudioWaveIcon,\n Beaker: TestTube01Icon,\n BubbleChatQuestion: BubbleChatQuestionIcon,\n Browser: BrowserIcon,\n Brain: BrainIcon,\n Bug: Bug01Icon,\n Calendar: Calendar03Icon,\n CalendarCheckOut02: CalendarCheckOut02Icon,\n CalendarSync: CalendarSyncIcon,\n ChatFeedback: ChatFeedback01Icon,\n ChartColumn: ChartColumnIcon,\n Check: Tick02Icon,\n ChevronDown: ArrowDown01Icon,\n ChevronLeft: ArrowLeft01Icon,\n ChevronRight: ArrowRight01Icon,\n ChevronUp: ArrowUp01Icon,\n ChevronsDown: ArrowDownDoubleIcon,\n ChevronsUp: ArrowUpDoubleIcon,\n Circle: CircleIcon,\n CircleArrowShrink: CircleArrowShrink01Icon,\n CircleCheck: CheckmarkCircle02Icon,\n CircleDashed: DashedLineCircleIcon,\n CircleQuestion: HelpCircleIcon,\n CircleX: CancelCircleIcon,\n Clock: Clock01Icon,\n Cloud: CloudIcon,\n Code: SourceCodeIcon,\n ComputerTerminal01: ComputerTerminal01Icon,\n Columns2: LayoutTwoColumnIcon,\n Container: CloudIcon,\n Copy: Copy01Icon,\n CornerDownLeft: ArrowMoveDownLeftIcon,\n CornerDownRight: ArrowMoveDownRightIcon,\n Discord: DiscordIcon,\n DateTime: DateTimeIcon,\n Github: GithubIcon,\n DragDropHorizontal: DragDropHorizontalIcon,\n DragDropVertical: DragDropVerticalIcon,\n Download: Download01Icon,\n Edit: Edit02Icon,\n EditFile: Edit04Icon,\n ElectricPlugs: ElectricPlugsIcon,\n Eye: ViewIcon,\n EyeOff: ViewOffIcon,\n Explore: Book02Icon,\n ExternalLink: LinkSquare02Icon,\n FileDiff: PlusMinusSquare01Icon,\n File: FileEmpty02Icon,\n FileAttachment: FileAttachmentIcon,\n FileQuestion: FileQuestionMarkIcon,\n FileText: File01Icon,\n FileX2: FileXIcon,\n Folder: FolderIcon,\n FolderGit: FolderGitTwoIcon,\n FolderOpen: Folder02Icon,\n FolderMinus: FolderRemoveIcon,\n FolderPlus: FolderAddIcon,\n Fork: GitForkIcon,\n GitBranch: GitBranchIcon,\n GitMerge: GitMergeIcon,\n GitPullRequest: GitPullRequestIcon,\n GitPullRequestArrow: GitPullRequestArrow,\n GitPullRequestClosed: GitPullRequestClosedIcon,\n GitPullRequestDraft: GitPullRequestDraftIcon,\n Globe: InternetIcon,\n GridView: GridViewIcon,\n Info: InformationCircleIcon,\n Laptop: LaptopIcon,\n Layers: Layers01Icon,\n ListView: ListViewIcon,\n SectionAdd: SectionAddStrokeRoundedIcon,\n ListTodo: CheckListIcon,\n Loading: Loading03Icon,\n Lock: LockIcon,\n Mail: Mail02Icon,\n MailOpen: MailOpen01Icon,\n Maximize2: ExpandIcon,\n MessageQuestion: MessageQuestionIcon,\n MessageCirclePlus: BubbleChatAddIcon,\n MessageSquarePlus: BubbleChatAddIcon,\n MessageSquare: BubbleChatIcon,\n Mic: Mic02Icon,\n Minimize2: CollapseIcon,\n MoreHorizontal: MoreHorizontalIcon,\n NewTab: DashedLine02Icon,\n PackageReceive: PackageReceiveIcon,\n Palette: PaletteStrokeRoundedIcon,\n PanelBottom: SidebarBottomIcon,\n PanelLeft: SidebarLeftIcon,\n PanelRight: SidebarRightIcon,\n Paperclip: AttachmentIcon,\n Pause: PauseIcon,\n Pin: PinIcon,\n PinOff: PinOffIcon,\n Play: PlayIcon,\n Plus: PlusSignIcon,\n Puzzle: PuzzleIcon,\n Repeat: RepeatIcon,\n RotateCcw: Refresh01Icon,\n Rows2: LayoutTwoRowIcon,\n Rows3: LayoutThreeRowIcon,\n Search: Search01Icon,\n Sent: SentIcon,\n Settings: Settings01Icon,\n SideChat: MessageAdd02Icon,\n ClosePluginPane: Cancel01Icon,\n CloseThreadPane: Cancel01Icon,\n SlidersHorizontal: SlidersHorizontalIcon,\n Smartphone: SmartPhone01Icon,\n Sort: Sorting01Icon,\n Spinner: DashedLineCircleIcon,\n Square: SquareIcon,\n Star: StarIcon,\n Target: Target02Icon,\n Terminal: ComputerTerminal01Icon,\n TextWrap: TextWrapIcon,\n TimeSchedule: TimeScheduleIcon,\n Toolbox: ToolboxIcon,\n Trash2: Delete02Icon,\n UserRound: UserIcon,\n UserRoundPlus: UserAdd01Icon,\n Workflow: WorkflowCircle03Icon,\n X: Cancel01Icon,\n Zap: ZapIcon,\n ZoomIn: ZoomInAreaIcon,\n ZoomOut: ZoomOutAreaIcon,\n} as const satisfies Record;\n\nexport type IconName = keyof typeof ICON_MAP;\n\nexport const ICON_NAMES = Object.keys(ICON_MAP) as readonly IconName[];\n\nexport interface IconProps {\n name: IconName;\n className?: string;\n \"aria-hidden\"?: boolean | \"true\" | \"false\";\n \"aria-label\"?: string;\n}\n\nexport function Icon({\n name,\n className,\n \"aria-hidden\": ariaHidden,\n \"aria-label\": ariaLabel,\n}: IconProps) {\n return (\n \n );\n}\n",
"type": "registry:ui",
"target": "components/ui/icon.tsx"
}
diff --git a/packages/plugin-sdk/bundled-types/bb-plugin-sdk-app.d.ts b/packages/plugin-sdk/bundled-types/bb-plugin-sdk-app.d.ts
index 114493d0d4..e7109a4bd2 100644
--- a/packages/plugin-sdk/bundled-types/bb-plugin-sdk-app.d.ts
+++ b/packages/plugin-sdk/bundled-types/bb-plugin-sdk-app.d.ts
@@ -480,9 +480,12 @@ interface PluginPendingInteractionRegistration {
interface PluginSidebarFooterActionContext {
/**
* Navigate to this plugin's detail page in Tools, where declarative settings
- * and `settingsSection` slots render.
+ * and `settingsSection` slots render. Pass a section id to target one of
+ * this plugin's registered settings sections directly.
*/
- openSettings(): void;
+ openSettings(options?: {
+ sectionId?: string;
+ }): void;
}
/**
* An icon button in the app sidebar footer (next to Settings / bug report).
diff --git a/packages/plugin-sdk/bundled-types/bb-plugin-sdk.d.ts b/packages/plugin-sdk/bundled-types/bb-plugin-sdk.d.ts
index dd723d59b6..35350f2254 100644
--- a/packages/plugin-sdk/bundled-types/bb-plugin-sdk.d.ts
+++ b/packages/plugin-sdk/bundled-types/bb-plugin-sdk.d.ts
@@ -244,6 +244,7 @@ type Environment = z$1.infer;
*/
declare const experimentsSchema: z$1.ZodObject<{
claudeCodeMockCliTraffic: z$1.ZodBoolean;
+ cloudAi: z$1.ZodBoolean;
newOnboarding: z$1.ZodBoolean;
toolsHub: z$1.ZodBoolean;
}, z$1.core.$strip>;
@@ -700,8 +701,8 @@ declare const threadEventSchema: z$1.ZodPipe;
@@ -889,10 +890,10 @@ declare const threadEventSchema: z$1.ZodPipe;
taskStatus: z$1.ZodEnum<{
pending: "pending";
- running: "running";
- paused: "paused";
completed: "completed";
failed: "failed";
+ paused: "paused";
+ running: "running";
killed: "killed";
stopped: "stopped";
}>;
@@ -908,8 +909,8 @@ declare const threadEventSchema: z$1.ZodPipe;
taskStatus: z$1.ZodEnum<{
pending: "pending";
- running: "running";
- paused: "paused";
completed: "completed";
failed: "failed";
+ paused: "paused";
+ running: "running";
killed: "killed";
stopped: "stopped";
}>;
@@ -1140,8 +1141,8 @@ declare const threadEventSchema: z$1.ZodPipe;
taskStatus: z$1.ZodEnum<{
pending: "pending";
- running: "running";
- paused: "paused";
completed: "completed";
failed: "failed";
+ paused: "paused";
+ running: "running";
killed: "killed";
stopped: "stopped";
}>;
@@ -1269,8 +1270,8 @@ declare const threadEventSchema: z$1.ZodPipe;
taskStatus: z$1.ZodEnum<{
pending: "pending";
- running: "running";
- paused: "paused";
completed: "completed";
failed: "failed";
+ paused: "paused";
+ running: "running";
killed: "killed";
stopped: "stopped";
}>;
@@ -1341,8 +1342,8 @@ declare const threadEventSchema: z$1.ZodPipe;
}, z$1.core.$strip>>;
}, z$1.core.$strip>;
@@ -2569,8 +2570,8 @@ type SkillFilesResponse = z$1.infer;
declare const projectResponseSchema: z$1.ZodObject<{
id: z$1.ZodString;
kind: z$1.ZodEnum<{
- personal: "personal";
standard: "standard";
+ personal: "personal";
}>;
name: z$1.ZodString;
gitRemoteUrl: z$1.ZodNullable;
@@ -2591,8 +2592,8 @@ type ProjectResponse = z$1.infer;
declare const projectWithThreadsResponseSchema: z$1.ZodObject<{
id: z$1.ZodString;
kind: z$1.ZodEnum<{
- personal: "personal";
standard: "standard";
+ personal: "personal";
}>;
name: z$1.ZodString;
gitRemoteUrl: z$1.ZodNullable;
@@ -2692,8 +2693,8 @@ declare const projectWithThreadsResponseSchema: z$1.ZodObject<{
ultra: "ultra";
}>;
permissionMode: z$1.ZodEnum<{
- auto: "auto";
"accept-edits": "accept-edits";
+ auto: "auto";
full: "full";
}>;
}, z$1.core.$strip>>;
@@ -6397,8 +6398,8 @@ declare const systemExecutionOptionsResponseSchema: z$1.ZodObject<{
supportsUserQuestion: z$1.ZodBoolean;
supportsFork: z$1.ZodBoolean;
supportedPermissionModes: z$1.ZodArray>;
}, z$1.core.$strip>;
@@ -6429,8 +6430,8 @@ declare const systemExecutionOptionsResponseSchema: z$1.ZodObject<{
available: z$1.ZodBoolean;
}, z$1.core.$strip>>;
permissionCeiling: z$1.ZodEnum<{
- auto: "auto";
"accept-edits": "accept-edits";
+ auto: "auto";
full: "full";
}>;
models: z$1.ZodArray>;
experiments: z$1.ZodObject<{
claudeCodeMockCliTraffic: z$1.ZodBoolean;
+ cloudAi: z$1.ZodBoolean;
newOnboarding: z$1.ZodBoolean;
toolsHub: z$1.ZodBoolean;
}, z$1.core.$strip>;
@@ -7871,10 +7873,10 @@ declare const createThreadRequestSchema: z$1.ZodObject<{
ultra: "ultra";
}>>;
permissionMode: z$1.ZodOptional, z$1.ZodLiteral<"workspace-write">]>, z$1.ZodTransform<"auto" | "accept-edits" | "full", "auto" | "accept-edits" | "full" | "workspace-write">>>;
+ }>, z$1.ZodLiteral<"workspace-write">]>, z$1.ZodTransform<"accept-edits" | "auto" | "full", "accept-edits" | "auto" | "full" | "workspace-write">>>;
executionInputSources: z$1.ZodOptional>>>;
title: z$1.ZodOptional;
permissionMode: z$1.ZodOptional, z$1.ZodLiteral<"workspace-write">]>, z$1.ZodTransform<"auto" | "accept-edits" | "full", "auto" | "accept-edits" | "full" | "workspace-write">>>;
+ }>, z$1.ZodLiteral<"workspace-write">]>, z$1.ZodTransform<"accept-edits" | "auto" | "full", "accept-edits" | "auto" | "full" | "workspace-write">>>;
visibility: z$1.ZodDefault>;
permissionMode: z$1.ZodOptional, z$1.ZodLiteral<"workspace-write">]>, z$1.ZodTransform<"auto" | "accept-edits" | "full", "auto" | "accept-edits" | "full" | "workspace-write">>>;
+ }>, z$1.ZodLiteral<"workspace-write">]>, z$1.ZodTransform<"accept-edits" | "auto" | "full", "accept-edits" | "auto" | "full" | "workspace-write">>>;
executionInputSources: z$1.ZodOptional>;
permissionMode: z$1.ZodOptional, z$1.ZodLiteral<"workspace-write">]>, z$1.ZodTransform<"auto" | "accept-edits" | "full", "auto" | "accept-edits" | "full" | "workspace-write">>>;
+ }>, z$1.ZodLiteral<"workspace-write">]>, z$1.ZodTransform<"accept-edits" | "auto" | "full", "accept-edits" | "auto" | "full" | "workspace-write">>>;
executionInputSources: z$1.ZodOptional;
permissionMode: z$1.ZodEnum<{
- auto: "auto";
"accept-edits": "accept-edits";
+ auto: "auto";
full: "full";
}>;
serviceTier: z$1.ZodEnum<{
@@ -9056,9 +9058,9 @@ declare const threadWithIncludesResponseSchema: z$1.ZodObject<{
isGitRepo: z$1.ZodBoolean;
isWorktree: z$1.ZodBoolean;
workspaceProvisionType: z$1.ZodEnum<{
- unmanaged: "unmanaged";
- "managed-worktree": "managed-worktree";
personal: "personal";
+ "managed-worktree": "managed-worktree";
+ unmanaged: "unmanaged";
}>;
branchName: z$1.ZodNullable;
baseBranch: z$1.ZodNullable;
@@ -9086,8 +9088,8 @@ declare const threadWithIncludesResponseSchema: z$1.ZodObject<{
connected: "connected";
}>;
maxPermissionMode: z$1.ZodEnum<{
- auto: "auto";
"accept-edits": "accept-edits";
+ auto: "auto";
full: "full";
}>;
lastSeenAt: z$1.ZodNullable;
@@ -9357,8 +9359,8 @@ declare const threadQueuedMessageListResponseSchema: z$1.ZodArray;
permissionMode: z$1.ZodEnum<{
- auto: "auto";
"accept-edits": "accept-edits";
+ auto: "auto";
full: "full";
}>;
serviceTier: z$1.ZodEnum<{
@@ -9726,8 +9728,8 @@ declare const threadTimelineResponseSchema: z$1.ZodObject<{
originalModel: z$1.ZodString;
fallbackModel: z$1.ZodString;
reason: z$1.ZodEnum<{
- refusal: "refusal";
provider: "provider";
+ refusal: "refusal";
}>;
message: z$1.ZodString;
}, z$1.core.$strip>>;
@@ -10266,9 +10268,12 @@ interface PluginPendingInteractionRegistration {
interface PluginSidebarFooterActionContext {
/**
* Navigate to this plugin's detail page in Tools, where declarative settings
- * and `settingsSection` slots render.
+ * and `settingsSection` slots render. Pass a section id to target one of
+ * this plugin's registered settings sections directly.
*/
- openSettings(): void;
+ openSettings(options?: {
+ sectionId?: string;
+ }): void;
}
/**
* An icon button in the app sidebar footer (next to Settings / bug report).
@@ -12962,6 +12967,53 @@ interface PluginHosts {
*/
declareSharedPorts(hostId: string, ports: readonly number[]): void;
}
+/** Why a cloud AI call failed; drives the host's local-fallback routing. */
+type CloudAiFailureCode = "unauthorized" | "quota_exhausted" | "unavailable";
+/**
+ * Result-shaped (never a thrown class) so the contract survives plugin
+ * bundling — an error class would have a different identity in the plugin
+ * bundle than in the host. A provider may still reject on abort; the host
+ * checks its own signal and treats that as a timeout.
+ */
+type CloudAiResult = {
+ ok: true;
+ value: T;
+} | {
+ ok: false;
+ code: CloudAiFailureCode;
+ message: string;
+};
+interface CloudAiCompleteArgs {
+ prompt: string;
+ /** JSON Schema (top-level `type: "object"`) the structured result must match. */
+ schema: {
+ [key: string]: JsonValue;
+ };
+ /** Host-owned abort signal; the host maps aborts to its timeout semantics. */
+ signal: AbortSignal;
+}
+interface CloudAiTranscribeArgs {
+ file: File;
+ prompt?: string;
+ /** Host-owned abort signal; the host maps aborts to its timeout semantics. */
+ signal: AbortSignal;
+}
+/**
+ * A cloud route for bb's small AI tasks (thread titles, commit messages,
+ * voice transcription). When the server's Cloud AI experiment is enabled and
+ * a registered provider `isAvailable()`, the host tries it before the locally
+ * configured `BB_INFERENCE`/`BB_TRANSCRIPTION` providers and falls back to
+ * those on `ok: false`.
+ */
+interface CloudAiProvider {
+ /**
+ * Cheap synchronous probe consulted per call — pairing state, user setting,
+ * failure latches. The host never calls `complete`/`transcribe` when false.
+ */
+ isAvailable(): boolean;
+ complete(args: CloudAiCompleteArgs): Promise>;
+ transcribe(args: CloudAiTranscribeArgs): Promise>;
+}
interface PluginStatusApi {
/**
* Mark this plugin `needs-configuration` (with a message shown in
@@ -13023,7 +13075,13 @@ interface BbPluginApi {
* The sanctioned place to clear timers and close connections.
*/
onDispose(hook: () => void | Promise): void;
+ /**
+ * EXPERIMENTAL (see docs/api_to_audit.md). Register a cloud route for bb's
+ * small AI tasks. Single slot host-wide: the most recent registration wins;
+ * it is unregistered automatically when this plugin reloads or disables.
+ */
+ experimental_registerCloudAiProvider(provider: CloudAiProvider): void;
}
export { PLUGIN_CLI_OUTPUT_MAX_BYTES, defineRpcContract };
-export type { BbContext, BbNavigate, BbPluginApi, ComposerCustomization, ComposerPlusMenuItem, ComposerRichTextSpec, ComposerStructuredDraft, ComposerView, JsonValue, MarkdownProps, NewThreadComposerProps, NewThreadRequest, PluginAgentConfiguration, PluginAgentConfigurationContext, PluginAgentToolContentPart, PluginAgentToolContext, PluginAgentToolExperimentalStatusLabels, PluginAgentToolRegistrationBase, PluginAgentToolResult, PluginAgentToolSelection, PluginAgents, PluginAppBuilder, PluginAppComposer, PluginAppContentScripts, PluginAppDefinition, PluginAppSetup, PluginAppSlots, PluginBackground, PluginCli, PluginCliCommandInfo, PluginCliContext, PluginCliExecutionResult, PluginCliOutputLimitError, PluginCliRegistration, PluginCliResult, PluginComposerApi, PluginComposerMention, PluginComposerScope, PluginComposerTextEffect, PluginComposerThreadRowStatus, PluginContentScriptContext, PluginContentScriptDisposer, PluginContentScriptRegistration, PluginEvents, PluginFileOpenerProps, PluginFileOpenerRegistration, PluginFileOpenerSource, PluginHomepageSectionProps, PluginHomepageSectionRegistration, PluginHosts, PluginHttp, PluginHttpAuthMode, PluginHttpHandler, PluginInteractionCancelReason, PluginInteractionRequest, PluginInteractionResult, PluginKvStorage, PluginLogger, PluginMentionItem, PluginMentionProviderRegistration, PluginMentionSearchContext, PluginMentionTrigger, PluginMessageActionContext, PluginMessageActionRegistration, PluginMessageActionThreadPanelOptions, PluginMessageDirectiveMessage, PluginMessageDirectiveOpenWorkspaceFile, PluginMessageDirectiveProps, PluginMessageDirectiveRegistration, PluginNavPanelProps, PluginNavPanelRegistration, PluginPendingInteractionProps, PluginPendingInteractionRegistration, PluginPendingInteractionView, PluginRealtime, PluginRealtimeConnectionState, PluginRpc, PluginRpcCallArgs, PluginRpcClient, PluginRpcContract, PluginRpcError, PluginRpcErrorCode, PluginRpcHandlers, PluginRpcIssuePathSegment, PluginRpcMethodContract, PluginRpcResult, PluginRpcValidationIssue, PluginSdkApp, PluginServerApi, PluginSettingDescriptor, PluginSettingDescriptors, PluginSettingValue, PluginSettings, PluginSettingsHandle, PluginSettingsSectionProps, PluginSettingsSectionRegistration, PluginSettingsState, PluginSettingsValues, PluginSharedPortTunnelIdentity, PluginSidebarFooterActionContext, PluginSidebarFooterActionProps, PluginSidebarFooterActionRegistration, PluginSidebarProject, PluginSidebarPullRequest, PluginSidebarSplitPane, PluginSidebarThread, PluginSidebarThreadActions, PluginSidebarThreadActivity, PluginSidebarThreadIndicator, PluginSidebarThreadPullRequestState, PluginSidebarThreadSplit, PluginSidebarThreadsState, PluginSidebarWorkspaceKind, PluginStatusApi, PluginStorage, PluginThreadEventHandler, PluginThreadEventName, PluginThreadEventPayloads, PluginThreadHeaderActionProps, PluginThreadHeaderActionRegistration, PluginThreadListProps, PluginThreadListRegistration, PluginThreadPanelActionContext, PluginThreadPanelActionRegistration, PluginThreadPanelProps, PluginUi, StandardSchemaV1, StandardSchemaV1InferInput, StandardSchemaV1InferOutput, StandardSchemaV1Issue, StandardSchemaV1Result, ThreadChatMessageAction, ThreadChatMessageReference, ThreadChatProps };
+export type { BbContext, BbNavigate, BbPluginApi, CloudAiCompleteArgs, CloudAiFailureCode, CloudAiProvider, CloudAiResult, CloudAiTranscribeArgs, ComposerCustomization, ComposerPlusMenuItem, ComposerRichTextSpec, ComposerStructuredDraft, ComposerView, JsonValue, MarkdownProps, NewThreadComposerProps, NewThreadRequest, PluginAgentConfiguration, PluginAgentConfigurationContext, PluginAgentToolContentPart, PluginAgentToolContext, PluginAgentToolExperimentalStatusLabels, PluginAgentToolRegistrationBase, PluginAgentToolResult, PluginAgentToolSelection, PluginAgents, PluginAppBuilder, PluginAppComposer, PluginAppContentScripts, PluginAppDefinition, PluginAppSetup, PluginAppSlots, PluginBackground, PluginCli, PluginCliCommandInfo, PluginCliContext, PluginCliExecutionResult, PluginCliOutputLimitError, PluginCliRegistration, PluginCliResult, PluginComposerApi, PluginComposerMention, PluginComposerScope, PluginComposerTextEffect, PluginComposerThreadRowStatus, PluginContentScriptContext, PluginContentScriptDisposer, PluginContentScriptRegistration, PluginEvents, PluginFileOpenerProps, PluginFileOpenerRegistration, PluginFileOpenerSource, PluginHomepageSectionProps, PluginHomepageSectionRegistration, PluginHosts, PluginHttp, PluginHttpAuthMode, PluginHttpHandler, PluginInteractionCancelReason, PluginInteractionRequest, PluginInteractionResult, PluginKvStorage, PluginLogger, PluginMentionItem, PluginMentionProviderRegistration, PluginMentionSearchContext, PluginMentionTrigger, PluginMessageActionContext, PluginMessageActionRegistration, PluginMessageActionThreadPanelOptions, PluginMessageDirectiveMessage, PluginMessageDirectiveOpenWorkspaceFile, PluginMessageDirectiveProps, PluginMessageDirectiveRegistration, PluginNavPanelProps, PluginNavPanelRegistration, PluginPendingInteractionProps, PluginPendingInteractionRegistration, PluginPendingInteractionView, PluginRealtime, PluginRealtimeConnectionState, PluginRpc, PluginRpcCallArgs, PluginRpcClient, PluginRpcContract, PluginRpcError, PluginRpcErrorCode, PluginRpcHandlers, PluginRpcIssuePathSegment, PluginRpcMethodContract, PluginRpcResult, PluginRpcValidationIssue, PluginSdkApp, PluginServerApi, PluginSettingDescriptor, PluginSettingDescriptors, PluginSettingValue, PluginSettings, PluginSettingsHandle, PluginSettingsSectionProps, PluginSettingsSectionRegistration, PluginSettingsState, PluginSettingsValues, PluginSharedPortTunnelIdentity, PluginSidebarFooterActionContext, PluginSidebarFooterActionProps, PluginSidebarFooterActionRegistration, PluginSidebarProject, PluginSidebarPullRequest, PluginSidebarSplitPane, PluginSidebarThread, PluginSidebarThreadActions, PluginSidebarThreadActivity, PluginSidebarThreadIndicator, PluginSidebarThreadPullRequestState, PluginSidebarThreadSplit, PluginSidebarThreadsState, PluginSidebarWorkspaceKind, PluginStatusApi, PluginStorage, PluginThreadEventHandler, PluginThreadEventName, PluginThreadEventPayloads, PluginThreadHeaderActionProps, PluginThreadHeaderActionRegistration, PluginThreadListProps, PluginThreadListRegistration, PluginThreadPanelActionContext, PluginThreadPanelActionRegistration, PluginThreadPanelProps, PluginUi, StandardSchemaV1, StandardSchemaV1InferInput, StandardSchemaV1InferOutput, StandardSchemaV1Issue, StandardSchemaV1Result, ThreadChatMessageAction, ThreadChatMessageReference, ThreadChatProps };
diff --git a/packages/plugin-sdk/src/__tests__/public-types.test.ts b/packages/plugin-sdk/src/__tests__/public-types.test.ts
index d52dd12199..d9f9754ac0 100644
--- a/packages/plugin-sdk/src/__tests__/public-types.test.ts
+++ b/packages/plugin-sdk/src/__tests__/public-types.test.ts
@@ -7,6 +7,7 @@ type ExpectedBbPluginApiKey =
| "background"
| "cli"
| "events"
+ | "experimental_registerCloudAiProvider"
| "hosts"
| "http"
| "log"
@@ -23,6 +24,11 @@ type ExpectedBbPluginApiKey =
const EXPECTED_BACKEND_ROOT_TYPE_EXPORTS = [
"BbPluginApi",
+ "CloudAiCompleteArgs",
+ "CloudAiFailureCode",
+ "CloudAiProvider",
+ "CloudAiResult",
+ "CloudAiTranscribeArgs",
"PluginAgents",
"PluginAgentConfiguration",
"PluginAgentConfigurationContext",
diff --git a/packages/plugin-sdk/src/app-contract.ts b/packages/plugin-sdk/src/app-contract.ts
index 9b0a8efcf5..a0c9276119 100644
--- a/packages/plugin-sdk/src/app-contract.ts
+++ b/packages/plugin-sdk/src/app-contract.ts
@@ -283,9 +283,10 @@ export interface PluginPendingInteractionRegistration {
export interface PluginSidebarFooterActionContext {
/**
* Navigate to this plugin's detail page in Tools, where declarative settings
- * and `settingsSection` slots render.
+ * and `settingsSection` slots render. Pass a section id to target one of
+ * this plugin's registered settings sections directly.
*/
- openSettings(): void;
+ openSettings(options?: { sectionId?: string }): void;
}
/**
diff --git a/packages/plugin-sdk/src/backend-contract.ts b/packages/plugin-sdk/src/backend-contract.ts
index 57fde780e4..578eddca13 100644
--- a/packages/plugin-sdk/src/backend-contract.ts
+++ b/packages/plugin-sdk/src/backend-contract.ts
@@ -640,6 +640,58 @@ export interface PluginHosts {
declareSharedPorts(hostId: string, ports: readonly number[]): void;
}
+// ---------------------------------------------------------------------------
+// Cloud AI provider (experimental).
+// ---------------------------------------------------------------------------
+
+/** Why a cloud AI call failed; drives the host's local-fallback routing. */
+export type CloudAiFailureCode =
+ | "unauthorized"
+ | "quota_exhausted"
+ | "unavailable";
+
+/**
+ * Result-shaped (never a thrown class) so the contract survives plugin
+ * bundling — an error class would have a different identity in the plugin
+ * bundle than in the host. A provider may still reject on abort; the host
+ * checks its own signal and treats that as a timeout.
+ */
+export type CloudAiResult =
+ | { ok: true; value: T }
+ | { ok: false; code: CloudAiFailureCode; message: string };
+
+export interface CloudAiCompleteArgs {
+ prompt: string;
+ /** JSON Schema (top-level `type: "object"`) the structured result must match. */
+ schema: { [key: string]: JsonValue };
+ /** Host-owned abort signal; the host maps aborts to its timeout semantics. */
+ signal: AbortSignal;
+}
+
+export interface CloudAiTranscribeArgs {
+ file: File;
+ prompt?: string;
+ /** Host-owned abort signal; the host maps aborts to its timeout semantics. */
+ signal: AbortSignal;
+}
+
+/**
+ * A cloud route for bb's small AI tasks (thread titles, commit messages,
+ * voice transcription). When the server's Cloud AI experiment is enabled and
+ * a registered provider `isAvailable()`, the host tries it before the locally
+ * configured `BB_INFERENCE`/`BB_TRANSCRIPTION` providers and falls back to
+ * those on `ok: false`.
+ */
+export interface CloudAiProvider {
+ /**
+ * Cheap synchronous probe consulted per call — pairing state, user setting,
+ * failure latches. The host never calls `complete`/`transcribe` when false.
+ */
+ isAvailable(): boolean;
+ complete(args: CloudAiCompleteArgs): Promise>;
+ transcribe(args: CloudAiTranscribeArgs): Promise>;
+}
+
// ---------------------------------------------------------------------------
// Status + the API root.
// ---------------------------------------------------------------------------
@@ -706,4 +758,10 @@ export interface BbPluginApi {
* The sanctioned place to clear timers and close connections.
*/
onDispose(hook: () => void | Promise): void;
+ /**
+ * EXPERIMENTAL (see docs/api_to_audit.md). Register a cloud route for bb's
+ * small AI tasks. Single slot host-wide: the most recent registration wins;
+ * it is unregistered automatically when this plugin reloads or disables.
+ */
+ experimental_registerCloudAiProvider(provider: CloudAiProvider): void;
}
diff --git a/packages/plugin-sdk/src/testing/fake-plugin-host.ts b/packages/plugin-sdk/src/testing/fake-plugin-host.ts
index 8b8049701a..f5c1957116 100644
--- a/packages/plugin-sdk/src/testing/fake-plugin-host.ts
+++ b/packages/plugin-sdk/src/testing/fake-plugin-host.ts
@@ -1857,6 +1857,11 @@ function createFakePluginHostInternal(
assertLive();
disposeHooks.push(hook);
},
+ experimental_registerCloudAiProvider() {
+ assertLive();
+ // The fake host has no AI chokepoints to route; registration is
+ // accepted and torn down with the host like the real one.
+ },
};
async function disposeHost(cleanupStorage: boolean): Promise {
diff --git a/packages/scripts/test/dev-instance-expectations.ts b/packages/scripts/test/dev-instance-expectations.ts
index 3bb73de030..b6726ecc97 100644
--- a/packages/scripts/test/dev-instance-expectations.ts
+++ b/packages/scripts/test/dev-instance-expectations.ts
@@ -3,6 +3,8 @@ import { isAbsolute, join, relative } from "node:path";
export interface ExpectedDevPortSet {
appPort: number;
+ cloudConnectPort: number;
+ cloudWebPort: number;
hostDaemonPort: number;
serverPort: number;
}
@@ -26,10 +28,20 @@ function expectedPortOffset(repoRoot: string): number {
);
}
+function expectedCloudConnectPort(offset: number): number {
+ let port = 35_000 + offset;
+ for (const reservedPort of [38_886, 38_887]) {
+ if (port >= reservedPort) port += 1;
+ }
+ return port;
+}
+
export function expectedDevPorts(repoRoot: string): ExpectedDevPortSet {
const offset = expectedPortOffset(repoRoot);
return {
appPort: 11_000 + offset,
+ cloudConnectPort: expectedCloudConnectPort(offset),
+ cloudWebPort: 43_002 + offset,
hostDaemonPort: 27_000 + offset,
serverPort: 19_000 + offset,
};
diff --git a/packages/scripts/test/run-dev.test.ts b/packages/scripts/test/run-dev.test.ts
index ccd2075f7f..82f5c5b7e4 100644
--- a/packages/scripts/test/run-dev.test.ts
+++ b/packages/scripts/test/run-dev.test.ts
@@ -57,7 +57,7 @@ describe("run-dev", () => {
expect(config.dataDir).toBe(expectedDevDataDir({ homeDir, repoRoot }));
expect(config.ports).toEqual(expectedDevPorts(repoRoot));
expect(config.serverUrl).toBe(expectedDevServerUrl(repoRoot));
- expect(new Set(Object.values(config.ports))).toHaveLength(3);
+ expect(new Set(Object.values(config.ports))).toHaveLength(5);
expect(Object.values(config.ports)).not.toContain(5173);
expect(Object.values(config.ports)).not.toContain(3334);
expect(Object.values(config.ports)).not.toContain(3002);
diff --git a/packages/shared-ui/src/components/ui/icon.tsx b/packages/shared-ui/src/components/ui/icon.tsx
index 0e7c1ca82c..6d5e71e93e 100644
--- a/packages/shared-ui/src/components/ui/icon.tsx
+++ b/packages/shared-ui/src/components/ui/icon.tsx
@@ -309,6 +309,7 @@ const ICON_MAP = {
CircleQuestion: HelpCircleIcon,
CircleX: CancelCircleIcon,
Clock: Clock01Icon,
+ Cloud: CloudIcon,
Code: SourceCodeIcon,
ComputerTerminal01: ComputerTerminal01Icon,
Columns2: LayoutTwoColumnIcon,
diff --git a/packages/templates/src/generated/plugin-sdk-dts.generated.ts b/packages/templates/src/generated/plugin-sdk-dts.generated.ts
index 6200c0e728..2ae86f72fd 100644
--- a/packages/templates/src/generated/plugin-sdk-dts.generated.ts
+++ b/packages/templates/src/generated/plugin-sdk-dts.generated.ts
@@ -2,6 +2,6 @@
// Generated by packages/templates/scripts/generate-templates.mjs from
// @bb/plugin-sdk/bundled-types. Do not edit directly.
-export const PLUGIN_SDK_DTS = "// Portable type declarations for `@bb/plugin-sdk`. Unpublished BB\n// workspace contracts are flattened; public subpaths may reuse the\n// package root without requiring any other @bb/* package.\n//\n// Confused by the API, or need a symbol that isn't here? Clone the BB repo\n// and read the real source: https://github.com/get-bb/bb\n\nimport * as react from 'react';\nimport { ComponentType, ReactNode } from 'react';\nimport * as z from 'zod';\nimport { z as z$1 } from 'zod';\nimport Database from 'better-sqlite3';\nimport { Context } from 'hono';\n\n/**\n * App-wide server-backed preferences.\n * Client-local settings stay in the frontend localStorage helpers instead.\n */\ndeclare const appSettingsSchema: z$1.ZodObject<{\n caffeinate: z$1.ZodBoolean;\n showKeyboardHints: z$1.ZodBoolean;\n steerActiveThreadOnEnter: z$1.ZodBoolean;\n showUnhandledProviderEvents: z$1.ZodBoolean;\n codexMemoryEnabled: z$1.ZodBoolean;\n claudeCodeMemoryEnabled: z$1.ZodBoolean;\n codexSubagentsDisabled: z$1.ZodBoolean;\n claudeCodeSubagentsDisabled: z$1.ZodBoolean;\n claudeCodeWorkflowsDisabled: z$1.ZodBoolean;\n onboardingCompletedAt: z$1.ZodNullable;\n}, z$1.core.$strict>;\ntype AppSettings = z$1.infer;\n\ndeclare const appKeybindingOverridesSchema: z$1.ZodArray;\n shortcut: z$1.ZodNullable>;\n}, z$1.core.$strict>>;\ntype AppKeybindingOverrides = z$1.infer;\n\ndeclare const appThemeSchema: z$1.ZodObject<{\n themeId: z$1.ZodString;\n customCss: z$1.ZodNullable;\n faviconColor: z$1.ZodEnum<{\n default: \"default\";\n red: \"red\";\n orange: \"orange\";\n yellow: \"yellow\";\n green: \"green\";\n teal: \"teal\";\n blue: \"blue\";\n purple: \"purple\";\n pink: \"pink\";\n }>;\n}, z$1.core.$strip>;\ntype AppTheme = z$1.infer;\n/**\n * The complete appearance selection a client sends when changing the palette\n * and/or favicon tint. The server validates `themeId` (built-in id or an\n * existing custom theme) and resolves the CSS from disk for custom themes.\n * Callers changing only one facet must carry the other facet forward explicitly.\n */\ndeclare const appThemeSelectionSchema: z$1.ZodObject<{\n themeId: z$1.ZodString;\n faviconColor: z$1.ZodEnum<{\n default: \"default\";\n red: \"red\";\n orange: \"orange\";\n yellow: \"yellow\";\n green: \"green\";\n teal: \"teal\";\n blue: \"blue\";\n purple: \"purple\";\n pink: \"pink\";\n }>;\n}, z$1.core.$strip>;\ntype AppThemeSelection = z$1.infer;\n\ndeclare const changedMessageSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{\n type: z$1.ZodLiteral<\"changed\">;\n entity: z$1.ZodLiteral<\"thread\">;\n id: z$1.ZodOptional;\n metadata: z$1.ZodOptional;\n eventTypes: z$1.ZodOptional>>>>;\n hasPendingInteraction: z$1.ZodOptional;\n projectId: z$1.ZodOptional;\n }, z$1.core.$strict>>;\n changes: z$1.ZodReadonly>>;\n}, z$1.core.$strict>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"changed\">;\n entity: z$1.ZodLiteral<\"project\">;\n id: z$1.ZodOptional;\n changes: z$1.ZodReadonly>>;\n}, z$1.core.$strict>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"changed\">;\n entity: z$1.ZodLiteral<\"environment\">;\n id: z$1.ZodOptional;\n changes: z$1.ZodReadonly>>;\n}, z$1.core.$strict>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"changed\">;\n entity: z$1.ZodLiteral<\"host\">;\n id: z$1.ZodOptional;\n changes: z$1.ZodReadonly>>;\n}, z$1.core.$strict>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"changed\">;\n entity: z$1.ZodLiteral<\"system\">;\n changes: z$1.ZodReadonly>>;\n}, z$1.core.$strict>], \"entity\">;\ntype ChangedMessage = z$1.infer;\n\ndeclare const environmentSchema: z$1.ZodObject<{\n id: z$1.ZodString;\n name: z$1.ZodNullable;\n projectId: z$1.ZodString;\n hostId: z$1.ZodString;\n path: z$1.ZodNullable;\n managed: z$1.ZodBoolean;\n isGitRepo: z$1.ZodBoolean;\n isWorktree: z$1.ZodBoolean;\n workspaceProvisionType: z$1.ZodEnum<{\n unmanaged: \"unmanaged\";\n \"managed-worktree\": \"managed-worktree\";\n personal: \"personal\";\n }>;\n branchName: z$1.ZodNullable;\n baseBranch: z$1.ZodNullable;\n defaultBranch: z$1.ZodNullable;\n mergeBaseBranch: z$1.ZodNullable;\n status: z$1.ZodEnum<{\n error: \"error\";\n provisioning: \"provisioning\";\n ready: \"ready\";\n retiring: \"retiring\";\n destroying: \"destroying\";\n destroyed: \"destroyed\";\n }>;\n createdAt: z$1.ZodNumber;\n updatedAt: z$1.ZodNumber;\n}, z$1.core.$strip>;\ntype Environment = z$1.infer;\n\n/**\n * User-opt-in experiments (the Settings → Experiments toggles). Distinct from\n * `FeatureFlags`: flags are operator-set via env at server start, experiments\n * are user-toggled at runtime and persisted server-side so server-owned\n * policy (e.g. skill injection) can honor them.\n *\n * Every experiment defaults to off — opting in is the point.\n */\ndeclare const experimentsSchema: z$1.ZodObject<{\n claudeCodeMockCliTraffic: z$1.ZodBoolean;\n newOnboarding: z$1.ZodBoolean;\n toolsHub: z$1.ZodBoolean;\n}, z$1.core.$strip>;\ntype Experiments = z$1.infer;\n\ndeclare const hostSchema: z$1.ZodObject<{\n id: z$1.ZodString;\n name: z$1.ZodString;\n type: z$1.ZodEnum<{\n persistent: \"persistent\";\n }>;\n status: z$1.ZodEnum<{\n connected: \"connected\";\n disconnected: \"disconnected\";\n }>;\n maxPermissionMode: z$1.ZodEnum<{\n full: \"full\";\n auto: \"auto\";\n \"accept-edits\": \"accept-edits\";\n }>;\n lastSeenAt: z$1.ZodNullable;\n lastRejectedProtocolVersion: z$1.ZodNullable;\n createdAt: z$1.ZodNumber;\n updatedAt: z$1.ZodNumber;\n}, z$1.core.$strip>;\ntype Host = z$1.infer;\n\ninterface JsonObject {\n [key: string]: JsonValue$1;\n}\ntype JsonValue$1 = string | number | boolean | null | JsonValue$1[] | JsonObject;\n\ndeclare const pendingInteractionResolutionSchema: z$1.ZodUnion;\n grantedPermissions: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n fileSystem: z$1.ZodNullable;\n write: z$1.ZodArray;\n }, z$1.core.$strip>>;\n }, z$1.core.$strict>>;\n}, z$1.core.$strip>, z$1.ZodObject<{\n decision: z$1.ZodLiteral<\"allow_for_session\">;\n grantedPermissions: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n fileSystem: z$1.ZodNullable;\n write: z$1.ZodArray;\n }, z$1.core.$strip>>;\n }, z$1.core.$strict>>;\n}, z$1.core.$strip>, z$1.ZodObject<{\n decision: z$1.ZodLiteral<\"deny\">;\n}, z$1.core.$strip>], \"decision\">, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"user_answer\">;\n answers: z$1.ZodRecord;\n freeText: z$1.ZodOptional;\n }, z$1.core.$strip>>;\n}, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"plugin_submitted\">;\n}, z$1.core.$strip>]>;\ntype PendingInteractionResolution = z$1.infer;\ndeclare const providerPendingInteractionSchema: z$1.ZodObject<{\n id: z$1.ZodString;\n threadId: z$1.ZodString;\n status: z$1.ZodEnum<{\n pending: \"pending\";\n interrupted: \"interrupted\";\n resolving: \"resolving\";\n resolved: \"resolved\";\n }>;\n statusReason: z$1.ZodNullable;\n createdAt: z$1.ZodNumber;\n expiresAt: z$1.ZodOptional>;\n resolvedAt: z$1.ZodNullable;\n turnId: z$1.ZodString;\n providerId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n providerRequestId: z$1.ZodString;\n origin: z$1.ZodOptional;\n providerId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n providerRequestId: z$1.ZodString;\n }, z$1.core.$strip>>;\n payload: z$1.ZodUnion;\n subject: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"command\">;\n itemId: z$1.ZodString;\n command: z$1.ZodString;\n cwd: z$1.ZodNullable;\n actions: z$1.ZodArray;\n command: z$1.ZodString;\n name: z$1.ZodString;\n path: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"listFiles\">;\n command: z$1.ZodString;\n path: z$1.ZodNullable;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"search\">;\n command: z$1.ZodString;\n query: z$1.ZodNullable;\n path: z$1.ZodNullable;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"unknown\">;\n command: z$1.ZodString;\n }, z$1.core.$strip>], \"type\">>;\n sessionGrant: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n fileSystem: z$1.ZodNullable;\n write: z$1.ZodArray;\n }, z$1.core.$strip>>;\n }, z$1.core.$strict>>;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"file_change\">;\n itemId: z$1.ZodString;\n writeScope: z$1.ZodNullable;\n sessionGrant: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n fileSystem: z$1.ZodNullable;\n write: z$1.ZodArray;\n }, z$1.core.$strip>>;\n }, z$1.core.$strict>>;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"permission_grant\">;\n itemId: z$1.ZodString;\n toolName: z$1.ZodNullable;\n permissions: z$1.ZodObject<{\n network: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n fileSystem: z$1.ZodNullable;\n write: z$1.ZodArray;\n }, z$1.core.$strip>>;\n }, z$1.core.$strict>;\n }, z$1.core.$strip>], \"kind\">;\n reason: z$1.ZodNullable;\n availableDecisions: z$1.ZodArray>;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"user_question\">;\n questions: z$1.ZodArray;\n multiSelect: z$1.ZodBoolean;\n options: z$1.ZodOptional;\n }, z$1.core.$strip>>>;\n allowFreeText: z$1.ZodBoolean;\n }, z$1.core.$strip>>;\n }, z$1.core.$strip>]>;\n resolution: z$1.ZodNullable;\n grantedPermissions: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n fileSystem: z$1.ZodNullable;\n write: z$1.ZodArray;\n }, z$1.core.$strip>>;\n }, z$1.core.$strict>>;\n }, z$1.core.$strip>, z$1.ZodObject<{\n decision: z$1.ZodLiteral<\"allow_for_session\">;\n grantedPermissions: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n fileSystem: z$1.ZodNullable;\n write: z$1.ZodArray;\n }, z$1.core.$strip>>;\n }, z$1.core.$strict>>;\n }, z$1.core.$strip>, z$1.ZodObject<{\n decision: z$1.ZodLiteral<\"deny\">;\n }, z$1.core.$strip>], \"decision\">, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"user_answer\">;\n answers: z$1.ZodRecord;\n freeText: z$1.ZodOptional;\n }, z$1.core.$strip>>;\n }, z$1.core.$strip>]>>;\n}, z$1.core.$strip>;\ntype ProviderPendingInteraction = z$1.infer;\ndeclare const pluginPendingInteractionSchema: z$1.ZodObject<{\n id: z$1.ZodString;\n threadId: z$1.ZodString;\n status: z$1.ZodEnum<{\n pending: \"pending\";\n interrupted: \"interrupted\";\n resolving: \"resolving\";\n resolved: \"resolved\";\n }>;\n statusReason: z$1.ZodNullable;\n createdAt: z$1.ZodNumber;\n expiresAt: z$1.ZodOptional>;\n resolvedAt: z$1.ZodNullable;\n turnId: z$1.ZodNullable;\n origin: z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"plugin\">;\n pluginId: z$1.ZodString;\n rendererId: z$1.ZodString;\n }, z$1.core.$strip>;\n payload: z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"plugin\">;\n title: z$1.ZodString;\n data: z$1.ZodType>;\n }, z$1.core.$strip>;\n resolution: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n}, z$1.core.$strip>;\ntype PluginPendingInteraction = z$1.infer;\ntype PendingInteraction = ProviderPendingInteraction | PluginPendingInteraction;\n\ndeclare const projectSourceSchema: z$1.ZodObject<{\n id: z$1.ZodString;\n projectId: z$1.ZodString;\n isDefault: z$1.ZodBoolean;\n createdAt: z$1.ZodNumber;\n updatedAt: z$1.ZodNumber;\n type: z$1.ZodLiteral<\"local_path\">;\n hostId: z$1.ZodString;\n path: z$1.ZodString;\n}, z$1.core.$strip>;\ntype ProjectSource = z$1.infer;\n\ndeclare const reasoningLevelSchema: z$1.ZodEnum<{\n none: \"none\";\n low: \"low\";\n medium: \"medium\";\n high: \"high\";\n xhigh: \"xhigh\";\n ultracode: \"ultracode\";\n max: \"max\";\n ultra: \"ultra\";\n}>;\ntype ReasoningLevel = z$1.infer;\ndeclare const serviceTierSchema: z$1.ZodEnum<{\n default: \"default\";\n fast: \"fast\";\n}>;\ntype ServiceTier = z$1.infer;\ndeclare const permissionModeSchema: z$1.ZodEnum<{\n full: \"full\";\n auto: \"auto\";\n \"accept-edits\": \"accept-edits\";\n}>;\ntype PermissionMode = z$1.infer;\ndeclare const promptInputSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{\n visibility: z$1.ZodOptional>;\n type: z$1.ZodLiteral<\"text\">;\n text: z$1.ZodString;\n mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"thread\">;\n threadId: z$1.ZodString;\n projectId: z$1.ZodOptional;\n label: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"project\">;\n projectId: z$1.ZodString;\n label: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"section\">;\n sectionId: z$1.ZodString;\n label: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"path\">;\n source: z$1.ZodEnum<{\n workspace: \"workspace\";\n \"thread-storage\": \"thread-storage\";\n }>;\n entryKind: z$1.ZodEnum<{\n file: \"file\";\n directory: \"directory\";\n }>;\n path: z$1.ZodString;\n label: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"command\">;\n trigger: z$1.ZodEnum<{\n \"/\": \"/\";\n }>;\n name: z$1.ZodString;\n source: z$1.ZodEnum<{\n command: \"command\";\n skill: \"skill\";\n }>;\n origin: z$1.ZodEnum<{\n user: \"user\";\n project: \"project\";\n builtin: \"builtin\";\n }>;\n label: z$1.ZodString;\n argumentHint: z$1.ZodNullable;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"plugin\">;\n pluginId: z$1.ZodString;\n icon: z$1.ZodOptional>;\n itemId: z$1.ZodString;\n label: z$1.ZodString;\n }, z$1.core.$strip>], \"kind\">>;\n }, z$1.core.$strip>>>;\n}, z$1.core.$strip>, z$1.ZodObject<{\n visibility: z$1.ZodOptional>;\n type: z$1.ZodLiteral<\"image\">;\n url: z$1.ZodString;\n}, z$1.core.$strip>, z$1.ZodObject<{\n visibility: z$1.ZodOptional>;\n type: z$1.ZodLiteral<\"localImage\">;\n path: z$1.ZodString;\n}, z$1.core.$strip>, z$1.ZodObject<{\n visibility: z$1.ZodOptional>;\n type: z$1.ZodLiteral<\"localFile\">;\n path: z$1.ZodString;\n name: z$1.ZodOptional;\n sizeBytes: z$1.ZodOptional;\n mimeType: z$1.ZodOptional;\n}, z$1.core.$strip>], \"type\">;\ntype PromptInput = z$1.infer;\ndeclare const resolvedThreadExecutionOptionsSchema: z$1.ZodObject<{\n seq: z$1.ZodOptional;\n model: z$1.ZodString;\n serviceTier: z$1.ZodEnum<{\n default: \"default\";\n fast: \"fast\";\n }>;\n reasoningLevel: z$1.ZodEnum<{\n none: \"none\";\n low: \"low\";\n medium: \"medium\";\n high: \"high\";\n xhigh: \"xhigh\";\n ultracode: \"ultracode\";\n max: \"max\";\n ultra: \"ultra\";\n }>;\n permissionMode: z$1.ZodEnum<{\n full: \"full\";\n auto: \"auto\";\n \"accept-edits\": \"accept-edits\";\n }>;\n source: z$1.ZodEnum<{\n \"client/thread/start\": \"client/thread/start\";\n \"client/turn/requested\": \"client/turn/requested\";\n \"client/turn/start\": \"client/turn/start\";\n }>;\n}, z$1.core.$strip>;\ntype ResolvedThreadExecutionOptions = z$1.infer;\ndeclare const projectExecutionDefaultsSchema: z$1.ZodObject<{\n providerId: z$1.ZodString;\n model: z$1.ZodString;\n serviceTier: z$1.ZodEnum<{\n default: \"default\";\n fast: \"fast\";\n }>;\n reasoningLevel: z$1.ZodEnum<{\n none: \"none\";\n low: \"low\";\n medium: \"medium\";\n high: \"high\";\n xhigh: \"xhigh\";\n ultracode: \"ultracode\";\n max: \"max\";\n ultra: \"ultra\";\n }>;\n permissionMode: z$1.ZodEnum<{\n full: \"full\";\n auto: \"auto\";\n \"accept-edits\": \"accept-edits\";\n }>;\n}, z$1.core.$strip>;\ntype ProjectExecutionDefaults = z$1.infer;\n\n/** All thread events — provider-originated or system-originated. */\ndeclare const threadEventSchema: z$1.ZodPipe;\n threadId: z$1.ZodString;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"thread/identity\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"turn/started\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n parentToolCallId: z$1.ZodOptional;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"turn/completed\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodNullable;\n status: z$1.ZodEnum<{\n completed: \"completed\";\n failed: \"failed\";\n interrupted: \"interrupted\";\n }>;\n error: z$1.ZodOptional>;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"turn/input/accepted\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n clientRequestId: z$1.ZodString;\n scope: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"thread\">;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"turn\">;\n turnId: z$1.ZodString;\n }, z$1.core.$strip>], \"kind\">;\n}, z$1.core.$strict>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"thread/name/updated\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n threadName: z$1.ZodString;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"thread/compacted\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"thread/goal/updated\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n objective: z$1.ZodString;\n status: z$1.ZodEnum<{\n paused: \"paused\";\n active: \"active\";\n budgetLimited: \"budgetLimited\";\n complete: \"complete\";\n }>;\n tokenBudget: z$1.ZodNullable;\n tokensUsed: z$1.ZodNumber;\n timeUsedSeconds: z$1.ZodNumber;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"thread/goal/cleared\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"item/started\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n item: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{\n type: z$1.ZodLiteral<\"userMessage\">;\n id: z$1.ZodString;\n content: z$1.ZodArray;\n text: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"image\">;\n url: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"localImage\">;\n path: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"localFile\">;\n path: z$1.ZodString;\n }, z$1.core.$strip>], \"type\">>;\n clientRequestId: z$1.ZodOptional;\n parentToolCallId: z$1.ZodOptional