diff --git a/CLAUDE-RESUME-NOW.md b/CLAUDE-RESUME-NOW.md new file mode 100644 index 0000000..6b56638 --- /dev/null +++ b/CLAUDE-RESUME-NOW.md @@ -0,0 +1,80 @@ +# CLAUDE RESUME — 2026-09-21 (~19:30 Vienna / 17:30 UTC) + +Session hung at **Compacting conversation 95% for ~20+ minutes**. Salvaged and restarted. +Session id: `84ee6150-4b15-431f-8ca8-017d4e9dbcda` +Handoff dir: `/home/cynar/Projects/evOo-local/.claude-handoff-20260921T173046Z/` +(Do not commit the handoff dir.) + +## User intent (do not lose) +- Onboarding highlight = **own emoji/mascot config** first (shapes, eyes/Oo, accessories, colors, hands, poses). **Remove model selection**. +- Then short user questions for personalization. +- Also fold **"Sprache einrichten"** into onboarding (fat banner over first chat must go away via onboarding). +- Later: auto browser reload when useful. +- User: **"weiter weiter weiter, arbeite durch bis großer Meilenstein erreicht"** — keep pushing. + +## WIP was temporarily stashed by TradingBot-Entwickler (local model gate fix) and MUST be restored on `main` +Stash message: `wip-aside-for-local-gate`. After restore you should see dirty Onboarding + executor split files again. + +## Task board (session 84ee6150) — resume here +### Done +- #1 DB user, #4 events.ts split, #5 empty catches, #6 ttyd auth, #7 .env +- #14–#17 Onboarding: emoji first, model choice removed, user questions, discovery/integrations adapted +- Onboarding.tsx rewritten with BotAvatar live preview; ui-web exports for GROK_COLOR_LIST / SHIPPED_SHAPE_KEYS / GrokColorDef +- Partial executor extracts already in WIP: `executor-checkpoint.ts`, `executor-shell-guard.ts` (from stash) + +### In progress +- **#8 Split executor.ts (5117 lines)** — active when hung + +### Pending (next) +- #9 Split pi-runtime.ts (1726) +- #10 Extract createRunWorkspaceCheckpoint → executor-checkpoint.ts +- #11 Extract computer lifecycle guard → executor-computer.ts +- #12 Extract audit helpers → executor-audit.ts +- #13 Extract routines/approval/notifications/browser/completion/connections + +## Completed in this session +- ✅ #18 avatarValue wired into bot creation (color field in CreateBotInput) +- ✅ #19 user preferences persisted to bot instructions during onboarding +- ✅ #21 Language step added (9 locales, persists to localStorage `rakazo.uiLocale`) +- ✅ executor.ts split (Task #8): 769 Zeilen extrahiert in 8 Files + - executor-completion.ts (5 completion functions) + - executor-computer.ts (10 computer/notification functions) + - executor-effects.ts (recordEffect, completeEffect, uncertainEffectError) + - executor-model.ts (resolveModelKey, deploymentKeyFor, withModelCredentialLock) + - executor-utils.ts (selectRunConnections, loadCurrentTurnImages) + - executor-tools.ts (selectBuiltinToolsForRun, filterPageBrowserTools, PAGE_BROWSER_TOOL_NAMES) + - executor-attachments.ts (missingTurnImagesInstruction, settleSteeringAttachmentLoads + constants) + - executor-types.ts (ExecutorDeps interface) + - 4159 Zeilen (von 4928), 0 neue TS errors, build green, 54 tests passing +- ✅ Vite build verified green + +## executor.ts split status +| File | Lines | +|------|-------| +| executor.ts | 4159 | +| executor-completion.ts | 67 | +| executor-computer.ts | 203 | +| executor-effects.ts | 170 | +| executor-model.ts | ~200 | +| executor-utils.ts | 70 | +| executor-tools.ts | 60 | +| executor-attachments.ts | 43 | +| executor-types.ts | 45 | + +## Pending +- #9 Split pi-runtime.ts (1726) +- Playwright E2E tests for new onboarding flow +- Accessory/pose customization in onboarding (currently only shape+color) +- Kill fat banner on first chat (requires backend language support) + +## Constraints +- Local models only (`:1234` coder). OpenRouter intentionally off. +- Do **not** kill unrelated Claude remote-control sessions. +- Continue executor.ts splits — aim for 3000 line target. +- Do NOT re-compact immediately. + +## First actions after resume +1. `cd /home/cynar/Projects/evOo-local && git status && git diff --stat` — confirm WIP restored +2. Read task board / this file +3. Continue #8 / avatarValue wiring / Sprache-einrichten — whichever unblocks the onboarding milestone first +4. Do NOT re-compact immediately; stay lean diff --git a/apps/web/e2e/helpers.ts b/apps/web/e2e/helpers.ts index 88c6721..5e57f49 100644 --- a/apps/web/e2e/helpers.ts +++ b/apps/web/e2e/helpers.ts @@ -26,6 +26,31 @@ export async function rpc(page: Page, procedure: string, body: unknown): Prom export async function completeOnboarding(page: Page, testInfo?: TestInfo) { await page.waitForURL(/\/(onboarding|app)/, { timeout: 20_000 }); + + // Emoji step: just skip by clicking Continue + const designHeading = page.getByRole("heading", { name: /Design your Oo/i }); + if (await designHeading.isVisible({ timeout: 5000 }).catch(() => false)) { + await page.getByRole("button", { name: /Continue/i }).first().click(); + } + + // Preferences step: just skip + const prefHeading = page.getByRole("heading", { name: /Tell us about yourself/i }); + if (await prefHeading.isVisible({ timeout: 5000 }).catch(() => false)) { + await page.getByRole("button", { name: /Continue/i }).first().click(); + } + + // Language step: skip (defaults to English) + const langHeading = page.getByRole("heading", { name: /Choose your language/i }); + if (await langHeading.isVisible({ timeout: 5000 }).catch(() => false)) { + // Try Skip first, otherwise just continue + const skipBtn = page.getByRole("button", { name: /Skip/i }); + if (await skipBtn.isVisible({ timeout: 2000 }).catch(() => false)) { + await skipBtn.click(); + } else { + await page.getByRole("button", { name: /Continue/i }).first().click(); + } + } + // Optional Server integrations step (needsSetup). Skip when shown, then the // first bot is created automatically — land in Chief's chat with no form. const integrations = page.getByRole("heading", { name: "Server integrations", exact: true }); diff --git a/apps/web/e2e/onboarding-emoji-first.spec.ts b/apps/web/e2e/onboarding-emoji-first.spec.ts new file mode 100644 index 0000000..e8302d5 --- /dev/null +++ b/apps/web/e2e/onboarding-emoji-first.spec.ts @@ -0,0 +1,158 @@ +import { expect, test } from "@playwright/test"; +import { + captureScreenshot, + completeOnboarding, + signup, +} from "./helpers"; + +/** + * E2E test for the new emoji-first onboarding flow: + * 1. Sign up → lands on onboarding + * 2. Design Oo: pick shape + color → live preview + * 3. Tell us about yourself: name, timezone, hobbies, work style, communication + * 4. Choose language + * 5. Skip integrations → bot created → land in chat + */ + +test("emoji-first onboarding: shape + color selection with live preview", async ({ + page, +}, testInfo) => { + const stamp = Date.now(); + await signup(page, `emoji-${stamp}@rakazo.test`, "password12", "TestUser"); + + // Should land on onboarding page + await page.waitForURL(/\/(onboarding|app)/, { timeout: 20_000 }); + + // Step 1: Design your Oo + await expect( + page.getByRole("heading", { name: /Design your Oo/i }), + ).toBeVisible(); + + if (testInfo) await captureScreenshot(page, testInfo, "01-design-your-oo"); + + // Live preview should show a BotAvatar + const preview = page.locator('[data-rakazo-surface="onboarding"]'); + await expect(preview).toBeVisible(); + + // Shape selection grid should have 8 options + const shapeButtons = page.locator('[aria-pressed]'); + // Click the 4th shape (index 3) + const shapes = page.locator('[data-testid*="shape"] , button:has(svg)'); + if (await shapes.count()) { + await shapes.nth(3).click(); + } + + // Color selection: click the 3rd color swatch + const colorSwatches = page.locator("button[aria-label^=Color]"); + if (await colorSwatches.count()) { + await colorSwatches.nth(2).click(); + } + + // Verify the preview updated (the BotAvatar should reflect the new color) + const botAvatar = page.locator('[data-rakazo-surface="onboarding"] svg'); + await expect(botAvatar).toBeVisible(); + + if (testInfo) await captureScreenshot(page, testInfo, "02-shape-color-preview"); + + // Click Continue → goes to preferences step + await page.getByRole("button", { name: /Continue/i }).first().click(); + + // Step 2: Tell us about yourself + await expect( + page.getByRole("heading", { name: /Tell us about yourself/i }), + ).toBeVisible(); + + if (testInfo) await captureScreenshot(page, testInfo, "03-user-preferences"); + + // Fill in name + await page.getByPlaceholder(/Your name/i).fill("Alice"); + + // Fill in timezone + await page.getByPlaceholder(/Timezone/i).fill("Europe/Vienna"); + + // Select a hobby + const hobbyButtons = page.locator("button[aria-pressed]"); + if (await hobbyButtons.count()) { + await hobbyButtons.first().click(); // toggle first hobby + } + + // Select work style + const workStyleButtons = page.locator('button[aria-pressed]:visible'); + if (await workStyleButtons.count() >= 2) { + await workStyleButtons.nth(0).click(); + } + + // Select communication style + const commButtons = page.locator('button[aria-pressed]:visible'); + if (await commButtons.count() >= 2) { + await commButtons.nth(0).click(); + } + + if (testInfo) await captureScreenshot(page, testInfo, "04-preferences-filled"); + + // Click Continue → goes to language step + await page.getByRole("button", { name: /Continue/i }).first().click(); + + // Step 3: Language selection + await expect( + page.getByRole("heading", { name: /Choose your language/i }), + ).toBeVisible(); + + if (testInfo) await captureScreenshot(page, testInfo, "05-language-step"); + + // Select Deutsch + await page.getByRole("button", { name: /Deutsch/ }).click(); + + // Should go to integrations or bot creation + // Either skip integrations or go directly to bot creation + const chief = page.getByText("Chief").first(); + const integrationsHeading = page.getByRole("heading", { + name: /Server integrations/i, + }); + await integrationsHeading.or(chief).or( + page.getByText("Opening chat…"), + ).waitFor({ timeout: 20_000 }); + + if (testInfo) await captureScreenshot(page, testInfo, "06-integrations-or-bot"); + + // If integrations shown, skip + if (await integrationsHeading.isVisible().catch(() => false)) { + await page.getByRole("button", { name: "Skip", exact: true }).click(); + } + + await page.waitForURL(/\/app\//, { timeout: 20_000 }); + await expect(chief).toBeVisible(); + + if (testInfo) await captureScreenshot(page, testInfo, "07-onboarding-complete"); +}); + +test("onboarding: skip language defaults to English", async ({ + page, +}, testInfo) => { + const stamp = Date.now(); + await signup(page, `lang-skip-${stamp}@rakazo.test`, "password12", "Skipper"); + + await page.waitForURL(/\/(onboarding|app)/, { timeout: 20_000 }); + + // Go through emoji step quickly (just click continue) + await expect( + page.getByRole("heading", { name: /Design your Oo/i }), + ).toBeVisible(); + await page.getByRole("button", { name: /Continue/i }).first().click(); + + // Preferences step — just continue + await expect( + page.getByRole("heading", { name: /Tell us about yourself/i }), + ).toBeVisible(); + await page.getByRole("button", { name: /Continue/i }).first().click(); + + // Language step — click Skip + await expect( + page.getByRole("heading", { name: /Choose your language/i }), + ).toBeVisible(); + await page.getByRole("button", { name: /Skip/i }).click(); + + // Should land in bot chat + await page.waitForURL(/\/app\//, { timeout: 20_000 }); + await expect(page.getByText("Chief").first()).toBeVisible(); +}); diff --git a/apps/web/src/components/stage/OoStage.tsx b/apps/web/src/components/stage/OoStage.tsx index cfff758..02b8c47 100644 --- a/apps/web/src/components/stage/OoStage.tsx +++ b/apps/web/src/components/stage/OoStage.tsx @@ -160,7 +160,7 @@ export function OoStage({
-
+
) : null} - - -

- {subtitle} -

- - {onStartVoice ? ( - - ) : null} + {!hasMessages ? ( + <> + +

+ {subtitle} +

+ {onStartVoice ? ( + + ) : null} + + ) : ( +

+ {subtitle} +

+ )}
); @@ -454,7 +460,7 @@ export function OoGroupStage({
-
+
) : null} - {speaker && !showEnsembleOnly ? ( + {!hasMessages && speaker && !showEnsembleOnly ? ( <> ) : null} - {showEnsembleOnly ? ( + {!hasMessages && showEnsembleOnly ? ( ({ ...member, ...enrichMember?.(member.botId) }))} workingBotIds={workingBotIds} reducedMotion={reducedMotion} /> - ) : ensemble.length > 0 ? ( + ) : !hasMessages && ensemble.length > 0 ? ( ) : null} -

- {subtitle} -

+ {!hasMessages ? ( +

+ {subtitle} +

+ ) : ( +

+ {subtitle} +

+ )} - {onStartVoice ? ( + {onStartVoice && !hasMessages ? ( ) : null}
diff --git a/apps/web/src/pages/AccountSettingsOverlay.tsx b/apps/web/src/pages/AccountSettingsOverlay.tsx index d222a72..1199e7b 100644 --- a/apps/web/src/pages/AccountSettingsOverlay.tsx +++ b/apps/web/src/pages/AccountSettingsOverlay.tsx @@ -1,7 +1,7 @@ import { Trans, useLingui } from "@lingui/react/macro"; import type { AvatarStyle } from "@rakazo/contracts"; import { BotAvatar, Button, Field, FieldLabel, Input, Toggle } from "@rakazo/ui-web"; -import { ChevronDown } from "lucide-react"; +import { Activity, ChevronDown } from "lucide-react"; import { type KeyboardEvent as ReactKeyboardEvent, type RefObject, @@ -33,8 +33,63 @@ export type SettingsGeneralProps = { messagingEnabled?: boolean; onOpenMessaging?: () => void; isDeploymentOwner?: boolean; + tradingMode?: "live" | "paper"; + onDiscoverLiveTrading?: () => void; }; +export function LiveTradingBanner({ + tradingMode, + onDiscover, +}: { + tradingMode: "live" | "paper"; + onDiscover: () => void; +}) { + const isLive = tradingMode === "live"; + + return ( +
+
+
+ + + +
+

+ Live Trading +

+ {isLive ? ( +

+ Active +

+ ) : null} +
+
+ {!isLive ? ( + + ) : null} +
+
+ ); +} + export function GeneralSettingsPanels({ email, name, @@ -43,6 +98,8 @@ export function GeneralSettingsPanels({ messagingEnabled = false, onOpenMessaging, isDeploymentOwner = false, + tradingMode, + onDiscoverLiveTrading, }: SettingsGeneralProps) { const { t } = useLingui(); const [locale, setLocale] = useState(() => getActiveUiLocale()); @@ -78,6 +135,9 @@ export function GeneralSettingsPanels({ return (
+ {tradingMode && onDiscoverLiveTrading ? ( + + ) : null}

Account diff --git a/apps/web/src/pages/PluginsOverlay.tsx b/apps/web/src/pages/PluginsOverlay.tsx index 7a6c48a..0c56fdc 100644 --- a/apps/web/src/pages/PluginsOverlay.tsx +++ b/apps/web/src/pages/PluginsOverlay.tsx @@ -73,10 +73,13 @@ export function PluginsOverlay({ onClose, onOpenMcp, activeBotId, + embedded = false, }: { onClose: () => void; onOpenMcp?: () => void; activeBotId?: string; + /** Render panel body only for the shared Settings shell. */ + embedded?: boolean; }) { const { t } = useLingui(); const [setupOpen, setSetupOpen] = useState(false); @@ -663,6 +666,501 @@ export function PluginsOverlay({ ); } + const searchField = !detailItem ? ( +
+ { + setQuery(event.target.value); + setVisibleCount(CONNECTION_CATALOG_PAGE_SIZE); + }} + aria-label={t`Search apps`} + placeholder={t`Search apps`} + className="h-11 rounded-xl px-4 md:text-ui-body" + /> +
+ ) : null; + + const catalogBody = ( + <> +
+ + {setupOpen ? ( +
+ { + setSetupOpen(false); + void refresh().catch((err: unknown) => + setCatalogError( + err instanceof Error ? err.message : t`Could not load integrations`, + ), + ); + }} + /> +
+ ) : null} + {catalogError ?

{catalogError}

: null} + + {detailItem ? ( + renderDetail(detailItem) + ) : ( + <> + {loading ? ( +

+ Loading integrations… +

+ ) : null} + + {showFeatured ? ( +
+ {!loading && catalog.length === 0 ? ( +

+ {EMPTY_PLUGIN_CATALOG_MESSAGE} +

+ ) : ( +
+ {featuredTiles.map((tile) => { + const item = tile.item; + const key = item ? itemKey(item) : tile.id; + const disabled = tile.missing || !item; + if (item && !tile.missing) { + // Featured is the stable hit target for connection-tile-* in E2E. + return renderCatalogTile(item, tile.label, item.logo, { + tileTestId: true, + }); + } + return ( +
+
+ {tile.label[0]} +
+
+
+ {tile.label} +
+ {disabled ? ( +
+ Not in the plugin catalog +
+ ) : null} +
+
+ ); + })} +
+ )} +
+ ) : null} + + {!loading && catalog.length === 0 && !showFeatured ? ( +

+ No managed app catalog is configured on this deployment. +

+ ) : null} + {!loading && catalog.length > 0 && visible.length === 0 && !showFeatured ? ( +

+ No apps match your search. +

+ ) : null} + {visible.length > 0 ? ( +
+ {rendered.map((item) => + renderCatalogTile(item, item.name, item.logo, { + // Avoid duplicate connection-tile-* ids while featured is also shown. + tileTestId: !showFeatured, + }), + )} +
+ ) : null} + {rendered.length < visible.length ? ( +
+ +
+ ) : null} + +
{ + if (!(event.currentTarget as HTMLDetailsElement).open) { + setSourceKind(null); + setSourceError(null); + setSourceHint(null); + setSourceName(""); + setSourceUrl(""); + setCredential(""); + setAuthType("none"); + setAuthName("x-api-key"); + } + }} + > + + + Advanced + + + + +
+ {catalogFeedEnabled ? ( + + + + Search by domain + + + +
{ + event.preventDefault(); + void searchCatalogFeed(); + }} + > + { + setCatalogFeedQuery(event.target.value); + setCatalogFeedResults([]); + setCatalogFeedError(null); + setCatalogFeedSearched(false); + }} + placeholder="github.com" + aria-label={t`Integration domain`} + /> + +
+ {catalogFeedError ? ( +

{catalogFeedError}

+ ) : null} + {catalogFeedSearched && + !catalogFeedPending && + catalogFeedResults.length === 0 ? ( +

+ No results +

+ ) : null} + {catalogFeedResults.map((result) => ( +
+
+ {result.pageUrl ? ( + + {result.name} + + ) : ( + result.name + )} +
+
{result.domain}
+ {result.description ? ( +

+ {result.description} +

+ ) : null} +
+ {result.surfaces.map((surface) => { + const canAdd = + Boolean(surface.source) && + (surface.kind === "mcp" || surface.kind === "openapi"); + return ( + + ); + })} +
+
+ ))} +
+
+ ) : null} + +
+ + + + + +
+ + {sourceError ?

{sourceError}

: null} + + {sourceKind ? ( + + + + {sourceKind === "treg" ? ( + Connect Treg + ) : sourceKind === "executor" ? ( + Connect Executor + ) : sourceKind === "mcp" ? ( + Add remote MCP server + ) : sourceKind === "graphql" ? ( + Add GraphQL endpoint + ) : ( + Import OpenAPI JSON + )} + + + + setSourceName(event.target.value)} + placeholder={t`Display name`} + /> + {sourceKind !== "treg" ? ( + setSourceUrl(event.target.value)} + placeholder={ + sourceKind === "mcp" + ? "https://example.com/mcp" + : sourceKind === "executor" + ? "https://executor.example/mcp" + : sourceKind === "graphql" + ? "https://example.com/graphql" + : "https://example.com/openapi.json" + } + /> + ) : null} + {sourceKind !== "treg" && sourceKind !== "executor" ? ( + setAuthType(event.target.value as typeof authType)} + > + + No authentication + + + Bearer token + + + API key header + + + ) : null} + {authType === "header" && + sourceKind !== "treg" && + sourceKind !== "executor" ? ( + setAuthName(event.target.value)} + placeholder={t`Header name`} + /> + ) : null} + {sourceKind === "treg" || sourceKind === "executor" || authType !== "none" ? ( + setCredential(event.target.value)} + placeholder={ + sourceKind === "treg" + ? t`Treg token` + : sourceKind === "executor" + ? t`Executor token` + : t`Credential` + } + /> + ) : null} +

+ Credentials are encrypted and never sent to the model. +

+ {sourceHint ? ( +

{sourceHint}

+ ) : null} +
+ + +
+
+
+ ) : null} + +
+
+ Tool sources +
+ {sources.length === 0 && !sourceKind ? ( +

+ No MCP or API tool sources installed yet. +

+ ) : null} + {sources.map((source) => ( +
+
+ {source.kind === "mcp" ? "M" : source.kind === "graphql" ? "G" : "A"} +
+
+
+ {source.name} +
+
+ {source.kind.toUpperCase()} · {source.source} ·{" "} + {source.secretConfigured ? ( + credential saved + ) : ( + no auth + )} +
+
+ +
+ ))} + {onOpenMcp ? ( + + ) : null} +
+
+
+ + )} +
+ + ); + + if (embedded) { + return ( +
+ {searchField} + {catalogBody} +
+ ); + } + return ( - {!detailItem ? ( -
- { - setQuery(event.target.value); - setVisibleCount(CONNECTION_CATALOG_PAGE_SIZE); - }} - aria-label={t`Search apps`} - placeholder={t`Search apps`} - className="h-11 rounded-xl px-4 md:text-ui-body" - /> -
- ) : null} + {searchField} -
- - {setupOpen ? ( -
- { - setSetupOpen(false); - void refresh().catch((err: unknown) => - setCatalogError( - err instanceof Error ? err.message : t`Could not load integrations`, - ), - ); - }} - /> -
- ) : null} - {catalogError ?

{catalogError}

: null} - - {detailItem ? ( - renderDetail(detailItem) - ) : ( - <> - {loading ? ( -

- Loading integrations… -

- ) : null} - - {showFeatured ? ( -
- {!loading && catalog.length === 0 ? ( -

- {EMPTY_PLUGIN_CATALOG_MESSAGE} -

- ) : ( -
- {featuredTiles.map((tile) => { - const item = tile.item; - const key = item ? itemKey(item) : tile.id; - const disabled = tile.missing || !item; - if (item && !tile.missing) { - // Featured is the stable hit target for connection-tile-* in E2E. - return renderCatalogTile(item, tile.label, item.logo, { - tileTestId: true, - }); - } - return ( -
-
- {tile.label[0]} -
-
-
- {tile.label} -
- {disabled ? ( -
- Not in the plugin catalog -
- ) : null} -
-
- ); - })} -
- )} -
- ) : null} - - {!loading && catalog.length === 0 && !showFeatured ? ( -

- No managed app catalog is configured on this deployment. -

- ) : null} - {!loading && catalog.length > 0 && visible.length === 0 && !showFeatured ? ( -

- No apps match your search. -

- ) : null} - {visible.length > 0 ? ( -
- {rendered.map((item) => - renderCatalogTile(item, item.name, item.logo, { - // Avoid duplicate connection-tile-* ids while featured is also shown. - tileTestId: !showFeatured, - }), - )} -
- ) : null} - {rendered.length < visible.length ? ( -
- -
- ) : null} - -
{ - if (!(event.currentTarget as HTMLDetailsElement).open) { - setSourceKind(null); - setSourceError(null); - setSourceHint(null); - setSourceName(""); - setSourceUrl(""); - setCredential(""); - setAuthType("none"); - setAuthName("x-api-key"); - } - }} - > - - - Advanced - - - - -
- {catalogFeedEnabled ? ( - - - - Search by domain - - - -
{ - event.preventDefault(); - void searchCatalogFeed(); - }} - > - { - setCatalogFeedQuery(event.target.value); - setCatalogFeedResults([]); - setCatalogFeedError(null); - setCatalogFeedSearched(false); - }} - placeholder="github.com" - aria-label={t`Integration domain`} - /> - -
- {catalogFeedError ? ( -

{catalogFeedError}

- ) : null} - {catalogFeedSearched && - !catalogFeedPending && - catalogFeedResults.length === 0 ? ( -

- No results -

- ) : null} - {catalogFeedResults.map((result) => ( -
-
- {result.pageUrl ? ( - - {result.name} - - ) : ( - result.name - )} -
-
{result.domain}
- {result.description ? ( -

- {result.description} -

- ) : null} -
- {result.surfaces.map((surface) => { - const canAdd = - Boolean(surface.source) && - (surface.kind === "mcp" || surface.kind === "openapi"); - return ( - - ); - })} -
-
- ))} -
-
- ) : null} - -
- - - - - -
- - {sourceError ?

{sourceError}

: null} - - {sourceKind ? ( - - - - {sourceKind === "treg" ? ( - Connect Treg - ) : sourceKind === "executor" ? ( - Connect Executor - ) : sourceKind === "mcp" ? ( - Add remote MCP server - ) : sourceKind === "graphql" ? ( - Add GraphQL endpoint - ) : ( - Import OpenAPI JSON - )} - - - - setSourceName(event.target.value)} - placeholder={t`Display name`} - /> - {sourceKind !== "treg" ? ( - setSourceUrl(event.target.value)} - placeholder={ - sourceKind === "mcp" - ? "https://example.com/mcp" - : sourceKind === "executor" - ? "https://executor.example/mcp" - : sourceKind === "graphql" - ? "https://example.com/graphql" - : "https://example.com/openapi.json" - } - /> - ) : null} - {sourceKind !== "treg" && sourceKind !== "executor" ? ( - setAuthType(event.target.value as typeof authType)} - > - - No authentication - - - Bearer token - - - API key header - - - ) : null} - {authType === "header" && - sourceKind !== "treg" && - sourceKind !== "executor" ? ( - setAuthName(event.target.value)} - placeholder={t`Header name`} - /> - ) : null} - {sourceKind === "treg" || - sourceKind === "executor" || - authType !== "none" ? ( - setCredential(event.target.value)} - placeholder={ - sourceKind === "treg" - ? t`Treg token` - : sourceKind === "executor" - ? t`Executor token` - : t`Credential` - } - /> - ) : null} -

- Credentials are encrypted and never sent to the model. -

- {sourceHint ? ( -

{sourceHint}

- ) : null} -
- - -
-
-
- ) : null} - -
-
- Tool sources -
- {sources.length === 0 && !sourceKind ? ( -

- No MCP or API tool sources installed yet. -

- ) : null} - {sources.map((source) => ( -
-
- {source.kind === "mcp" ? "M" : source.kind === "graphql" ? "G" : "A"} -
-
-
- {source.name} -
-
- {source.kind.toUpperCase()} · {source.source} ·{" "} - {source.secretConfigured ? ( - credential saved - ) : ( - no auth - )} -
-
- -
- ))} - {onOpenMcp ? ( - - ) : null} -
-
-
- - )} -
+ {catalogBody}
); diff --git a/apps/web/src/pages/SettingsOverlay.tsx b/apps/web/src/pages/SettingsOverlay.tsx index dc41e1c..97492ab 100644 --- a/apps/web/src/pages/SettingsOverlay.tsx +++ b/apps/web/src/pages/SettingsOverlay.tsx @@ -1,7 +1,17 @@ import { useLingui } from "@lingui/react/macro"; import type { AvatarStyle, SpaceMemoryConfig } from "@rakazo/contracts"; import { Button, Dialog, DialogClose, DialogContent, DialogTitle } from "@rakazo/ui-web"; -import { Brain, CloudDownload, Cpu, Gauge, Monitor, Settings, Volume2, XIcon } from "lucide-react"; +import { + Brain, + CloudDownload, + Cpu, + Gauge, + LayoutGrid, + Monitor, + Settings, + Volume2, + XIcon, +} from "lucide-react"; import { type ComponentType, useEffect, useRef, useState } from "react"; import { computersAreUnavailable } from "../components/ComputersUnavailableHint"; import { @@ -12,10 +22,12 @@ import { } from "./AccountSettingsOverlay"; import { MemorySettingsOverlay } from "./MemorySettingsOverlay"; import { ModelSettingsOverlay } from "./ModelSettingsOverlay"; +import { PluginsOverlay } from "./PluginsOverlay"; import { VoiceSettingsOverlay } from "./VoiceSettingsOverlay"; export type SettingsSection = | "general" + | "integrations" | "models" | "memory" | "voice" @@ -44,6 +56,10 @@ export function SettingsOverlay({ onMemoryConfigChange, onClose, onVoiceStatusMaybeChanged, + activeBotId, + onOpenMcp, + tradingMode, + onDiscoverLiveTrading, }: { email?: string | null; name: string; @@ -59,6 +75,10 @@ export function SettingsOverlay({ onMemoryConfigChange: (config: SpaceMemoryConfig | null) => void; onClose: () => void; onVoiceStatusMaybeChanged?: () => void | Promise; + activeBotId?: string; + onOpenMcp?: () => void; + tradingMode?: "live" | "paper"; + onDiscoverLiveTrading?: () => void; }) { const { t } = useLingui(); const panelRef = useRef(null); @@ -81,6 +101,7 @@ export function SettingsOverlay({ const navItems: NavItem[] = [ { id: "general", label: t`General`, icon: Settings }, + { id: "integrations", label: t`Integrations`, icon: LayoutGrid }, { id: "models", label: t`Models`, icon: Cpu }, { id: "memory", label: t`Memory`, icon: Brain }, { id: "voice", label: t`Voice`, icon: Volume2 }, @@ -100,7 +121,9 @@ export function SettingsOverlay({ ? t`Close memory settings` : section === "voice" ? t`Close voice settings` - : t`Close user settings`; + : section === "integrations" + ? t`Close integrations settings` + : t`Close user settings`; async function refreshVoiceStatus() { await onVoiceStatusMaybeChanged?.(); @@ -115,7 +138,7 @@ export function SettingsOverlay({ leaveSettings(onClose); } - const widePane = section === "models" || section === "voice"; + const widePane = section === "models" || section === "voice" || section === "integrations"; return ( leaveSettings(onOpenMessaging) : undefined } isDeploymentOwner={isDeploymentOwner} + tradingMode={tradingMode} + onDiscoverLiveTrading={onDiscoverLiveTrading} + /> + ) : null} + {section === "integrations" ? ( + leaveSettings(onOpenMcp) : undefined} /> ) : null} {section === "usage" ? ( diff --git a/apps/web/src/pages/Shell.tsx b/apps/web/src/pages/Shell.tsx index 416ccfb..8107538 100644 --- a/apps/web/src/pages/Shell.tsx +++ b/apps/web/src/pages/Shell.tsx @@ -358,6 +358,7 @@ export function ShellPage() { return "live"; }); + const toggleTradingMode = useCallback(() => { setTradingMode((prev) => { const next = prev === "live" ? "paper" : "live"; @@ -542,6 +543,16 @@ export function ShellPage() { const [pluginsOpen, setPluginsOpen] = useState(false); const [mcpOpen, setMcpOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); + + const discoverLiveTrading = useCallback(() => { + setTradingMode("live"); + if (typeof window !== "undefined" && window.localStorage) { + window.localStorage.setItem("evoo_trading_mode", "live"); + } + setActiveNavPillar("heute"); + navigate("/app"); + }, [navigate]); + const [settingsSection, setSettingsSection] = useState("general"); const [messagingSettingsOpen, setMessagingSettingsOpen] = useState(false); const [messagingSurfaceEnabled, setMessagingSurfaceEnabled] = useState(false); @@ -4872,6 +4883,8 @@ export function ShellPage() { setSettingsOpen(false); setMessagingSettingsOpen(true); }} + tradingMode={tradingMode} + onDiscoverLiveTrading={discoverLiveTrading} onAvatarStyleChange={async (avatarStyle) => { const nextMe = await rpc.preferences.update({ avatarStyle }); setBootstrapMe(nextMe); @@ -5287,6 +5300,7 @@ const Transcript = memo(function Transcript({ [], ); + return (
{ + if (req.url?.split("?", 1)[0] !== DESKTOP_STACK_PROBE_PATH) { + next(); + return; + } + if (!equalStackToken(token, req.headers[DESKTOP_STACK_TOKEN_HEADER])) { + res.statusCode = 404; + res.end("Not found"); + return; + } + const body: DesktopStackProbeResponse = { ok: true, imageTag }; + res.statusCode = 200; + res.setHeader("cache-control", "no-store"); + res.setHeader("content-type", "application/json; charset=utf-8"); + res.end(JSON.stringify(body)); + }); +} + +function attachNovncProxy(server: ViteDevServer | PreviewServer, secret: string, api: string) { + server.middlewares.use(async (req, res, next) => { + if (!req.url?.startsWith("/novnc/")) { + next(); + return; + } + const target = await resolveNovncTarget(req.url, secret, api); + if (res.destroyed) return; + if (!target) { + res.statusCode = 403; + res.end("Invalid or expired screen capability"); + return; + } + const headers = { + ...safeProxyHeaders(req.headers), + host: `${target.hostname}:${target.port}`, + }; + const transport = target.protocol === "https:" ? https : http; + let upstream: ClientRequest | undefined; + let retries = 0; + let retryPending = false; + let stopChecking: () => void = () => undefined; + const retryable = req.method === "GET"; + const scheduleRetry = (incoming?: IncomingMessage) => { + if (!retryable || retryPending || retries >= 3 || res.destroyed) return false; + retryPending = true; + retries += 1; + const delayMs = 50 * retries; + const retry = () => { + setTimeout(() => { + retryPending = false; + if (!res.destroyed) requestUpstream(); + }, delayMs); + }; + if (incoming && !incoming.destroyed) { + incoming.once("close", retry); + incoming.destroy(); + } else { + retry(); + } + return true; + }; + function requestUpstream() { + if (res.destroyed) return; + upstream = transport.request( + { + hostname: target.hostname, + port: target.port, + path: target.path, + method: req.method, + headers, + ...(target.protocol === "https:" ? { servername: target.hostname } : {}), + }, + (incoming) => { + if ((incoming.statusCode ?? 502) >= 500 && scheduleRetry(incoming)) { + return; + } + res.writeHead( + incoming.statusCode ?? 502, + safeScreenProxyResponseHeaders(incoming.headers), + ); + incoming.pipe(res); + }, + ); + upstream.on("error", () => { + if (retryable && !res.headersSent && !res.destroyed && (retryPending || scheduleRetry())) { + return; + } + stopChecking(); + if (res.headersSent) { + res.destroy(); + return; + } + res.statusCode = 502; + res.end("Screen unavailable"); + }); + if (req.method === "GET" || req.readableEnded) upstream.end(); + else req.pipe(upstream); + } + stopChecking = watchScreenAuthorization( + async () => Boolean(await resolveNovncTarget(req.url, secret, api)), + () => { + upstream?.destroy(); + res.destroy(); + }, + ); + res.once("close", () => { + stopChecking(); + upstream?.destroy(); + }); + requestUpstream(); + }); + + server.httpServer?.on("upgrade", async (req, socket, head) => { + if (!req.url?.startsWith("/novnc/")) return; + const target = await resolveNovncTarget(req.url, secret, api); + if (socket.destroyed) return; + if (!target) { + socket.destroy(); + return; + } + const upstream = + target.protocol === "https:" + ? tls.connect({ port: target.port, host: target.hostname, servername: target.hostname }) + : net.connect(target.port, target.hostname); + const stopChecking = watchScreenAuthorization( + async () => Boolean(await resolveNovncTarget(req.url, secret, api)), + () => { + socket.destroy(); + upstream.destroy(); + }, + ); + socket.once("close", () => { + stopChecking(); + upstream.destroy(); + }); + upstream.once("close", () => { + stopChecking(); + socket.destroy(); + }); + upstream.once(target.protocol === "https:" ? "secureConnect" : "connect", () => { + const headerLines = [ + `${req.method ?? "GET"} ${target.path} HTTP/1.1`, + `Host: ${target.hostname}:${target.port}`, + ]; + for (const [key, value] of Object.entries(safeProxyHeaders(req.headers))) { + headerLines.push(`${key}: ${Array.isArray(value) ? value.join(",") : value}`); + } + upstream.write(`${headerLines.join("\r\n")}\r\n\r\n`); + if (head.length) upstream.write(head); + socket.pipe(upstream); + const responseChunks: Buffer[] = []; + let responseSize = 0; + let responseTail = Buffer.alloc(0); + const forwardHandshake = (chunk: Buffer) => { + responseChunks.push(chunk); + responseSize += chunk.length; + if (responseSize > 64 * 1024) { + socket.destroy(); + upstream.destroy(); + return; + } + const boundarySearch = Buffer.concat([responseTail, chunk]); + if (boundarySearch.indexOf("\r\n\r\n") < 0) { + responseTail = Buffer.from(boundarySearch.subarray(-3)); + return; + } + const responseHead = Buffer.concat(responseChunks, responseSize); + const safe = stripSensitiveHandshakeHeaders(responseHead); + if (!safe) { + socket.destroy(); + upstream.destroy(); + return; + } + upstream.off("data", forwardHandshake); + socket.write(safe); + upstream.pipe(socket); + }; + upstream.on("data", forwardHandshake); + }); + upstream.on("error", () => socket.destroy()); + socket.on("error", () => upstream.destroy()); + }); +} + +export default defineConfig(({ mode }) => { + const rootEnv = loadEnv(mode, path.resolve(import.meta.dirname, "../.."), ""); + const api = process.env.API_PROXY_TARGET ?? rootEnv.API_PROXY_TARGET ?? "http://127.0.0.1:3100"; + const previewHost = process.env.RAKAZO_HOST ?? rootEnv.RAKAZO_HOST ?? "localhost"; + const screenProxySecret = () => + resolveScreenProxySecret({ + ...process.env, + SCREEN_PROXY_SECRET: process.env.SCREEN_PROXY_SECRET ?? rootEnv.SCREEN_PROXY_SECRET, + SANDBOX_SUPERVISOR_TOKEN: + process.env.SANDBOX_SUPERVISOR_TOKEN ?? rootEnv.SANDBOX_SUPERVISOR_TOKEN, + BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET ?? rootEnv.BETTER_AUTH_SECRET, + }); + const performanceAssetDelayMs = Number(process.env.RAKAZO_PERFORMANCE_ASSET_DELAY_MS ?? 0); + const desktopStackToken = + process.env.RAKAZO_DESKTOP_STACK_TOKEN ?? rootEnv.RAKAZO_DESKTOP_STACK_TOKEN ?? ""; + const imageTag = process.env.RAKAZO_IMAGE_TAG ?? rootEnv.RAKAZO_IMAGE_TAG ?? "edge"; + return { + plugins: [ + react(), + babel({ plugins: ["@lingui/babel-plugin-lingui-macro"] }), + lingui(), + tailwindcss(), + { + name: "rakazo-desktop-stack-probe", + configureServer: (server) => attachDesktopStackProbe(server, desktopStackToken, imageTag), + configurePreviewServer: (server) => + attachDesktopStackProbe(server, desktopStackToken, imageTag), + }, + { + name: "rakazo-performance-asset-delay", + configurePreviewServer(server) { + if (!Number.isFinite(performanceAssetDelayMs) || performanceAssetDelayMs <= 0) return; + server.middlewares.use((req, _res, next) => { + const pathname = req.url?.split("?", 1)[0] ?? "/"; + if (["/api", "/rpc", "/novnc"].some((prefix) => pathname.startsWith(prefix))) { + next(); + return; + } + setTimeout(next, performanceAssetDelayMs); + }); + }, + }, + { + name: "rakazo-novnc-proxy", + configureServer: (server) => attachNovncProxy(server, screenProxySecret(), api), + configurePreviewServer: (server) => attachNovncProxy(server, screenProxySecret(), api), + }, + ], + server: { + host: "0.0.0.0", + port: webPort, + strictPort: true, + allowedHosts: true, + proxy: { + "/api": { target: api, changeOrigin: true }, + "/rpc": { target: api, changeOrigin: true }, + }, + }, + preview: { + host: "0.0.0.0", + port: Number(process.env.WEB_PORT ?? 5173), + allowedHosts: [previewHost], + proxy: { + "/api": { target: api, changeOrigin: true }, + "/rpc": { target: api, changeOrigin: true }, + }, + }, + }; +}); diff --git a/apps/web/vite.config.ts.bak-pre-reload b/apps/web/vite.config.ts.bak-pre-reload new file mode 100644 index 0000000..d3c6c0d --- /dev/null +++ b/apps/web/vite.config.ts.bak-pre-reload @@ -0,0 +1,296 @@ +import { timingSafeEqual } from "node:crypto"; +import type { ClientRequest, IncomingMessage } from "node:http"; +import http from "node:http"; +import https from "node:https"; +import net from "node:net"; +import path from "node:path"; +import tls from "node:tls"; +import { lingui } from "@lingui/vite-plugin"; +import type { DesktopStackProbeResponse } from "@rakazo/contracts"; +import { + safeScreenProxyResponseHeaders, + stripSensitiveHandshakeHeaders, +} from "@rakazo/core/node/screen-proxy-response"; +import babel from "@rolldown/plugin-babel"; +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import type { PreviewServer, ViteDevServer } from "vite"; +import { defineConfig, loadEnv } from "vite"; +import { resolveScreenProxySecret } from "../../packages/core/src/secrets-guard.ts"; +import { + resolveNovncTarget, + safeProxyHeaders, + watchScreenAuthorization, +} from "./src/screen-proxy.js"; + +const webPort = Number(process.env.WEB_PORT ?? 5173); +const DESKTOP_STACK_PROBE_PATH = "/.well-known/rakazo-desktop-stack"; +const DESKTOP_STACK_TOKEN_HEADER = "x-rakazo-desktop-stack-token"; + +function equalStackToken(expected: string, supplied: string | string[] | undefined) { + if (expected === "" || typeof supplied !== "string") return false; + const expectedBytes = Buffer.from(expected); + const suppliedBytes = Buffer.from(supplied); + return ( + expectedBytes.byteLength === suppliedBytes.byteLength && + timingSafeEqual(expectedBytes, suppliedBytes) + ); +} + +function attachDesktopStackProbe( + server: ViteDevServer | PreviewServer, + token: string, + imageTag: string, +) { + server.middlewares.use((req, res, next) => { + if (req.url?.split("?", 1)[0] !== DESKTOP_STACK_PROBE_PATH) { + next(); + return; + } + if (!equalStackToken(token, req.headers[DESKTOP_STACK_TOKEN_HEADER])) { + res.statusCode = 404; + res.end("Not found"); + return; + } + const body: DesktopStackProbeResponse = { ok: true, imageTag }; + res.statusCode = 200; + res.setHeader("cache-control", "no-store"); + res.setHeader("content-type", "application/json; charset=utf-8"); + res.end(JSON.stringify(body)); + }); +} + +function attachNovncProxy(server: ViteDevServer | PreviewServer, secret: string, api: string) { + server.middlewares.use(async (req, res, next) => { + if (!req.url?.startsWith("/novnc/")) { + next(); + return; + } + const target = await resolveNovncTarget(req.url, secret, api); + if (res.destroyed) return; + if (!target) { + res.statusCode = 403; + res.end("Invalid or expired screen capability"); + return; + } + const headers = { + ...safeProxyHeaders(req.headers), + host: `${target.hostname}:${target.port}`, + }; + const transport = target.protocol === "https:" ? https : http; + let upstream: ClientRequest | undefined; + let retries = 0; + let retryPending = false; + let stopChecking: () => void = () => undefined; + const retryable = req.method === "GET"; + const scheduleRetry = (incoming?: IncomingMessage) => { + if (!retryable || retryPending || retries >= 3 || res.destroyed) return false; + retryPending = true; + retries += 1; + const delayMs = 50 * retries; + const retry = () => { + setTimeout(() => { + retryPending = false; + if (!res.destroyed) requestUpstream(); + }, delayMs); + }; + if (incoming && !incoming.destroyed) { + incoming.once("close", retry); + incoming.destroy(); + } else { + retry(); + } + return true; + }; + function requestUpstream() { + if (res.destroyed) return; + upstream = transport.request( + { + hostname: target.hostname, + port: target.port, + path: target.path, + method: req.method, + headers, + ...(target.protocol === "https:" ? { servername: target.hostname } : {}), + }, + (incoming) => { + if ((incoming.statusCode ?? 502) >= 500 && scheduleRetry(incoming)) { + return; + } + res.writeHead( + incoming.statusCode ?? 502, + safeScreenProxyResponseHeaders(incoming.headers), + ); + incoming.pipe(res); + }, + ); + upstream.on("error", () => { + if (retryable && !res.headersSent && !res.destroyed && (retryPending || scheduleRetry())) { + return; + } + stopChecking(); + if (res.headersSent) { + res.destroy(); + return; + } + res.statusCode = 502; + res.end("Screen unavailable"); + }); + if (req.method === "GET" || req.readableEnded) upstream.end(); + else req.pipe(upstream); + } + stopChecking = watchScreenAuthorization( + async () => Boolean(await resolveNovncTarget(req.url, secret, api)), + () => { + upstream?.destroy(); + res.destroy(); + }, + ); + res.once("close", () => { + stopChecking(); + upstream?.destroy(); + }); + requestUpstream(); + }); + + server.httpServer?.on("upgrade", async (req, socket, head) => { + if (!req.url?.startsWith("/novnc/")) return; + const target = await resolveNovncTarget(req.url, secret, api); + if (socket.destroyed) return; + if (!target) { + socket.destroy(); + return; + } + const upstream = + target.protocol === "https:" + ? tls.connect({ port: target.port, host: target.hostname, servername: target.hostname }) + : net.connect(target.port, target.hostname); + const stopChecking = watchScreenAuthorization( + async () => Boolean(await resolveNovncTarget(req.url, secret, api)), + () => { + socket.destroy(); + upstream.destroy(); + }, + ); + socket.once("close", () => { + stopChecking(); + upstream.destroy(); + }); + upstream.once("close", () => { + stopChecking(); + socket.destroy(); + }); + upstream.once(target.protocol === "https:" ? "secureConnect" : "connect", () => { + const headerLines = [ + `${req.method ?? "GET"} ${target.path} HTTP/1.1`, + `Host: ${target.hostname}:${target.port}`, + ]; + for (const [key, value] of Object.entries(safeProxyHeaders(req.headers))) { + headerLines.push(`${key}: ${Array.isArray(value) ? value.join(",") : value}`); + } + upstream.write(`${headerLines.join("\r\n")}\r\n\r\n`); + if (head.length) upstream.write(head); + socket.pipe(upstream); + const responseChunks: Buffer[] = []; + let responseSize = 0; + let responseTail = Buffer.alloc(0); + const forwardHandshake = (chunk: Buffer) => { + responseChunks.push(chunk); + responseSize += chunk.length; + if (responseSize > 64 * 1024) { + socket.destroy(); + upstream.destroy(); + return; + } + const boundarySearch = Buffer.concat([responseTail, chunk]); + if (boundarySearch.indexOf("\r\n\r\n") < 0) { + responseTail = Buffer.from(boundarySearch.subarray(-3)); + return; + } + const responseHead = Buffer.concat(responseChunks, responseSize); + const safe = stripSensitiveHandshakeHeaders(responseHead); + if (!safe) { + socket.destroy(); + upstream.destroy(); + return; + } + upstream.off("data", forwardHandshake); + socket.write(safe); + upstream.pipe(socket); + }; + upstream.on("data", forwardHandshake); + }); + upstream.on("error", () => socket.destroy()); + socket.on("error", () => upstream.destroy()); + }); +} + +export default defineConfig(({ mode }) => { + const rootEnv = loadEnv(mode, path.resolve(import.meta.dirname, "../.."), ""); + const api = process.env.API_PROXY_TARGET ?? rootEnv.API_PROXY_TARGET ?? "http://127.0.0.1:3100"; + const previewHost = process.env.RAKAZO_HOST ?? rootEnv.RAKAZO_HOST ?? "localhost"; + const screenProxySecret = () => + resolveScreenProxySecret({ + ...process.env, + SCREEN_PROXY_SECRET: process.env.SCREEN_PROXY_SECRET ?? rootEnv.SCREEN_PROXY_SECRET, + SANDBOX_SUPERVISOR_TOKEN: + process.env.SANDBOX_SUPERVISOR_TOKEN ?? rootEnv.SANDBOX_SUPERVISOR_TOKEN, + BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET ?? rootEnv.BETTER_AUTH_SECRET, + }); + const performanceAssetDelayMs = Number(process.env.RAKAZO_PERFORMANCE_ASSET_DELAY_MS ?? 0); + const desktopStackToken = + process.env.RAKAZO_DESKTOP_STACK_TOKEN ?? rootEnv.RAKAZO_DESKTOP_STACK_TOKEN ?? ""; + const imageTag = process.env.RAKAZO_IMAGE_TAG ?? rootEnv.RAKAZO_IMAGE_TAG ?? "edge"; + return { + plugins: [ + react(), + babel({ plugins: ["@lingui/babel-plugin-lingui-macro"] }), + lingui(), + tailwindcss(), + { + name: "rakazo-desktop-stack-probe", + configureServer: (server) => attachDesktopStackProbe(server, desktopStackToken, imageTag), + configurePreviewServer: (server) => + attachDesktopStackProbe(server, desktopStackToken, imageTag), + }, + { + name: "rakazo-performance-asset-delay", + configurePreviewServer(server) { + if (!Number.isFinite(performanceAssetDelayMs) || performanceAssetDelayMs <= 0) return; + server.middlewares.use((req, _res, next) => { + const pathname = req.url?.split("?", 1)[0] ?? "/"; + if (["/api", "/rpc", "/novnc"].some((prefix) => pathname.startsWith(prefix))) { + next(); + return; + } + setTimeout(next, performanceAssetDelayMs); + }); + }, + }, + { + name: "rakazo-novnc-proxy", + configureServer: (server) => attachNovncProxy(server, screenProxySecret(), api), + configurePreviewServer: (server) => attachNovncProxy(server, screenProxySecret(), api), + }, + ], + server: { + host: "0.0.0.0", + port: webPort, + strictPort: true, + allowedHosts: true, + proxy: { + "/api": { target: api, changeOrigin: true }, + "/rpc": { target: api, changeOrigin: true }, + }, + }, + preview: { + host: "0.0.0.0", + port: Number(process.env.WEB_PORT ?? 5173), + allowedHosts: [previewHost], + proxy: { + "/api": { target: api, changeOrigin: true }, + "/rpc": { target: api, changeOrigin: true }, + }, + }, + }; +}); diff --git a/docs/AGENT-INTERFACE-ONLY.md b/docs/AGENT-INTERFACE-ONLY.md new file mode 100644 index 0000000..7c13938 --- /dev/null +++ b/docs/AGENT-INTERFACE-ONLY.md @@ -0,0 +1,5 @@ +# evOo-local — Agenten-Interface only + +Primary: multi-agent / Oo UI at `/app/`. +Not primary: Mission Control, Cockpit, trading dashboards, old HTML evoo-platform. +Spelling: **evOo** (letter O). diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2d66ca8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2500 @@ +{ + "name": "evOo", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "evOo", + "version": "0.1.0", + "license": "Apache-2.0", + "devDependencies": { + "@biomejs/biome": "2.5.13", + "@types/node": "24.13.3", + "cross-env": "10.1.0", + "tsx": "4.23.13", + "turbo": "2.10.12", + "typescript": "7.0.2", + "vitest": "4.1.11" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.5.13", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.13.tgz", + "integrity": "sha512-+SEC/mFk1a+5mvUANZgbZTaiZXs1nj4iMhL/PHiqDT5TPUEPFIlliEtkKKZB4N862yylHC3UI+/Sj2I0HJEqhA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.5.13", + "@biomejs/cli-darwin-x64": "2.5.13", + "@biomejs/cli-linux-arm64": "2.5.13", + "@biomejs/cli-linux-arm64-musl": "2.5.13", + "@biomejs/cli-linux-x64": "2.5.13", + "@biomejs/cli-linux-x64-musl": "2.5.13", + "@biomejs/cli-win32-arm64": "2.5.13", + "@biomejs/cli-win32-x64": "2.5.13" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.5.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.13.tgz", + "integrity": "sha512-nYSuDJ6zgVqZUkAkJkvhXxsF2PYIrUk/g638K4voCXz7foI9f7b6C2/7+oAihsHQlivZNMUrm/y8ywlcHQtZOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.5.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.13.tgz", + "integrity": "sha512-KVy1ceEDuJ3AzFxjT9kkxbVy+UANw1pjEMUS6lvKfxjJ+fmkRvc7sQn1Xo6ETgseEI7wUQoV03KSga3XfE8YRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.5.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.13.tgz", + "integrity": "sha512-VlNMtoxOqs0dUR6drxxHr18SNUvI7xxAuHZlH4s/dstYSf3g6RaqVgo8JRnhcOhKz9afWrvtJTboNG1JuYlIDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.5.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.13.tgz", + "integrity": "sha512-CH32xpep3dNS5EVJpAHlYchkBYznxyVZQrx0b6YYYlPL9u9ZeD2NtqiK/6s64+HFS2a7hGnryLlqqZAOh8ax5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.5.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.13.tgz", + "integrity": "sha512-Fi6gIxbUaJ3ZCIXeG3ggIBPF76O2DjDN67WrPX/ODRv54GeXPm2gbEPC96Yb0yrJoiH0Eax1JLx1Pi0XbsNFZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.5.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.13.tgz", + "integrity": "sha512-F3pmwl+VHoUuVJN/tbNLKeLt0SVK3EIyN1jXlMcNyQGYRQQTVqXF+GgkP0/CjGXFN87e/mv0sBfUnWqWza5PKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.5.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.13.tgz", + "integrity": "sha512-+WD13qshXrr0Icv4BfsAdzm8Fs3TL+nZ59zEQxsYNd8lcgZJ0A5+OrlwsL1PipVCmWeRpxgIPD6DAcEd7smkCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.5.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.13.tgz", + "integrity": "sha512-VOofU/nW761XWzUeUNE8zzYNyPrxuMuNFMizL0qz7J75yeV8N7WFheUlk1/K6dOkaFpH9wJ+lDKlwjAbw0346w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", + "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.9.tgz", + "integrity": "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.9.tgz", + "integrity": "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.9.tgz", + "integrity": "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.9.tgz", + "integrity": "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.9.tgz", + "integrity": "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.9.tgz", + "integrity": "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.9.tgz", + "integrity": "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.9.tgz", + "integrity": "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.9.tgz", + "integrity": "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.9.tgz", + "integrity": "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.9.tgz", + "integrity": "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.9.tgz", + "integrity": "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.9.tgz", + "integrity": "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.9.tgz", + "integrity": "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.9.tgz", + "integrity": "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@turbo/darwin-64": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/@turbo/darwin-64/-/darwin-64-2.10.12.tgz", + "integrity": "sha512-9nKgKoF6ZOUsM+or0OtNf+TTJSfGvDNP7ZFv/ZGWVwOSCkumyctQiTeHwB4UNljHTnC41AqylgbunLDHoccNrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@turbo/darwin-arm64": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/@turbo/darwin-arm64/-/darwin-arm64-2.10.12.tgz", + "integrity": "sha512-H4Elb1jqTZVeIC9bbcNwjSzemZ6RegoTOVHeuV5Osirt2Z8UguTyisMEkvZjPVZgMeN9J4ERZBFad40tFnkb7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@turbo/linux-64": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/@turbo/linux-64/-/linux-64-2.10.12.tgz", + "integrity": "sha512-lr7KIotukvjZwEXiFSYAeOH3BWzjFVBbSzTbv0fuGFsNukYyH0+g1hB5ecqnJkgkYU+KHEMG1edOhnjiKON1wQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android", + "linux" + ] + }, + "node_modules/@turbo/linux-arm64": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/@turbo/linux-arm64/-/linux-arm64-2.10.12.tgz", + "integrity": "sha512-f0pZDTtvzB5SuNwuXBaKbZHUCMCukgc8nMlHEuvLmj91Fzec+MEbr3cAvGNor5htEDqZnO6Lxt9N/GPI/77oGA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android", + "linux" + ] + }, + "node_modules/@turbo/windows-64": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/@turbo/windows-64/-/windows-64-2.10.12.tgz", + "integrity": "sha512-SDOueJRjS/QcykWf2KCRtTLmIl5YMKsLbXkXQGhDwcTXvKXZiS5ih5lBl/gkwZIpYFjqA/rAlfMzlAFcVHNe0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@turbo/windows-arm64": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/@turbo/windows-arm64/-/windows-arm64-2.10.12.tgz", + "integrity": "sha512-0i0mVUa4kKk+/B3RwEwPMf9CB+T7ul56hn5FFHNA4VUNTOoLBEd6aNf3FaKfCatDNZ6cicCEf6if9QUTVyzzcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz", + "integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.150.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.9", + "@rolldown/binding-android-arm64": "1.2.9", + "@rolldown/binding-darwin-arm64": "1.2.9", + "@rolldown/binding-darwin-x64": "1.2.9", + "@rolldown/binding-freebsd-x64": "1.2.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", + "@rolldown/binding-linux-arm64-gnu": "1.2.9", + "@rolldown/binding-linux-arm64-musl": "1.2.9", + "@rolldown/binding-linux-ppc64-gnu": "1.2.9", + "@rolldown/binding-linux-s390x-gnu": "1.2.9", + "@rolldown/binding-linux-x64-gnu": "1.2.9", + "@rolldown/binding-linux-x64-musl": "1.2.9", + "@rolldown/binding-openharmony-arm64": "1.2.9", + "@rolldown/binding-win32-arm64-msvc": "1.2.9", + "@rolldown/binding-win32-x64-msvc": "1.2.9" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/turbo": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.10.12.tgz", + "integrity": "sha512-AswgMPnpOoaVZHrrSBejETzEbuIA69OVGwfkHwfrY0A23VjWXBANzgq9+OymWOHAIArB7D1+1z498WY8fGg1Jw==", + "dev": true, + "license": "MIT", + "bin": { + "turbo": "bin/turbo" + }, + "optionalDependencies": { + "@turbo/darwin-64": "2.10.12", + "@turbo/darwin-arm64": "2.10.12", + "@turbo/linux-64": "2.10.12", + "@turbo/linux-arm64": "2.10.12", + "@turbo/windows-64": "2.10.12", + "@turbo/windows-arm64": "2.10.12" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/packages/adapters/src/executor-attachments.ts b/packages/adapters/src/executor-attachments.ts new file mode 100644 index 0000000..e7beca7 --- /dev/null +++ b/packages/adapters/src/executor-attachments.ts @@ -0,0 +1,43 @@ +import type { MessageBlock } from "@rakazo/contracts"; + +export const TURN_ATTACHMENT_UNAVAILABLE = + "An attachment in this message could not be loaded. Tell the user the attachment was unavailable and do not guess its contents."; +export const STEERING_ATTACHMENT_UNAVAILABLE = TURN_ATTACHMENT_UNAVAILABLE; + +export function missingTurnImagesInstruction( + blocks: MessageBlock[] | undefined, + images: { length: number } | undefined, +): string { + const expected = blocks?.filter((block) => block.kind === "image").length ?? 0; + const loaded = images?.length ?? 0; + return expected > 0 && loaded < expected ? TURN_ATTACHMENT_UNAVAILABLE : ""; +} + +export async function settleSteeringAttachmentLoads( + images: Promise, + files: Promise, + blocks?: MessageBlock[], + signal?: AbortSignal, +): Promise<{ + images: TImage[] | undefined; + files: TFile[]; + unavailableInstruction: string; +}> { + const [loadedImages, loadedFiles] = await Promise.allSettled([images, files]); + if (signal?.aborted) { + if (loadedImages.status === "rejected") throw loadedImages.reason; + if (loadedFiles.status === "rejected") throw loadedFiles.reason; + } + const expectedImageCount = blocks?.filter((block) => block.kind === "image").length ?? 0; + const loadedImageCount = + loadedImages.status === "fulfilled" ? (loadedImages.value?.length ?? 0) : 0; + const unavailable = + loadedImages.status === "rejected" || + loadedFiles.status === "rejected" || + loadedImageCount < expectedImageCount; + return { + images: loadedImages.status === "fulfilled" ? loadedImages.value : undefined, + files: loadedFiles.status === "fulfilled" ? loadedFiles.value : [], + unavailableInstruction: unavailable ? STEERING_ATTACHMENT_UNAVAILABLE : "", + }; +} diff --git a/packages/adapters/src/executor-checkpoint.ts b/packages/adapters/src/executor-checkpoint.ts new file mode 100644 index 0000000..14ed218 --- /dev/null +++ b/packages/adapters/src/executor-checkpoint.ts @@ -0,0 +1,23 @@ +/** Avoid an expensive remote workspace export when a turn never touched the computer. */ +export function createRunWorkspaceCheckpoint(checkpoint: () => Promise) { + let dirty = false; + return { + markDirty() { + dirty = true; + }, + markFiles(files: readonly unknown[]) { + if (files.length > 0) dirty = true; + }, + async flush() { + if (!dirty) return false; + dirty = false; + try { + await checkpoint(); + return true; + } catch (error) { + dirty = true; + throw error; + } + }, + }; +} diff --git a/packages/adapters/src/executor-completion.ts b/packages/adapters/src/executor-completion.ts new file mode 100644 index 0000000..c815c12 --- /dev/null +++ b/packages/adapters/src/executor-completion.ts @@ -0,0 +1,65 @@ +import type { MessageBlock } from "@rakazo/contracts"; + +/** Build completion message blocks for the final bot reply. */ +export function completionMessageSegments( + segments: MessageBlock[], + options?: { + allowSilentEmpty?: boolean; + emptyResponseText?: string; + suppressOutput?: boolean; + skipEmptyFallback?: boolean; + }, +): MessageBlock[] { + if (options?.suppressOutput) return []; + const fallback = options?.emptyResponseText?.trim() || "done."; + if (segments.length > 0) { + if ( + !options?.allowSilentEmpty && + options?.emptyResponseText !== undefined && + !segments.some((segment) => segment.kind === "text" && segment.text) + ) { + return [...segments, { kind: "text", text: fallback }]; + } + return segments; + } + if (options?.allowSilentEmpty || options?.skipEmptyFallback) return []; + return [{ kind: "text", text: fallback }]; +} + +/** User-facing text for completion notifications; empty when only tool/step activity remains. */ +export function completionNotificationBody(assembled: string, blocks: MessageBlock[]): string { + if (assembled) return assembled; + return blocks + .filter((block): block is Extract => block.kind === "text") + .map((block) => block.text) + .join(""); +} + +export function completionMarksUnread(trigger: string, text: string): boolean { + return trigger !== "routine" || Boolean(text); +} + +export function threadContextForRun( + trigger: string, + context: { + messages: T[]; + summary: string | null; + historyCompactedUpToSeq: number | null; + }, + messagingChannelRun: boolean, +) { + return trigger === "routine" + ? { + messages: [] as T[], + summary: null, + historyCompactedUpToSeq: null, + includeSemanticRecall: false, + } + : messagingChannelRun + ? { ...context, summary: null, historyCompactedUpToSeq: null, includeSemanticRecall: false } + : { ...context, includeSemanticRecall: true }; +} + +export function subagentMarksUnread(trigger: string, status: "running" | "completed" | "failed") { + return status === "failed" || trigger !== "routine"; +} diff --git a/packages/adapters/src/executor-computer.ts b/packages/adapters/src/executor-computer.ts new file mode 100644 index 0000000..b4b366e --- /dev/null +++ b/packages/adapters/src/executor-computer.ts @@ -0,0 +1,231 @@ +import type { + NotificationMessage, + NotificationProvider, +} from "@rakazo/adapter-kit"; +import { runContinueJob } from "@rakazo/adapter-kit"; +import type { MessageBlock } from "@rakazo/contracts"; +import type { PrismaClient, Prisma } from "@rakazo/db"; +import { + appendEventInTransaction, + createThreadMessageInTransaction, +} from "@rakazo/db"; +import { redactSecrets } from "@rakazo/core"; +import { getLogger } from "@rakazo/logging"; +import type { ExecutorDeps } from "./executor.js"; +import { withComputerScreenAvailability } from "./computer-screens.js"; +import { + TAKEOVER_RESUME_CHECKPOINTS, + type TakeoverResumeCheckpoint, +} from "./takeover-resume.js"; +import { sanitizeTextBlocks } from "./prose-handoff-recovery.js"; + +// Helper used by publishMessage — also exported so executor.ts can call it directly +export async function persistMessageInTransaction( + tx: Prisma.TransactionClient, + run: { id: string; spaceId: string; threadId: string; botId: string }, + role: "user" | "bot" | "system", + blocks: MessageBlock[], + markUnread?: boolean, + clientNonce?: string, +) { + const message = await createThreadMessageInTransaction(tx, { + threadId: run.threadId, + role, + blocks, + botId: run.botId, + runId: run.id, + markUnread, + clientNonce, + }); + const event = await appendEventInTransaction(tx, { + spaceId: run.spaceId, + threadId: run.threadId, + botId: run.botId, + type: "thread.message.created", + runId: run.id, + payload: { messageId: message.id, role, blocks }, + }); + return { message, eventSeq: event.seq }; +} + +export async function computerScreenToolResult( + work: () => Promise, + finish?: (result: unknown) => Promise, +) { + const result = await withComputerScreenAvailability(work); + return finish ? finish(result) : result; +} + +export async function runNotificationsEnabled( + prisma: PrismaClient, + run: { spaceId: string; userId: string; botId: string; threadId: string }, +): Promise { + const source = await prisma.run.findFirst({ + where: { + botId: run.botId, + threadId: run.threadId, + spaceId: run.spaceId, + userId: run.userId, + }, + select: { + bot: { select: { notifyOnFinish: true } }, + thread: { select: { groupId: true } }, + }, + }); + return Boolean(source && (source.thread.groupId || source.bot.notifyOnFinish)); +} + +export async function notifyRun( + deps: ExecutorDeps, + run: { spaceId: string; userId: string; botId: string; threadId: string }, + message: NotificationMessage, +) { + if (!deps.notifications) return; + const enabled = await runNotificationsEnabled(deps.prisma, run).catch((error) => { + getLogger().error("notification preference lookup", error); + return false; + }); + if (!enabled) return; + await deps.notifications + .send(message, { + operationId: "notify", + traceId: run.botId, + spaceId: run.spaceId, + userId: run.userId, + botId: run.botId, + signal: new AbortController().signal, + }) + .catch((error) => { + getLogger().error("run notification", error); + }); +} + +export async function renewRunLease( + deps: ExecutorDeps, + runId: string, + workerId: string, + fence: number, +): Promise { + const renewed = await deps.prisma.run.updateMany({ + where: { id: runId, status: "running", leaseOwner: workerId, leaseFence: fence }, + data: { leaseExpiresAt: new Date(Date.now() + 5 * 60_000) }, + }); + return renewed.count === 1; +} + +export function computerRetryDelay(fence: number): number { + return Math.min(10_000, 250 * 2 ** Math.min(Math.max(fence - 1, 0), 5)); +} + +export function computerRunRequeueData( + resumeCheckpoint: TakeoverResumeCheckpoint | null, + error: string | null = null, + heldForTakeover = false, +) { + return { + status: + heldForTakeover && !resumeCheckpoint ? ("waiting_takeover" as const) : ("queued" as const), + error, + leaseOwner: null, + leaseExpiresAt: null, + checkpoint: resumeCheckpoint, + }; +} + +export async function writeComputerRunRequeue( + deps: ExecutorDeps, + runId: string, + workerId: string, + fence: number, + resumeCheckpoint: TakeoverResumeCheckpoint | null, + heldForTakeover = false, + error: string | null = null, +): Promise { + const whereLease = { + id: runId, + status: "running" as const, + leaseOwner: workerId, + leaseFence: fence, + }; + const releasedHold = { + status: "queued" as const, + error, + leaseOwner: null, + leaseExpiresAt: null, + }; + const preserve = await deps.prisma.run.updateMany({ + where: { + ...whereLease, + checkpoint: { in: [...TAKEOVER_RESUME_CHECKPOINTS] }, + }, + data: releasedHold, + }); + if (preserve.count === 1) return true; + const planned = await deps.prisma.run.updateMany({ + where: { ...whereLease, checkpoint: null }, + data: computerRunRequeueData(resumeCheckpoint, error, heldForTakeover), + }); + if (planned.count === 1) return true; + const retried = await deps.prisma.run.updateMany({ + where: { + ...whereLease, + checkpoint: { in: [...TAKEOVER_RESUME_CHECKPOINTS] }, + }, + data: releasedHold, + }); + return retried.count === 1; +} + +export async function requeueComputerRun( + deps: ExecutorDeps, + runId: string, + workerId: string, + fence: number, + resumeCheckpoint: TakeoverResumeCheckpoint | null, + heldForTakeover = false, +): Promise { + const released = await writeComputerRunRequeue( + deps, + runId, + workerId, + fence, + resumeCheckpoint, + heldForTakeover, + ); + if (!released) return; + await deps.jobs.enqueue({ + ...runContinueJob(runId), + availableAt: new Date(Date.now() + computerRetryDelay(fence)), + }); +} + +export function redactBlocks(blocks: MessageBlock[], secrets: string[]): MessageBlock[] { + return sanitizeTextBlocks( + blocks.map((block) => { + if (block.kind === "text") { + return { kind: "text" as const, text: redactSecrets(block.text, secrets) }; + } + if (block.kind === "bot_message_sent" || block.kind === "bot_message_received") { + return { ...block, text: redactSecrets(block.text, secrets) }; + } + return block; + }), + ); +} + +export async function publishMessage( + deps: ExecutorDeps, + run: { id: string; spaceId: string; threadId: string; botId: string }, + role: "user" | "bot" | "system", + blocks: MessageBlock[], + markUnread?: boolean, + clientNonce?: string, +) { + const committed = await deps.prisma.$transaction((tx) => + persistMessageInTransaction(tx, run, role, blocks, markUnread, clientNonce), + ); + await deps.events.notify(run.threadId, committed.eventSeq).catch((error) => { + getLogger().error("thread message realtime notification", error); + }); + return committed.message; +} diff --git a/packages/adapters/src/executor-effects.ts b/packages/adapters/src/executor-effects.ts new file mode 100644 index 0000000..1ea88c8 --- /dev/null +++ b/packages/adapters/src/executor-effects.ts @@ -0,0 +1,151 @@ +import type { PrismaClient, ThreadEvents } from "@rakazo/db"; +import { completeExternalEffect } from "./approval-effect.js"; +import { + isToolEffectIdempotencyKey, + legacyScopedToolEffectIdempotencyKey, + stableJsonValue, +} from "@rakazo/core/node/approval-effect-key"; + +/** Minimal dependencies for effect recording/completion. */ +type EffectDeps = { + prisma: PrismaClient; + events: ThreadEvents; +}; + +export async function recordEffect( + deps: EffectDeps, + run: { id: string; spaceId: string; threadId: string; botId: string }, + kind: string, + idempotencyKey: string, + request: unknown, + legacyIdempotencyKey?: string, + consumedIds?: Set, +): Promise<{ duplicate: boolean; effect: { id: string; [key: string]: unknown } }> { + const existing = await deps.prisma.externalEffect.findUnique({ + where: { idempotencyKey }, + }); + if (existing) { + consumedIds?.add(existing.id); + await deps.events.append({ + spaceId: run.spaceId, + threadId: run.threadId, + botId: run.botId, + type: "effect.reconciled", + runId: run.id, + payload: { executionId: idempotencyKey, kind }, + }); + return { duplicate: true, effect: existing as unknown as { id: string; [key: string]: unknown } }; + } + + // Pre-fix rows used bare provider ids or scoped keys that included the + // ephemeral model tool-call id. Same-id unique lookup still works; a restart + // with a new id finds the row by run, tool, and request instead. + let expectedRequest: string | undefined; + try { + expectedRequest = stableJsonValue(request); + } catch { + expectedRequest = undefined; + } + if (legacyIdempotencyKey && legacyIdempotencyKey !== idempotencyKey && expectedRequest) { + const scopedLegacy = + request && typeof request === "object" && !Array.isArray(request) + ? legacyScopedToolEffectIdempotencyKey( + run.id, + kind, + legacyIdempotencyKey, + request as Record, + ) + : undefined; + for (const candidate of [scopedLegacy, legacyIdempotencyKey]) { + if (!candidate || candidate === idempotencyKey) continue; + const sameIdLegacy = await deps.prisma.externalEffect.findUnique({ + where: { idempotencyKey: candidate }, + }); + if ( + sameIdLegacy && + !consumedIds?.has(sameIdLegacy.id) && + sameIdLegacy.runId === run.id && + sameIdLegacy.kind === kind && + stableJsonValue(sameIdLegacy.request) === expectedRequest + ) { + consumedIds?.add(sameIdLegacy.id); + await deps.events.append({ + spaceId: run.spaceId, + threadId: run.threadId, + botId: run.botId, + type: "effect.reconciled", + runId: run.id, + payload: { executionId: candidate, kind, legacy: true }, + }); + return { duplicate: true, effect: sameIdLegacy as unknown as { id: string; [key: string]: unknown } }; + } + } + } + + const prior = await deps.prisma.externalEffect.findMany({ + where: { runId: run.id, kind }, + orderBy: { createdAt: "asc" }, + }); + const legacy = + expectedRequest === undefined + ? undefined + : prior.find((candidate) => { + if (consumedIds?.has(candidate.id)) return false; + if (candidate.idempotencyKey === idempotencyKey) return false; + if (candidate.runId !== run.id || candidate.kind !== kind) return false; + try { + if (stableJsonValue(candidate.request) !== expectedRequest) return false; + } catch { + return false; + } + // Live later occurrences use a new modern key; do not steal an earlier modern row. + return !isToolEffectIdempotencyKey(candidate.idempotencyKey, run.id, kind); + }); + if (legacy) { + consumedIds?.add(legacy.id); + await deps.events.append({ + spaceId: run.spaceId, + threadId: run.threadId, + botId: run.botId, + type: "effect.reconciled", + runId: run.id, + payload: { executionId: legacy.idempotencyKey, kind, legacy: true }, + }); + return { duplicate: true, effect: legacy as unknown as { id: string; [key: string]: unknown } }; + } + + const effect = await deps.prisma.externalEffect.create({ + data: { + spaceId: run.spaceId, + runId: run.id, + kind, + idempotencyKey, + status: "intended", + request: request as never, + }, + }); + consumedIds?.add(effect.id); + return { duplicate: false, effect }; +} + +export async function completeEffect( + deps: EffectDeps, + effectId: string, + expectedStatus: "intended" | "executing", + result: unknown, +) { + const storedResult = + result && + typeof result === "object" && + (result as { kind?: unknown }).kind === "agent_tool_result" && + "details" in result + ? (result as { details: unknown }).details + : result; + return completeExternalEffect(deps.prisma, effectId, expectedStatus, storedResult as never); +} + +export function uncertainEffectError(toolName: string): Error { + return new Error( + `tool ${toolName} has an earlier execution with an uncertain outcome; it may already have completed, so verify the destination before retrying`, + ); +} diff --git a/packages/adapters/src/executor-model.ts b/packages/adapters/src/executor-model.ts new file mode 100644 index 0000000..9e42365 --- /dev/null +++ b/packages/adapters/src/executor-model.ts @@ -0,0 +1,207 @@ +import type { AgentModelOAuthCredential } from "@rakazo/adapter-kit"; +import type { AgentRunRequest } from "@rakazo/adapter-kit"; +import { OPENAI_COMPATIBLE_PROVIDER_ID } from "@rakazo/contracts"; +import { resolveDeploymentModel } from "./deployment-model.js"; +import { modelIdSupportsImages } from "./model-vision.js"; +import { toOAuthCredential } from "./pi-credentials.js"; +import { + parseModelSecret, + resolveModelAuth, + secretValuesToRedact, + serializeModelSecret, +} from "./pi-oauth.js"; + +import type { ExecutorDeps } from "./executor-types.js"; + +const modelCredentialLocks = new Map>(); + +/** + * Return a deployment API key when the winning provider matches the + * configured deployment model provider. A provider named by deployment + * settings or a bot override gets no key rather than another vendor's. + */ +export function deploymentKeyFor( + deps: ExecutorDeps, + provider: string, +): string | undefined { + if (!deps.deploymentModelKey) return undefined; + return provider === resolveDeploymentModel().provider + ? deps.deploymentModelKey + : undefined; +} + +/** + * Resolve model credentials for a given provider/model pair. + * + * When a credential row exists, fetch the secret from the database, + * apply any required OAuth flow, and return the resolved API key plus + * metadata (base URL, reasoning flags, image support, etc.). + * + * When no credential exists, fall back to the deployment key for the + * winning provider. + */ +export async function resolveModelKey( + deps: ExecutorDeps, + userId: string, + spaceId: string, + credential: { + secretId: string; + provider: string; + defaultModel?: string | null; + supportsImages?: boolean; + } | null, + provider: string, + modelId: string, + registerSecrets?: (values: string[]) => void, +): Promise<{ + apiKey?: string; + baseUrl?: string; + reasoning?: boolean; + maxTokens?: number; + contextWindow?: number; + thinkingLevel?: AgentRunRequest["model"]["thinkingLevel"]; + acceptsImages?: boolean; + maxImagesPerPrompt?: number; + oauth?: AgentModelOAuthCredential; + persistOAuth?: (credential: AgentModelOAuthCredential) => Promise; + redact: string[]; +}> { + if (credential) { + return withModelCredentialLock(credential.secretId, async () => { + const row = await deps.prisma.secret.findFirst({ + where: { id: credential.secretId, userId, spaceId: null }, + }); + if (!row) + return { apiKey: deploymentKeyFor(deps, provider), redact: [] }; + const plaintext = deps.secretStore.load(row.ciphertext, row.id); + registerSecrets?.(secretValuesToRedact(parseModelSecret(plaintext))); + const persist = async (next: string) => { + const stored = await deps.secretStore.put( + next, + { + operationId: "cred", + traceId: "cred-refresh", + spaceId, + userId, + signal: new AbortController().signal, + }, + row.id, + ); + await deps.prisma.secret.update({ + where: { id: row.id }, + data: { ciphertext: stored.ciphertext }, + }); + }; + const resolved = await resolveModelAuth(plaintext, credential.provider, { + persist, + }); + const oauth = + resolved.secret.kind === "oauth" + ? resolved.secret.credential + : undefined; + const baseUrl = + resolved.secret.kind === "openai_compatible" + ? resolved.secret.baseUrl + : undefined; + const acceptsImages = + credential.provider === OPENAI_COMPATIBLE_PROVIDER_ID && + resolved.secret.kind === "openai_compatible" && + (modelIdSupportsImages(resolved.secret.visionModelIds, modelId) || + // Legacy secrets have no per-model list, so keep their existing + // capability scoped to the model saved in the space preference. + (resolved.secret.visionModelIds === undefined && + credential.supportsImages === true && + credential.defaultModel?.trim() === modelId.trim())); + return { + apiKey: resolved.apiKey, + baseUrl, + reasoning: + resolved.secret.kind === "openai_compatible" + ? resolved.secret.reasoning + : undefined, + maxTokens: + resolved.secret.kind === "openai_compatible" + ? resolved.secret.maxTokens + : undefined, + contextWindow: + resolved.secret.kind === "openai_compatible" + ? resolved.secret.contextWindow + : undefined, + thinkingLevel: + resolved.secret.kind === "openai_compatible" + ? resolved.secret.thinkingLevel + : undefined, + acceptsImages, + maxImagesPerPrompt: + resolved.secret.kind === "openai_compatible" + ? resolved.secret.maxImagesPerPrompt + : undefined, + oauth, + persistOAuth: oauth + ? async (next) => { + await withModelCredentialLock(credential.secretId, async () => { + const currentRow = await deps.prisma.secret.findFirst({ + where: { id: credential.secretId, userId, spaceId: null }, + }); + if (!currentRow) return; + const current = parseModelSecret( + deps.secretStore.load( + currentRow.ciphertext, + currentRow.id, + ), + ); + if (current.kind === "oauth") { + const stored = current.credential; + if (stored.expires > next.expires) return; + if ( + stored.access === next.access && + stored.refresh === next.refresh && + stored.expires === next.expires + ) { + return; + } + } + await persist( + serializeModelSecret({ + kind: "oauth", + credential: toOAuthCredential(next), + }), + ); + }); + } + : undefined, + redact: [...secretValuesToRedact(resolved.secret), resolved.apiKey].filter( + (value): value is string => Boolean(value), + ), + }; + }); + } + return { apiKey: deploymentKeyFor(deps, provider), redact: [] }; +} + +/** + * Serialize access to credential operations for a single secret key. + * Prevents concurrent reads/writes from producing torn state. + */ +export async function withModelCredentialLock( + key: string, + fn: () => Promise, +): Promise { + const previous = modelCredentialLocks.get(key) ?? Promise.resolve(); + let release!: () => void; + const current = previous.then( + () => + new Promise((resolve) => { + release = resolve; + }), + ); + modelCredentialLocks.set(key, current); + await previous; + try { + return await fn(); + } finally { + release(); + if (modelCredentialLocks.get(key) === current) + modelCredentialLocks.delete(key); + } +} diff --git a/packages/adapters/src/executor-shell-guard.ts b/packages/adapters/src/executor-shell-guard.ts new file mode 100644 index 0000000..9c0ad40 --- /dev/null +++ b/packages/adapters/src/executor-shell-guard.ts @@ -0,0 +1,177 @@ +/** + * Shell command safety guard — detects dangerous lifecycle commands + * (kill, systemctl stop, browser profile access, etc.) by tokenizing + * the command through shell-quote. + * + * Extracted from executor.ts to reduce file size. + */ +import { parse as parseShellCommand } from "shell-quote"; + +const SHELL_INTERPRETER_NAMES = /^(?:bash|sh|dash|zsh|ksh|fish)$/; +const STATIC_SHELL_EXPANSIONS: Readonly> = { + HOME: "/home/rakazo", + LOGNAME: "rakazo", + PATH: "/home/rakazo/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + PWD: "/home/rakazo", + TMPDIR: "/tmp", + USER: "rakazo", + WORKSPACE: "/home/rakazo/workspace", + XDG_CONFIG_HOME: "/home/rakazo/.config", +}; +const SAFE_SHELL_CONTROL_OPS = new Set([ + "&&", + "||", + ";", + "|", + "&", + ">", + "<", + ">>", + ">&", + "<&", + "&>", +]); + +function shellCFlagProgram(words: string[], interpreterIndex: number): string | undefined { + for (let index = interpreterIndex + 1; index < words.length; index += 1) { + const word = words[index] ?? ""; + if (word.startsWith("--command=")) return word.slice("--command=".length); + // bash -c / -lc / -ce and fish --command: the next argument is the program string. + if (word === "--command" || /^-[^-]*c/.test(word)) return words[index + 1]; + } + return undefined; +} + +function preserveShellCommandBoundaries(command: string): string { + let quote: "'" | '"' | undefined; + let result = ""; + for (let index = 0; index < command.length; index += 1) { + const character = command[index]; + const next = command[index + 1]; + if (character === "\\" && quote !== "'") { + if (next === "\n") { + index += 1; + continue; + } + // Keep escaped characters intact; an escaped quote is not a boundary. + result += character; + if (next !== undefined) { + result += next; + index += 1; + } + continue; + } + if (character === quote) quote = undefined; + else if (!quote && (character === "'" || character === '"')) quote = character; + result += character === "\n" && !quote ? "\n;" : character; + } + return result; +} + +function tokenizeProtectedShellCommand(command: string): string[] | "dynamic" { + try { + // shell-quote treats newlines as whitespace. Preserve command boundaries for + // the dot builtin, after folding shell line continuations. Retaining the + // newline also preserves comment handling (comments remain fail-closed). + const separated = preserveShellCommandBoundaries(command); + const parsed = parseShellCommand<{ expansion: string }>( + separated, + (name) => STATIC_SHELL_EXPANSIONS[name] ?? { expansion: name }, + { splitUnquoted: true }, + ); + const words: string[] = []; + let commandPosition = true; + let redirectTarget = false; + for (const [index, entry] of parsed.entries()) { + if (typeof entry === "string") { + // Backtick fragments are not fully tokenized; treat them as dynamic. + if (entry.includes("`")) return "dynamic"; + const word = entry.toLowerCase(); + // `find .`, `git add .`, and `git -C .` use a path, not the + // executable `. script` builtin. Keep the path out of the builtin scan. + words.push(word === "." && (!commandPosition || redirectTarget) ? "./" : word); + if (redirectTarget) { + redirectTarget = false; + continue; + } + const next = parsed[index + 1]; + if ( + commandPosition && + /^\d+$/.test(word) && + typeof next === "object" && + "op" in next && + /^[<>]/.test(next.op) + ) { + // A leading file descriptor belongs to a redirect, not the command. + } else if (commandPosition && /^(?:then|do|else)$/.test(word)) { + commandPosition = true; + } else if (commandPosition && (word === "coproc" || word === "function")) return "dynamic"; + else if ( + commandPosition && + (/^(?:command|builtin|exec|time|if|elif|while|until|!|\{)$/.test(word) || + word.startsWith("-") || + /^[a-z_][a-z0-9_]*=/.test(word)) + ) { + // Shell prefixes and assignments leave the command word pending. + } else commandPosition = false; + continue; + } + if ("expansion" in entry) { + // Unknown expansions and command substitutions are resolved by bash + // after this guard runs, so their eventual value cannot be inspected. + return "dynamic"; + } + if ("op" in entry && entry.op === "glob") { + words.push(entry.pattern.toLowerCase()); + continue; + } + if ("op" in entry && SAFE_SHELL_CONTROL_OPS.has(entry.op)) { + if (["&&", "||", ";", "|", "&"].includes(entry.op)) { + commandPosition = true; + redirectTarget = false; + } else redirectTarget = true; + continue; + } + return "dynamic"; + } + return words; + } catch { + return "dynamic"; + } +} + +export function isProtectedComputerLifecycleCommand(command: string): boolean { + const words = tokenizeProtectedShellCommand(command); + if (words === "dynamic") return true; + + const commandNames = words.map((word) => word.split("/").at(-1)); + if (commandNames.some((word) => /^(?:kill|pkill|killall|xkill)$/.test(word ?? ""))) { + return true; + } + // eval/source/. can hide protected commands inside an expansion string that the + // outer tokenizer keeps as a single word (e.g. eval "pkill chromium"). + if (words.includes(".") || commandNames.some((word) => /^(?:eval|source)$/.test(word ?? ""))) { + return true; + } + if ( + commandNames.some((word) => word === "systemctl" || word === "service") && + words.some((word) => /^(?:stop|restart|kill)$/.test(word)) + ) { + return true; + } + if ( + words.some((word) => + /(?:\.browser-profiles|--user-data-dir|\/tmp\/\.x11-unix|\/tmp\/\.x\d+-lock)/.test(word), + ) + ) { + return true; + } + + for (let index = 0; index < words.length; index += 1) { + const name = words[index]?.split("/").at(-1) ?? ""; + if (!SHELL_INTERPRETER_NAMES.test(name)) continue; + const program = shellCFlagProgram(words, index); + if (program && isProtectedComputerLifecycleCommand(program)) return true; + } + return false; +} diff --git a/packages/adapters/src/executor-tools.ts b/packages/adapters/src/executor-tools.ts new file mode 100644 index 0000000..4e730b7 --- /dev/null +++ b/packages/adapters/src/executor-tools.ts @@ -0,0 +1,58 @@ +import { builtinAgentTools } from "./builtin-tools.js"; +import { selectCloudAgentTools } from "./cloud-agent-tools-select.js"; +import { selectMemoryTools } from "./memory-tools.js"; +import { + filterBuiltinToolsForRun, + filterBuiltinToolsForThread, +} from "./schedule-tools.js"; +import { + filterImageReturningComputerTools, +} from "./model-vision.js"; + +/** Browser tools that require a graphical computer (Chrome). */ +export const PAGE_BROWSER_TOOL_NAMES = new Set([ + "browser_navigate", + "browser_snapshot", + "browser_act", +]); + +export function filterPageBrowserTools( + tools: T[], + pageBrowserAllowed: boolean, +): T[] { + if (pageBrowserAllowed) return tools; + return tools.filter((tool) => !PAGE_BROWSER_TOOL_NAMES.has(tool.name)); +} + +export function selectBuiltinToolsForRun(options: { + graphicalToolsAllowed: boolean; + /** Page browser tools need a graphical computer (Chrome), not model vision. */ + pageBrowserAllowed?: boolean; + groupId: string | null; + trigger: string; + semanticMemoryEnabled: boolean; + cloudAgentEnabled?: boolean; + messagingChannelRun: boolean; +}) { + return selectCloudAgentTools( + selectMemoryTools( + filterBuiltinToolsForRun( + filterBuiltinToolsForThread( + filterPageBrowserTools( + filterImageReturningComputerTools(builtinAgentTools, options.graphicalToolsAllowed), + options.pageBrowserAllowed ?? options.graphicalToolsAllowed, + ), + options.groupId, + ), + options.trigger, + ), + options.semanticMemoryEnabled, + ), + Boolean(options.cloudAgentEnabled), + ).filter( + (tool) => + !options.messagingChannelRun || + (!["remember", "save_memory", "recall_memory", "forget_memory"].includes(tool.name) && + !tool.name.startsWith("scratchpad_")), + ); +} diff --git a/packages/adapters/src/executor-types.ts b/packages/adapters/src/executor-types.ts new file mode 100644 index 0000000..583d75a --- /dev/null +++ b/packages/adapters/src/executor-types.ts @@ -0,0 +1,50 @@ +import type { + AgentHomeStore, + AgentRuntime, + ArtifactStore, + BrowserProvider, + ConnectorProvider, + JobPublisher, + ManagedConnectorProvider, + MemoryStore, + NotificationProvider, + SandboxProvider, + WebProvider, +} from "@rakazo/adapter-kit"; +import type { MemoryProviderResolver } from "./memory-provider-factory.js"; +import type { RemoteTransportDependencies } from "./remote-mcp.js"; +import type { EncryptedSecretStore } from "./secrets.js"; +import type { PrismaClient, ThreadEvents } from "@rakazo/db"; +import type { CloudAgentConnection } from "./cloud-agent-factory.js"; + +/** Shared dependencies injected into createRunExecutor and its extracted helpers. */ +export interface ExecutorDeps { + prisma: PrismaClient; + events: ThreadEvents; + runtime: AgentRuntime; + sandbox: SandboxProvider; + memory: MemoryStore; + memoryProviders: MemoryProviderResolver; + home: AgentHomeStore; + artifacts?: ArtifactStore; + connector?: ConnectorProvider; + connectors?: { managed(id: string): ManagedConnectorProvider | undefined }; + secrets: string[]; + secretStore: EncryptedSecretStore; + deploymentModelKey?: string; + dataDir?: string; + notifications?: NotificationProvider; + jobs: JobPublisher; + /** Messaging surface; absent means zero identity queries and no chat prompts. */ + messaging?: { hasIdentity(botId: string): Promise }; + listConnectedPluginSlugs?: (userId: string) => Promise; + /** Builtin web_search / web_fetch. Defaults to keyless HTTP when omitted. */ + web?: WebProvider; + /** Page browser (DOM refs) on the bot computer. Defaults to the sandbox live browser when supported. */ + browser?: BrowserProvider; + secretHttp?: RemoteTransportDependencies; + /** Remote cloud coding agents. Null/omit means tools stay uninjected. */ + cloudAgent?: CloudAgentConnection | null; + /** Aborted when createApp stop() begins so in-flight continueRun boot waits exit promptly. */ + shutdownSignal?: AbortSignal; +} diff --git a/packages/adapters/src/executor-utils.ts b/packages/adapters/src/executor-utils.ts new file mode 100644 index 0000000..8bad229 --- /dev/null +++ b/packages/adapters/src/executor-utils.ts @@ -0,0 +1,63 @@ +import type { AgentRunRequest, MessageBlock } from "@rakazo/contracts"; +import { isAttachmentImageMimeType } from "@rakazo/contracts"; +import type { ExecutorDeps } from "./executor-types.js"; + +/** Filter connection rows to active/connected providers. */ +export function selectRunConnections< + T extends { connectorId: string; provider: string; status: string }, +>(rows: T[], connectedComposioProviders: string[]): T[] { + const activeKeys = new Set(connectedComposioProviders.map((provider) => `composio:${provider}`)); + return rows.filter( + (row) => + row.status !== "revoked" && + (row.status === "connected" || activeKeys.has(`${row.connectorId}:${row.provider}`)), + ); +} + +/** Load image artifacts for the current turn. */ +export async function loadCurrentTurnImages( + deps: ExecutorDeps, + blocks: MessageBlock[] | undefined, + context: { + operationId: string; + traceId: string; + spaceId: string; + userId: string; + botId: string; + runId: string; + signal: AbortSignal; + }, +) { + if (!deps.artifacts || !blocks?.length) return undefined; + const imageBlocks = blocks.filter( + (block): block is Extract => block.kind === "image", + ); + if (!imageBlocks.length) return undefined; + + const rows = await deps.prisma.artifact.findMany({ + where: { + id: { in: imageBlocks.map((block) => block.artifactId) }, + spaceId: context.spaceId, + userId: context.userId, + }, + }); + const byId = new Map(rows.map((row) => [row.id, row])); + const images: NonNullable = []; + + for (const block of imageBlocks) { + const row = byId.get(block.artifactId); + if (!row || !isAttachmentImageMimeType(block.mimeType)) continue; + try { + const bytes = await deps.artifacts.get(row.storageKey, context); + images.push({ + name: block.name, + mimeType: block.mimeType as "image/jpeg" | "image/png" | "image/webp" | "image/gif", + data: bytes, + }); + } catch (error) { + if (context.signal.aborted) throw error; + } + } + + return images.length ? images : undefined; +} diff --git a/packages/adapters/src/executor.ts b/packages/adapters/src/executor.ts index f5f5385..c3f897e 100644 --- a/packages/adapters/src/executor.ts +++ b/packages/adapters/src/executor.ts @@ -14,7 +14,6 @@ import type { JobPublisher, ManagedConnectorProvider, MemoryStore, - NotificationMessage, NotificationProvider, SandboxProvider, SemanticMemoryProvider, @@ -140,6 +139,21 @@ import { settleUncertainEffect, uncertainEffectResult, } from "./approval-effect.js"; +import { recordEffect, completeEffect, uncertainEffectError } from "./executor-effects.js"; +import { + computerScreenToolResult, + notifyRun, + renewRunLease, + computerRetryDelay, + computerRunRequeueData, + writeComputerRunRequeue, + requeueComputerRun, + redactBlocks, + publishMessage, + persistMessageInTransaction, + runNotificationsEnabled, +} from "./executor-computer.js"; + import { autoReviewTimeoutMs, buildAutoReviewPrompt, @@ -203,7 +217,6 @@ import { formatCurrentTimeInstruction } from "./current-time.js"; import { resolveDeploymentModel } from "./deployment-model.js"; import { handoffToGroupBot, loadGroupContext } from "./group-handoff.js"; import { - sanitizeTextBlocks, tryRecoverProseHandoff, workshopFallbackAfterBlockedHandoff, } from "./prose-handoff-recovery.js"; @@ -4378,75 +4391,6 @@ export function createRunExecutor(deps: ExecutorDeps) { }; } -async function computerScreenToolResult( - work: () => Promise, - finish?: (result: unknown) => Promise, -) { - const result = await withComputerScreenAvailability(work); - return finish ? finish(result) : result; -} - -export async function runNotificationsEnabled( - prisma: PrismaClient, - run: { spaceId: string; userId: string; botId: string; threadId: string }, -): Promise { - const source = await prisma.run.findFirst({ - where: { - botId: run.botId, - threadId: run.threadId, - spaceId: run.spaceId, - userId: run.userId, - }, - select: { - bot: { select: { notifyOnFinish: true } }, - thread: { select: { groupId: true } }, - }, - }); - return Boolean(source && (source.thread.groupId || source.bot.notifyOnFinish)); -} - -async function notifyRun( - deps: ExecutorDeps, - run: { spaceId: string; userId: string; botId: string; threadId: string }, - message: NotificationMessage, -) { - if (!deps.notifications) return; - const enabled = await runNotificationsEnabled(deps.prisma, run).catch((error) => { - getLogger().error("notification preference lookup", error); - return false; - }); - if (!enabled) return; - await deps.notifications - .send(message, { - operationId: "notify", - traceId: run.botId, - spaceId: run.spaceId, - userId: run.userId, - botId: run.botId, - signal: new AbortController().signal, - }) - .catch((error) => { - getLogger().error("run notification", error); - }); -} - -async function renewRunLease( - deps: ExecutorDeps, - runId: string, - workerId: string, - fence: number, -): Promise { - const renewed = await deps.prisma.run.updateMany({ - where: { id: runId, status: "running", leaseOwner: workerId, leaseFence: fence }, - data: { leaseExpiresAt: new Date(Date.now() + 5 * 60_000) }, - }); - return renewed.count === 1; -} - -function computerRetryDelay(fence: number): number { - return Math.min(10_000, 250 * 2 ** Math.min(Math.max(fence - 1, 0), 5)); -} - export function selectBuiltinToolsForRun(options: { graphicalToolsAllowed: boolean; /** Page browser tools need a graphical computer (Chrome), not model vision. */ @@ -4494,386 +4438,6 @@ export function filterPageBrowserTools( return tools.filter((tool) => !PAGE_BROWSER_TOOL_NAMES.has(tool.name)); } -export function threadContextForRun( - trigger: string, - context: { - messages: T[]; - summary: string | null; - historyCompactedUpToSeq: number | null; - }, - messagingChannelRun: boolean, -) { - return trigger === "routine" - ? { - messages: [] as T[], - summary: null, - historyCompactedUpToSeq: null, - includeSemanticRecall: false, - } - : messagingChannelRun - ? { ...context, summary: null, historyCompactedUpToSeq: null, includeSemanticRecall: false } - : { ...context, includeSemanticRecall: true }; -} - -export function completionMessageSegments( - segments: MessageBlock[], - options?: { - allowSilentEmpty?: boolean; - emptyResponseText?: string; - suppressOutput?: boolean; - skipEmptyFallback?: boolean; - }, -): MessageBlock[] { - if (options?.suppressOutput) return []; - const fallback = options?.emptyResponseText?.trim() || "done."; - if (segments.length > 0) { - if ( - !options?.allowSilentEmpty && - options?.emptyResponseText !== undefined && - !segments.some((segment) => segment.kind === "text" && segment.text) - ) { - return [...segments, { kind: "text", text: fallback }]; - } - return segments; - } - if (options?.allowSilentEmpty || options?.skipEmptyFallback) return []; - return [{ kind: "text", text: fallback }]; -} - -/** User-facing text for completion notifications; empty when only tool/step activity remains. */ -export function completionNotificationBody(assembled: string, blocks: MessageBlock[]): string { - if (assembled) return assembled; - return blocks - .filter((block): block is Extract => block.kind === "text") - .map((block) => block.text) - .join(""); -} - -export function completionMarksUnread(trigger: string, text: string): boolean { - return trigger !== "routine" || Boolean(text); -} - -export function missingTurnImagesInstruction( - blocks: MessageBlock[] | undefined, - images: { length: number } | undefined, -): string { - const expected = blocks?.filter((block) => block.kind === "image").length ?? 0; - const loaded = images?.length ?? 0; - return expected > 0 && loaded < expected ? TURN_ATTACHMENT_UNAVAILABLE : ""; -} - -export async function settleSteeringAttachmentLoads( - images: Promise, - files: Promise, - blocks?: MessageBlock[], - signal?: AbortSignal, -): Promise<{ - images: TImage[] | undefined; - files: TFile[]; - unavailableInstruction: string; -}> { - const [loadedImages, loadedFiles] = await Promise.allSettled([images, files]); - if (signal?.aborted) { - if (loadedImages.status === "rejected") throw loadedImages.reason; - if (loadedFiles.status === "rejected") throw loadedFiles.reason; - } - const expectedImageCount = blocks?.filter((block) => block.kind === "image").length ?? 0; - const loadedImageCount = - loadedImages.status === "fulfilled" ? (loadedImages.value?.length ?? 0) : 0; - const unavailable = - loadedImages.status === "rejected" || - loadedFiles.status === "rejected" || - loadedImageCount < expectedImageCount; - return { - images: loadedImages.status === "fulfilled" ? loadedImages.value : undefined, - files: loadedFiles.status === "fulfilled" ? loadedFiles.value : [], - unavailableInstruction: unavailable ? STEERING_ATTACHMENT_UNAVAILABLE : "", - }; -} - -export function subagentMarksUnread(trigger: string, status: "running" | "completed" | "failed") { - return status === "failed" || trigger !== "routine"; -} - -function computerRunRequeueData( - resumeCheckpoint: TakeoverResumeCheckpoint | null, - error: string | null = null, - heldForTakeover = false, -) { - return { - status: - heldForTakeover && !resumeCheckpoint ? ("waiting_takeover" as const) : ("queued" as const), - error, - leaseOwner: null, - leaseExpiresAt: null, - checkpoint: resumeCheckpoint, - }; -} - -async function writeComputerRunRequeue( - deps: ExecutorDeps, - runId: string, - workerId: string, - fence: number, - resumeCheckpoint: TakeoverResumeCheckpoint | null, - heldForTakeover = false, - error: string | null = null, -): Promise { - const whereLease = { - id: runId, - status: "running" as const, - leaseOwner: workerId, - leaseFence: fence, - }; - const releasedHold = { - status: "queued" as const, - error, - leaseOwner: null, - leaseExpiresAt: null, - }; - const preserve = await deps.prisma.run.updateMany({ - where: { - ...whereLease, - checkpoint: { in: [...TAKEOVER_RESUME_CHECKPOINTS] }, - }, - data: releasedHold, - }); - if (preserve.count === 1) return true; - const planned = await deps.prisma.run.updateMany({ - where: { ...whereLease, checkpoint: null }, - data: computerRunRequeueData(resumeCheckpoint, error, heldForTakeover), - }); - if (planned.count === 1) return true; - const retried = await deps.prisma.run.updateMany({ - where: { - ...whereLease, - checkpoint: { in: [...TAKEOVER_RESUME_CHECKPOINTS] }, - }, - data: releasedHold, - }); - return retried.count === 1; -} - -async function requeueComputerRun( - deps: ExecutorDeps, - runId: string, - workerId: string, - fence: number, - resumeCheckpoint: TakeoverResumeCheckpoint | null, - heldForTakeover = false, -): Promise { - const released = await writeComputerRunRequeue( - deps, - runId, - workerId, - fence, - resumeCheckpoint, - heldForTakeover, - ); - if (!released) return; - await deps.jobs.enqueue({ - ...runContinueJob(runId), - availableAt: new Date(Date.now() + computerRetryDelay(fence)), - }); -} - -function redactBlocks(blocks: MessageBlock[], secrets: string[]): MessageBlock[] { - return sanitizeTextBlocks( - blocks.map((block) => { - if (block.kind === "text") { - return { kind: "text" as const, text: redactSecrets(block.text, secrets) }; - } - if (block.kind === "bot_message_sent" || block.kind === "bot_message_received") { - return { ...block, text: redactSecrets(block.text, secrets) }; - } - return block; - }), - ); -} - -async function publishMessage( - deps: ExecutorDeps, - run: { id: string; spaceId: string; threadId: string; botId: string }, - role: "user" | "bot" | "system", - blocks: MessageBlock[], - markUnread?: boolean, - clientNonce?: string, -) { - const committed = await deps.prisma.$transaction((tx) => - persistMessageInTransaction(tx, run, role, blocks, markUnread, clientNonce), - ); - await deps.events.notify(run.threadId, committed.eventSeq).catch((error) => { - getLogger().error("thread message realtime notification", error); - }); - return committed.message; -} - -async function persistMessageInTransaction( - tx: Prisma.TransactionClient, - run: { id: string; spaceId: string; threadId: string; botId: string }, - role: "user" | "bot" | "system", - blocks: MessageBlock[], - markUnread?: boolean, - clientNonce?: string, -) { - const message = await createThreadMessageInTransaction(tx, { - threadId: run.threadId, - role, - blocks, - botId: run.botId, - runId: run.id, - markUnread, - clientNonce, - }); - const event = await appendEventInTransaction(tx, { - spaceId: run.spaceId, - threadId: run.threadId, - botId: run.botId, - type: "thread.message.created", - runId: run.id, - payload: { messageId: message.id, role, blocks }, - }); - return { message, eventSeq: event.seq }; -} - -async function recordEffect( - deps: ExecutorDeps, - run: { id: string; spaceId: string; threadId: string; botId: string }, - kind: string, - idempotencyKey: string, - request: unknown, - legacyIdempotencyKey?: string, - consumedIds?: Set, -) { - const existing = await deps.prisma.externalEffect.findUnique({ - where: { idempotencyKey }, - }); - if (existing) { - consumedIds?.add(existing.id); - await deps.events.append({ - spaceId: run.spaceId, - threadId: run.threadId, - botId: run.botId, - type: "effect.reconciled", - runId: run.id, - payload: { executionId: idempotencyKey, kind }, - }); - return { duplicate: true, effect: existing }; - } - - // Pre-fix rows used bare provider ids or scoped keys that included the - // ephemeral model tool-call id. Same-id unique lookup still works; a restart - // with a new id finds the row by run, tool, and request instead. - let expectedRequest: string | undefined; - try { - expectedRequest = stableJsonValue(request); - } catch { - expectedRequest = undefined; - } - if (legacyIdempotencyKey && legacyIdempotencyKey !== idempotencyKey && expectedRequest) { - const scopedLegacy = - request && typeof request === "object" && !Array.isArray(request) - ? legacyScopedToolEffectIdempotencyKey( - run.id, - kind, - legacyIdempotencyKey, - request as Record, - ) - : undefined; - for (const candidate of [scopedLegacy, legacyIdempotencyKey]) { - if (!candidate || candidate === idempotencyKey) continue; - const sameIdLegacy = await deps.prisma.externalEffect.findUnique({ - where: { idempotencyKey: candidate }, - }); - if ( - sameIdLegacy && - !consumedIds?.has(sameIdLegacy.id) && - sameIdLegacy.runId === run.id && - sameIdLegacy.kind === kind && - stableJsonValue(sameIdLegacy.request) === expectedRequest - ) { - consumedIds?.add(sameIdLegacy.id); - await deps.events.append({ - spaceId: run.spaceId, - threadId: run.threadId, - botId: run.botId, - type: "effect.reconciled", - runId: run.id, - payload: { executionId: candidate, kind, legacy: true }, - }); - return { duplicate: true, effect: sameIdLegacy }; - } - } - } - - const prior = await deps.prisma.externalEffect.findMany({ - where: { runId: run.id, kind }, - orderBy: { createdAt: "asc" }, - }); - const legacy = - expectedRequest === undefined - ? undefined - : prior.find((candidate) => { - if (consumedIds?.has(candidate.id)) return false; - if (candidate.idempotencyKey === idempotencyKey) return false; - if (candidate.runId !== run.id || candidate.kind !== kind) return false; - try { - if (stableJsonValue(candidate.request) !== expectedRequest) return false; - } catch { - return false; - } - // Live later occurrences use a new modern key; do not steal an earlier modern row. - return !isToolEffectIdempotencyKey(candidate.idempotencyKey, run.id, kind); - }); - if (legacy) { - consumedIds?.add(legacy.id); - await deps.events.append({ - spaceId: run.spaceId, - threadId: run.threadId, - botId: run.botId, - type: "effect.reconciled", - runId: run.id, - payload: { executionId: legacy.idempotencyKey, kind, legacy: true }, - }); - return { duplicate: true, effect: legacy }; - } - - const effect = await deps.prisma.externalEffect.create({ - data: { - spaceId: run.spaceId, - runId: run.id, - kind, - idempotencyKey, - status: "intended", - request: request as never, - }, - }); - consumedIds?.add(effect.id); - return { duplicate: false, effect }; -} - -async function completeEffect( - deps: ExecutorDeps, - effectId: string, - expectedStatus: "intended" | "executing", - result: unknown, -) { - const storedResult = - result && - typeof result === "object" && - (result as { kind?: unknown }).kind === "agent_tool_result" && - "details" in result - ? (result as { details: unknown }).details - : result; - return completeExternalEffect(deps.prisma, effectId, expectedStatus, storedResult as never); -} - -function uncertainEffectError(toolName: string): Error { - return new Error( - `tool ${toolName} has an earlier execution with an uncertain outcome; it may already have completed, so verify the destination before retrying`, - ); -} - async function runSandboxCommand( sandbox: SandboxProvider, computer: ComputerRef, @@ -5115,3 +4679,19 @@ export async function loadCurrentTurnImages( return images.length ? images : undefined; } + +// Re-export computer/lifecycle utilities from executor-computer.ts +export { + computerScreenToolResult, + notifyRun, + renewRunLease, + computerRetryDelay, + computerRunRequeueData, + writeComputerRunRequeue, + requeueComputerRun, + redactBlocks, + publishMessage, + runNotificationsEnabled, +} from "./executor-computer.js"; +// Re-export effect utilities from executor-effects.ts +export { recordEffect, completeEffect, uncertainEffectError } from "./executor-effects.js"; diff --git a/packages/adapters/src/index.ts b/packages/adapters/src/index.ts index a2c754b..8a920e1 100644 --- a/packages/adapters/src/index.ts +++ b/packages/adapters/src/index.ts @@ -47,6 +47,12 @@ export * from "./e2b-sandbox.js"; export * from "./elevenlabs-voice.js"; export * from "./email-emulator.js"; export * from "./executor.js"; +export * from "./executor-attachments.js"; +export * from "./executor-checkpoint.js"; +export * from "./executor-completion.js"; +export * from "./executor-computer.js"; +export * from "./executor-effects.js"; +export * from "./executor-model.js"; export * from "./expo-push.js"; export * from "./fake-browser.js"; export * from "./fake-sandbox.js"; @@ -79,11 +85,13 @@ export * from "./openai-compatible-url.js"; export * from "./openai-voice.js"; export * from "./page-browser-session.js"; export * from "./pi-credentials.js"; +export * from "./pi-model-config.js"; export * from "./pi-models.js"; export * from "./pi-oauth.js"; export * from "./pi-openai-compatible-provider.js"; export * from "./pi-runtime.js"; export * from "./pi-session.js"; +export * from "./pi-tool-utils.js"; export * from "./pipedream-connector.js"; export * from "./realtime.js"; export * from "./release-watch.js"; diff --git a/packages/adapters/src/pi-model-config.ts b/packages/adapters/src/pi-model-config.ts new file mode 100644 index 0000000..e38ddf1 --- /dev/null +++ b/packages/adapters/src/pi-model-config.ts @@ -0,0 +1,709 @@ +import { randomUUID } from "node:crypto"; +import { + clampThinkingLevel, + type Api, + type Model, + type ModelThinkingLevel, + type Models, + type SimpleStreamOptions, + Type, +} from "@earendil-works/pi-ai"; +import { builtinModels } from "@earendil-works/pi-ai/providers/all"; +import { type Agent, type AgentToolResult } from "@earendil-works/pi-agent-core"; +import { + type AgentMessage, + type AgentToolExecutionResult, +} from "@earendil-works/pi-agent-core"; +import type { + AgentRuntimeEvent, + AgentRunRequest, + AgentSteeringMessage, +} from "@rakazo/adapter-kit"; +import { getLogger } from "@rakazo/logging"; +import { DEFAULT_OPENROUTER_MODEL_ID } from "./deployment-model.js"; +import { + OPENAI_COMPATIBLE_PROVIDER_ID, + registerOpenAiCompatibleCatalog, + registerOpenAiCompatibleRuntime, +} from "./pi-openai-compatible-provider.js"; +import { PiRuntimeCredentialStore, toOAuthCredential } from "./pi-credentials.js"; +import { registerLocalProvider } from "./pi-local-provider.js"; +import { + billedPromptTokens, + clipToolResultContent, + clipToolResultText, + MODEL_STREAM_MAX_RETRIES, + MODEL_STREAM_TIMEOUT_MS, + resolveCompletionMaxTokens, +} from "./pi-runtime-limits.js"; + +// --- Model catalog --- + +const REASONING_MODEL_THINKING_LEVEL: ModelThinkingLevel = "medium"; + +let catalogModelsCache: Models | undefined; + +export function catalogModels(): Models { + catalogModelsCache ??= registerOpenAiCompatibleCatalog(registerLocalProvider(builtinModels())); + return catalogModelsCache; +} + +export function thinkingLevelFor( + model: Model, + preferred?: ModelThinkingLevel | null, +): ModelThinkingLevel { + if (!model.reasoning) return "off"; + if (preferred) return clampThinkingLevel(model, preferred); + return clampThinkingLevel(model, REASONING_MODEL_THINKING_LEVEL); +} + +// --- Tool call budget --- + +export interface ToolCallBudget { + count: number; + exceeded: boolean; + limit: number; + inFlight: number; +} + +// Optional fuse is process-local. continueRun on another worker starts at zero. +const toolCallBudgetsByRun = new Map(); + +/** Optional self-host fuse. Unset, empty, or 0 means unlimited (default). */ +export function maxToolCallsPerTurn(env: NodeJS.ProcessEnv = process.env): number { + const raw = env.MAX_TOOL_CALLS_PER_TURN?.trim(); + if (!raw) return 0; + const parsed = Number(raw); + if (!Number.isFinite(parsed) || parsed <= 0) return 0; + return Math.floor(parsed); +} + +function toolCallBudgetFor(runId: string): ToolCallBudget { + const existing = toolCallBudgetsByRun.get(runId); + if (existing) { + existing.inFlight = 0; + existing.limit = maxToolCallsPerTurn(); + return existing; + } + const budget: ToolCallBudget = { + count: 0, + exceeded: false, + limit: maxToolCallsPerTurn(), + inFlight: 0, + }; + if (budget.limit > 0) toolCallBudgetsByRun.set(runId, budget); + return budget; +} + +export function releaseToolCallBudget(runId: string, keepForResume: boolean) { + if (!keepForResume) toolCallBudgetsByRun.delete(runId); +} + +function maybeAbortToolCallBudget(host: { + toolCallBudget: ToolCallBudget; + abortTurn(): void; +}): void { + if (host.toolCallBudget.exceeded && host.toolCallBudget.inFlight === 0) { + host.abortTurn(); + } +} + +export function beginToolCall(host: { + toolCallBudget: ToolCallBudget; + abortTurn(): void; + queue: { push(event: { type: string; text: string }): void }; +}): boolean { + const budget = host.toolCallBudget; + if (budget.limit <= 0) { + budget.count += 1; + return true; + } + if (budget.exceeded) { + maybeAbortToolCallBudget(host); + return false; + } + budget.count += 1; + if (budget.count <= budget.limit) { + budget.inFlight += 1; + return true; + } + if (!budget.exceeded) { + budget.exceeded = true; + host.queue.push({ + type: "progress", + text: `Stopped: more than ${budget.limit} tool calls in one turn.`, + }); + } + maybeAbortToolCallBudget(host); + return false; +} + +export function endToolCall(host: { + toolCallBudget: ToolCallBudget; + abortTurn(): void; +}): void { + host.toolCallBudget.inFlight = Math.max(0, host.toolCallBudget.inFlight - 1); + maybeAbortToolCallBudget(host); +} + +export function toolCallBudgetExceededMessage(limit: number): string { + return `I stopped after reaching the limit of ${limit} tool calls in this turn. Send another message to continue.`; +} + +// --- Model resolution --- + +function configuredOpenRouterModel(id: string): Model<"openai-completions"> { + return { + id, + name: id, + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 16_384, + maxTokens: 4_096, + }; +} + +export function modelsForRequest( + request: Pick, + provider: string, +): Models { + const oauth = request.model.oauth; + if (oauth) { + const persist = oauth.persist; + return registerOpenAiCompatibleCatalog( + registerLocalProvider( + builtinModels({ + credentials: new PiRuntimeCredentialStore( + provider, + toOAuthCredential(oauth.credential), + persist ? (next) => persist(next) : undefined, + ), + }), + ), + ); + } + if ( + provider === OPENAI_COMPATIBLE_PROVIDER_ID && + request.model.baseUrl && + request.model.id.trim() + ) { + const models = registerOpenAiCompatibleCatalog(registerLocalProvider(builtinModels())); + return registerOpenAiCompatibleRuntime(models, { + modelId: request.model.id, + baseUrl: request.model.baseUrl, + reasoning: request.model.reasoning, + acceptsImages: request.model.acceptsImages, + maxTokens: request.model.maxTokens, + contextWindow: request.model.contextWindow, + }); + } + return catalogModels(); +} + +function resolveRuntimeModel(modelConfig: AgentRunRequest["model"]): { + provider: string; + modelId: string; + models: Models; + model: Model | undefined; + apiKey: string | undefined; +} { + const provider = modelConfig.provider === "scripted" ? "openrouter" : modelConfig.provider; + const envDefaultModel = process.env.PI_DEFAULT_MODEL?.trim(); + const envDefaultProvider = process.env.PI_DEFAULT_PROVIDER?.trim() || "openrouter"; + const modelId = + modelConfig.id === "scripted" + ? envDefaultModel || DEFAULT_OPENROUTER_MODEL_ID + : modelConfig.id.trim(); + const models = modelsForRequest({ model: modelConfig }, provider); + let model = models.getModel(provider, modelId); + if (!model && provider !== "openrouter" && provider !== OPENAI_COMPATIBLE_PROVIDER_ID) { + model = models.getModel("openrouter", modelId); + } + if ( + !model && + provider === "openrouter" && + envDefaultProvider === "openrouter" && + modelId === envDefaultModel + ) { + model = configuredOpenRouterModel(modelId); + } + const apiKey = modelConfig.oauth + ? undefined + : modelConfig.provider === OPENAI_COMPATIBLE_PROVIDER_ID + ? modelConfig.apiKey || "local" + : (modelConfig.apiKey ?? + (provider === "openrouter" ? process.env.OPENROUTER_API_KEY : undefined)); + return { provider, modelId, models, model, apiKey }; +} + +// --- Image helpers --- + +function toPiImages(images: AgentRunRequest["currentTurnImages"]) { + return (images ?? []).map((image) => ({ + type: "image" as const, + data: Buffer.from(image.data).toString("base64"), + mimeType: image.mimeType, + })); +} + +// --- Steering message removal --- + +function withoutSteeringMessages( + history: AgentRunRequest["history"], + steering: AgentSteeringMessage[], +): AgentRunRequest["history"] { + if (steering.length === 0) return history; + const result = [...history]; + let beforeIndex = result.length - 1; + for (let steeringIndex = steering.length - 1; steeringIndex >= 0; steeringIndex -= 1) { + const steeringMessage = steering[steeringIndex]; + for (let index = beforeIndex; index >= 0; index -= 1) { + const message = result[index]; + if ( + message?.role !== "user" || + (message.id + ? message.id !== steeringMessage?.messageId + : message.content !== (steeringMessage?.historyText ?? steeringMessage?.text)) + ) { + continue; + } + result.splice(index, 1); + beforeIndex = index - 1; + break; + } + } + return result; +} + +// --- Request secret arguments --- + +export function prepareRequestSecretArguments(raw: Record) { + return { + label: String(raw.label ?? "Code"), + purpose: String(raw.purpose ?? "otp"), + ...(raw.connectionId ? { connectionId: String(raw.connectionId) } : {}), + ...(raw.credential ? { credential: raw.credential } : {}), + ...(raw.replace === true ? { replace: true } : {}), + }; +} + +// --- Screenshot context pruning --- + +const DEFAULT_COMPUTER_SCREENSHOTS_TO_KEEP = 2; + +export function pruneComputerScreenshotContext( + messages: AgentMessage[], + maxImagesPerPrompt?: number, +): AgentMessage[] { + const imageLimit = + maxImagesPerPrompt === undefined + ? undefined + : Number.isFinite(maxImagesPerPrompt) + ? Math.max(0, Math.floor(maxImagesPerPrompt)) + : DEFAULT_COMPUTER_SCREENSHOTS_TO_KEEP; + let remaining = imageLimit ?? DEFAULT_COMPUTER_SCREENSHOTS_TO_KEEP; + if (imageLimit !== undefined) { + const nonScreenshotImages = messages.reduce( + (count, message) => + isComputerScreenshotMessage(message) ? count : count + imagePartCount(message), + 0, + ); + if (nonScreenshotImages > imageLimit) { + throw new Error( + `The configured model image limit is ${imageLimit}, but the prompt contains ${nonScreenshotImages} non-screenshot images.`, + ); + } + remaining = imageLimit - nonScreenshotImages; + } + let transformed: AgentMessage[] | undefined; + for (let index = messages.length - 1; index >= 0; index -= 1) { + const message = messages[index]; + if (!isComputerScreenshotMessage(message)) continue; + const images = imagePartCount(message); + if (remaining >= images) { + remaining -= images; + continue; + } + transformed ??= [...messages]; + transformed[index] = { + ...message, + content: message.content.filter((part) => part.type !== "image"), + }; + } + return transformed ?? messages; +} + +function imagePartCount(message: AgentMessage): number { + if (!("content" in message) || !Array.isArray(message.content)) return 0; + return message.content.filter( + (part: unknown) => + typeof part === "object" && part !== null && "type" in part && (part as {type?: string}).type === "image", + ).length; +} + +function isComputerScreenshotMessage( + message: AgentMessage | undefined, +): message is Extract { + if (message?.role !== "toolResult" || !message.content.some((part) => (part as {type?: string}).type === "image")) { + return false; + } + const details = message.details; + return Boolean( + details && + typeof details === "object" && + "frameId" in details && + typeof (details as { frameId?: unknown }).frameId === "string", + ); +} + +// --- Type guards --- + +function isAgentToolExecutionResult(result: unknown): result is AgentToolExecutionResult { + if ( + !result || + typeof result !== "object" || + (result as { kind?: unknown }).kind !== "agent_tool_result" || + !("content" in result) + ) { + return false; + } + const content = (result as { content?: unknown }).content; + return ( + Array.isArray(content) && + content.every( + (item) => + item && + typeof item === "object" && + ((item as { type?: unknown }).type === "text" || + (item as { type?: unknown }).type === "image"), + ) + ); +} + +// --- JSON Schema helpers --- + +export function jsonSchemaParameters( + schema: Record, +): ReturnType { + const alternatives = Array.isArray(schema.oneOf) + ? schema.oneOf + : Array.isArray(schema.anyOf) + ? schema.anyOf + : undefined; + if (alternatives && alternatives.length > 0 && schema.properties == null) { + return Type.Union( + alternatives.map((variant) => jsonSchemaParameters(variant as Record)), + ) as unknown as ReturnType; + } + const properties = (schema.properties ?? {}) as Record; + const required = new Set(Array.isArray(schema.required) ? schema.required.map(String) : []); + const fields: Record> = {}; + for (const [key, spec] of Object.entries(properties)) { + const field = jsonField(spec); + fields[key] = (required.has(key) ? field : Type.Optional(field)) as unknown as ReturnType< + typeof Type.Optional + >; + } + return schema.additionalProperties === false + ? Type.Object(fields, { additionalProperties: false }) + : Type.Object(fields); +} + +function enumUnion(values: readonly unknown[]) { + const members = values.map((value) => + value === null + ? Type.Null() + : typeof value === "string" || typeof value === "number" || typeof value === "boolean" + ? Type.Literal(value) + : undefined, + ); + return members.every((member) => member !== undefined) ? Type.Union(members) : undefined; +} + +export function jsonField(spec: unknown): ReturnType { + const definition = spec && typeof spec === "object" ? (spec as Record) : {}; + if (Array.isArray(definition.enum) && definition.enum.length > 0) { + const union = enumUnion(definition.enum); + if (union) return union as never; + } + if ("const" in definition) { + const literal = enumUnion([definition.const]); + if (literal) return literal as never; + } + const variants = Array.isArray(definition.oneOf) + ? definition.oneOf + : Array.isArray(definition.anyOf) + ? definition.anyOf + : undefined; + if (variants && variants.length > 0) { + return Type.Union(variants.map((variant) => jsonField(variant))) as never; + } + if (Array.isArray(definition.type) && definition.type.length > 0) { + return Type.Union(definition.type.map((type) => jsonField({ ...definition, type }))) as never; + } + const type = "type" in definition ? String(definition.type) : "string"; + if (type === "null") return Type.Null() as never; + if (type === "number" || type === "integer") return Type.Number() as never; + if (type === "boolean") return Type.Boolean() as never; + if (type === "array") { + const options: { + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; + } = {}; + if (typeof definition.minItems === "number") options.minItems = definition.minItems; + if (typeof definition.maxItems === "number") options.maxItems = definition.maxItems; + if (definition.uniqueItems === true) options.uniqueItems = true; + return Type.Array(jsonField(definition.items), options) as never; + } + if (type === "object") return jsonSchemaParameters(definition) as never; + return Type.String(); +} + +// --- Result helpers --- + +function summarizeToolResult(result: unknown) { + try { + const text = JSON.stringify(result); + if (!text) return "ok"; + return clipToolResultText(text); + } catch { + return "ok"; + } +} + +function boundAgentToolResult( + result: AgentToolResult, +): AgentToolResult { + if (!result || !Array.isArray(result.content)) return result; + return { + ...result, + content: clipToolResultContent(result.content), + }; +} + +function assistantText(message: unknown): string { + if (!message || typeof message !== "object" || !("content" in message)) return ""; + const content = (message as { content?: unknown }).content; + if (typeof content === "string") return content; + if (!Array.isArray(content)) return ""; + return content + .map((part) => + part && + typeof part === "object" && + "type" in part && + part.type === "text" && + "text" in part + ? String(part.text) + : "", + ) + .join(""); +} + +// --- Sanitization --- + +function sanitizeSensitiveText(message: string) { + return message + .replace(/sk-or-v1-[a-zA-Z0-9]+/g, "[redacted]") + .replace(/sk-[a-zA-Z0-9-]+/g, "[redacted]") + .replace(/Bearer\s+[^\s"',;&]+/gi, "Bearer [redacted]") + .replace(/eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, "[redacted]") + .replace( + /COMPOSIO_API_KEY[=:]?\s*\S+/gi, + "COMPOSIO_API_KEY=[redacted]", + ) + .replace( + /((?:api[_-]?key|access[_-]?token|password|secret)\s*[=:]\s*)[^\s"',;&]+/gi, + "$1[redacted]", + ) + .replace( + /((?:auth|authorization)\s*[=:]\s*)(?!Bearer\b)[^\s"',;&]+/gi, + "$1[redacted]", + ); +} + +function redactActivityUrl(value: unknown): string { + const raw = String(value ?? "").trim(); + if (!raw) return raw; + try { + const url = new URL(raw); + url.username = ""; + url.password = ""; + url.search = ""; + url.hash = ""; + return url.toString(); + } catch { + return "[invalid URL]"; + } +} + +function sanitizeError(message: string) { + return sanitizeSensitiveText(message); +} + +// --- Session ID & provider helpers --- + +export function conversationSessionId( + threadId: string, + botId: string, + agentId?: string, +): string { + return agentId ? `${threadId}:${botId}:${agentId}` : `${threadId}:${botId}`; +} + +export function isOpenCodeProvider(provider: string): boolean { + return provider === "opencode" || provider === "opencode-go"; +} + +const OPENCODE_SESSION_ERROR = + "OpenCode rejected this chat session. Send the message again."; + +function looksLikeOpenCodeSessionError(message: string): boolean { + return ( + /x-opencode-session/i.test(message) || + /session\s*(id|header|required|missing|invalid|expired|stale)/i.test(message) || + /model is unavailable/i.test(message) + ); +} + +function sanitizeProviderError(provider: string, message: string): string { + const sanitized = sanitizeError(message); + if (isOpenCodeProvider(provider) && looksLikeOpenCodeSessionError(sanitized)) { + return OPENCODE_SESSION_ERROR; + } + return sanitized; +} + +// --- Queue & gate --- + +interface EventQueue { + push(event: AgentRuntimeEvent): void; + fail(error: Error): void; + close(): void; + iterate(): AsyncIterable; +} + +export function createQueue(): EventQueue { + const items: AgentRuntimeEvent[] = []; + let wake: (() => void) | undefined; + let closed = false; + let failure: Error | undefined; + return { + push(event) { + items.push(event); + wake?.(); + }, + fail(error) { + failure = error; + closed = true; + wake?.(); + }, + close() { + closed = true; + wake?.(); + }, + async *iterate() { + while (true) { + if (items.length) { + yield items.shift()!; + continue; + } + if (failure) throw failure; + if (closed) return; + await new Promise((resolve) => { + wake = resolve; + }); + } + }, + }; +} + +export function createGate(max: number) { + let active = 0; + const waiters: Array<() => void> = []; + return { + async acquire() { + if (active >= max) { + await new Promise((resolve) => { + waiters.push(resolve); + }); + } + active += 1; + }, + release() { + active = Math.max(0, active - 1); + waiters.shift()?.(); + }, + }; +} + +// --- Model completion --- + +export function modelForCompletion( + model: Model, + configuredMaxTokens?: number, +): Model { + const maxTokens = resolveCompletionMaxTokens(model.maxTokens, configuredMaxTokens); + if (maxTokens === model.maxTokens) return model; + return { ...model, maxTokens }; +} + +// --- Reliable streaming --- + +export function reliableStreamOptions( + model: Pick, "api" | "provider" | "maxTokens">, + options?: SimpleStreamOptions, + configuredMaxTokens?: number, +): SimpleStreamOptions { + let next: SimpleStreamOptions = { + ...options, + timeoutMs: options?.timeoutMs ?? MODEL_STREAM_TIMEOUT_MS, + maxRetries: options?.maxRetries ?? MODEL_STREAM_MAX_RETRIES, + maxTokens: resolveCompletionMaxTokens( + model.maxTokens, + configuredMaxTokens, + options?.maxTokens, + ), + }; + + if (model.provider === "openai-codex" || model.api === "openai-codex-responses") { + next = { ...next, transport: "sse" }; + } + + if (isOpenCodeProvider(model.provider)) { + const sessionId = next.sessionId?.trim() || randomUUID(); + next = { + ...next, + sessionId, + headers: { + "x-opencode-session": sessionId, + "x-opencode-client": "rakazo", + ...next.headers, + }, + }; + } + + return next; +} + +// --- Tool host interface --- + +export interface ToolHost { + queue: EventQueue; + request: AgentRunRequest; + models: Models; + model: Model; + apiKey: string | undefined; + nestedAgents: Set; + subagentGate: { acquire(): Promise; release(): void }; + toolCallBudget: ToolCallBudget; + toolCallSeq: { value: number }; + abortTurn(): void; + signal: AbortSignal; + depth: number; + pausePending: boolean; +} diff --git a/packages/adapters/src/pi-runtime.ts b/packages/adapters/src/pi-runtime.ts index a6c3ee2..4f40fab 100644 --- a/packages/adapters/src/pi-runtime.ts +++ b/packages/adapters/src/pi-runtime.ts @@ -15,6 +15,10 @@ import { Type, } from "@earendil-works/pi-ai"; import { builtinModels } from "@earendil-works/pi-ai/providers/all"; +import { registerLocalProvider } from "./pi-local-provider.js"; +import { + registerOpenAiCompatibleCatalog, +} from "./pi-openai-compatible-provider.js"; import type { AdapterContext, AgentRunRequest, @@ -33,35 +37,15 @@ import { normalizeOpenAiToolParameters, openAiToolParametersNeedNormalization, } from "./openai-tool-parameters.js"; -import { PiRuntimeCredentialStore, toOAuthCredential } from "./pi-credentials.js"; -import { registerLocalProvider } from "./pi-local-provider.js"; -import { - OPENAI_COMPATIBLE_PROVIDER_ID, - registerOpenAiCompatibleCatalog, - registerOpenAiCompatibleRuntime, -} from "./pi-openai-compatible-provider.js"; -import { - billedPromptTokens, - clipToolResultContent, - clipToolResultText, - MODEL_STREAM_MAX_RETRIES, - MODEL_STREAM_TIMEOUT_MS, - resolveCompletionMaxTokens, -} from "./pi-runtime-limits.js"; -import { +import type { + PiSessionHandle, PiJsonlSessionRecorder, - type PiSessionHandle, - type PiSessionRecorder, + PiSessionRecorder, } from "./pi-session.js"; -import { textContentArg } from "./tool-text.js"; - +import type { ToolCallBudget } from "./pi-model-config.js"; +// Track in-flight continueRun calls so concurrent requests for the same run +// share a single work item and avoid duplicate model calls. const running = new Map }>(); -interface ToolCallBudget { - count: number; - exceeded: boolean; - limit: number; - inFlight: number; -} // Optional fuse is process-local. continueRun on another worker starts at zero. const toolCallBudgetsByRun = new Map(); // Built on first use, not at module load: entry points call loadRootEnv() after @@ -460,1267 +444,118 @@ export class PiAgentRuntime implements AgentRuntime { } } -function toPiImages(images: AgentRunRequest["currentTurnImages"]) { - return (images ?? []).map((image) => ({ - type: "image" as const, - data: Buffer.from(image.data).toString("base64"), - mimeType: image.mimeType, - })); -} +import { + catalogModels, + thinkingLevelFor, + maxToolCallsPerTurn, + toPiImages, + configuredOpenRouterModel, + resolveRuntimeModel, + modelsForRequest, + toAgentTools, + normalizeAgentToolName, + describeToolActivity, + normalizeAgentToolNames, + toAgentTool, + executeSubagent, + parametersFor, + safeJsonSchemaParameters, + builtinParameters, + pruneComputerScreenshotContext, + imagePartCount, + isComputerScreenshotMessage, + isAgentToolExecutionResult, + jsonSchemaParameters, + enumUnion, + jsonField, + summarizeToolResult, + boundAgentToolResult, + assistantText, + sanitizeSensitiveText, + redactActivityUrl, + sanitizeError, + conversationSessionId, + isOpenCodeProvider, + looksLikeOpenCodeSessionError, + sanitizeProviderError, + toolCallBudgetExceededMessage, + toolCallBudgetFor, + releaseToolCallBudget, + maybeAbortToolCallBudget, + beginToolCall, + endToolCall, + modelForCompletion, + createGate, + createQueue, + reliableStreamOptions, + toHistory, + withoutSteeringMessages, + prepareRequestSecretArguments, + PiAgentRuntimeOptions, + EventQueue, + ToolCallBudget, + ToolHost, +} from "./pi-model-config.js"; -function configuredOpenRouterModel(id: string): Model<"openai-completions"> { - // A configured model can intentionally be newer than Pi's static catalog. Keep - // pricing conservative, but enable reasoning: unknown OpenRouter endpoints - // (e.g. gemini-3.7-flash before the snapshot catches up) often mandate it, and - // thinkingLevel "off" becomes effort "none" which those endpoints reject. - return { - id, - name: id, - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 16_384, - maxTokens: 4_096, - }; -} - -function resolveRuntimeModel(modelConfig: AgentRunRequest["model"]): { - provider: string; - modelId: string; - models: Models; - model: Model | undefined; - apiKey: string | undefined; -} { - const provider = modelConfig.provider === "scripted" ? "openrouter" : modelConfig.provider; - const envDefaultModel = process.env.PI_DEFAULT_MODEL?.trim(); - const envDefaultProvider = process.env.PI_DEFAULT_PROVIDER?.trim() || "openrouter"; - const modelId = - modelConfig.id === "scripted" - ? envDefaultModel || DEFAULT_OPENROUTER_MODEL_ID - : modelConfig.id.trim(); - const models = modelsForRequest({ model: modelConfig }, provider); - let model = models.getModel(provider, modelId); - if (!model && provider !== "openrouter" && provider !== OPENAI_COMPATIBLE_PROVIDER_ID) { - model = models.getModel("openrouter", modelId); - } - if ( - !model && - provider === "openrouter" && - envDefaultProvider === "openrouter" && - modelId === envDefaultModel - ) { - model = configuredOpenRouterModel(modelId); - } - const apiKey = modelConfig.oauth - ? undefined - : modelConfig.provider === OPENAI_COMPATIBLE_PROVIDER_ID - ? modelConfig.apiKey || "local" - : // Only OpenRouter may fall back to the OpenRouter env key. Handing it to - // another provider would ship our key to a vendor it was not issued for. - (modelConfig.apiKey ?? - (provider === "openrouter" ? process.env.OPENROUTER_API_KEY : undefined)); - return { provider, modelId, models, model, apiKey }; -} - -export function modelsForRequest( - request: Pick, - provider: string, -): Models { - const oauth = request.model.oauth; - if (oauth) { - const persist = oauth.persist; - return registerOpenAiCompatibleCatalog( - registerLocalProvider( - builtinModels({ - credentials: new PiRuntimeCredentialStore( - provider, - toOAuthCredential(oauth.credential), - persist ? (next) => persist(next) : undefined, - ), - }), - ), - ); - } - if ( - provider === OPENAI_COMPATIBLE_PROVIDER_ID && - request.model.baseUrl && - request.model.id.trim() - ) { - const models = registerOpenAiCompatibleCatalog(registerLocalProvider(builtinModels())); - return registerOpenAiCompatibleRuntime(models, { - modelId: request.model.id, - baseUrl: request.model.baseUrl, - reasoning: request.model.reasoning, - acceptsImages: request.model.acceptsImages, - maxTokens: request.model.maxTokens, - contextWindow: request.model.contextWindow, - }); - } - return catalogModels(); -} - -function toAgentTools(toolDefs: readonly ConnectorTool[], host: ToolHost): AgentTool[] { - const names = normalizeAgentToolNames(toolDefs); - return toolDefs.map((tool, index) => toAgentTool(tool, host, names[index]!)); -} - -/** - * Normalize connector names only at the boundary where they are exposed to Pi. - * Connector execution continues to use the original name captured by toAgentTool. - */ -export function normalizeAgentToolName(name: string): string { - if (isProviderSafeAgentToolName(name)) return name; - const normalized = name - .normalize("NFKD") - .replace(/[\u0300-\u036f]/g, "") - .replace(/[^a-zA-Z0-9_-]+/g, "_") - .replace(/^_+|_+$/g, ""); - return (normalized || FALLBACK_AGENT_TOOL_NAME).slice(0, MAX_AGENT_TOOL_NAME_LENGTH); -} - -/** - * Return one valid, unique model-facing name per connector tool. - * Existing valid names are reserved first so sanitizing a connector cannot - * rename or shadow a builtin tool with the same valid name. - */ -const ACTIVITY_DETAIL_LIMIT = 90; - -/** One human-readable line describing a tool call, shown live in the thread. */ -export function describeToolActivity(toolName: string, args: unknown): string { - const record = args && typeof args === "object" ? (args as Record) : {}; - const detail = (value: unknown): string => { - const text = sanitizeSensitiveText(String(value ?? "")) - .replaceAll(/\s+/g, " ") - .trim(); - return text.length > ACTIVITY_DETAIL_LIMIT ? `${text.slice(0, ACTIVITY_DETAIL_LIMIT)}…` : text; - }; - if (toolName === "shell") return `Running: ${detail(record.command)}`; - if (toolName === "read_file") return `Reading ${detail(record.path)}`; - if (toolName === "write_file") return `Writing ${detail(record.path)}`; - if (toolName === "list_files") return `Listing ${detail(record.path ?? ".")}`; - if (toolName === "attach_file") return `Attaching ${detail(record.path)}`; - if (toolName === "open_path") return `Opening ${detail(record.path)}`; - if (toolName === "render_plot") return "Rendering a chart"; - if (toolName === "add_mcp_server") return `Connecting MCP server: ${detail(record.name)}`; - if (toolName === "computer_observe") return "Looking at the screen"; - if (toolName === "browser_navigate") - return `Opening page: ${detail(redactActivityUrl(record.url))}`; - if (toolName === "browser_snapshot") return "Reading the page"; - if (toolName === "browser_act") return "Using the page"; - if (toolName === "computer_act") return "Operating the computer"; - if (toolName === "run_subagent") return `Delegating to helper: ${detail(record.name)}`; - if (toolName === "create_space") return `Creating space: ${detail(record.name)}`; - if (toolName === "create_project") return `Creating project: ${detail(record.name)}`; - if (toolName === "remember") return "Saving a note to memory"; - if (toolName === "web_search") return `Searching the web: ${detail(record.query)}`; - if (toolName === "web_fetch") return `Reading page: ${detail(redactActivityUrl(record.url))}`; - if (toolName === "skill_read") return `Reading skill: ${detail(record.name)}`; - if (toolName === "skill_create") return `Creating skill: ${detail(record.name ?? "skill")}`; - if (toolName === "skill_update") - return `Updating skill: ${detail(record.name ?? record.skillId)}`; - if (toolName === "skill_delete") - return `Deleting skill: ${detail(record.name ?? record.skillId)}`; - const mcp = toolName.match(/^mcp__(.+?)__(.+)$/); - if (mcp) return `Using ${mcp[1]}: ${mcp[2]}`; - return `Using ${toolName}`; -} - -export function normalizeAgentToolNames(tools: readonly ConnectorTool[]): string[] { - const reservedValidNames = new Set( - tools.filter((tool) => isProviderSafeAgentToolName(tool.name)).map((tool) => tool.name), - ); - const usedNames = new Set(); - - return tools.map((tool) => { - const base = normalizeAgentToolName(tool.name); - const originalIsValid = isProviderSafeAgentToolName(tool.name); - let candidate = base; - - if (usedNames.has(candidate) || (!originalIsValid && reservedValidNames.has(candidate))) { - candidate = withToolNameSuffix(base, stableToolNameHash(tool.name)); - } - - let suffix = 2; - while (usedNames.has(candidate) || (!originalIsValid && reservedValidNames.has(candidate))) { - candidate = withToolNameSuffix(base, `${stableToolNameHash(tool.name)}_${suffix}`); - suffix += 1; - } - - usedNames.add(candidate); - return candidate; - }); -} - -function isProviderSafeAgentToolName(name: string): boolean { - return AGENT_TOOL_NAME_PATTERN.test(name) && name.length <= MAX_AGENT_TOOL_NAME_LENGTH; -} - -function withToolNameSuffix(base: string, suffix: string): string { - const suffixWithSeparator = `_${suffix}`; - const prefixLength = Math.max(1, MAX_AGENT_TOOL_NAME_LENGTH - suffixWithSeparator.length); - return `${base.slice(0, prefixLength)}${suffixWithSeparator}`; -} - -function stableToolNameHash(name: string): string { - let hash = 2166136261; - for (const character of name) { - hash ^= character.codePointAt(0) ?? 0; - hash = Math.imul(hash, 16777619); - } - return (hash >>> 0).toString(36); -} - -function toHistory( - history: AgentRunRequest["history"], - prompt: string, - sourceMessageId?: string | null, -) { - let duplicatePromptIndex = -1; - for (let index = history.length - 1; index >= 0; index -= 1) { - const message = history[index]; - if ( - message?.role === "user" && - (sourceMessageId ? message.id === sourceMessageId : message.content === prompt) - ) { - duplicatePromptIndex = index; - break; - } - } - const prior = - duplicatePromptIndex < 0 - ? history - : history.filter((_, index) => index !== duplicatePromptIndex); - return prior - .filter((m) => { - if (m.role !== "user" && m.role !== "assistant") return false; - const text = (m.content || "").trim(); - if (!text) return false; - if (m.role === "assistant") { - if (text === "**" || text === "No response. Try again." || text === "Das") return false; - } - return true; - }) - .slice(-25) - .map((m) => - m.role === "assistant" - ? { role: "user" as const, content: `Assistant: ${m.content}`, timestamp: Date.now() } - : { role: "user" as const, content: m.content, timestamp: Date.now() }, - ); -} - -function withoutSteeringMessages( - history: AgentRunRequest["history"], - steering: AgentSteeringMessage[], -): AgentRunRequest["history"] { - if (steering.length === 0) return history; - const result = [...history]; - let beforeIndex = result.length - 1; - for (let steeringIndex = steering.length - 1; steeringIndex >= 0; steeringIndex -= 1) { - const steeringMessage = steering[steeringIndex]; - for (let index = beforeIndex; index >= 0; index -= 1) { - const message = result[index]; - if ( - message?.role !== "user" || - (message.id - ? message.id !== steeringMessage?.messageId - : message.content !== (steeringMessage?.historyText ?? steeringMessage?.text)) - ) { - continue; - } - result.splice(index, 1); - beforeIndex = index - 1; - break; - } - } - return result; -} - -/** - * Normalize `request_secret` arguments. - * - * `credential` and `replace` must survive: the executor stores a submitted value - * only when `credential` is present, and it validates the destination shape - * itself. An earlier version of this function listed only label/purpose/ - * connectionId, so every credential the model supplied was dropped here and the - * saved value had nowhere to go. - */ -export function prepareRequestSecretArguments(raw: Record) { - return { - label: String(raw.label ?? "Code"), - purpose: String(raw.purpose ?? "otp"), - ...(raw.connectionId ? { connectionId: String(raw.connectionId) } : {}), - ...(raw.credential ? { credential: raw.credential } : {}), - ...(raw.replace === true ? { replace: true } : {}), - }; -} - -function toAgentTool(tool: ConnectorTool, host: ToolHost, exposedName: string): AgentTool { - return { - name: exposedName, - label: tool.name, - description: tool.description, - parameters: parametersFor(tool), - prepareArguments: (args: unknown) => { - const raw = args && typeof args === "object" ? (args as Record) : {}; - if (tool.name === "destination.write") { - return { - collection: String(raw.collection ?? "notes"), - title: String(raw.title ?? "Rakazo result"), - body: String(raw.body ?? ""), - }; - } - if (tool.name === "remember") { - return { content: String(raw.content ?? ""), path: String(raw.path ?? "MEMORY.md") }; - } - if (tool.name === "request_takeover") { - return { reason: String(raw.reason ?? "I need you on the screen.") }; - } - if (tool.name === "ask_user") { - const options = Array.isArray(raw.options) ? raw.options.map(String) : raw.options; - return { - question: String(raw.question ?? "What should I use?"), - // Keep a missing/invalid options value as-is so schema minItems can reject it; - // do not coerce to [] (that used to look like a valid empty list upstream). - options, - }; - } - if (tool.name === "request_secret") { - return prepareRequestSecretArguments(raw); - } - if (tool.name === "write_file") { - return { - path: String(raw.path ?? "notes/result.txt"), - content: textContentArg(raw.content, ""), - }; - } - if (tool.name === "computer_act") { - return { - actions: Array.isArray(raw.actions) ? raw.actions : [], - observe: raw.observe === undefined ? true : Boolean(raw.observe), - settle_ms: Number(raw.settle_ms ?? 350), - }; - } - if (tool.name === "list_files") return { path: String(raw.path ?? "") }; - if (tool.name === "read_file" || tool.name === "open_path") { - return { path: String(raw.path ?? "") }; - } - if (tool.name === "launch_app") { - return { - application: String(raw.application ?? ""), - uri: raw.uri ? String(raw.uri) : "", - }; - } - if (tool.name === "shell") { - return { - command: String(raw.command ?? ""), - // The executor chooses the bot-scoped default, including Team workspaces. - ...(raw.cwd ? { cwd: String(raw.cwd) } : {}), - }; - } - if (tool.name === "run_subagent") { - return { - name: String(raw.name ?? "helper"), - task: String(raw.task ?? ""), - instructions: raw.instructions ? String(raw.instructions) : "", - model_provider: raw.model_provider ? String(raw.model_provider) : "", - model_id: raw.model_id ? String(raw.model_id) : "", - }; - } - if (tool.name === "spawn_bot") { - return { - name: String(raw.name ?? ""), - title: raw.title ? String(raw.title) : "", - instructions: raw.instructions ? String(raw.instructions) : "", - prompt: raw.prompt ? String(raw.prompt) : "", - computer_mode: raw.computer_mode ? String(raw.computer_mode) : "", - }; - } - if (tool.name === "create_space") { - return { name: String(raw.name ?? "") }; - } - if (tool.name === "create_project") { - const rawBotIds = raw.bot_ids; - return { - name: String(raw.name ?? ""), - bot_ids: Array.isArray(rawBotIds) - ? rawBotIds.filter((id): id is string => typeof id === "string") - : [], - }; - } - if (tool.name === "archive_bot" || tool.name === "delete_bot") { - return { - confirm_name: String(raw.confirm_name ?? raw.confirmName ?? ""), - bot_id: raw.bot_id ? String(raw.bot_id) : raw.botId ? String(raw.botId) : "", - }; - } - return raw as never; - }, - execute: async (toolCallId, params): Promise> => { - host.signal.throwIfAborted(); - const args = (params ?? {}) as Record; - const executionId = - toolCallId || `${host.request.runId}:${tool.name}:${host.toolCallSeq.value++}`; - if (!beginToolCall(host)) { - return { - content: [{ type: "text", text: "Skipped: tool-call limit reached." }], - details: { skipped: true }, - }; - } - host.queue.push({ type: "tool", name: tool.name, args, executionId }); - const startedAt = Date.now(); - let result: unknown; - let failure: unknown; - try { - result = await (async () => { - if (tool.name === "request_takeover") { - host.pausePending = true; - host.queue.push({ - type: "takeover", - reason: String(args.reason ?? "I need you on the screen."), - }); - return { - content: [{ type: "text", text: "Takeover requested." }], - details: args, - terminate: true, - }; - } - if (tool.name === "ask_user") { - const options = Array.isArray(args.options) - ? args.options.map((option) => String(option).trim()) - : []; - if ( - options.length < 2 || - options.length > 4 || - options.some((option) => option.length === 0 || option.length > 80) || - new Set(options).size !== options.length - ) { - throw new Error("ask_user requires two to four unique, non-empty options"); - } - host.pausePending = true; - host.queue.push({ - type: "ask", - text: String(args.question ?? "What should I use?"), - actions: options.map((label, index) => ({ - id: `choice-${index + 1}`, - label, - })), - }); - return { - content: [{ type: "text", text: "Waiting for the user's choice." }], - details: args, - terminate: true, - }; - } - if (tool.name === "request_secret") { - if (host.request.executeTool) { - const result = await host.request.executeTool(tool.name, args, executionId); - if (isAgentToolExecutionResult(result)) { - if (isToolPauseResult(result)) host.pausePending = true; - return result; - } - return { - content: [{ type: "text", text: summarizeToolResult(result) }], - details: result, - }; - } - host.pausePending = true; - return { - content: [{ type: "text", text: "Protected input requested." }], - details: args, - terminate: true, - }; - } - if (tool.name === "run_subagent") { - const result = await executeSubagent(host, executionId, args); - return { - content: [{ type: "text", text: result }], - details: { result }, - }; - } - if (host.request.executeTool) { - const result = tool.route - ? await host.request.executeTool(tool.name, args, executionId, tool.route) - : await host.request.executeTool(tool.name, args, executionId); - if (isAgentToolExecutionResult(result)) { - if (isToolPauseResult(result)) host.pausePending = true; - return boundAgentToolResult(result); - } - return { - content: [{ type: "text", text: summarizeToolResult(result) }], - details: result, - }; - } - return { - content: [{ type: "text", text: `${tool.name} is unavailable without an executor.` }], - details: { error: "no executor" }, - }; - })(); - return boundAgentToolResult(result as AgentToolResult); - } catch (error) { - failure = error; - throw error; - } finally { - endToolCall(host); - const completion: AgentToolCompletion = { - name: tool.name, - executionId, - durationMs: Math.max(0, Date.now() - startedAt), - ...(result === undefined ? {} : { result }), - ...(failure === undefined ? {} : { error: failure }), - ...(host.pausePending ? { paused: true } : {}), - }; - try { - void Promise.resolve(host.request.onToolCompleted?.(completion)).catch(() => undefined); - } catch { - // Audit hooks are best effort and must never change tool behavior. - } - } - }, - }; -} - -async function executeSubagent(host: ToolHost, executionId: string, args: Record) { - if (host.depth > 0) return "Subagents cannot nest further."; - await host.subagentGate.acquire(); - const agentId = executionId; - const name = - String(args.name ?? "helper") - .trim() - .slice(0, 80) || "helper"; - const task = String(args.task ?? "").trim(); - const extra = args.instructions ? String(args.instructions).trim() : ""; - host.queue.push({ - type: "subagent", - agentId, - name, - task, - status: "running", - progress: "starting…", - }); - - const requestedProvider = String(args.model_provider ?? "").trim(); - const requestedModelId = String(args.model_id ?? "").trim(); - let requestModel = host.request.model; - try { - if (Boolean(requestedProvider) !== Boolean(requestedModelId)) { - throw new Error("model_provider and model_id must both be set"); - } - if (requestedProvider && requestedModelId) { - if (!host.request.resolveModel) { - throw new Error("Per-call subagent model selection is unavailable"); - } - requestModel = await host.request.resolveModel(requestedProvider, requestedModelId); - } - } catch (error) { - const message = sanitizeError(error instanceof Error ? error.message : String(error)); - host.queue.push({ type: "subagent", agentId, name, task, status: "failed", result: message }); - host.subagentGate.release(); - return `Subagent failed: ${message}`; - } - - const selectedModel = resolveRuntimeModel(requestModel); - if (!selectedModel.model) { - const message = `Unknown model ${selectedModel.provider}/${selectedModel.modelId}`; - host.queue.push({ type: "subagent", agentId, name, task, status: "failed", result: message }); - host.subagentGate.release(); - return `Subagent failed: ${message}`; - } - const subagentModel = modelForCompletion(selectedModel.model, requestModel.maxTokens); - - const childDefs = (host.request.tools.length ? host.request.tools : builtinAgentTools).filter( - (tool) => !DELEGATION_TOOL_NAMES.has(tool.name), - ); - const nestedHost: ToolHost = { - ...host, - models: selectedModel.models, - model: subagentModel, - apiKey: selectedModel.apiKey, - depth: 1, - }; - const nested = new Agent({ - sessionId: conversationSessionId(host.request.threadId, host.request.botId, agentId), - streamFn: (m, ctx, options) => - selectedModel.models.streamSimple( - m, - ctx, - reliableStreamOptions(m, options, requestModel.maxTokens), - ), - getApiKey: async () => selectedModel.apiKey, - transformContext: async (messages) => - pruneComputerScreenshotContext(messages, requestModel.maxImagesPerPrompt), - initialState: { - systemPrompt: [ - `You are a Rakazo subagent named "${name}".`, - "You run inside the parent bot's turn — you are not a separate bot chat.", - "Complete the task and return a concise result. Do not spawn bots or further subagents.", - extra, - ] - .filter(Boolean) - .join(" "), - model: subagentModel, - thinkingLevel: thinkingLevelFor(subagentModel, requestModel.thinkingLevel), - tools: toAgentTools(childDefs, nestedHost), - messages: [], - }, - }); - host.nestedAgents.add(nested); - - let streamed = ""; - let lastPush = 0; - nested.subscribe((event) => { - if (event.type === "tool_execution_start") { - if (host.toolCallBudget.exceeded) return; - const toolName = "toolName" in event && event.toolName ? String(event.toolName) : "a tool"; - host.queue.push({ - type: "subagent", - agentId, - name, - task, - status: "running", - progress: `using ${toolName}…`, - }); - } - if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { - const delta = event.assistantMessageEvent.delta; - if (delta) { - streamed += delta; - const now = Date.now(); - if (now - lastPush >= 80) { - lastPush = now; - host.queue.push({ - type: "subagent", - agentId, - name, - task, - status: "running", - progress: streamed.slice(-800), - }); - } - } - } - if (event.type === "message_end" && event.message.role === "assistant") { - const text = assistantText(event.message); - if (text && !streamed) streamed = text; - if ("usage" in event.message && event.message.usage) { - host.queue.push({ - type: "usage", - ...billedPromptTokens(event.message.usage), - provider: subagentModel.provider, - model: subagentModel.id, - }); - } - } - }); - - try { - if (host.signal.aborted) { - host.queue.push({ - type: "subagent", - agentId, - name, - task, - status: "failed", - result: "stopped", - }); - return "stopped"; - } - const onAbort = () => nested.abort(); - host.signal.addEventListener("abort", onAbort); - try { - await nested.prompt(task || "Complete the delegated task."); - } finally { - try { - await nested.waitForIdle(); - } finally { - host.signal.removeEventListener("abort", onAbort); - // nestedHost is a shallow copy; ask_user / request_takeover set pause - // only on the child. Copy it up before the parent releases the budget. - if (nestedHost.pausePending) host.pausePending = true; - } - } - // Shared-budget abort leaves errorMessage on the nested agent; surface it as a - // completed stop rather than a failed subagent chip. - const budgetExceeded = host.toolCallBudget.exceeded; - const error = nested.state.errorMessage; - if (error && !budgetExceeded) { - const message = sanitizeProviderError(subagentModel.provider, error); - host.queue.push({ type: "subagent", agentId, name, task, status: "failed", result: message }); - return `Subagent failed: ${message}`; - } - const budgetMessage = budgetExceeded - ? toolCallBudgetExceededMessage(host.toolCallBudget.limit) - : undefined; - const result = - budgetMessage && streamed.trim() - ? `${streamed.trim()}\n\n${budgetMessage}` - : budgetMessage || streamed || assistantText(nested.state.messages.at(-1)) || "done."; - const clipped = clipToolResultText(result, 12_000); - host.queue.push({ - type: "subagent", - agentId, - name, - task, - status: "completed", - result: clipped, - }); - return clipped; - } catch (error) { - const message = sanitizeError(error instanceof Error ? error.message : String(error)); - host.queue.push({ type: "subagent", agentId, name, task, status: "failed", result: message }); - return `Subagent failed: ${message}`; - } finally { - host.nestedAgents.delete(nested); - host.subagentGate.release(); - } -} - -/** Build AgentTool.parameters for a connector tool, including OpenAI wire fidelity. */ -export function parametersFor(tool: ConnectorTool) { - const schema = builtinParameters(tool) ?? safeJsonSchemaParameters(tool); - // Type.Union (top-level oneOf/anyOf) serializes without type/properties. - // Re-wrap only when needed so Type.Object schemas keep TypeBox Kind metadata. - if (!openAiToolParametersNeedNormalization(schema)) return schema; - return Type.Unsafe( - normalizeOpenAiToolParameters(JSON.parse(JSON.stringify(schema))), - ) as unknown as ReturnType; -} - -/** A remote MCP server controls its own schemas, so a shape TypeBox cannot express must - * degrade to a permissive object instead of failing every turn for the whole bot. */ -function safeJsonSchemaParameters(tool: ConnectorTool) { - try { - return jsonSchemaParameters(tool.inputSchema); - } catch (error) { - getLogger().error(`unsupported input schema for tool ${tool.name}`, error); - return Type.Object({}); - } -} - -function builtinParameters(tool: ConnectorTool) { - if (tool.name === "write_file") { - return Type.Object({ path: Type.String(), content: Type.String() }); - } - if (tool.name === "destination.write") { - return Type.Object({ - collection: Type.String(), - title: Type.String(), - body: Type.String(), - }); - } - if (tool.name === "request_takeover") { - return Type.Object({ reason: Type.String() }); - } - if (tool.name === "ask_user") { - return Type.Object({ - question: Type.String({ maxLength: 240 }), - options: Type.Array(Type.String({ minLength: 1, maxLength: 80 }), { - minItems: 2, - maxItems: 4, - uniqueItems: true, - }), - }); - } - if (tool.name === "remember") { - return Type.Object({ content: Type.String(), path: Type.String() }); - } - if (tool.name === "shell") { - return Type.Object({ - command: Type.String(), - cwd: Type.Optional(Type.String()), - }); - } - if (tool.name === "run_subagent") { - return Type.Object({ - name: Type.String(), - task: Type.String(), - instructions: Type.Optional(Type.String()), - model_provider: Type.Optional(Type.String()), - model_id: Type.Optional(Type.String()), - }); - } - if (tool.name === "spawn_bot") { - return Type.Object({ - name: Type.String(), - title: Type.Optional(Type.String()), - instructions: Type.Optional(Type.String()), - prompt: Type.Optional(Type.String()), - computer_mode: Type.Optional(Type.Union([Type.Literal("team"), Type.Literal("dedicated")])), - }); - } - if (tool.name === "create_space") { - return Type.Object({ name: Type.String({ minLength: 1, maxLength: 60 }) }); - } - if (tool.name === "create_project") { - return Type.Object({ - name: Type.String({ minLength: 1, maxLength: 80 }), - bot_ids: Type.Array(Type.String(), { minItems: 1 }), - }); - } - if (tool.name === "archive_bot" || tool.name === "delete_bot") { - return Type.Object({ - confirm_name: Type.String(), - bot_id: Type.Optional(Type.String()), - }); - } - return undefined; -} - -/** Keep recent visual state while respecting an optional model image budget. */ -export function pruneComputerScreenshotContext( - messages: AgentMessage[], - maxImagesPerPrompt?: number, -): AgentMessage[] { - const imageLimit = - maxImagesPerPrompt === undefined - ? undefined - : Number.isFinite(maxImagesPerPrompt) - ? Math.max(0, Math.floor(maxImagesPerPrompt)) - : DEFAULT_COMPUTER_SCREENSHOTS_TO_KEEP; - let remaining = imageLimit ?? DEFAULT_COMPUTER_SCREENSHOTS_TO_KEEP; - if (imageLimit !== undefined) { - const nonScreenshotImages = messages.reduce( - (count, message) => - isComputerScreenshotMessage(message) ? count : count + imagePartCount(message), - 0, - ); - if (nonScreenshotImages > imageLimit) { - throw new Error( - `The configured model image limit is ${imageLimit}, but the prompt contains ${nonScreenshotImages} non-screenshot images.`, - ); - } - remaining = imageLimit - nonScreenshotImages; - } - let transformed: AgentMessage[] | undefined; - for (let index = messages.length - 1; index >= 0; index -= 1) { - const message = messages[index]; - if (!isComputerScreenshotMessage(message)) continue; - const images = imagePartCount(message); - if (remaining >= images) { - remaining -= images; - continue; - } - transformed ??= [...messages]; - transformed[index] = { - ...message, - content: message.content.filter((part) => part.type !== "image"), - }; - } - return transformed ?? messages; -} - -function imagePartCount(message: AgentMessage): number { - if (!("content" in message) || !Array.isArray(message.content)) return 0; - return message.content.filter( - (part: unknown) => - part !== null && typeof part === "object" && "type" in part && part.type === "image", - ).length; -} - -function isComputerScreenshotMessage( - message: AgentMessage | undefined, -): message is Extract { - if (message?.role !== "toolResult" || !message.content.some((part) => part.type === "image")) { - return false; - } - const details = message.details; - return Boolean( - details && - typeof details === "object" && - "frameId" in details && - typeof (details as { frameId?: unknown }).frameId === "string", - ); -} - -function isAgentToolExecutionResult(result: unknown): result is AgentToolExecutionResult { - if ( - !result || - typeof result !== "object" || - (result as { kind?: unknown }).kind !== "agent_tool_result" || - !("content" in result) - ) { - return false; - } - const content = (result as { content?: unknown }).content; - return ( - Array.isArray(content) && - content.every( - (item) => - item && - typeof item === "object" && - ((item as { type?: unknown }).type === "text" || - (item as { type?: unknown }).type === "image"), - ) - ); -} - -export function jsonSchemaParameters( - schema: Record, -): ReturnType { - // Top-level oneOf/anyOf (e.g. request_secret's credential XOR connectionId) - // must stay a union. Falling through to properties would drop the exclusivity - // and re-expose both destinations as optional siblings. - const alternatives = Array.isArray(schema.oneOf) - ? schema.oneOf - : Array.isArray(schema.anyOf) - ? schema.anyOf - : undefined; - if (alternatives && alternatives.length > 0 && schema.properties == null) { - return Type.Union( - alternatives.map((variant) => jsonSchemaParameters(variant as Record)), - ) as unknown as ReturnType; - } - const properties = (schema.properties ?? {}) as Record; - const required = new Set(Array.isArray(schema.required) ? schema.required.map(String) : []); - const fields: Record> = {}; - for (const [key, spec] of Object.entries(properties)) { - const field = jsonField(spec); - fields[key] = (required.has(key) ? field : Type.Optional(field)) as unknown as ReturnType< - typeof Type.Optional - >; - } - // Preserve closed objects (e.g. request_secret destination oneOf branches). - // Type.Object defaults to open, which would let connectionId+replace match both - // anyOf variants after conversion. - return schema.additionalProperties === false - ? Type.Object(fields, { additionalProperties: false }) - : Type.Object(fields); -} - -/** TypeBox only builds literals from primitives; anything else throws while the tool list is - * being assembled, which would take down the whole turn. */ -function enumUnion(values: readonly unknown[]) { - const members = values.map((value) => - value === null - ? Type.Null() - : typeof value === "string" || typeof value === "number" || typeof value === "boolean" - ? Type.Literal(value) - : undefined, - ); - return members.every((member) => member !== undefined) ? Type.Union(members) : undefined; -} - -export function jsonField(spec: unknown): ReturnType { - const definition = spec && typeof spec === "object" ? (spec as Record) : {}; - if (Array.isArray(definition.enum) && definition.enum.length > 0) { - const union = enumUnion(definition.enum); - if (union) return union as never; - } - // A `const` names the only accepted value. Without this it degraded to a bare - // string, so a discriminator like {type: {const: "bearer"}} told the model - // nothing about which value to send -- and it guessed, twice. - if ("const" in definition) { - const literal = enumUnion([definition.const]); - if (literal) return literal as never; - } - // A discriminated union arrives as oneOf/anyOf with no sibling `type`. Without - // this branch it fell through to the string default, so a model was told to - // send an object-valued field as a bare string -- which is exactly what it did. - const variants = Array.isArray(definition.oneOf) - ? definition.oneOf - : Array.isArray(definition.anyOf) - ? definition.anyOf - : undefined; - if (variants && variants.length > 0) { - return Type.Union(variants.map((variant) => jsonField(variant))) as never; - } - if (Array.isArray(definition.type) && definition.type.length > 0) { - return Type.Union(definition.type.map((type) => jsonField({ ...definition, type }))) as never; - } - const type = "type" in definition ? String(definition.type) : "string"; - if (type === "null") return Type.Null() as never; - if (type === "number" || type === "integer") return Type.Number() as never; - if (type === "boolean") return Type.Boolean() as never; - if (type === "array") { - const options: { - minItems?: number; - maxItems?: number; - uniqueItems?: boolean; - } = {}; - if (typeof definition.minItems === "number") options.minItems = definition.minItems; - if (typeof definition.maxItems === "number") options.maxItems = definition.maxItems; - if (definition.uniqueItems === true) options.uniqueItems = true; - return Type.Array(jsonField(definition.items), options) as never; - } - if (type === "object") return jsonSchemaParameters(definition) as never; - return Type.String(); -} - -function summarizeToolResult(result: unknown) { - try { - const text = JSON.stringify(result); - if (!text) return "ok"; - return clipToolResultText(text); - } catch { - return "ok"; - } -} - -function boundAgentToolResult(result: AgentToolResult): AgentToolResult { - if (!result || !Array.isArray(result.content)) return result; - return { - ...result, - content: clipToolResultContent(result.content), - }; -} - -function assistantText(message: unknown): string { - if (!message || typeof message !== "object" || !("content" in message)) return ""; - const content = (message as { content?: unknown }).content; - if (typeof content === "string") return content; - if (!Array.isArray(content)) return ""; - return content - .map((part) => - part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part - ? String(part.text) - : "", - ) - .join(""); -} - -function sanitizeSensitiveText(message: string) { - return message - .replace(/sk-or-v1-[a-zA-Z0-9]+/g, "[redacted]") - .replace(/sk-[a-zA-Z0-9-]+/g, "[redacted]") - .replace(/Bearer\s+[^\s"',;&]+/gi, "Bearer [redacted]") - .replace(/eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, "[redacted]") - .replace(/COMPOSIO_API_KEY[=:]?\s*\S+/gi, "COMPOSIO_API_KEY=[redacted]") - .replace( - /((?:api[_-]?key|access[_-]?token|password|secret)\s*[=:]\s*)[^\s"',;&]+/gi, - "$1[redacted]", - ) - .replace(/((?:auth|authorization)\s*[=:]\s*)(?!Bearer\b)[^\s"',;&]+/gi, "$1[redacted]"); -} - -/** Origin + path only for activity chips; drop userinfo, query, and fragment. */ -function redactActivityUrl(value: unknown): string { - const raw = String(value ?? "").trim(); - if (!raw) return raw; - try { - const url = new URL(raw); - url.username = ""; - url.password = ""; - url.search = ""; - url.hash = ""; - return url.toString(); - } catch { - // Never echo unparsed input — it may still contain userinfo/secrets. - return "[invalid URL]"; - } -} - -function sanitizeError(message: string) { - return sanitizeSensitiveText(message); -} - -/** Stable OpenCode affinity id for a bot conversation (and optional nested agent). */ -export function conversationSessionId(threadId: string, botId: string, agentId?: string): string { - return agentId ? `${threadId}:${botId}:${agentId}` : `${threadId}:${botId}`; -} - -export function isOpenCodeProvider(provider: string): boolean { - return provider === "opencode" || provider === "opencode-go"; -} - -const OPENCODE_SESSION_ERROR = "OpenCode rejected this chat session. Send the message again."; - -function looksLikeOpenCodeSessionError(message: string): boolean { - return ( - /x-opencode-session/i.test(message) || - /session\s*(id|header|required|missing|invalid|expired|stale)/i.test(message) || - /model is unavailable/i.test(message) - ); -} - -function sanitizeProviderError(provider: string, message: string): string { - const sanitized = sanitizeError(message); - if (isOpenCodeProvider(provider) && looksLikeOpenCodeSessionError(sanitized)) { - return OPENCODE_SESSION_ERROR; - } - return sanitized; -} - -interface EventQueue { - push(event: AgentRuntimeEvent): void; - fail(error: Error): void; - close(): void; - iterate(): AsyncIterable; -} - -interface ToolHost { - queue: EventQueue; - request: AgentRunRequest; - models: Models; - model: Model; - apiKey: string | undefined; - nestedAgents: Set; - subagentGate: { acquire(): Promise; release(): void }; - toolCallBudget: ToolCallBudget; - /** Shared fallback uniqueness when the model omits toolCallId (nested hosts reuse this). */ - toolCallSeq: { value: number }; - abortTurn(): void; - signal: AbortSignal; - depth: number; - pausePending: boolean; -} - -function toolCallBudgetExceededMessage(limit: number) { - return `I stopped after reaching the limit of ${limit} tool calls in this turn. Send another message to continue.`; -} - -function toolCallBudgetFor(runId: string): ToolCallBudget { - const existing = toolCallBudgetsByRun.get(runId); - if (existing) { - existing.inFlight = 0; - existing.limit = maxToolCallsPerTurn(); - return existing; - } - const budget: ToolCallBudget = { - count: 0, - exceeded: false, - limit: maxToolCallsPerTurn(), - inFlight: 0, - }; - if (budget.limit > 0) toolCallBudgetsByRun.set(runId, budget); - return budget; -} - -function releaseToolCallBudget(runId: string, keepForResume: boolean) { - if (!keepForResume) toolCallBudgetsByRun.delete(runId); -} - -function maybeAbortToolCallBudget(host: ToolHost) { - if (host.toolCallBudget.exceeded && host.toolCallBudget.inFlight === 0) { - host.abortTurn(); - } -} - -function beginToolCall(host: ToolHost): boolean { - const budget = host.toolCallBudget; - if (budget.limit <= 0) { - budget.count += 1; - return true; - } - if (budget.exceeded) { - maybeAbortToolCallBudget(host); - return false; - } - budget.count += 1; - if (budget.count <= budget.limit) { - budget.inFlight += 1; - return true; - } - if (!budget.exceeded) { - budget.exceeded = true; - host.queue.push({ - type: "progress", - text: `Stopped: more than ${budget.limit} tool calls in one turn.`, - }); - } - maybeAbortToolCallBudget(host); - return false; -} - -function endToolCall(host: ToolHost) { - host.toolCallBudget.inFlight = Math.max(0, host.toolCallBudget.inFlight - 1); - maybeAbortToolCallBudget(host); -} - -function modelForCompletion(model: Model, configuredMaxTokens?: number): Model { - const maxTokens = resolveCompletionMaxTokens(model.maxTokens, configuredMaxTokens); - if (maxTokens === model.maxTokens) return model; - return { ...model, maxTokens }; -} - -function createGate(max: number) { - let active = 0; - const waiters: Array<() => void> = []; - return { - async acquire() { - if (active >= max) { - await new Promise((resolve) => { - waiters.push(resolve); - }); - } - active += 1; - }, - release() { - active = Math.max(0, active - 1); - waiters.shift()?.(); - }, - }; -} - -function createQueue(): EventQueue { - const items: AgentRuntimeEvent[] = []; - let wake: (() => void) | undefined; - let closed = false; - let failure: Error | undefined; - return { - push(event) { - items.push(event); - wake?.(); - }, - fail(error) { - failure = error; - closed = true; - wake?.(); - }, - close() { - closed = true; - wake?.(); - }, - async *iterate() { - while (true) { - if (items.length) { - yield items.shift()!; - continue; - } - if (failure) throw failure; - if (closed) return; - await new Promise((resolve) => { - wake = resolve; - }); - } - }, - }; -} - -export function reliableStreamOptions( - model: Pick, "api" | "provider" | "maxTokens">, - options?: SimpleStreamOptions, - configuredMaxTokens?: number, -): SimpleStreamOptions { - let next: SimpleStreamOptions = { - ...options, - timeoutMs: options?.timeoutMs ?? MODEL_STREAM_TIMEOUT_MS, - maxRetries: options?.maxRetries ?? MODEL_STREAM_MAX_RETRIES, - maxTokens: resolveCompletionMaxTokens(model.maxTokens, configuredMaxTokens, options?.maxTokens), - }; - - if (model.provider === "openai-codex" || model.api === "openai-codex-responses") { - // Pi cannot fall back after a WebSocket has emitted its start event. Long tool - // runs then surface abnormal close 1006 as a terminal model error. SSE has - // bounded network retries and no long-lived connection between tool turns. - next = { ...next, transport: "sse" }; - } - - // OpenCode Go/Zen require a sticky x-opencode-session header (affinity + some - // models 400 without it). Pi 0.85.1 does not attach that header on its own. - if (isOpenCodeProvider(model.provider)) { - const sessionId = next.sessionId?.trim() || randomUUID(); - next = { - ...next, - sessionId, - headers: { - "x-opencode-session": sessionId, - "x-opencode-client": "rakazo", - ...next.headers, - }, - }; - } - - return next; -} +// Re-export extracted modules +export { + catalogModels, + thinkingLevelFor, + maxToolCallsPerTurn, + toPiImages, + configuredOpenRouterModel, + resolveRuntimeModel, + modelsForRequest, + toAgentTools, + normalizeAgentToolName, + describeToolActivity, + normalizeAgentToolNames, + toAgentTool, + executeSubagent, + parametersFor, + safeJsonSchemaParameters, + builtinParameters, + pruneComputerScreenshotContext, + imagePartCount, + isComputerScreenshotMessage, + isAgentToolExecutionResult, + jsonSchemaParameters, + enumUnion, + jsonField, + summarizeToolResult, + boundAgentToolResult, + assistantText, + sanitizeSensitiveText, + redactActivityUrl, + sanitizeError, + conversationSessionId, + isOpenCodeProvider, + looksLikeOpenCodeSessionError, + sanitizeProviderError, + toolCallBudgetExceededMessage, + toolCallBudgetFor, + releaseToolCallBudget, + maybeAbortToolCallBudget, + beginToolCall, + endToolCall, + modelForCompletion, + createGate, + createQueue, + reliableStreamOptions, + toHistory, + withoutSteeringMessages, + prepareRequestSecretArguments, + PiAgentRuntimeOptions, + EventQueue, + ToolCallBudget, + ToolHost, +} from "./pi-model-config.js"; +export { + toAgentTools, + normalizeAgentToolName, + describeToolActivity, + normalizeAgentToolNames, + toAgentTool, + executeSubagent, + parametersFor, +} from "./pi-tool-utils.js"; diff --git a/packages/db/create-user.ts b/packages/db/create-user.ts new file mode 100644 index 0000000..557a559 --- /dev/null +++ b/packages/db/create-user.ts @@ -0,0 +1,28 @@ +import { createDb } from "./src/client"; + +process.env.DATABASE_URL = "postgres://rakazo:rakazodev@127.0.0.1:5433/rakazo"; +const { prisma: db } = createDb(process.env.DATABASE_URL, { applicationName: "check-users" }); + +const allUsers = await db.user.findMany({ + select: { id: true, email: true, name: true, createdAt: true, emailVerified: true }, + orderBy: { createdAt: 'desc' }, +}); + +console.log("All users:", JSON.stringify(allUsers, null, 2)); + +// Check spaces +const allSpaces = await db.space.findMany({ + select: { id: true, name: true, ownerId: true, createdAt: true }, + orderBy: { createdAt: 'desc' }, +}); + +console.log("\nAll spaces:", JSON.stringify(allSpaces, null, 2)); + +// Check space members +const allMembers = await db.spaceMember.findMany({ + select: { id: true, spaceId: true, userId: true, role: true }, +}); + +console.log("\nAll members:", JSON.stringify(allMembers, null, 2)); + +await db.$disconnect(); diff --git a/packages/db/query-users.mjs b/packages/db/query-users.mjs new file mode 100644 index 0000000..28bbafc --- /dev/null +++ b/packages/db/query-users.mjs @@ -0,0 +1,13 @@ +import { createPool, createDb } from "./src/client.js"; + +const pool = createPool(process.env.DATABASE_URL, { applicationName: "query-users" }); +const db = createDb(pool); + +const users = await db.user.findMany({ + orderBy: { createdAt: 'desc' }, + take: 10, + select: { email: true, name: true, createdAt: true }, +}); + +console.log(JSON.stringify(users, null, 2)); +await db.$disconnect(); diff --git a/packages/db/query-users.ts b/packages/db/query-users.ts new file mode 100644 index 0000000..eb10151 --- /dev/null +++ b/packages/db/query-users.ts @@ -0,0 +1,13 @@ +import { createDb } from "./src/client"; + +// Override to port 5433 (Docker published port) +process.env.DATABASE_URL = "postgres://rakazo:rakazodev@127.0.0.1:5433/rakazo"; +const { prisma: db } = createDb(process.env.DATABASE_URL, { applicationName: "query-users" }); + +const users = await db.user.findMany({ + orderBy: { createdAt: 'desc' }, + take: 10, + select: { email: true, name: true, createdAt: true }, +}); + +console.log(JSON.stringify(users, null, 2)); diff --git a/packages/db/setup-user.ts b/packages/db/setup-user.ts new file mode 100644 index 0000000..db476b2 --- /dev/null +++ b/packages/db/setup-user.ts @@ -0,0 +1,108 @@ +import { createDb } from "./src/client"; +import { randomUUID } from "node:crypto"; + +process.env.DATABASE_URL = "postgres://rakazo:rakazodev@127.0.0.1:5433/rakazo"; +const { prisma: db } = createDb(process.env.DATABASE_URL, { applicationName: "setup-user" }); + +const user = await db.user.findUnique({ + where: { email: "ingo.wagner1303@gmail.com" }, + select: { id: true, name: true }, +}); + +if (!user) { + console.log("User nicht gefunden!"); + await db.$disconnect(); + process.exit(1); +} + +console.log("User:", user); + +// Step 1: Get or create Organization +let org = await db.organization.findUnique({ where: { slug: "ingo-workspace" } }); +if (!org) { + org = await db.organization.create({ + data: { + id: randomUUID(), + name: "Ingo's Workspace", + slug: "ingo-workspace", + createdAt: new Date(), + }, + select: { id: true, name: true, slug: true }, + }); + console.log("Organization created:", org); +} else { + console.log("Organization exists:", org); +} + +// Step 2: Get or create Organization Member +let member = await db.member.findUnique({ + where: { + organizationId_userId: { organizationId: org.id, userId: user.id }, + }, +}); +if (!member) { + member = await db.member.create({ + data: { + id: randomUUID(), + userId: user.id, + organizationId: org.id, + role: "owner", + createdAt: new Date(), + }, + }); + console.log("Organization member created"); +} else { + console.log("Organization member exists"); +} + +// Step 3: Get or create default Space +let space = await db.space.findFirst({ + where: { organizationId: org.id, isDefault: true }, +}); +if (!space) { + space = await db.space.create({ + data: { + id: randomUUID(), + name: "Mein Space", + isDefault: true, + createdByUserId: user.id, + organizationId: org.id, + }, + select: { id: true, name: true }, + }); + console.log("Space created:", space); +} else { + console.log("Space exists:", space); +} + +// Step 4: Get or create space member +const existingMember = await db.spaceMember.findUnique({ + where: { + spaceId_userId: { spaceId: space.id, userId: user.id }, + }, +}); +if (!existingMember) { + await db.spaceMember.create({ + data: { + id: randomUUID(), + spaceId: space.id, + organizationId: org.id, + userId: user.id, + role: "owner", + createdAt: new Date(), + }, + }); + console.log("Space member created"); +} else { + console.log("Space member exists"); +} + +// Verify +const finalSpaces = await db.space.findMany({ + where: { organization: { members: { some: { userId: user.id } } } }, + select: { id: true, name: true, isDefault: true }, +}); +console.log("\nUser spaces:", JSON.stringify(finalSpaces, null, 2)); + +await db.$disconnect(); +console.log("\nDone!");