Repository: cyberdesk-hq/cyberdesk Branch: main Commit: 6536bfbb543b Files: 568 Total size: 3.4 MB Directory structure: gitextract_b1wrjbeg/ ├── .gitignore ├── LICENSE ├── README.md ├── apps/ │ ├── api/ │ │ ├── .dockerignore │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── drizzle/ │ │ │ └── migrations/ │ │ │ ├── 0000_oval_outlaw_kid.sql │ │ │ ├── 0001_busy_warstar.sql │ │ │ ├── 0002_regular_doctor_faustus.sql │ │ │ ├── 0003_superb_betty_brant.sql │ │ │ ├── 0004_simple_komodo.sql │ │ │ ├── 0005_mighty_hiroim.sql │ │ │ ├── 0006_icy_black_bird.sql │ │ │ ├── 0007_panoramic_tomorrow_man.sql │ │ │ └── meta/ │ │ │ ├── 0000_snapshot.json │ │ │ ├── 0001_snapshot.json │ │ │ ├── 0002_snapshot.json │ │ │ ├── 0003_snapshot.json │ │ │ ├── 0004_snapshot.json │ │ │ ├── 0005_snapshot.json │ │ │ ├── 0006_snapshot.json │ │ │ ├── 0007_snapshot.json │ │ │ └── _journal.json │ │ ├── drizzle.config.ts │ │ ├── fly.toml │ │ ├── package.json │ │ ├── src/ │ │ │ ├── db/ │ │ │ │ ├── dbActions.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── index.ts │ │ │ ├── lib/ │ │ │ │ ├── cache.ts │ │ │ │ ├── errors.ts │ │ │ │ ├── hono.ts │ │ │ │ ├── posthog.ts │ │ │ │ └── ratelimit.ts │ │ │ ├── routes/ │ │ │ │ └── desktop.ts │ │ │ └── schema/ │ │ │ ├── desktop.ts │ │ │ ├── errors.ts │ │ │ └── gateway.ts │ │ └── tsconfig.json │ ├── docs/ │ │ ├── .gitignore │ │ ├── .map.ts │ │ ├── README.md │ │ ├── app/ │ │ │ ├── api/ │ │ │ │ └── search/ │ │ │ │ └── route.ts │ │ │ ├── docs/ │ │ │ │ ├── [[...slug]]/ │ │ │ │ │ └── page.tsx │ │ │ │ └── layout.tsx │ │ │ ├── global.css │ │ │ ├── layout.config.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── source.ts │ │ ├── content/ │ │ │ └── docs/ │ │ │ ├── api-reference.mdx │ │ │ ├── conceptual-guide.mdx │ │ │ ├── index.mdx │ │ │ ├── introduction.mdx │ │ │ ├── meta.json │ │ │ ├── quickstart.mdx │ │ │ └── tutorials.mdx │ │ ├── mdx-components.tsx │ │ ├── next-env.d.ts │ │ ├── next.config.mjs │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── scripts/ │ │ │ └── generate-docs.mjs │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── web/ │ ├── .eslintrc.json │ ├── .gitignore │ ├── LICENSE.md │ ├── README.md │ ├── components.json │ ├── config.ts │ ├── middleware.ts │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── prettier.config.js │ ├── radiant/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── eslint.config.mjs │ │ ├── package.json │ │ ├── sanity.cli.ts │ │ ├── sanity.config.ts │ │ ├── schemaTypes/ │ │ │ └── index.ts │ │ ├── static/ │ │ │ └── .gitkeep │ │ └── tsconfig.json │ ├── sanity-typegen.json │ ├── sanity.cli.ts │ ├── sanity.config.ts │ ├── src/ │ │ ├── app/ │ │ │ ├── api/ │ │ │ │ ├── playground/ │ │ │ │ │ ├── chat/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── kill-desktop/ │ │ │ │ │ └── route.ts │ │ │ │ ├── stripe/ │ │ │ │ │ ├── checkout/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── portal/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── webhook/ │ │ │ │ │ └── route.ts │ │ │ │ └── unkey/ │ │ │ │ └── route.ts │ │ │ ├── auth/ │ │ │ │ └── callback/ │ │ │ │ └── route.ts │ │ │ ├── blog/ │ │ │ │ ├── [slug]/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── feed.xml/ │ │ │ │ │ └── route.ts │ │ │ │ └── page.tsx │ │ │ ├── company/ │ │ │ │ └── page.tsx │ │ │ ├── dashboard/ │ │ │ │ ├── dashboard-content.tsx │ │ │ │ └── page.tsx │ │ │ ├── demo/ │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── login/ │ │ │ │ ├── login-form.d.ts │ │ │ │ ├── login-form.tsx │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ ├── playground/ │ │ │ │ └── page.tsx │ │ │ ├── pricing/ │ │ │ │ └── page.tsx │ │ │ ├── privacy/ │ │ │ │ └── page.tsx │ │ │ ├── studio/ │ │ │ │ └── [[...tool]]/ │ │ │ │ └── page.tsx │ │ │ └── terms/ │ │ │ └── page.tsx │ │ ├── components/ │ │ │ ├── LogoText.tsx │ │ │ ├── PostHogProvider.tsx │ │ │ ├── animated-number.tsx │ │ │ ├── bento-card.tsx │ │ │ ├── bento-section.tsx │ │ │ ├── button.tsx │ │ │ ├── container.tsx │ │ │ ├── dark-bento-section.tsx │ │ │ ├── dashboard/ │ │ │ │ ├── api-key-manager.tsx │ │ │ │ ├── api-key-section.tsx │ │ │ │ ├── dashboard-layout.tsx │ │ │ │ ├── desktop-sidebar.tsx │ │ │ │ ├── faq-section.tsx │ │ │ │ ├── mobile-header.tsx │ │ │ │ ├── mobile-sidebar.tsx │ │ │ │ ├── sidebar-navigation.tsx │ │ │ │ ├── subscription-section.tsx │ │ │ │ └── vm-instances-manager.tsx │ │ │ ├── demo-section.tsx │ │ │ ├── feature-section.tsx │ │ │ ├── footer.tsx │ │ │ ├── gradient.tsx │ │ │ ├── hero.tsx │ │ │ ├── keyboard.tsx │ │ │ ├── link.tsx │ │ │ ├── linked-avatars.tsx │ │ │ ├── logo-cloud.tsx │ │ │ ├── logo-cluster.tsx │ │ │ ├── logo-timeline.tsx │ │ │ ├── logo.tsx │ │ │ ├── map.tsx │ │ │ ├── markdown-text.tsx │ │ │ ├── navbar.tsx │ │ │ ├── playground/ │ │ │ │ ├── chat-error.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── markdown.tsx │ │ │ │ ├── message.tsx │ │ │ │ ├── project-info.tsx │ │ │ │ └── prompt-suggestions.tsx │ │ │ ├── plus-grid.tsx │ │ │ ├── screenshot.tsx │ │ │ ├── shared/ │ │ │ │ └── app-logo.tsx │ │ │ ├── stripe/ │ │ │ │ ├── checkout-button.tsx │ │ │ │ ├── client-pricing-card.tsx │ │ │ │ ├── client-pricing-cards.tsx │ │ │ │ ├── payment-success.tsx │ │ │ │ └── subscription-management.tsx │ │ │ ├── testimonials.tsx │ │ │ ├── text.tsx │ │ │ ├── thread-list.tsx │ │ │ ├── thread.tsx │ │ │ ├── tooltip-icon-button.tsx │ │ │ ├── ui/ │ │ │ │ ├── button.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── resizable.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ └── tooltip.tsx │ │ │ └── yc-banner.tsx │ │ ├── sanity/ │ │ │ ├── client.ts │ │ │ ├── env.ts │ │ │ ├── image.ts │ │ │ ├── queries.ts │ │ │ ├── schema.ts │ │ │ ├── types/ │ │ │ │ ├── author.ts │ │ │ │ ├── block-content.ts │ │ │ │ ├── category.ts │ │ │ │ └── post.ts │ │ │ └── types.ts │ │ ├── styles/ │ │ │ └── tailwind.css │ │ ├── types/ │ │ │ └── database.ts │ │ └── utils/ │ │ ├── misc-utils.ts │ │ ├── playground/ │ │ │ ├── cyberdesk-client.ts │ │ │ ├── misc-demo-utils.ts │ │ │ ├── server-actions.ts │ │ │ ├── tools.ts │ │ │ └── use-scroll-to-bottom.ts │ │ ├── posthog/ │ │ │ └── posthog.ts │ │ ├── stripe/ │ │ │ ├── stripe-server.ts │ │ │ ├── stripe.ts │ │ │ └── tiers.ts │ │ └── supabase/ │ │ ├── client.ts │ │ ├── middleware.ts │ │ ├── server.ts │ │ ├── supabaseClient.js │ │ └── supabaseServerClient.ts │ └── tsconfig.json ├── cyberdesk-architecture.md ├── infra/ │ ├── README.md │ ├── kubernetes/ │ │ ├── azure-snapshot-class.yaml │ │ ├── cdi-cr.yaml │ │ ├── cdi-operator.yaml │ │ ├── cluster-issuer.yaml │ │ ├── cyberdesk-cr-v2.yaml │ │ ├── cyberdesk-cr.yaml │ │ ├── cyberdesk-operator.yaml │ │ ├── default-backend.yaml │ │ ├── gateway-deploy.yaml │ │ ├── gateway-ingress-dev.yaml │ │ ├── gateway-ingress-prod.yaml │ │ ├── golden-vm-snapshot-request.yaml │ │ ├── kubevirt-cr.yaml │ │ ├── kubevirt-operator.yaml │ │ ├── readme-todo.txt │ │ ├── start-cyberdesk-operator-cr.yaml │ │ └── warm-pool.yaml │ └── terraform/ │ ├── .terraform.lock.hcl │ ├── main.tf │ └── variables.tf ├── sdks/ │ ├── openapi.json │ ├── py-sdk/ │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── cyberdesk/ │ │ │ ├── __init__.py │ │ │ ├── actions.py │ │ │ ├── client.py │ │ │ └── types.py │ │ ├── openapi_client/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── api_reference_client/ │ │ │ │ ├── __init__.py │ │ │ │ ├── api/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── desktop/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── get_v1_desktop_id.py │ │ │ │ │ ├── post_v1_desktop.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action.py │ │ │ │ │ └── post_v1_desktop_id_stop.py │ │ │ │ ├── client.py │ │ │ │ ├── errors.py │ │ │ │ ├── models/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── get_v1_desktop_id_response_200.py │ │ │ │ │ ├── get_v1_desktop_id_response_200_status.py │ │ │ │ │ ├── get_v1_desktop_id_response_400.py │ │ │ │ │ ├── get_v1_desktop_id_response_400_status.py │ │ │ │ │ ├── get_v1_desktop_id_response_401.py │ │ │ │ │ ├── get_v1_desktop_id_response_401_status.py │ │ │ │ │ ├── get_v1_desktop_id_response_403.py │ │ │ │ │ ├── get_v1_desktop_id_response_403_status.py │ │ │ │ │ ├── get_v1_desktop_id_response_404.py │ │ │ │ │ ├── get_v1_desktop_id_response_404_status.py │ │ │ │ │ ├── get_v1_desktop_id_response_409.py │ │ │ │ │ ├── get_v1_desktop_id_response_409_status.py │ │ │ │ │ ├── get_v1_desktop_id_response_429.py │ │ │ │ │ ├── get_v1_desktop_id_response_429_status.py │ │ │ │ │ ├── get_v1_desktop_id_response_500.py │ │ │ │ │ ├── get_v1_desktop_id_response_500_status.py │ │ │ │ │ ├── get_v1_desktop_id_response_502.py │ │ │ │ │ ├── get_v1_desktop_id_response_502_status.py │ │ │ │ │ ├── post_v1_desktop_body.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_body.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_200.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_400.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_400_status.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_401.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_401_status.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_403.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_403_status.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_404.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_404_status.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_409.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_409_status.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_429.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_429_status.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_500.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_500_status.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_502.py │ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_502_status.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_click_mouse_action.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_click_mouse_action_button.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_click_mouse_action_click_type.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_click_mouse_action_type.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_drag_mouse_action.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_drag_mouse_action_end.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_drag_mouse_action_start.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_drag_mouse_action_type.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_get_cursor_position_action.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_get_cursor_position_action_type.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_move_mouse_action.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_move_mouse_action_type.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_press_keys_action.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_press_keys_action_key_action_type.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_press_keys_action_type.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_200.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_400.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_400_status.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_401.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_401_status.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_403.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_403_status.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_404.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_404_status.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_409.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_409_status.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_429.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_429_status.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_500.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_500_status.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_502.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_502_status.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_screenshot_action.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_screenshot_action_type.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_scroll_action.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_scroll_action_direction.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_scroll_action_type.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_type_text_action.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_type_text_action_type.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_wait_action.py │ │ │ │ │ ├── post_v1_desktop_id_computer_action_wait_action_type.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_200.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_200_status.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_400.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_400_status.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_401.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_401_status.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_403.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_403_status.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_404.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_404_status.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_409.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_409_status.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_429.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_429_status.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_500.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_500_status.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_502.py │ │ │ │ │ ├── post_v1_desktop_id_stop_response_502_status.py │ │ │ │ │ ├── post_v1_desktop_response_200.py │ │ │ │ │ ├── post_v1_desktop_response_200_status.py │ │ │ │ │ ├── post_v1_desktop_response_400.py │ │ │ │ │ ├── post_v1_desktop_response_400_status.py │ │ │ │ │ ├── post_v1_desktop_response_401.py │ │ │ │ │ ├── post_v1_desktop_response_401_status.py │ │ │ │ │ ├── post_v1_desktop_response_403.py │ │ │ │ │ ├── post_v1_desktop_response_403_status.py │ │ │ │ │ ├── post_v1_desktop_response_404.py │ │ │ │ │ ├── post_v1_desktop_response_404_status.py │ │ │ │ │ ├── post_v1_desktop_response_409.py │ │ │ │ │ ├── post_v1_desktop_response_409_status.py │ │ │ │ │ ├── post_v1_desktop_response_429.py │ │ │ │ │ ├── post_v1_desktop_response_429_status.py │ │ │ │ │ ├── post_v1_desktop_response_500.py │ │ │ │ │ ├── post_v1_desktop_response_500_status.py │ │ │ │ │ ├── post_v1_desktop_response_502.py │ │ │ │ │ └── post_v1_desktop_response_502_status.py │ │ │ │ ├── py.typed │ │ │ │ └── types.py │ │ │ └── pyproject.toml │ │ ├── pyproject.toml │ │ └── scripts/ │ │ └── generate.py │ ├── sandbox/ │ │ └── py-sdk/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── test_sdk.py │ └── ts-sdk/ │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── openapi-ts.config.ts │ ├── package.json │ ├── src/ │ │ ├── client/ │ │ │ ├── client.gen.ts │ │ │ ├── index.ts │ │ │ ├── sdk.gen.ts │ │ │ └── types.gen.ts │ │ └── index.ts │ └── tsconfig.json ├── self-host.md └── services/ ├── cyberdesk-operator/ │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── checklist.md │ ├── docs/ │ │ └── troubleshooting.md │ ├── handlers/ │ │ └── controller.py │ ├── requirements.txt │ └── tests/ │ ├── README.md │ ├── test-cyberdesk-cr.yaml │ ├── test-start-operator-cr.yaml │ ├── test-start-operator-crd.yaml │ └── test.py └── gateway/ ├── Dockerfile ├── README.md ├── main.py ├── noVNC/ │ ├── .github/ │ │ ├── ISSUE_TEMPLATE/ │ │ │ ├── bug_report.md │ │ │ ├── config.yml │ │ │ └── feature_request.md │ │ └── workflows/ │ │ ├── deploy.yml │ │ ├── lint.yml │ │ ├── test.yml │ │ └── translate.yml │ ├── .gitignore │ ├── .gitmodules │ ├── AUTHORS │ ├── LICENSE.txt │ ├── README.md │ ├── app/ │ │ ├── error-handler.js │ │ ├── images/ │ │ │ └── icons/ │ │ │ └── Makefile │ │ ├── locale/ │ │ │ ├── README │ │ │ ├── cs.json │ │ │ ├── de.json │ │ │ ├── el.json │ │ │ ├── es.json │ │ │ ├── fr.json │ │ │ ├── it.json │ │ │ ├── ja.json │ │ │ ├── ko.json │ │ │ ├── nl.json │ │ │ ├── pl.json │ │ │ ├── pt_BR.json │ │ │ ├── ru.json │ │ │ ├── sv.json │ │ │ ├── tr.json │ │ │ ├── zh_CN.json │ │ │ └── zh_TW.json │ │ ├── localization.js │ │ ├── sounds/ │ │ │ ├── CREDITS │ │ │ └── bell.oga │ │ ├── styles/ │ │ │ ├── base.css │ │ │ ├── constants.css │ │ │ └── input.css │ │ ├── ui.js │ │ └── webutil.js │ ├── core/ │ │ ├── base64.js │ │ ├── crypto/ │ │ │ ├── aes.js │ │ │ ├── bigint.js │ │ │ ├── crypto.js │ │ │ ├── des.js │ │ │ ├── dh.js │ │ │ ├── md5.js │ │ │ └── rsa.js │ │ ├── decoders/ │ │ │ ├── copyrect.js │ │ │ ├── h264.js │ │ │ ├── hextile.js │ │ │ ├── jpeg.js │ │ │ ├── raw.js │ │ │ ├── rre.js │ │ │ ├── tight.js │ │ │ ├── tightpng.js │ │ │ ├── zlib.js │ │ │ └── zrle.js │ │ ├── deflator.js │ │ ├── display.js │ │ ├── encodings.js │ │ ├── inflator.js │ │ ├── input/ │ │ │ ├── domkeytable.js │ │ │ ├── fixedkeys.js │ │ │ ├── gesturehandler.js │ │ │ ├── keyboard.js │ │ │ ├── keysym.js │ │ │ ├── keysymdef.js │ │ │ ├── util.js │ │ │ ├── vkeys.js │ │ │ └── xtscancodes.js │ │ ├── ra2.js │ │ ├── rfb.js │ │ ├── util/ │ │ │ ├── browser.js │ │ │ ├── cursor.js │ │ │ ├── element.js │ │ │ ├── events.js │ │ │ ├── eventtarget.js │ │ │ ├── int.js │ │ │ ├── logging.js │ │ │ └── strings.js │ │ └── websock.js │ ├── defaults.json │ ├── docs/ │ │ ├── API-internal.md │ │ ├── API.md │ │ ├── EMBEDDING.md │ │ ├── LIBRARY.md │ │ ├── LICENSE.BSD-2-Clause │ │ ├── LICENSE.BSD-3-Clause │ │ ├── LICENSE.MPL-2.0 │ │ ├── LICENSE.OFL-1.1 │ │ ├── flash_policy.txt │ │ ├── links │ │ ├── notes │ │ ├── novnc_proxy.1 │ │ └── rfb_notes │ ├── eslint.config.mjs │ ├── karma.conf.js │ ├── mandatory.json │ ├── package.json │ ├── po/ │ │ ├── Makefile │ │ ├── cs.po │ │ ├── de.po │ │ ├── el.po │ │ ├── es.po │ │ ├── fr.po │ │ ├── it.po │ │ ├── ja.po │ │ ├── ko.po │ │ ├── nl.po │ │ ├── noVNC.pot │ │ ├── pl.po │ │ ├── po2js │ │ ├── pt_BR.po │ │ ├── ru.po │ │ ├── sv.po │ │ ├── tr.po │ │ ├── xgettext-html │ │ ├── zh_CN.po │ │ └── zh_TW.po │ ├── snap/ │ │ ├── hooks/ │ │ │ └── configure │ │ ├── local/ │ │ │ └── svc_wrapper.sh │ │ └── snapcraft.yaml │ ├── tests/ │ │ ├── assertions.js │ │ ├── fake.websocket.js │ │ ├── playback-ui.js │ │ ├── playback.js │ │ ├── test.base64.js │ │ ├── test.browser.js │ │ ├── test.copyrect.js │ │ ├── test.deflator.js │ │ ├── test.display.js │ │ ├── test.gesturehandler.js │ │ ├── test.h264.js │ │ ├── test.helper.js │ │ ├── test.hextile.js │ │ ├── test.inflator.js │ │ ├── test.int.js │ │ ├── test.jpeg.js │ │ ├── test.keyboard.js │ │ ├── test.localization.js │ │ ├── test.raw.js │ │ ├── test.rfb.js │ │ ├── test.rre.js │ │ ├── test.tight.js │ │ ├── test.tightpng.js │ │ ├── test.util.js │ │ ├── test.websock.js │ │ ├── test.webutil.js │ │ ├── test.zlib.js │ │ ├── test.zrle.js │ │ └── vnc_playback.html │ ├── utils/ │ │ ├── README.md │ │ ├── b64-to-binary.pl │ │ ├── convert.js │ │ ├── genkeysymdef.js │ │ ├── novnc_proxy │ │ ├── u2x11 │ │ └── validate │ ├── vendor/ │ │ └── pako/ │ │ ├── LICENSE │ │ ├── README.md │ │ └── lib/ │ │ ├── utils/ │ │ │ └── common.js │ │ └── zlib/ │ │ ├── adler32.js │ │ ├── constants.js │ │ ├── crc32.js │ │ ├── deflate.js │ │ ├── gzheader.js │ │ ├── inffast.js │ │ ├── inflate.js │ │ ├── inftrees.js │ │ ├── messages.js │ │ ├── trees.js │ │ └── zstream.js │ ├── vnc.html │ └── vnc_lite.html └── requirements.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Local .terraform directories **/.terraform/* # .tfstate files *.tfstate *.tfstate.* # Crash log files crash.log crash.*.log # Exclude all .tfvars files, which are likely to contain sensitive data *.tfvars *.tfvars.json # Ignore override files as they are usually used for local development override.tf override.tf.json *_override.tf *_override.tf.json # Ignore CLI configuration files .terraformrc terraform.rc # Ignore kubeconfig kubeconfig.yaml # Ignore testing-do-not-push testing-do-not-push/* user-data.yaml user-data-base64.txt # Kubernetes secrets - DO NOT COMMIT REAL CREDENTIALS infra/kubernetes/cyberdesk-secret.yaml infra/kubernetes/checkpoint-cyberdesk-secret.yaml infra/kubernetes/golden-vm-deploy.yaml # Local development environment variables .env infra/kubernetes/test-secret.yaml ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[ ]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2024 Cyberdesk Team Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================

Cyberdesk Logo

The open source infra for virtual desktop orchestration, tailored for computer agents

NPM Version NPM Downloads PyPI Version PyPI Downloads

Discord License: Apache 2.0 GitHub Stars

Cyberdesk Demo
A computer agent operating a Cyberdesk virtual desktop from a user prompt

--- ## 🚀 Quick Start ### TypeScript ```bash npm install cyberdesk@0.2.1 ``` ```typescript import { createCyberdeskClient } from 'cyberdesk'; const cyberdesk = createCyberdeskClient({ apiKey: 'YOUR_API_KEY' }); const launchResult = await cyberdesk.launchDesktop({ body: { timeout_ms: 10000 } }); const desktopId = launchResult.id; // Take a screenshot const screenshot = await cyberdesk.executeComputerAction({ path: { id: desktopId }, body: { type: 'screenshot' } }); // Left click at (100, 150) await cyberdesk.executeComputerAction({ path: { id: desktopId }, body: { type: 'click_mouse', x: 100, y: 150, button: 'left' } }); ``` ### Python ```bash pip install cyberdesk==0.2.7 ``` ```python from cyberdesk import CyberdeskClient from cyberdesk.actions import click_mouse, screenshot, ClickMouseButton client = CyberdeskClient(api_key="YOUR_API_KEY") result = client.launch_desktop(timeout_ms=10000) desktop_id = result.id # Take a screenshot screenshot_action = screenshot() screenshot_result = client.execute_computer_action(desktop_id, screenshot_action) # Left click at (100, 150) click_action = click_mouse(x=100, y=150, button=ClickMouseButton.LEFT) client.execute_computer_action(desktop_id, click_action) ``` 👉 For more details and advanced usage, see the [Quickstart Guide](https://docs.cyberdesk.io/docs/quickstart) and [Official Documentation](#-official-documentation). --- ## ✨ Features

🚀 Fast Launch
Spin up virtual desktops in seconds, ready for automation or remote use.


🖱️ Full Automation
Control mouse, keyboard, and more—perfect for computer agents.


🖥️ Cloud Native
Runs on AKS, or self-hosted on your own infrastructure.


🔒 Secure & Auditable
Session logs, API keys, and enterprise-grade security.


🧩 Type-Safe SDKs
Official Python & TypeScript SDKs with full type hints.


🤖 AI-Ready
Tailor built for the next generation of computer use agents

--- ## 📚 Official Documentation - [Quickstart Guide](https://docs.cyberdesk.io/docs/quickstart) - [API Reference](https://docs.cyberdesk.io/docs/api-reference) - [TypeScript SDK](sdks/ts-sdk/README.md) - [Python SDK](sdks/py-sdk/README.md) --- ## 🛠️ Project Structure ### /apps - **web**: Landing page and dashboard ([README](apps/web/README.md)) - **api**: Developer-facing API ([README](apps/api/README.md)) - **docs**: Documentation site ([README](apps/docs/README.md)) ### /services - **cyberdesk-operator**: Kubernetes operator for managing Cyberdesk Custom Resources, and starting/stopping Kubevirt virtual machines ([README](services/cyberdesk-operator/README.md)) - **gateway**: HTTP service that proxies requests to the Kubevirt API, and routes them to the correct virtual machine ([README](services/gateway/README.md)) ### /sdks - **ts-sdk**: TypeScript SDK ([README](sdks/ts-sdk/README.md)) - **py-sdk**: Python SDK ([README](sdks/py-sdk/README.md)) ### /infrastructure - **terraform**: AKS Cluster Setup (Terraform) ([README](infrastructure/README.md)) - **kubernetes**: Kubernetes resources for the Cyberdesk operator --- ## 🤝 Contributing We welcome contributions! - Join the [Discord](https://discord.gg/ws5ddx5yZ8) for discussion and support - Get a personal 1-1 walkthrough of how to self host the project by contacting us on [Discord](https://discord.gg/ws5ddx5yZ8) --- ## 📣 Community & Support - [Discord](https://discord.gg/ws5ddx5yZ8) for help and chat - [Good First Issues](https://github.com/cyberdesk-hq/cyberdesk/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - [Open an Issue](https://github.com/cyberdesk-hq/cyberdesk/issues) --- ## 💡 Philosophy > At **Cyberdesk** our mission is to make building computer agents as easy as playing with legos. We believe in open, simple, and extensible tools for the new generation of developers: *computer agent developers*. --- ## 📄 License Apache License 2.0. See [LICENSE](LICENSE). ---

Made with ❤️ by the Cyberdesk Team

================================================ FILE: apps/api/.dockerignore ================================================ /.git /node_modules .dockerignore .env Dockerfile fly.toml ================================================ FILE: apps/api/.gitignore ================================================ # prod dist/ # dev .yarn/ !.yarn/releases .vscode/* !.vscode/launch.json !.vscode/*.code-snippets .idea/workspace.xml .idea/usage.statistics.xml .idea/shelf # deps node_modules/ .wrangler # env .env .env.production .dev.vars # logs logs/ *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* # misc .DS_Store # turbo .turbo ================================================ FILE: apps/api/Dockerfile ================================================ # syntax = docker/dockerfile:1 # Adjust NODE_VERSION as desired ARG NODE_VERSION=20.18.0 FROM node:${NODE_VERSION}-slim AS base LABEL fly_launch_runtime="Node.js" # Node.js app lives here WORKDIR /app # Set production environment ENV NODE_ENV="production" # Throw-away build stage to reduce size of final image FROM base AS build # Install packages needed to build node modules RUN apt-get update -qq && \ apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3 # Install node modules COPY package.json ./ RUN npm install --include=dev # Copy application code COPY . . # Build application RUN npm run build # Remove development dependencies RUN npm prune --omit=dev # Final stage for app image FROM base # Copy built application COPY --from=build /app /app # Start the server by default, this can be overwritten at runtime EXPOSE 3000 CMD [ "npm", "run", "start" ] ================================================ FILE: apps/api/README.md ================================================ # Created by Unkey's toolbox This API is built with speed, and security in mind. The API is built with [Hono](https://hono.dev), [Unkey](https://unkey.com) and [Supabase](https://supabase.com) with hosting on [Fly.io](https://fly.io). ## Getting Started You will need a free account for both Unkey and Supabase to run this project. ### Unkey For Unkey you will need your API ID and a root key scoped to: - Create Key - Create Namespace - Limit You can of course add more scopes as required. ### Supabase For Supabase, you'll need to create a project and get your: - Supabase URL - Supabase Anon Key - Supabase Connection String (found in the Database settings under Connection Pooling) ## Environment Variables To run this project, you will need to add the following environment variables to your .dev.vars file ```env SUPABASE_URL=https://your-project-id.supabase.co SUPABASE_ANON_KEY=your-supabase-anon-key SUPABASE_CONNECTION_STRING=postgres://postgres:your-password@your-project-id.supabase.co:5432/postgres?pgbouncer=true UNKEY_API_ID=UNKEY_API_ID UNKEY_ROOT_KEY=UNKEY_ROOT_KEY ``` ## Usage Make sure that you have run: ```bash npm run db:generate npm run db:push ``` You can then run `npm run dev` Then you will have access to the following routes: `/keys/create` - To create an API key to use with the other endpoints. Then the desktop routes (all under the `/v1` prefix): ```bash /v1/desktop # Create a new desktop instance /v1/desktop/{id}/stop # Stop a desktop instance /v1/desktop/{id}/computer-action # Perform a computer action /v1/desktop/{id}/bash-action # Execute a bash command ``` You also have access to the open-api spec found at [http://localhost:8787/open-api](http://localhost:8787/open-api) ================================================ FILE: apps/api/drizzle/migrations/0000_oval_outlaw_kid.sql ================================================ CREATE TABLE IF NOT EXISTS "desktop_instances" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "user_id" uuid NOT NULL, "created_at" timestamp DEFAULT now(), "ended_at" timestamp ); --> statement-breakpoint DO $$ BEGIN ALTER TABLE "desktop_instances" ADD CONSTRAINT "desktop_instances_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE no action ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN null; END $$; ================================================ FILE: apps/api/drizzle/migrations/0001_busy_warstar.sql ================================================ ALTER TABLE "desktop_instances" ADD COLUMN "remote_id" varchar(255); ================================================ FILE: apps/api/drizzle/migrations/0002_regular_doctor_faustus.sql ================================================ CREATE TABLE IF NOT EXISTS "profiles" ( "id" uuid PRIMARY KEY NOT NULL, "unkey_key_id" varchar(255), "stripe_customer_id" varchar(255), "stripe_subscription_id" varchar(255), "current_period_end" timestamp, "subscription_status" varchar(50), "plan_id" varchar(100), "cancel_at_period_end" boolean DEFAULT false, "created_at" timestamp DEFAULT now(), "updated_at" timestamp DEFAULT now() ); --> statement-breakpoint ALTER TABLE "desktop_instances" ALTER COLUMN "remote_id" SET NOT NULL;--> statement-breakpoint DO $$ BEGIN ALTER TABLE "profiles" ADD CONSTRAINT "profiles_id_users_id_fk" FOREIGN KEY ("id") REFERENCES "auth"."users"("id") ON DELETE no action ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN null; END $$; ================================================ FILE: apps/api/drizzle/migrations/0003_superb_betty_brant.sql ================================================ ALTER TABLE "desktop_instances" ADD COLUMN "stream_url" varchar(1024) NOT NULL; ================================================ FILE: apps/api/drizzle/migrations/0004_simple_komodo.sql ================================================ ALTER TABLE "desktop_instances" ALTER COLUMN "stream_url" SET DEFAULT 'https://placeholder-stream-url.cyberdesk.dev'; ================================================ FILE: apps/api/drizzle/migrations/0005_mighty_hiroim.sql ================================================ ALTER TABLE "desktop_instances" DROP CONSTRAINT "desktop_instances_user_id_users_id_fk"; --> statement-breakpoint ALTER TABLE "profiles" DROP CONSTRAINT "profiles_id_users_id_fk"; --> statement-breakpoint DO $$ BEGIN ALTER TABLE "desktop_instances" ADD CONSTRAINT "desktop_instances_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN null; END $$; --> statement-breakpoint DO $$ BEGIN ALTER TABLE "profiles" ADD CONSTRAINT "profiles_id_users_id_fk" FOREIGN KEY ("id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN null; END $$; ================================================ FILE: apps/api/drizzle/migrations/0006_icy_black_bird.sql ================================================ DO $$ BEGIN CREATE TYPE "public"."instance_status" AS ENUM('pending', 'running', 'completed', 'error'); EXCEPTION WHEN duplicate_object THEN null; END $$; --> statement-breakpoint CREATE TABLE IF NOT EXISTS "cyberdesk_instances" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "user_id" uuid NOT NULL, "created_at" timestamp DEFAULT now(), "updated_at" timestamp, "status" "instance_status" DEFAULT 'pending' NOT NULL, "timeout_at" timestamp DEFAULT NOW() + interval '24 hours' NOT NULL, "stream_url" varchar(1024) ); --> statement-breakpoint DO $$ BEGIN ALTER TABLE "cyberdesk_instances" ADD CONSTRAINT "cyberdesk_instances_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN null; END $$; ================================================ FILE: apps/api/drizzle/migrations/0007_panoramic_tomorrow_man.sql ================================================ ALTER TYPE "instance_status" ADD VALUE 'terminated'; ================================================ FILE: apps/api/drizzle/migrations/meta/0000_snapshot.json ================================================ { "id": "1816b590-90c5-4f05-9726-cc8f33d18251", "prevId": "00000000-0000-0000-0000-000000000000", "version": "6", "dialect": "postgresql", "tables": { "public.desktop_instances": { "name": "desktop_instances", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true, "default": "gen_random_uuid()" }, "user_id": { "name": "user_id", "type": "uuid", "primaryKey": false, "notNull": true }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "ended_at": { "name": "ended_at", "type": "timestamp", "primaryKey": false, "notNull": false } }, "indexes": {}, "foreignKeys": { "desktop_instances_user_id_users_id_fk": { "name": "desktop_instances_user_id_users_id_fk", "tableFrom": "desktop_instances", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "user_id" ], "columnsTo": [ "id" ], "onDelete": "no action", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} } }, "enums": {}, "schemas": {}, "_meta": { "columns": {}, "schemas": {}, "tables": {} } } ================================================ FILE: apps/api/drizzle/migrations/meta/0001_snapshot.json ================================================ { "id": "61aff1d0-bd56-4301-852c-bc67c46a1cd9", "prevId": "1816b590-90c5-4f05-9726-cc8f33d18251", "version": "6", "dialect": "postgresql", "tables": { "public.desktop_instances": { "name": "desktop_instances", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true, "default": "gen_random_uuid()" }, "remote_id": { "name": "remote_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "user_id": { "name": "user_id", "type": "uuid", "primaryKey": false, "notNull": true }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "ended_at": { "name": "ended_at", "type": "timestamp", "primaryKey": false, "notNull": false } }, "indexes": {}, "foreignKeys": { "desktop_instances_user_id_users_id_fk": { "name": "desktop_instances_user_id_users_id_fk", "tableFrom": "desktop_instances", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "user_id" ], "columnsTo": [ "id" ], "onDelete": "no action", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} } }, "enums": {}, "schemas": {}, "_meta": { "columns": {}, "schemas": {}, "tables": {} } } ================================================ FILE: apps/api/drizzle/migrations/meta/0002_snapshot.json ================================================ { "id": "4d86e969-3e44-4e26-bb6d-b76b9daa733e", "prevId": "61aff1d0-bd56-4301-852c-bc67c46a1cd9", "version": "6", "dialect": "postgresql", "tables": { "public.desktop_instances": { "name": "desktop_instances", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true, "default": "gen_random_uuid()" }, "remote_id": { "name": "remote_id", "type": "varchar(255)", "primaryKey": false, "notNull": true }, "user_id": { "name": "user_id", "type": "uuid", "primaryKey": false, "notNull": true }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "ended_at": { "name": "ended_at", "type": "timestamp", "primaryKey": false, "notNull": false } }, "indexes": {}, "foreignKeys": { "desktop_instances_user_id_users_id_fk": { "name": "desktop_instances_user_id_users_id_fk", "tableFrom": "desktop_instances", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "user_id" ], "columnsTo": [ "id" ], "onDelete": "no action", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} }, "public.profiles": { "name": "profiles", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true }, "unkey_key_id": { "name": "unkey_key_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "stripe_customer_id": { "name": "stripe_customer_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "stripe_subscription_id": { "name": "stripe_subscription_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "current_period_end": { "name": "current_period_end", "type": "timestamp", "primaryKey": false, "notNull": false }, "subscription_status": { "name": "subscription_status", "type": "varchar(50)", "primaryKey": false, "notNull": false }, "plan_id": { "name": "plan_id", "type": "varchar(100)", "primaryKey": false, "notNull": false }, "cancel_at_period_end": { "name": "cancel_at_period_end", "type": "boolean", "primaryKey": false, "notNull": false, "default": false }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "updated_at": { "name": "updated_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" } }, "indexes": {}, "foreignKeys": { "profiles_id_users_id_fk": { "name": "profiles_id_users_id_fk", "tableFrom": "profiles", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "id" ], "columnsTo": [ "id" ], "onDelete": "no action", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} } }, "enums": {}, "schemas": {}, "_meta": { "columns": {}, "schemas": {}, "tables": {} } } ================================================ FILE: apps/api/drizzle/migrations/meta/0003_snapshot.json ================================================ { "id": "9b509c40-ab3f-410b-ac01-9401df6598c2", "prevId": "4d86e969-3e44-4e26-bb6d-b76b9daa733e", "version": "6", "dialect": "postgresql", "tables": { "public.desktop_instances": { "name": "desktop_instances", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true, "default": "gen_random_uuid()" }, "remote_id": { "name": "remote_id", "type": "varchar(255)", "primaryKey": false, "notNull": true }, "user_id": { "name": "user_id", "type": "uuid", "primaryKey": false, "notNull": true }, "stream_url": { "name": "stream_url", "type": "varchar(1024)", "primaryKey": false, "notNull": true }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "ended_at": { "name": "ended_at", "type": "timestamp", "primaryKey": false, "notNull": false } }, "indexes": {}, "foreignKeys": { "desktop_instances_user_id_users_id_fk": { "name": "desktop_instances_user_id_users_id_fk", "tableFrom": "desktop_instances", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "user_id" ], "columnsTo": [ "id" ], "onDelete": "no action", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} }, "public.profiles": { "name": "profiles", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true }, "unkey_key_id": { "name": "unkey_key_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "stripe_customer_id": { "name": "stripe_customer_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "stripe_subscription_id": { "name": "stripe_subscription_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "current_period_end": { "name": "current_period_end", "type": "timestamp", "primaryKey": false, "notNull": false }, "subscription_status": { "name": "subscription_status", "type": "varchar(50)", "primaryKey": false, "notNull": false }, "plan_id": { "name": "plan_id", "type": "varchar(100)", "primaryKey": false, "notNull": false }, "cancel_at_period_end": { "name": "cancel_at_period_end", "type": "boolean", "primaryKey": false, "notNull": false, "default": false }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "updated_at": { "name": "updated_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" } }, "indexes": {}, "foreignKeys": { "profiles_id_users_id_fk": { "name": "profiles_id_users_id_fk", "tableFrom": "profiles", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "id" ], "columnsTo": [ "id" ], "onDelete": "no action", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} } }, "enums": {}, "schemas": {}, "_meta": { "columns": {}, "schemas": {}, "tables": {} } } ================================================ FILE: apps/api/drizzle/migrations/meta/0004_snapshot.json ================================================ { "id": "36e12ff1-cb7e-46bd-bdc4-796464df0849", "prevId": "9b509c40-ab3f-410b-ac01-9401df6598c2", "version": "6", "dialect": "postgresql", "tables": { "public.desktop_instances": { "name": "desktop_instances", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true, "default": "gen_random_uuid()" }, "remote_id": { "name": "remote_id", "type": "varchar(255)", "primaryKey": false, "notNull": true }, "user_id": { "name": "user_id", "type": "uuid", "primaryKey": false, "notNull": true }, "stream_url": { "name": "stream_url", "type": "varchar(1024)", "primaryKey": false, "notNull": true, "default": "'https://placeholder-stream-url.cyberdesk.io'" }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "ended_at": { "name": "ended_at", "type": "timestamp", "primaryKey": false, "notNull": false } }, "indexes": {}, "foreignKeys": { "desktop_instances_user_id_users_id_fk": { "name": "desktop_instances_user_id_users_id_fk", "tableFrom": "desktop_instances", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "user_id" ], "columnsTo": [ "id" ], "onDelete": "no action", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} }, "public.profiles": { "name": "profiles", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true }, "unkey_key_id": { "name": "unkey_key_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "stripe_customer_id": { "name": "stripe_customer_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "stripe_subscription_id": { "name": "stripe_subscription_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "current_period_end": { "name": "current_period_end", "type": "timestamp", "primaryKey": false, "notNull": false }, "subscription_status": { "name": "subscription_status", "type": "varchar(50)", "primaryKey": false, "notNull": false }, "plan_id": { "name": "plan_id", "type": "varchar(100)", "primaryKey": false, "notNull": false }, "cancel_at_period_end": { "name": "cancel_at_period_end", "type": "boolean", "primaryKey": false, "notNull": false, "default": false }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "updated_at": { "name": "updated_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" } }, "indexes": {}, "foreignKeys": { "profiles_id_users_id_fk": { "name": "profiles_id_users_id_fk", "tableFrom": "profiles", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "id" ], "columnsTo": [ "id" ], "onDelete": "no action", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} } }, "enums": {}, "schemas": {}, "_meta": { "columns": {}, "schemas": {}, "tables": {} } } ================================================ FILE: apps/api/drizzle/migrations/meta/0005_snapshot.json ================================================ { "id": "0efe305c-c062-4173-bc2d-8d92af93250f", "prevId": "36e12ff1-cb7e-46bd-bdc4-796464df0849", "version": "6", "dialect": "postgresql", "tables": { "public.desktop_instances": { "name": "desktop_instances", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true, "default": "gen_random_uuid()" }, "remote_id": { "name": "remote_id", "type": "varchar(255)", "primaryKey": false, "notNull": true }, "user_id": { "name": "user_id", "type": "uuid", "primaryKey": false, "notNull": true }, "stream_url": { "name": "stream_url", "type": "varchar(1024)", "primaryKey": false, "notNull": true, "default": "'https://placeholder-stream-url.cyberdesk.io'" }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "ended_at": { "name": "ended_at", "type": "timestamp", "primaryKey": false, "notNull": false } }, "indexes": {}, "foreignKeys": { "desktop_instances_user_id_users_id_fk": { "name": "desktop_instances_user_id_users_id_fk", "tableFrom": "desktop_instances", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "user_id" ], "columnsTo": [ "id" ], "onDelete": "cascade", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} }, "public.profiles": { "name": "profiles", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true }, "unkey_key_id": { "name": "unkey_key_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "stripe_customer_id": { "name": "stripe_customer_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "stripe_subscription_id": { "name": "stripe_subscription_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "current_period_end": { "name": "current_period_end", "type": "timestamp", "primaryKey": false, "notNull": false }, "subscription_status": { "name": "subscription_status", "type": "varchar(50)", "primaryKey": false, "notNull": false }, "plan_id": { "name": "plan_id", "type": "varchar(100)", "primaryKey": false, "notNull": false }, "cancel_at_period_end": { "name": "cancel_at_period_end", "type": "boolean", "primaryKey": false, "notNull": false, "default": false }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "updated_at": { "name": "updated_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" } }, "indexes": {}, "foreignKeys": { "profiles_id_users_id_fk": { "name": "profiles_id_users_id_fk", "tableFrom": "profiles", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "id" ], "columnsTo": [ "id" ], "onDelete": "cascade", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} } }, "enums": {}, "schemas": {}, "_meta": { "columns": {}, "schemas": {}, "tables": {} } } ================================================ FILE: apps/api/drizzle/migrations/meta/0006_snapshot.json ================================================ { "id": "1f1576f8-a386-4198-a4b4-dbe02dbc8468", "prevId": "0efe305c-c062-4173-bc2d-8d92af93250f", "version": "6", "dialect": "postgresql", "tables": { "public.cyberdesk_instances": { "name": "cyberdesk_instances", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true, "default": "gen_random_uuid()" }, "user_id": { "name": "user_id", "type": "uuid", "primaryKey": false, "notNull": true }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "updated_at": { "name": "updated_at", "type": "timestamp", "primaryKey": false, "notNull": false }, "status": { "name": "status", "type": "instance_status", "typeSchema": "public", "primaryKey": false, "notNull": true, "default": "'pending'" }, "timeout_at": { "name": "timeout_at", "type": "timestamp", "primaryKey": false, "notNull": true, "default": "NOW() + interval '24 hours'" }, "stream_url": { "name": "stream_url", "type": "varchar(1024)", "primaryKey": false, "notNull": false } }, "indexes": {}, "foreignKeys": { "cyberdesk_instances_user_id_users_id_fk": { "name": "cyberdesk_instances_user_id_users_id_fk", "tableFrom": "cyberdesk_instances", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "user_id" ], "columnsTo": [ "id" ], "onDelete": "cascade", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} }, "public.desktop_instances": { "name": "desktop_instances", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true, "default": "gen_random_uuid()" }, "remote_id": { "name": "remote_id", "type": "varchar(255)", "primaryKey": false, "notNull": true }, "user_id": { "name": "user_id", "type": "uuid", "primaryKey": false, "notNull": true }, "stream_url": { "name": "stream_url", "type": "varchar(1024)", "primaryKey": false, "notNull": true, "default": "'https://placeholder-stream-url.cyberdesk.io'" }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "ended_at": { "name": "ended_at", "type": "timestamp", "primaryKey": false, "notNull": false } }, "indexes": {}, "foreignKeys": { "desktop_instances_user_id_users_id_fk": { "name": "desktop_instances_user_id_users_id_fk", "tableFrom": "desktop_instances", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "user_id" ], "columnsTo": [ "id" ], "onDelete": "cascade", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} }, "public.profiles": { "name": "profiles", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true }, "unkey_key_id": { "name": "unkey_key_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "stripe_customer_id": { "name": "stripe_customer_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "stripe_subscription_id": { "name": "stripe_subscription_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "current_period_end": { "name": "current_period_end", "type": "timestamp", "primaryKey": false, "notNull": false }, "subscription_status": { "name": "subscription_status", "type": "varchar(50)", "primaryKey": false, "notNull": false }, "plan_id": { "name": "plan_id", "type": "varchar(100)", "primaryKey": false, "notNull": false }, "cancel_at_period_end": { "name": "cancel_at_period_end", "type": "boolean", "primaryKey": false, "notNull": false, "default": false }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "updated_at": { "name": "updated_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" } }, "indexes": {}, "foreignKeys": { "profiles_id_users_id_fk": { "name": "profiles_id_users_id_fk", "tableFrom": "profiles", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "id" ], "columnsTo": [ "id" ], "onDelete": "cascade", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} } }, "enums": { "public.instance_status": { "name": "instance_status", "schema": "public", "values": [ "pending", "running", "completed", "error" ] } }, "schemas": {}, "_meta": { "columns": {}, "schemas": {}, "tables": {} } } ================================================ FILE: apps/api/drizzle/migrations/meta/0007_snapshot.json ================================================ { "id": "39ccedc0-27c0-448a-971b-3217e72e8dfe", "prevId": "1f1576f8-a386-4198-a4b4-dbe02dbc8468", "version": "6", "dialect": "postgresql", "tables": { "public.cyberdesk_instances": { "name": "cyberdesk_instances", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true, "default": "gen_random_uuid()" }, "user_id": { "name": "user_id", "type": "uuid", "primaryKey": false, "notNull": true }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "updated_at": { "name": "updated_at", "type": "timestamp", "primaryKey": false, "notNull": false }, "status": { "name": "status", "type": "instance_status", "typeSchema": "public", "primaryKey": false, "notNull": true, "default": "'pending'" }, "timeout_at": { "name": "timeout_at", "type": "timestamp", "primaryKey": false, "notNull": true, "default": "NOW() + interval '24 hours'" }, "stream_url": { "name": "stream_url", "type": "varchar(1024)", "primaryKey": false, "notNull": false } }, "indexes": {}, "foreignKeys": { "cyberdesk_instances_user_id_users_id_fk": { "name": "cyberdesk_instances_user_id_users_id_fk", "tableFrom": "cyberdesk_instances", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "user_id" ], "columnsTo": [ "id" ], "onDelete": "cascade", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} }, "public.desktop_instances": { "name": "desktop_instances", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true, "default": "gen_random_uuid()" }, "remote_id": { "name": "remote_id", "type": "varchar(255)", "primaryKey": false, "notNull": true }, "user_id": { "name": "user_id", "type": "uuid", "primaryKey": false, "notNull": true }, "stream_url": { "name": "stream_url", "type": "varchar(1024)", "primaryKey": false, "notNull": true, "default": "'https://placeholder-stream-url.cyberdesk.io'" }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "ended_at": { "name": "ended_at", "type": "timestamp", "primaryKey": false, "notNull": false } }, "indexes": {}, "foreignKeys": { "desktop_instances_user_id_users_id_fk": { "name": "desktop_instances_user_id_users_id_fk", "tableFrom": "desktop_instances", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "user_id" ], "columnsTo": [ "id" ], "onDelete": "cascade", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} }, "public.profiles": { "name": "profiles", "schema": "", "columns": { "id": { "name": "id", "type": "uuid", "primaryKey": true, "notNull": true }, "unkey_key_id": { "name": "unkey_key_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "stripe_customer_id": { "name": "stripe_customer_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "stripe_subscription_id": { "name": "stripe_subscription_id", "type": "varchar(255)", "primaryKey": false, "notNull": false }, "current_period_end": { "name": "current_period_end", "type": "timestamp", "primaryKey": false, "notNull": false }, "subscription_status": { "name": "subscription_status", "type": "varchar(50)", "primaryKey": false, "notNull": false }, "plan_id": { "name": "plan_id", "type": "varchar(100)", "primaryKey": false, "notNull": false }, "cancel_at_period_end": { "name": "cancel_at_period_end", "type": "boolean", "primaryKey": false, "notNull": false, "default": false }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" }, "updated_at": { "name": "updated_at", "type": "timestamp", "primaryKey": false, "notNull": false, "default": "now()" } }, "indexes": {}, "foreignKeys": { "profiles_id_users_id_fk": { "name": "profiles_id_users_id_fk", "tableFrom": "profiles", "tableTo": "users", "schemaTo": "auth", "columnsFrom": [ "id" ], "columnsTo": [ "id" ], "onDelete": "cascade", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {} } }, "enums": { "public.instance_status": { "name": "instance_status", "schema": "public", "values": [ "pending", "running", "terminated", "error" ] } }, "schemas": {}, "_meta": { "columns": {}, "schemas": {}, "tables": {} } } ================================================ FILE: apps/api/drizzle/migrations/meta/_journal.json ================================================ { "version": "5", "dialect": "postgresql", "entries": [ { "idx": 0, "version": "6", "when": 1743009299784, "tag": "0000_oval_outlaw_kid", "breakpoints": true }, { "idx": 1, "version": "6", "when": 1743018990086, "tag": "0001_busy_warstar", "breakpoints": true }, { "idx": 2, "version": "6", "when": 1743040137195, "tag": "0002_regular_doctor_faustus", "breakpoints": true }, { "idx": 3, "version": "6", "when": 1743119400161, "tag": "0003_superb_betty_brant", "breakpoints": true }, { "idx": 4, "version": "6", "when": 1743119816127, "tag": "0004_simple_komodo", "breakpoints": true }, { "idx": 5, "version": "6", "when": 1743122830319, "tag": "0005_mighty_hiroim", "breakpoints": true }, { "idx": 6, "version": "6", "when": 1744919270291, "tag": "0006_icy_black_bird", "breakpoints": true }, { "idx": 7, "version": "6", "when": 1745534258042, "tag": "0007_panoramic_tomorrow_man", "breakpoints": true } ] } ================================================ FILE: apps/api/drizzle.config.ts ================================================ import { defineConfig } from "drizzle-kit"; export default defineConfig({ schema: "./src/db/supabase.ts", schemaFilter: ["public"], out: "./drizzle/migrations", dialect: "postgresql", dbCredentials: { url: process.env.SUPABASE_CONNECTION_STRING!, }, }); ================================================ FILE: apps/api/fly.toml ================================================ # fly.toml app configuration file generated for cyberdesk-mvp-backend on 2025-03-27T05:00:43Z # # See https://fly.io/docs/reference/configuration/ for information about how to use this file. # app = 'cyberdesk-mvp-backend' primary_region = 'dfw' [build] [http_service] internal_port = 3000 force_https = true auto_stop_machines = 'stop' auto_start_machines = true min_machines_running = 0 processes = ['app'] [[vm]] memory = '1gb' cpu_kind = 'shared' cpus = 1 memory_mb = 1024 ================================================ FILE: apps/api/package.json ================================================ { "name": "api", "type": "module", "scripts": { "build": "tsc", "dev": "tsx watch src/index.ts", "start": "node dist/index.js", "env": "dotenv", "db:push": "npm run env -- drizzle-kit push", "db:studio": "npm run env -- drizzle-kit studio", "db:generate": "npm run env -- drizzle-kit generate", "db:migrate": "npm run env -- drizzle-kit migrate" }, "dependencies": { "@hono/node-server": "^1.14.0", "@hono/zod-openapi": "^0.14.5", "@libsql/client": "^0.6.2", "@supabase/supabase-js": "^2.49.3", "@unkey/api": "^0.20.7", "@unkey/cache": "^1.0.2", "@unkey/hono": "^1.2.0", "@unkey/ratelimit": "^0.1.12", "axios": "^1.9.0", "dotenv": "^16.4.7", "drizzle-orm": "^0.30.10", "hono": "^4.4.7", "postgres": "^3.4.5", "posthog-node": "^4.17.1", "zod": "^3.23.8" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240529.0", "@types/node": "^22.15.0", "dotenv-cli": "^7.4.2", "drizzle-kit": "^0.21.4", "eslint-plugin-drizzle": "^0.2.3", "tsx": "^3.10.6", "typescript": "^5.8.2", "wrangler": "^3.57.2" } } ================================================ FILE: apps/api/src/db/dbActions.ts ================================================ import { eq, and, sql } from "drizzle-orm"; import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; import { schema } from "../index.js"; import { InstanceStatus, instanceStatusEnum } from "./schema.js"; import { NotFoundError } from "../lib/errors.js"; /** * Creates a new Cyberdesk instance for a user. * @param db Drizzle database instance * @param userId The user ID to create the instance for * @param timeoutMs Optional timeout in milliseconds. Defaults to 24 hours. * @returns The newly created Cyberdesk instance details (id, status) */ export async function addDbInstance( db: PostgresJsDatabase, userId: string, timeoutMs?: number ) { const timeoutInterval = timeoutMs ? `${timeoutMs} milliseconds` : '24 hours'; const [newInstance] = await db .insert(schema.cyberdeskInstances) .values({ userId, status: InstanceStatus.Pending, timeoutAt: sql`NOW() + interval '${sql.raw(timeoutInterval)}'`, }) .returning({ id: schema.cyberdeskInstances.id, status: schema.cyberdeskInstances.status, }); return newInstance; } /** * Updates the status of a specific Cyberdesk instance, verifying ownership. * @param db Drizzle database instance * @param id The UUID of the Cyberdesk instance to update * @param userId The user ID making the request (for authorization) * @param status The new status to set * @returns The updated instance details * @throws NotFoundError if the instance is not found or the user is not authorized */ export async function updateDbInstanceStatus( db: PostgresJsDatabase, id: string, userId: string, status: InstanceStatus ) { const [updatedInstance] = await db .update(schema.cyberdeskInstances) .set({ status: status, updatedAt: new Date(), }) .where( and( eq(schema.cyberdeskInstances.id, id), eq(schema.cyberdeskInstances.userId, userId) ) ) .returning({ id: schema.cyberdeskInstances.id, status: schema.cyberdeskInstances.status, }); if (!updatedInstance) { throw new NotFoundError("Desktop instance not found or user not authorized."); } return updatedInstance; } /** * Gets specific details (id, status, createdAt, timeoutAt) for a Cyberdesk instance by ID, verifying ownership. * @param db Drizzle database instance * @param id The UUID of the Cyberdesk instance to get details for * @param userId The user ID making the request (for authorization) * @returns The instance details * @throws NotFoundError if the instance is not found or the user is not authorized */ export async function getDbInstanceDetails( db: PostgresJsDatabase, id: string, userId: string ) { const [result] = await db .select({ id: schema.cyberdeskInstances.id, status: schema.cyberdeskInstances.status, createdAt: schema.cyberdeskInstances.createdAt, timeoutAt: schema.cyberdeskInstances.timeoutAt, streamUrl: schema.cyberdeskInstances.streamUrl, }) .from(schema.cyberdeskInstances) .where( and( eq(schema.cyberdeskInstances.id, id), eq(schema.cyberdeskInstances.userId, userId) ) ) .limit(1); if (!result) { throw new NotFoundError("Desktop instance not found or user not authorized."); } return result; } ================================================ FILE: apps/api/src/db/index.ts ================================================ import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; import * as dotenv from 'dotenv'; // Ensure environment variables are loaded dotenv.config(); import { schema } from "../index.js"; function connectDatabase(env: { SUPABASE_CONNECTION_STRING: string }): PostgresJsDatabase { // Create a Postgres client for Drizzle const connectionString = env.SUPABASE_CONNECTION_STRING; const client = postgres(connectionString); // Return a Drizzle instance with the schema return drizzle(client, { schema }); } // Use the connection string directly from process.env const connectionString = process.env.SUPABASE_CONNECTION_STRING || ""; let db: PostgresJsDatabase; try { db = connectDatabase({ SUPABASE_CONNECTION_STRING: connectionString, }); } catch (error) { console.error("Failed to connect to database:", error); throw error; } export { db }; ================================================ FILE: apps/api/src/db/schema.ts ================================================ import { pgTable, serial, text, index, varchar, uuid, timestamp, pgSchema, jsonb, boolean, pgEnum } from "drizzle-orm/pg-core"; import { sql } from "drizzle-orm"; // Define the auth schema and users table const authSchema = pgSchema('auth'); const users = authSchema.table('users', { id: uuid('id').primaryKey(), // You can add other fields from auth.users if needed }); // Define the profiles table export const profiles = pgTable("profiles", { id: uuid("id").primaryKey().references(() => users.id, { onDelete: 'cascade' }), unkeyKeyId: varchar("unkey_key_id", { length: 255 }), stripeCustomerId: varchar("stripe_customer_id", { length: 255 }), stripeSubscriptionId: varchar("stripe_subscription_id", { length: 255 }), currentPeriodEnd: timestamp("current_period_end"), subscriptionStatus: varchar("subscription_status", { length: 50 }), planId: varchar("plan_id", { length: 100 }), cancelAtPeriodEnd: boolean("cancel_at_period_end").default(false), createdAt: timestamp("created_at").defaultNow(), updatedAt: timestamp("updated_at").defaultNow(), }); // Define the status enum type export enum InstanceStatus { Pending = 'pending', Running = 'running', Terminated = 'terminated', Error = 'error', } // Define the status enum type for Drizzle using the TS enum values // Assert type as [string, ...string[]] to satisfy pgEnum's expectation export const instanceStatusEnum = pgEnum('instance_status', Object.values(InstanceStatus) as [string, ...string[]]); // Define the desktop_instances table export const desktopInstances = pgTable("desktop_instances", { id: uuid("id").primaryKey().defaultRandom(), remoteId: varchar("remote_id", { length: 255 }).notNull(), userId: uuid("user_id").notNull().references(() => users.id, { onDelete: 'cascade' }), // Required field streamUrl: varchar("stream_url", { length: 1024 }).notNull().default("https://placeholder-stream-url.cyberdesk.io"), // Default value for existing records createdAt: timestamp("created_at").defaultNow(), endedAt: timestamp("ended_at"), // Optional field (nullable by default) }); // Define the cyberdesk_instances table (MVP 3) export const cyberdeskInstances = pgTable("cyberdesk_instances", { id: uuid("id").primaryKey().defaultRandom(), userId: uuid("user_id").notNull().references(() => users.id, { onDelete: 'cascade' }), // Required field createdAt: timestamp("created_at").defaultNow(), updatedAt: timestamp("updated_at"), status: instanceStatusEnum("status").notNull().default(InstanceStatus.Pending), timeoutAt: timestamp("timeout_at").notNull().default(sql`NOW() + interval '24 hours'`), // Set default to 24 hours from now streamUrl: varchar("stream_url", { length: 1024 }) }); ================================================ FILE: apps/api/src/index.ts ================================================ import { newApp } from "./lib/hono.js"; import desktop from "./routes/desktop.js"; import { serve } from "@hono/node-server"; import * as dotenv from 'dotenv' export * as schema from "./db/schema.js" // Load environment variables from .env.local file dotenv.config() const app = newApp(); // app.use(initCache()); // app.use(initRatelimiter()); app.route("/v1/", desktop); // Use PORT environment variable with fallback to 3000 for local development const port = process.env.PORT ? parseInt(process.env.PORT) : 3000; serve( { fetch: app.fetch, port: port }, (info) => { console.log(`Server is running on port ${info.port}`); } ); ================================================ FILE: apps/api/src/lib/cache.ts ================================================ import { createCache, type Cache as C} from "@unkey/cache"; import { MemoryStore } from "@unkey/cache/stores"; import type { Context, Middleware } from "./hono.js"; import type { Next } from "hono"; export type CacheNamespaces = { // Define new namespaces here as needed } export type Cache = C const persistentMap = new Map(); export function initCache(): Middleware { return async (c: Context, next: Next) => { const memory = new MemoryStore({ persistentMap: new Map() }); const cache = createCache({ // Add new namespaces as needed }); c.set("cache", cache); return next(); }; } ================================================ FILE: apps/api/src/lib/errors.ts ================================================ import { type Context, type Next } from 'hono'; import { HTTPException } from 'hono/http-exception'; import { ZodError } from 'zod'; import type { ContentfulStatusCode } from 'hono/utils/http-status'; // Base API Error class export class ApiError extends Error { public readonly statusCode: number; constructor(message: string, statusCode: number) { super(message); this.statusCode = statusCode; this.name = this.constructor.name; // Set the error name to the class name Error.captureStackTrace(this, this.constructor); // Capture stack trace } } // Specific Error Types export class BadRequestError extends ApiError { constructor(message = 'Bad Request') { super(message, 400); } } export class UnauthorizedError extends ApiError { constructor(message = 'Unauthorized') { super(message, 401); } } export class NotFoundError extends ApiError { constructor(message = 'Not Found') { super(message, 404); } } export class ConflictError extends ApiError { constructor(message = 'Conflict') { super(message, 409); } } export class GatewayError extends ApiError { constructor(message = 'Bad Gateway') { super(message, 502); } } export class InternalServerError extends ApiError { constructor(message = 'Internal Server Error') { super(message, 500); } } // Specific error for when a command fails *inside* the CyberDesk instance // We might still want to return a 200 OK with an error status for this scenario. export class ActionExecutionError extends Error { public readonly details?: string; // e.g., stderr output constructor(message: string, details?: string) { super(message); this.details = details; this.name = this.constructor.name; Error.captureStackTrace(this, this.constructor); } } // Hono Error Handler Middleware export const honoErrorHandler = (err: Error, c: Context) => { console.error("Error caught by middleware:", err); // Log the full error // Handle Zod validation errors specifically if (err instanceof ZodError) { const validationErrors = err.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', '); return c.json( { status: "error", error: `Validation failed: ${validationErrors}`, docs: "https://docs.cyberdesk.io/docs/api-reference/", }, 400 // Bad Request for validation errors ); } // Handle our custom ApiError instances if (err instanceof ApiError) { return c.json( { status: "error", error: err.message, docs: "https://docs.cyberdesk.io/docs/api-reference/", }, err.statusCode as ContentfulStatusCode ); } // Handle Hono's built-in HTTPException if (err instanceof HTTPException) { return c.json( { status: "error", error: err.message, docs: "https://docs.cyberdesk.io/docs/api-reference/", }, err.status as ContentfulStatusCode ); } // Handle ActionExecutionError specifically (return 200 OK with error status) if (err instanceof ActionExecutionError) { return c.json( { status: "error", error: err.message, details: err.details, // Optionally include details like stderr }, 200 // Special case: Action failed but API communication was successful ); } // Fallback for unexpected errors return c.json( { status: "error", error: "An unexpected internal server error occurred.", docs: "https://docs.cyberdesk.io/docs/api-reference/", }, 500 as const ); }; ================================================ FILE: apps/api/src/lib/hono.ts ================================================ import { OpenAPIHono, z } from "@hono/zod-openapi"; import type { UnkeyContext } from "@unkey/hono"; import type { Ratelimit } from "@unkey/ratelimit"; import type { Context as GenericContext, MiddlewareHandler } from "hono"; import type { ZodError } from "zod"; import type { Cache } from "./cache.js"; export type HonoEnv = { Bindings: { SUPABASE_CONNECTION_STRING: string; // Unkey credentials UNKEY_ROOT_KEY: string; UNKEY_API_ID: string; GATEWAY_URL: string; }; Variables: { cache: Cache unkey: UnkeyContext; ratelimit: Ratelimit; }; }; export function parseZodErrorMessage(err: z.ZodError): string { try { const arr = JSON.parse(err.message) as Array<{ message: string; path: Array; }>; const { path, message } = arr[0]; return `${path.join(".")}: ${message}`; } catch { return err.message; } } export function handleZodError( result: | { success: true; data: any; } | { success: false; error: ZodError; }, c: Context ) { if (!result.success) { return c.json( { error: parseZodErrorMessage(result.error), }, { status: 400 } ); } } export function newApp() { const app = new OpenAPIHono({ defaultHook: handleZodError, }); app.onError((err: Error, c: Context) => { console.error(err); return c.json( { error: err.message, }, { status: 500 } ); }); app.doc("/openapi.json", { openapi: "3.1.0", info: { title: "API Reference", version: "1.2.1", description: "API for Cyberdesk, to create, control, and manage virtual desktop instances.", }, servers: [ { url: "https://api.cyberdesk.io", description: "Production server" } ], }); app.openAPIRegistry.registerComponent("securitySchemes", "apiKeyAuth", { type: "apiKey", in: "header", name: "x-api-key" }); return app; } export type App = ReturnType; export type Context = GenericContext; export type Middleware = MiddlewareHandler; ================================================ FILE: apps/api/src/lib/posthog.ts ================================================ import { PostHog } from 'posthog-node' import * as dotenv from 'dotenv'; // Ensure environment variables are loaded dotenv.config(); const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY; if (!POSTHOG_API_KEY) { throw new Error('POSTHOG_API_KEY is not set'); } const POSTHOG_HOST = process.env.POSTHOG_HOST || 'https://us.i.posthog.com'; const client = new PostHog( POSTHOG_API_KEY, { host: POSTHOG_HOST, enableExceptionAutocapture: true }, ) export default client; ================================================ FILE: apps/api/src/lib/ratelimit.ts ================================================ import { Ratelimit } from "@unkey/ratelimit"; import type { Context, Middleware } from "./hono.js"; import type { Next } from "hono"; export function initRatelimiter(): Middleware { return async (c: Context, next: Next) => { const ratelimit = new Ratelimit({ rootKey: c.env.UNKEY_ROOT_KEY, namespace: "api-toolbox", limit: 10, duration: "30s", async: true, }); c.set("ratelimit", ratelimit); return next(); }; } ================================================ FILE: apps/api/src/routes/desktop.ts ================================================ import { OpenAPIHono } from "@hono/zod-openapi"; import { env } from 'hono/adapter'; import { unkey, type UnkeyContext } from "@unkey/hono"; import { z } from "@hono/zod-openapi"; import axios from "axios"; import { profiles, cyberdeskInstances, InstanceStatus } from "../db/schema.js"; import { bashAction, computerAction, createDesktop, stopDesktop, ComputerActionSchema, CreateDesktopParamsSchema, getDesktop, BashActionSchema, } from "../schema/desktop.js"; import { GatewayExecuteCommandRequestSchema, GatewayExecuteCommandResponseSchema } from "../schema/gateway.js"; import { db } from "../db/index.js"; import { addDbInstance, getDbInstanceDetails, updateDbInstanceStatus, } from "../db/dbActions.js"; import { ApiError, NotFoundError, ConflictError, BadRequestError, GatewayError, ActionExecutionError, honoErrorHandler, UnauthorizedError, InternalServerError, } from "../lib/errors.js"; import posthogClient from '../lib/posthog.js'; import type { Context } from "hono"; // Type definitions type EnvVars = { UNKEY_API_ID: string; SUPABASE_URL: string; SUPABASE_ANON_KEY: string; SUPABASE_CONNECTION_STRING?: string; WEB_URL: string; GATEWAY_URL: string; }; // Use the schema type for computer actions type ComputerAction = z.infer; // Use the schema type for desktop creation parameters type CreateDesktopParams = z.infer; type BashAction = z.infer; // Create Hono instance const desktop = new OpenAPIHono<{ Variables: { unkey: UnkeyContext; userId: string; }; }>(); // Register the global error handler desktop.onError(honoErrorHandler); // API key verification middleware desktop.use("*", async (c, next) => { const { UNKEY_API_ID } = env(c); const handler = unkey({ apiId: UNKEY_API_ID, getKey: (c) => c.req.header("x-api-key"), }); await handler(c, next); }); // Helper function to capture API events in PostHog async function captureApiEvent( c: Context, userId: string, eventName: string, additionalProperties: Record = {} ) { const properties = { path: c.req.path, method: c.req.method, userAgent: c.req.header('User-Agent'), ...additionalProperties, }; posthogClient.capture({ distinctId: userId, event: eventName, properties: properties, }); } // Authentication and database connection middleware desktop.use("*", async (c, next) => { const result = c.get("unkey"); if (!result?.valid) { throw new UnauthorizedError("Invalid API key"); } const userId = result.ownerId; if (!userId) { throw new UnauthorizedError("No user associated with this key"); } c.set("userId", userId); await next(); }); // Route for getting a desktop instance's details desktop.openapi(getDesktop, async (c) => { const userId = c.get("userId"); const id = c.req.param("id"); const { GATEWAY_URL } = env(c); if (!id) { throw new BadRequestError("Instance ID is required"); } await captureApiEvent(c, userId, 'Viewed Desktop Details'); const instanceDetails = await getDbInstanceDetails(db, id, userId); // Adjust stream_url for dev environment if needed let streamUrl: string | null = instanceDetails.streamUrl ?? null; if (streamUrl && GATEWAY_URL.includes('dev-gateway')) { // Replace any 'gateway.' (with or without subdomain) with 'dev-gateway.' // This is to support the dev environment, where the gateway is accessible via dev-gateway.cyberdesk.io (or whatever subdomain you've set up) streamUrl = streamUrl.replace(/\bgateway\./g, 'dev-gateway.'); } return c.json({ id: instanceDetails.id, status: instanceDetails.status, created_at: (instanceDetails.createdAt || new Date(0)).toISOString(), timeout_at: instanceDetails.timeoutAt.toISOString(), stream_url: streamUrl }, 200); }); // Route for creating a new desktop instance desktop.openapi(createDesktop, async (c) => { const { GATEWAY_URL } = env(c); const userId = c.get("userId"); let createDesktopParams: CreateDesktopParams; try { const body = await c.req.json().catch(() => ({})); createDesktopParams = CreateDesktopParamsSchema.parse(body); await captureApiEvent(c, userId, 'Created Desktop'); const newInstance = await addDbInstance(db, userId, createDesktopParams.timeout_ms); try { const provisioningUrl = `${GATEWAY_URL}/cyberdesk/${newInstance.id}`; const response = await axios.post(provisioningUrl, { timeoutMs: createDesktopParams.timeout_ms }); console.log('Provisioning request successful:', response.data); return c.json( { id: newInstance.id, status: newInstance.status, }, 200 ); } catch (provisioningError) { console.error('Error calling provisioning service during creation:', provisioningError); await updateDbInstanceStatus(db, newInstance.id, userId, InstanceStatus.Error).catch(console.error); if (axios.isAxiosError(provisioningError)) { throw new GatewayError(`Failed to provision via Gateway: ${provisioningError.response?.statusText || provisioningError.message}`); } else { throw new GatewayError('Failed to initiate provisioning of Cyberdesk resource via Gateway for instance ' + newInstance.id); } } } catch (error) { if (error instanceof z.ZodError) { throw error; } else if (error instanceof ApiError) { throw error; } else { console.error('Unexpected error during desktop creation:', error); throw new InternalServerError("Failed to create desktop instance due to an unexpected error."); } } }); // Route for stopping a desktop instance desktop.openapi(stopDesktop, async (c) => { const userId = c.get("userId"); const id = c.req.param("id"); const { GATEWAY_URL } = env(c); await captureApiEvent(c, userId, 'Stopped Desktop'); const updatedInstance = await updateDbInstanceStatus(db, id, userId, InstanceStatus.Terminated); try { const provisioningUrl = `${GATEWAY_URL}/cyberdesk/${id}/stop`; await axios.post(provisioningUrl); } catch (provisioningError) { console.error('Error calling provisioning service during stop:', provisioningError); } const responsePayload: { status: InstanceStatus } = { status: updatedInstance.status as InstanceStatus, }; return c.json( responsePayload, 200 ); }); async function executeComputerAction( id: string, userId: string, action: ComputerAction, GATEWAY_URL: string ): Promise { console.log("Executing computer action:", action); const instance = await getDbInstanceDetails(db, id, userId); if (!instance) { throw new NotFoundError("Instance not found or unauthorized"); } if (instance.status !== 'running') { throw new ConflictError(`Instance is not running (status: ${instance.status}). Cannot perform action.`); } let command: string; const displayPrefix = "export DISPLAY=:99;"; switch (action.type) { case "click_mouse": { const { x, y, button = "left", num_of_clicks = 1, click_type = "click" } = action; const buttonMap: { [key: string]: number } = { left: 1, middle: 2, right: 3 }; const btn = buttonMap[button] || 1; let moveCmd = ""; if (x !== undefined && y !== undefined) { moveCmd = `xdotool mousemove ${x} ${y} && `; } let clickCmd: string; if (click_type === "click") { clickCmd = `xdotool click --repeat ${num_of_clicks} ${btn}`; } else if (click_type === "down") { clickCmd = `xdotool mousedown ${btn}`; } else { // up clickCmd = `xdotool mouseup ${btn}`; } command = `${displayPrefix} ${moveCmd}${clickCmd}`; break; } case "scroll": { const { direction, amount } = action; const directionMap: { [key: string]: number } = { up: 4, down: 5, left: 6, right: 7 }; const btn = directionMap[direction]; // Ensure amount is a reasonable positive integer const repeatCount = Math.max(1, Math.min(Math.floor(amount), 500)); // Cap repeat at 500 for sanity const delayMs = 25; // Reduce delay for faster scrolling command = `${displayPrefix} xdotool click --repeat ${repeatCount} --delay ${delayMs} ${btn}`; break; } case "move_mouse": { command = `${displayPrefix} xdotool mousemove ${action.x} ${action.y}`; break; } case "drag_mouse": { const { start, end } = action; command = `${displayPrefix} xdotool mousemove ${start.x} ${start.y} mousedown 1 mousemove ${end.x} ${end.y} mouseup 1`; break; } case "type": { const escapedText = action.text.replace(/'/g, "'\''"); command = `${displayPrefix} xdotool type --clearmodifiers --delay 50 '${escapedText}'`; break; } case "press_keys": { const { keys, key_action_type = "press" } = action; const keyString = Array.isArray(keys) ? keys.join('+') : keys; let keyCmd: string; if (key_action_type === "down") { keyCmd = `keydown`; } else if (key_action_type === "up") { keyCmd = `keyup`; } else { // press keyCmd = `key`; } command = `${displayPrefix} xdotool ${keyCmd} --clearmodifiers ${keyString}`; break; } case "wait": { const seconds = Math.max(0, action.ms / 1000); command = `sleep ${seconds}`; break; } case "screenshot": { command = `${displayPrefix} scrot -q 100 /tmp/screen.jpg && base64 /tmp/screen.jpg && rm /tmp/screen.jpg`; break; } case "get_cursor_position": { command = `${displayPrefix} xdotool getmouselocation --shell`; break; } default: throw new BadRequestError(`Unsupported action type: ${(action as any).type}`); } console.log(`Executing command for instance ${id}: ${command}`); const provisioningUrl = `${GATEWAY_URL}/cyberdesk/${id}/execute-command`; const requestBody = GatewayExecuteCommandRequestSchema.parse({ command }); try { const response = await axios.post>( provisioningUrl, requestBody ); const parsedResponse = GatewayExecuteCommandResponseSchema.parse(response.data); if (parsedResponse.vm_response.return_code !== 0) { throw new ActionExecutionError( `Command failed with code ${parsedResponse.vm_response.return_code}`, parsedResponse.vm_response.stderr || 'No stderr output' ); } return parsedResponse.vm_response.stdout.trim(); } catch (error: any) { console.error(`Error executing command for instance ${id}:`, error); if (error instanceof z.ZodError) { throw new GatewayError(`Invalid response structure from gateway: ${error.errors.map(e => e.message).join(', ')}`); } else if (axios.isAxiosError(error)) { const gatewayMessage = error.response?.data?.message || error.message || "Failed to execute command via gateway"; throw new GatewayError(`Action execution failed via gateway: ${gatewayMessage}`); } else if (error instanceof ApiError) { throw error; } else { throw new InternalServerError(`Unexpected error during action execution: ${error instanceof Error ? error.message : 'Unknown error'}`); } } } /** * Executes a raw bash command on the target instance via the gateway. * @param id Instance ID * @param userId User ID for authorization * @param command The bash command string to execute * @param GATEWAY_URL Gateway URL * @returns Promise Resolves with stdout on success * @throws Error on command failure or communication issues */ async function executeBashCommand( id: string, userId: string, command: string, GATEWAY_URL: string ): Promise { const instance = await getDbInstanceDetails(db, id, userId); if (!instance) { throw new NotFoundError("Instance not found or unauthorized"); } if (instance.status !== 'running') { throw new ConflictError(`Instance is not running (status: ${instance.status}). Cannot execute command.`); } console.log(`Executing bash command for instance ${id}: ${command}`); const provisioningUrl = `${GATEWAY_URL}/cyberdesk/${id}/execute-command`; const requestBody = GatewayExecuteCommandRequestSchema.parse({ command }); try { const response = await axios.post>( provisioningUrl, requestBody ); const parsedResponse = GatewayExecuteCommandResponseSchema.parse(response.data); console.log(`Bash command execution response for instance ${id}:`, parsedResponse); return parsedResponse.vm_response.stdout.trim() || parsedResponse.vm_response.stderr.trim(); } catch (error: any) { console.error(`Error executing bash command for instance ${id}:`, error); if (error instanceof z.ZodError) { throw new GatewayError(`Invalid response structure from gateway: ${error.errors.map(e => e.message).join(', ')}`); } else if (axios.isAxiosError(error)) { const gatewayMessage = error.response?.data?.message || error.message || "Failed to execute command via gateway"; throw new GatewayError(`Bash command execution failed via gateway: ${gatewayMessage}`); } else if (error instanceof ApiError) { throw error; } else { throw new InternalServerError(`Unexpected error during bash command execution: ${error instanceof Error ? error.message : 'Unknown error'}`); } } } // Route for performing a computer action on a desktop desktop.openapi(computerAction, async (c) => { const userId = c.get("userId"); const id = c.req.param("id"); const { GATEWAY_URL } = env(c); let action: ComputerAction; try { const body = await c.req.json().catch(() => ({})); action = ComputerActionSchema.parse(body); // Capture event with actionType await captureApiEvent(c, userId, 'Performed Computer Action', { actionType: action.type }); } catch (parseError: any) { if (parseError instanceof z.ZodError) { throw parseError; } throw new BadRequestError(`Invalid request body: ${parseError?.message || 'Unknown parsing error'}`); } const resultString = await executeComputerAction(id, userId, action, GATEWAY_URL); if (action.type === "screenshot") { // Remove all whitespace (including newlines) from base64 string const cleanedBase64 = resultString.replace(/\s+/g, ''); return c.json({ base64_image: cleanedBase64 }, 200); } else if (action.type === "get_cursor_position") { return c.json({ output: resultString, }, 200); } else { return c.json({ output: resultString, }, 200); } }); // Route for executing a bash command on a desktop desktop.openapi(bashAction, async (c) => { const userId = c.get("userId"); const id = c.req.param("id"); const { GATEWAY_URL } = env(c); let bashAction: BashAction; try { const body = await c.req.json().catch(() => ({})); bashAction = BashActionSchema.parse(body); await captureApiEvent(c, userId, 'Executed Bash Command'); } catch (parseError: any) { if (parseError instanceof z.ZodError) { throw parseError; } throw new BadRequestError(`Invalid request body: ${parseError?.message || 'Unknown parsing error'}`); } const resultString = await executeBashCommand(id, userId, bashAction.command, GATEWAY_URL); return c.json({ status: "success", output: resultString, }, 200); }); export default desktop; ================================================ FILE: apps/api/src/schema/desktop.ts ================================================ import { createRoute, z } from "@hono/zod-openapi"; import { openApiErrorResponses } from "./errors.js"; import { InstanceStatus, instanceStatusEnum } from "../db/schema.js"; // Header schema for API key authentication const HeadersSchema = z.object({ "x-api-key": z.string().openapi({ description: "API key for authentication", example: "api_12345", }), }); // Common schema for action oriented API responses const ActionResponseSchema = z.object({ output: z.string().optional().openapi({ description: "Raw string output from the executed command (if any)", example: "X=500 Y=300", }), error: z.string().optional().openapi({ description: "Error message if the operation failed (also indicated by non-2xx HTTP status)", example: "Command failed with code 1: xdotool: command not found", }), base64_image: z.string().optional().openapi({ description: "Base64 encoded JPEG image data (only returned for screenshot actions)", example: "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQ...", }) }); // Point schema for coordinates const PointSchema = z.object({ x: z.number().int().openapi({ description: "X coordinate on the screen", example: 500, }), y: z.number().int().openapi({ description: "Y coordinate on the screen", example: 300, }), }); // Schema for desktop creation parameters export const CreateDesktopParamsSchema = z.object({ timeout_ms: z.number().int().optional().openapi({ description: "Timeout in milliseconds for the desktop session", example: 3600000, }), }); // Schema for the response of the create desktop endpoint export const CreateDesktopResponseSchema = z.object({ id: z.string().openapi({ description: "Unique identifier for the desktop instance", example: "desktop_12345", }), status: z.enum(instanceStatusEnum.enumValues).openapi({ description: "Initial status of the desktop instance after creation request", example: InstanceStatus.Pending, }), }); // Schema for the response of the stop desktop endpoint export const StopDesktopResponseSchema = z.object({ status: z.enum(instanceStatusEnum.enumValues).openapi({ description: "Status of the desktop instance after stopping", example: InstanceStatus.Terminated, }), }); // Computer action schema with discriminated union export const ComputerActionSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal("click_mouse").openapi({ description: "Perform a mouse action: click, press (down), or release (up). Defaults to a single left click at the current position.", example: "click_mouse", }), x: z.number().int().optional().openapi({ description: "X coordinate for the action (optional, uses current position if omitted)", example: 500, }), y: z.number().int().optional().openapi({ description: "Y coordinate for the action (optional, uses current position if omitted)", example: 300, }), button: z.enum(["left", "right", "middle"]).optional().openapi({ description: "Mouse button to use (optional, defaults to 'left')", example: "left", }), num_of_clicks: z.number().int().min(0).optional().openapi({ description: "Number of clicks to perform (optional, defaults to 1, only applicable for 'click' type)", example: 1, }), click_type: z.enum(["click", "down", "up"]).optional().openapi({ description: "Type of mouse action (optional, defaults to 'click')", example: "click", }), }).openapi({ title: "Click Mouse Action" }), z.object({ type: z.literal("scroll").openapi({ description: "Scroll the mouse wheel in the specified direction", example: "scroll", }), direction: z.enum(["up", "down", "left", "right"]).openapi({ description: "Direction to scroll", example: "down", }), amount: z.number().int().openapi({ description: "Amount to scroll in pixels", example: 100, }), }).openapi({ title: "Scroll Action" }), z.object({ type: z.literal("move_mouse").openapi({ description: "Move the mouse cursor to the specified coordinates", example: "move_mouse", }), x: z.number().int().openapi({ description: "X coordinate to move to", example: 500, }), y: z.number().int().openapi({ description: "Y coordinate to move to", example: 300, }), }).openapi({ title: "Move Mouse Action" }), z.object({ type: z.literal("drag_mouse").openapi({ description: "Drag the mouse from start to end coordinates", example: "drag_mouse", }), start: PointSchema.openapi({ description: "Starting coordinates for the drag operation", example: { x: 100, y: 100 }, }), end: PointSchema.openapi({ description: "Ending coordinates for the drag operation", example: { x: 300, y: 300 }, }), }).openapi({ title: "Drag Mouse Action" }), z.object({ type: z.literal("type").openapi({ description: "Type text at the current cursor position", example: "type", }), text: z.string().openapi({ description: "Text to type", example: "Hello, World!", }), }).openapi({ title: "Type Text Action" }), z.object({ type: z.literal("press_keys").openapi({ description: "Press, hold down, or release one or more keyboard keys. Defaults to a single press and release.", example: "press_keys", }), keys: z.union([ z.string().openapi({ description: "Single key to press", example: "Enter", }), z.array(z.string()).openapi({ description: "Multiple keys to press simultaneously", example: ["Control", "c"], }) ]), key_action_type: z.enum(["press", "down", "up"]).optional().openapi({ description: "Type of key action (optional, defaults to 'press' which is a down and up action)", example: "press", }), }).openapi({ title: "Press Keys Action" }), z.object({ type: z.literal("wait").openapi({ description: "Wait for the specified number of milliseconds", example: "wait", }), ms: z.number().int().openapi({ description: "Time to wait in milliseconds", example: 1000, }), }).openapi({ title: "Wait Action" }), z.object({ type: z.literal("screenshot").openapi({ description: "Take a screenshot of the desktop", example: "screenshot", }), }).openapi({ title: "Screenshot Action" }), z.object({ type: z.literal("get_cursor_position").openapi({ description: "Get the current mouse cursor position", example: "get_cursor_position", }), }).openapi({ title: "Get Cursor Position Action" }), ]); // Schema for Bash Action parameters export const BashActionSchema = z.object({ command: z.string().openapi({ description: "Bash command to execute", example: "echo 'Hello, World!'", }), }); // Create Desktop Route export const createDesktop = createRoute({ method: "post", path: "/desktop", tags: ["Desktop"], summary: "Create a new virtual desktop instance", description: "Creates a new virtual desktop instance and returns its ID and stream URL", request: { headers: HeadersSchema, body: { content: { "application/json": { schema: CreateDesktopParamsSchema, }, }, }, }, responses: { 200: { content: { "application/json": { schema: CreateDesktopResponseSchema, }, }, description: "Desktop creation initiated successfully", }, ...openApiErrorResponses, }, }); // Stop Desktop Route export const stopDesktop = createRoute({ method: "post", path: "/desktop/{id}/stop", tags: ["Desktop"], summary: "Stop a running desktop instance", description: "Stops a running desktop instance and cleans up resources", request: { headers: HeadersSchema, params: z.object({ id: z.string().openapi({ description: "Desktop instance ID to stop", example: "desktop_12345", }), }), }, responses: { 200: { content: { "application/json": { schema: StopDesktopResponseSchema, }, }, description: "Desktop stopped successfully", }, ...openApiErrorResponses, }, }); // Get Desktop Route Response Schema const GetDesktopResponseSchema = z.object({ id: z.string().uuid().openapi({ description: "Unique identifier for the desktop instance", example: "a1b2c3d4-e5f6-7890-1234-567890abcdef", }), status: z.enum(instanceStatusEnum.enumValues).openapi({ description: "Current status of the desktop instance", example: "running", }), stream_url: z.string().nullable().openapi({ description: "URL for the desktop stream (null if the desktop is not running)", example: "https://cyberdesk.com/vnc/a1b2c3d4-e5f6-7890-1234-567890abcdef", }), created_at: z.string().datetime().openapi({ description: "Timestamp when the instance was created", example: "2023-10-27T10:00:00Z", }), timeout_at: z.string().datetime().openapi({ description: "Timestamp when the instance will automatically time out", example: "2023-10-28T10:00:00Z", }), }); // Get Desktop Route export const getDesktop = createRoute({ method: "get", path: "/desktop/{id}", tags: ["Desktop"], summary: "Get details of a specific desktop instance", description: "Returns the ID, status, creation timestamp, and timeout timestamp for a given desktop instance.", request: { headers: HeadersSchema, params: z.object({ id: z.string().uuid().openapi({ description: "The UUID of the desktop instance to retrieve", example: "a1b2c3d4-e5f6-7890-1234-567890abcdef", }), }), }, responses: { 200: { content: { "application/json": { schema: GetDesktopResponseSchema, }, }, description: "Desktop instance details retrieved successfully", }, ...openApiErrorResponses, }, }); // Computer Action Route export const computerAction = createRoute({ method: "post", path: "/desktop/{id}/computer-action", tags: ["Desktop"], summary: "Perform an action on the desktop", description: "Executes a computer action such as mouse clicks, keyboard input, or screenshots on the desktop", request: { headers: HeadersSchema, params: z.object({ id: z.string().openapi({ description: "Desktop instance ID to perform the action on", example: "desktop_12345", }), }), body: { content: { "application/json": { schema: ComputerActionSchema, }, }, }, }, responses: { 200: { content: { "application/json": { schema: ActionResponseSchema, }, }, description: "Action executed successfully. Response may contain output or image data depending on the action.", }, ...openApiErrorResponses, }, }); // Bash Action Route export const bashAction = createRoute({ method: "post", path: "/desktop/{id}/bash-action", tags: ["Desktop"], summary: "Execute a bash command on the desktop", description: "Runs a bash command on the desktop and returns the command output", request: { headers: HeadersSchema, params: z.object({ id: z.string().openapi({ description: "Desktop instance ID to run the command on", example: "desktop_12345", }), }), body: { content: { "application/json": { schema: BashActionSchema, }, }, }, }, responses: { 200: { content: { "application/json": { schema: ActionResponseSchema, }, }, description: "Command executed successfully. Response contains command output.", }, ...openApiErrorResponses, }, }); ================================================ FILE: apps/api/src/schema/errors.ts ================================================ import { z } from "@hono/zod-openapi"; const errorSchema = z.object({ status: z.literal("error").openapi({ example: "error" }), error: z.string().openapi({ description: "Error message detailing what went wrong", example: "Instance not found or unauthorized" }) }); export const openApiErrorResponses = { 400: { description: "The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).", content: { "application/json": { schema: errorSchema, }, }, }, 401: { description: `Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response.`, content: { "application/json": { schema: errorSchema, }, }, }, 403: { description: "The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server.", content: { "application/json": { schema: errorSchema, }, }, }, 404: { description: "The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.", content: { "application/json": { schema: errorSchema, }, }, }, 409: { description: "This response is sent when a request conflicts with the current state of the server.", content: { "application/json": { schema: errorSchema, }, }, }, 429: { description: `The user has sent too many requests in a given amount of time ("rate limiting")`, content: { "application/json": { schema: errorSchema, }, }, }, 500: { description: "The server has encountered a situation it does not know how to handle.", content: { "application/json": { schema: errorSchema, }, }, }, 502: { description: "The server, while acting as a gateway or proxy, received an invalid response from the upstream server.", content: { "application/json": { schema: errorSchema, }, }, }, }; ================================================ FILE: apps/api/src/schema/gateway.ts ================================================ import { z } from 'zod'; // Schema for Gateway Execute Command Request export const GatewayExecuteCommandRequestSchema = z.object({ command: z.string() }); // Schema for Gateway Execute Command Response export const GatewayExecuteCommandResponseSchema = z.object({ status: z.string(), vm_status_code: z.number().int(), vm_response: z.object({ args: z.array(z.string()).optional(), return_code: z.number().int(), stdout: z.string(), stderr: z.string(), duration_s: z.number().optional(), }) }); ================================================ FILE: apps/api/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "module": "esnext", "moduleResolution": "bundler", "strict": true, "verbatimModuleSyntax": false, "esModuleInterop": true, "skipLibCheck": true, "types": [ "node" ], "jsx": "react-jsx", "jsxImportSource": "hono/jsx", "outDir": "./dist", "incremental": true }, "include": ["src/**/*.ts", "src/**/*.tsx"], "exclude": ["node_modules"] } ================================================ FILE: apps/docs/.gitignore ================================================ # Dependencies node_modules/ .pnp .pnp.js # Build outputs .next/ out/ build/ dist/ .cache/ # Generated files .docusaurus/ .cache-loader/ .vercel/ # Environment variables .env .env.local .env.development.local .env.test.local .env.production.local # Debug logs npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # Editor directories and files .idea/ .vscode/ *.swp *.swo .DS_Store # Temporary files *.log *.tmp tmp/ temp/ # Coverage directory coverage/ ================================================ FILE: apps/docs/.map.ts ================================================ /** Auto-generated **/ declare const map: Record export { map } ================================================ FILE: apps/docs/README.md ================================================ # docs This is a Next.js application generated with [Create Fumadocs](https://github.com/fuma-nama/fumadocs). Run development server: ```bash npm run dev # or pnpm dev # or yarn dev ``` Open http://localhost:3000/docs to view the documentation. You can also view the reference documentation for the API at https://docs.cyberdesk.io/docs/api-reference which is generated from the /scripts/ directory. ## Learn More To learn more about Fumadocs features , take a look at the following resources: - [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs ================================================ FILE: apps/docs/app/api/search/route.ts ================================================ import { getPages } from "@/app/source"; import { createSearchAPI } from "fumadocs-core/search/server"; export const { GET } = createSearchAPI("advanced", { indexes: getPages().map((page) => ({ title: page.data.title, structuredData: page.data.exports.structuredData, id: page.url, url: page.url, })), }); ================================================ FILE: apps/docs/app/docs/[[...slug]]/page.tsx ================================================ import { getPage, getPages } from "@/app/source"; import { DocsBody, DocsPage } from "fumadocs-ui/page"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; export default async function Page({ params, }: { params: { slug?: string[] }; }) { const page = getPage(params.slug); if (page == null) { notFound(); } const MDX = page.data.exports.default; return (

{page.data.title}

); } export async function generateStaticParams() { return getPages().map((page) => ({ slug: page.slugs, })); } export function generateMetadata({ params }: { params: { slug?: string[] } }) { const page = getPage(params.slug); if (page == null) notFound(); return { title: page.data.title, description: page.data.description, } satisfies Metadata; } ================================================ FILE: apps/docs/app/docs/layout.tsx ================================================ import { DocsLayout } from "fumadocs-ui/layout"; import type { ReactNode } from "react"; import { baseOptions } from "../layout.config"; import { pageTree } from "../source"; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ================================================ FILE: apps/docs/app/global.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; ================================================ FILE: apps/docs/app/layout.config.tsx ================================================ import { type BaseLayoutProps } from "fumadocs-ui/layout"; // basic configuration here export const baseOptions: BaseLayoutProps = { nav: { title: "Cyberdesk Docs", }, links: [ { text: "Documentation", url: "/docs", active: "nested-url", }, { text: "Main Site", url: "https://cyberdesk.io", } ], githubUrl: "https://github.com/cyberdesk-hq/cyberdesk", }; ================================================ FILE: apps/docs/app/layout.tsx ================================================ import "./global.css"; import { RootProvider } from "fumadocs-ui/provider"; import { Inter } from "next/font/google"; import type { ReactNode } from "react"; import { Analytics } from "@vercel/analytics/react"; const inter = Inter({ subsets: ["latin"], }); export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ================================================ FILE: apps/docs/app/page.tsx ================================================ import { redirect } from 'next/navigation'; export default function HomePage() { redirect('/docs'); } ================================================ FILE: apps/docs/app/source.ts ================================================ import { map } from "@/.map"; import { loader } from "fumadocs-core/source"; import { createMDXSource } from "fumadocs-mdx"; export const { getPage, getPages, pageTree } = loader({ baseUrl: "/docs", rootDir: "docs", source: createMDXSource(map), }); ================================================ FILE: apps/docs/content/docs/api-reference.mdx ================================================ --- title: API Reference description: API for Cyberdesk, to create, control, and manage virtual desktop instances. full: true toc: false --- import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request } from "fumadocs-ui/components/api"; ## Get details of a specific desktop instance Returns the ID, status, creation timestamp, and timeout timestamp for a given desktop instance. ### Path Parameters The UUID of the desktop instance to retrieve Example: `"a1b2c3d4-e5f6-7890-1234-567890abcdef"` Format: `"uuid"` ### Header Parameters API key for authentication Example: `"api_12345"` | Status code | Description | | ----------- | ----------- | | `200` | Desktop instance details retrieved successfully | | `400` | The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). | | `401` | Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response. | | `403` | The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. | | `404` | The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web. | | `409` | This response is sent when a request conflicts with the current state of the server. | | `429` | The user has sent too many requests in a given amount of time ("rate limiting") | | `500` | The server has encountered a situation it does not know how to handle. | | `502` | The server, while acting as a gateway or proxy, received an invalid response from the upstream server. | ```bash curl -X GET "https://api.cyberdesk.io/v1/desktop/:id" \ -H "x-api-key: api_12345" ``` ```js fetch("https://api.cyberdesk.io/v1/desktop/:id", { method: "GET", headers: { "x-api-key": "api_12345" } }); ``` ```json { "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "status": "running", "stream_url": "https://cyberdesk.com/vnc/a1b2c3d4-e5f6-7890-1234-567890abcdef", "created_at": "2023-10-27T10:00:00Z", "timeout_at": "2023-10-28T10:00:00Z" } ``` ```ts export interface Response { /** * Unique identifier for the desktop instance */ id: string; /** * Current status of the desktop instance */ status: "pending" | "running" | "terminated" | "error"; /** * URL for the desktop stream (null if the desktop is not running) */ stream_url: string; /** * Timestamp when the instance was created */ created_at: string; /** * Timestamp when the instance will automatically time out */ timeout_at: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ## Create a new virtual desktop instance Creates a new virtual desktop instance and returns its ID and stream URL ### Request Body (Optional) Timeout in milliseconds for the desktop session Example: `3600000` ### Header Parameters API key for authentication Example: `"api_12345"` | Status code | Description | | ----------- | ----------- | | `200` | Desktop creation initiated successfully | | `400` | The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). | | `401` | Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response. | | `403` | The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. | | `404` | The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web. | | `409` | This response is sent when a request conflicts with the current state of the server. | | `429` | The user has sent too many requests in a given amount of time ("rate limiting") | | `500` | The server has encountered a situation it does not know how to handle. | | `502` | The server, while acting as a gateway or proxy, received an invalid response from the upstream server. | ```bash curl -X POST "https://api.cyberdesk.io/v1/desktop" \ -H "x-api-key: api_12345" \ -d '{ "timeout_ms": 3600000 }' ``` ```js fetch("https://api.cyberdesk.io/v1/desktop", { method: "POST", headers: { "x-api-key": "api_12345" } }); ``` ```json { "id": "desktop_12345", "status": "pending" } ``` ```ts export interface Response { /** * Unique identifier for the desktop instance */ id: string; /** * Initial status of the desktop instance after creation request */ status: "pending" | "running" | "terminated" | "error"; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ## Stop a running desktop instance Stops a running desktop instance and cleans up resources ### Path Parameters Desktop instance ID to stop Example: `"desktop_12345"` ### Header Parameters API key for authentication Example: `"api_12345"` | Status code | Description | | ----------- | ----------- | | `200` | Desktop stopped successfully | | `400` | The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). | | `401` | Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response. | | `403` | The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. | | `404` | The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web. | | `409` | This response is sent when a request conflicts with the current state of the server. | | `429` | The user has sent too many requests in a given amount of time ("rate limiting") | | `500` | The server has encountered a situation it does not know how to handle. | | `502` | The server, while acting as a gateway or proxy, received an invalid response from the upstream server. | ```bash curl -X POST "https://api.cyberdesk.io/v1/desktop/:id/stop" \ -H "x-api-key: api_12345" ``` ```js fetch("https://api.cyberdesk.io/v1/desktop/:id/stop", { method: "POST", headers: { "x-api-key": "api_12345" } }); ``` ```json { "status": "terminated" } ``` ```ts export interface Response { /** * Status of the desktop instance after stopping */ status: "pending" | "running" | "terminated" | "error"; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ## Perform an action on the desktop Executes a computer action such as mouse clicks, keyboard input, or screenshots on the desktop ### Request Body (Optional) Perform a mouse action: click, press (down), or release (up). Defaults to a single left click at the current position. Example: `"click_mouse"` Value in: `"click_mouse"` X coordinate for the action (optional, uses current position if omitted) Example: `500` Y coordinate for the action (optional, uses current position if omitted) Example: `300` Mouse button to use (optional, defaults to 'left') Example: `"left"` Value in: `"left" | "right" | "middle"` Number of clicks to perform (optional, defaults to 1, only applicable for 'click' type) Example: `1` Minimum: `0` Type of mouse action (optional, defaults to 'click') Example: `"click"` Value in: `"click" | "down" | "up"` Scroll the mouse wheel in the specified direction Example: `"scroll"` Value in: `"scroll"` Direction to scroll Example: `"down"` Value in: `"up" | "down" | "left" | "right"` Amount to scroll in pixels Example: `100` Move the mouse cursor to the specified coordinates Example: `"move_mouse"` Value in: `"move_mouse"` X coordinate to move to Example: `500` Y coordinate to move to Example: `300` Drag the mouse from start to end coordinates Example: `"drag_mouse"` Value in: `"drag_mouse"` Starting coordinates for the drag operation Example: `{"x":100,"y":100}` X coordinate on the screen Example: `500` Y coordinate on the screen Example: `300` Ending coordinates for the drag operation Example: `{"x":300,"y":300}` X coordinate on the screen Example: `500` Y coordinate on the screen Example: `300` Type text at the current cursor position Example: `"type"` Value in: `"type"` Text to type Example: `"Hello, World!"` Press, hold down, or release one or more keyboard keys. Defaults to a single press and release. Example: `"press_keys"` Value in: `"press_keys"` "} required={true} deprecated={undefined}> "} required={false} deprecated={undefined}> Multiple keys to press simultaneously Example: `["Control","c"]` Type of key action (optional, defaults to 'press' which is a down and up action) Example: `"press"` Value in: `"press" | "down" | "up"` Wait for the specified number of milliseconds Example: `"wait"` Value in: `"wait"` Time to wait in milliseconds Example: `1000` Take a screenshot of the desktop Example: `"screenshot"` Value in: `"screenshot"` Get the current mouse cursor position Example: `"get_cursor_position"` Value in: `"get_cursor_position"` ### Path Parameters Desktop instance ID to perform the action on Example: `"desktop_12345"` ### Header Parameters API key for authentication Example: `"api_12345"` | Status code | Description | | ----------- | ----------- | | `200` | Action executed successfully. Response may contain output or image data depending on the action. | | `400` | The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). | | `401` | Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response. | | `403` | The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. | | `404` | The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web. | | `409` | This response is sent when a request conflicts with the current state of the server. | | `429` | The user has sent too many requests in a given amount of time ("rate limiting") | | `500` | The server has encountered a situation it does not know how to handle. | | `502` | The server, while acting as a gateway or proxy, received an invalid response from the upstream server. | ```bash curl -X POST "https://api.cyberdesk.io/v1/desktop/:id/computer-action" \ -H "x-api-key: api_12345" \ -d '{ "type": "click_mouse", "x": 500, "y": 300, "button": "left", "num_of_clicks": 1, "click_type": "click" }' ``` ```js fetch("https://api.cyberdesk.io/v1/desktop/:id/computer-action", { method: "POST", headers: { "x-api-key": "api_12345" } }); ``` ```json { "output": "X=500 Y=300", "error": "Command failed with code 1: xdotool: command not found", "base64_image": "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQ..." } ``` ```ts export interface Response { /** * Raw string output from the executed command (if any) */ output?: string; /** * Error message if the operation failed (also indicated by non-2xx HTTP status) */ error?: string; /** * Base64 encoded JPEG image data (only returned for screenshot actions) */ base64_image?: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ## Execute a bash command on the desktop Runs a bash command on the desktop and returns the command output ### Request Body (Optional) Bash command to execute Example: `"echo 'Hello, World!'"` ### Path Parameters Desktop instance ID to run the command on Example: `"desktop_12345"` ### Header Parameters API key for authentication Example: `"api_12345"` | Status code | Description | | ----------- | ----------- | | `200` | Command executed successfully. Response contains command output. | | `400` | The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). | | `401` | Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response. | | `403` | The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. | | `404` | The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web. | | `409` | This response is sent when a request conflicts with the current state of the server. | | `429` | The user has sent too many requests in a given amount of time ("rate limiting") | | `500` | The server has encountered a situation it does not know how to handle. | | `502` | The server, while acting as a gateway or proxy, received an invalid response from the upstream server. | ```bash curl -X POST "https://api.cyberdesk.io/v1/desktop/:id/bash-action" \ -H "x-api-key: api_12345" \ -d '{ "command": "echo 'Hello, World!'" }' ``` ```js fetch("https://api.cyberdesk.io/v1/desktop/:id/bash-action", { method: "POST", headers: { "x-api-key": "api_12345" } }); ``` ```json { "output": "X=500 Y=300", "error": "Command failed with code 1: xdotool: command not found", "base64_image": "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQ..." } ``` ```ts export interface Response { /** * Raw string output from the executed command (if any) */ output?: string; /** * Error message if the operation failed (also indicated by non-2xx HTTP status) */ error?: string; /** * Base64 encoded JPEG image data (only returned for screenshot actions) */ base64_image?: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ```json { "status": "error", "error": "Instance not found or unauthorized" } ``` ```ts export interface Response { status: "error"; /** * Error message detailing what went wrong */ error: string; } ``` ================================================ FILE: apps/docs/content/docs/conceptual-guide.mdx ================================================ --- title: Conceptual Guide description: Understanding core concepts of Cyberdesk --- Understanding the core concepts of Cyberdesk will help you build more effective applications. ## Architecture Overview Cyberdesk is built on a microservices architecture that provides scalable, reliable access to virtual desktop environments. The system consists of: - **API Service**: The backend service accessed via the SDK. - **Desktop Pool**: Managed virtual desktop instances. - **Stream Service**: Real-time visual streaming of desktop environments. - **Database**: Persistent storage for desktop metadata and user information. ## Desktop Instances A desktop instance is a virtual desktop environment running in the cloud. Each instance: - Has a unique identifier (UUID format). - Runs in an isolated environment for security. - Can be controlled programmatically through the **SDK methods**. - Provides a `stream_url` (obtained via SDK methods) for visual feedback. - Persists until explicitly stopped (via `terminateDesktop`) or times out. - Has configurable resources (CPU, memory, storage) - future feature. When creating a desktop instance using `launchDesktop`, you can specify a custom timeout in milliseconds using the `body.timeout_ms` parameter. This allows you to control how long the desktop instance will remain active before being automatically terminated. If not specified, a default timeout is applied. Check your plan for maximum allowed timeout values. ## Computer Actions Cyberdesk supports various computer actions to interact with the desktop via the **`executeComputerAction` SDK method**. Actions are specified using a `body` object containing a `type` field and associated parameters: ### Mouse Actions - **`type: 'click_mouse'`**: Perform a mouse action (click, press down, or release up). - `x`, `y` (optional): Coordinates for the click. - `button` (optional): `left`, `right`, or `middle` (defaults to `left`). - `num_of_clicks` (optional): Number of clicks (defaults to 1, for `click_type: 'click'`). - `click_type` (optional): `click` (down and up), `down` (press), or `up` (release) (defaults to `click`). - **`type: 'scroll'`**: Scroll the mouse wheel. - `direction`: `up`, `down`, `left`, or `right`. - `amount`: Amount to scroll in pixels. - **`type: 'move_mouse'`**: Move the mouse cursor to specific coordinates. - `x`, `y`: Target coordinates. - **`type: 'drag_mouse'`**: Drag the mouse from a start point to an end point. - `start`: `{ x, y }` starting coordinates. - `end`: `{ x, y }` ending coordinates. ### Keyboard Actions - **`type: 'type'`**: Type text at the current cursor position. - `text`: The string to type. - **`type: 'press_keys'`**: Press, hold down, or release keyboard keys. - `keys`: A single key string (e.g., `'Enter'`) or an array of keys to press simultaneously (e.g., `['Control', 'c']`). - `key_action_type` (optional): `press` (down and up), `down` (hold), or `up` (release) (defaults to `press`). ### Other Actions - **`type: 'wait'`**: Pause execution for a specified duration. - `ms`: Time to wait in milliseconds. - **`type: 'screenshot'`**: Capture the current state of the desktop. - Returns a `base64_image` field in the successful response object. - **`type: 'get_cursor_position'`**: Get the current mouse cursor coordinates. - Returns `x`, `y` coordinates in the successful response object. ## Bash Actions Bash actions allow you to execute shell commands on the desktop instance via the **`executeBashAction` SDK method**. This is useful for: - Installing software - Running applications - Manipulating files - Executing scripts - System configuration The method accepts a `body.command` parameter containing the shell command to execute and returns the command `output` (stdout/stderr) as a string in the successful response object. ## Desktop Streaming Cyberdesk provides real-time streaming of desktop visuals. The `stream_url` obtained from the `launchDesktop` or `getDesktop` SDK methods can be used for: 1. **Web-based Viewer**: Opening the URL in a browser. 2. **VNC Protocol**: Connecting with a VNC client (if the stream URL supports it). 3. **Embedded Iframe**: Embedding the desktop view directly in your web applications. The streaming service aims for low-latency transmission suitable for real-time interaction. ## Authentication and Security Cyberdesk uses API keys for authentication. The **SDK client is initialized with your API key**, which is then automatically included in all requests. - API keys are associated with a specific user account. - Rate limits apply to prevent abuse. - Keys can be revoked or regenerated from your dashboard. - Keep your API key secure. Communication with the API is encrypted using TLS/SSL. ## Resource Management Cyberdesk automatically manages resources: - Desktop instances are cleaned up when stopped via the **`terminateDesktop` SDK method** or when they reach their timeout. - Users are responsible for stopping instances when no longer needed to manage costs and resources. - Resource quotas and limits may apply depending on your subscription plan. ## Error Handling The Cyberdesk **SDK methods** simplify error handling. Each method returns a promise that resolves to an object. You should check if this object contains an `error` property: ```typescript const result = await cyberdesk.someMethod(...); if ('error' in result) { // Handle the error console.error('Cyberdesk SDK Error:', result.error); // The 'error' property contains the descriptive error message from the API } else { // Process the successful result console.log('Success:', result); } ``` Common error scenarios include: - Invalid parameters in the request body (`body` property of the method call). - Missing or invalid API key (during client initialization or if the key is revoked). - Attempting an action not allowed by your plan. - Referencing a non-existent desktop instance ID (`path.id`). - Rate limit exceeded. - Server-side issues on the Cyberdesk platform. Check the specific error message returned in the `error` property for details. ## Performance Considerations When building applications with the Cyberdesk SDK, consider: - **API Call Latency**: Network latency affects the time it takes for SDK methods to complete. - **Action Duration**: Some actions (like `wait` or long bash commands) naturally take time. - **Streaming Latency**: There will be some delay in the visual stream. - **Error Retries**: For transient issues (like network errors or potential server-side hiccups), consider implementing retry logic around your SDK calls, possibly with exponential backoff. ## Billing and Usage Cyberdesk billing is typically based on factors like: - **Active Desktop Time**: The duration desktop instances are running (often billed per second or minute). Monitor your usage and understand the pricing model via the Cyberdesk dashboard (coming soon!). ================================================ FILE: apps/docs/content/docs/index.mdx ================================================ --- title: Cyberdesk Documentation description: Comprehensive documentation for Cyberdesk service --- Welcome to the official documentation for Cyberdesk, a powerful service for creating, controlling, and managing virtual desktop instances in the cloud. This documentation focuses on using the **official TypeScript SDK**, the recommended way to interact with the Cyberdesk API. We also provide a Python SDK (docs coming soon!). Whether you're building automation tools, testing applications, or creating AI agents that can interact with computers, the Cyberdesk SDK provides the tools you need. ## What's New - **TypeScript + Python SDKs**: The easiest way to integrate Cyberdesk into your applications. - **AI Integration Guide**: Learn how to integrate Cyberdesk with AI models like Claude to create agents that can use computers. - **Responsive Desktop Viewer**: Implement a responsive viewer for desktop streams that works across all devices. - **Enhanced API Reference**: Complete API documentation with examples and response formats ## Documentation Sections - [Introduction](/docs/introduction) - Learn what Cyberdesk is and its key features - [Quickstart](/docs/quickstart) - Get up and running with the Cyberdesk SDK in minutes - [Tutorials](/docs/tutorials) - Step-by-step guides using the SDK for common use cases, including integration with AI agents - [Conceptual Guide](/docs/conceptual-guide) - Understand the core concepts of Cyberdesk - [API Reference](/docs/api-reference) - Detailed REST API documentation (primarily for reference, SDK usage is recommended) ## Key Features - **Virtual Desktop Creation**: Create cloud-based desktop instances on demand - **Programmatic Control**: Control mouse, keyboard, and system actions via API - **Bash Command Execution**: Run shell commands on desktop instances - **Visual Streaming**: Stream desktop visuals to your applications - **AI Integration**: Easily integrate with AI models for computer control - **Secure Authentication**: API key-based authentication for secure access ## Getting Started The fastest way to get started with Cyberdesk is to follow our [Quickstart Guide](/docs/quickstart), which will walk you through setting up the SDK, creating your first desktop instance, and performing basic interactions. For more detailed examples and use cases using the SDK, check out our [Tutorials](/docs/tutorials), which include step-by-step guides for common scenarios. ## Support If you need help or have questions about Cyberdesk, you can: - Join our [Discord community](https://discord.gg/ws5ddx5yZ8) for discussions and support - Contact us at dev@cyberdesk.io, for issues and feature requests We're constantly improving Cyberdesk based on user feedback, so please let us know how we can make it better! ================================================ FILE: apps/docs/content/docs/introduction.mdx ================================================ --- title: Introduction description: Introduction to Cyberdesk --- Cyberdesk provides a seamless way to create and interact with virtual desktop environments through a simple API. Whether you're building automation tools, testing applications, creating AI agents that can use computers, or developing remote desktop solutions, Cyberdesk offers a robust platform to handle your virtual desktop needs. ## What is Cyberdesk? Cyberdesk is a cloud-based service that allows you to: - Create virtual desktop instances on demand via the SDK - Control desktop interactions programmatically (mouse movements, clicks, keyboard input) using SDK methods - Execute bash commands remotely via the SDK - Manage desktop lifecycle (creation, interaction, termination) through the SDK - Stream desktop visuals to your applications or AI agents ## Key Features - **Simple SDK**: An easy-to-use TypeScript SDK for Node.js and browser environments - **Programmatic Control**: Full control over mouse, keyboard, and system actions via intuitive SDK functions - **Secure Authentication**: Simple API key configuration within the SDK - **Streaming Capability**: Stream desktop visuals to your applications with VNC support (SDK helps manage instance details) - **Resource Management**: Efficient creation and cleanup of desktop instances managed via SDK calls - **AI Integration**: Easily integrate with AI models like Claude or GPT to create agents that can use computers (see tutorials) - **Cross-Platform**: Works across different operating systems and environments - **Low Latency**: Fast response times for real-time interaction ## Use Cases Cyberdesk is ideal for a variety of applications: - **AI Agents**: Create AI assistants that can interact with desktop applications - **Automated Testing**: Test desktop applications with programmatic control - **Remote Automation**: Control desktop environments from anywhere - **Demo Environments**: Create disposable demo environments for software showcases - **Training Data Generation**: Generate training data for computer vision models - **Virtual Workstations**: Provide remote access to virtual desktop environments ## Getting Started To get started with Cyberdesk: 1. [Sign up](https://cyberdesk.io/signup) for an account 2. Obtain your API key from the dashboard 3. Follow our [Quickstart Guide](/docs/quickstart) to set up the SDK and create your first desktop instance 4. Explore our [Tutorials](/docs/tutorials) for common use cases Ready to dive deeper? Check out our [Conceptual Guide](/docs/conceptual-guide) to understand the core concepts behind Cyberdesk. ================================================ FILE: apps/docs/content/docs/meta.json ================================================ { "title": "Docs", "pages": ["index", "introduction", "quickstart", "tutorials", "conceptual-guide", "api-reference"], "defaultOpen": true, "root": true } ================================================ FILE: apps/docs/content/docs/quickstart.mdx ================================================ --- title: Quickstart description: Get started quickly with Cyberdesk --- import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; Get up and running with Cyberdesk in minutes. ## Fastest Start: Cyberdesk Starter Assistant UI The quickest way to get started with Cyberdesk is to use our [AI SDK Starter](https://github.com/cyberdesk-hq/cyberdesk-ai-sdk-starter) template. This ready-to-use Next.js application demonstrates how to build an AI assistant with virtual desktop control capabilities using the Cyberdesk API and Anthropic's Claude AI model. ### Features - Interactive virtual desktop with streaming capabilities - AI assistant chat interface powered by Claude 3.7 Sonnet - Desktop control via AI (mouse clicks, keyboard input, screenshots) - Bash command execution in the virtual environment ### Quick Setup 1. Clone the repository (alternatively, click "Use this template" on GitHub): ```bash git clone https://github.com/cyberdesk-hq/cyberdesk-ai-sdk-starter.git cd cyberdesk-ai-sdk-starter ``` 2. Install dependencies: ```bash npm install ``` 3. Create a `.env.local` file with your API keys: ``` CYBERDESK_API_KEY=your_cyberdesk_api_key_here ANTHROPIC_API_KEY=your_anthropic_api_key_here ``` 4. Start the development server: ```bash npm run dev ``` 5. Open [http://localhost:3000](http://localhost:3000) in your browser This template provides a complete working example that you can customize for your specific use case. For more details, check out the [repository README](https://github.com/cyberdesk-hq/cyberdesk-ai-sdk-starter). ## Building a Custom Integration If you want to build your own integration, you can use either the **official TypeScript SDK** or the **official Python SDK**. See the code tabs below for both options. ### Prerequisites - Node.js or Python 3.8+ - A Cyberdesk API key (obtain from your [Cyberdesk dashboard](https://cyberdesk.io/dashboard)) ### Installation ```bash npm install cyberdesk # or yarn add cyberdesk # or pnpm add cyberdesk ``` ```bash pip install cyberdesk ``` ### 1. Initialize the Client ```typescript import { createCyberdeskClient } from 'cyberdesk'; const cyberdesk = createCyberdeskClient({ apiKey: 'YOUR_API_KEY', // Optionally, you can override the baseUrl or provide a custom fetch implementation }); ``` ```python from cyberdesk import CyberdeskClient client = CyberdeskClient(api_key="YOUR_API_KEY") ``` ### 2. Launch a Desktop Instance ```typescript const launchResult = await cyberdesk.launchDesktop({ body: { timeout_ms: 600000 } // Optional: 10-minute timeout }); if ('error' in launchResult) { throw new Error('Failed to launch desktop: ' + launchResult.error); } const desktopId = launchResult.id; ``` ```python result = client.launch_desktop(timeout_ms=10000) # Optional: set a timeout for the desktop session if result.error: raise Exception('Failed to launch desktop: ' + str(result.error)) desktop_id = result.id ``` ### 3. Get Desktop Information (Including Stream URL) ```typescript const info = await cyberdesk.getDesktop({ path: { id: desktopId } }); if ('error' in info) { throw new Error('Failed to get desktop info: ' + info.error); } console.log('Desktop info:', info); console.log('Stream URL:', info.stream_url); ``` ```python info = client.get_desktop(desktop_id) if info.error: raise Exception('Failed to get desktop info: ' + str(info.error)) print('Desktop info:', info) # If available: print('Stream URL:', info.stream_url) ``` ### 4. Control the Desktop #### Perform a Mouse Click ```typescript const actionResult = await cyberdesk.executeComputerAction({ path: { id: desktopId }, body: { type: 'click_mouse', x: 100, y: 150, button: 'left' } }); if ('error' in actionResult) { throw new Error('Mouse click failed: ' + actionResult.error); } ``` ```python from cyberdesk.actions import click_mouse, ClickMouseButton action = click_mouse(x=100, y=150, button=ClickMouseButton.LEFT) action_result = client.execute_computer_action(desktop_id, action) if action_result.error: raise Exception('Action failed: ' + str(action_result.error)) ``` #### Run a Bash Command ```typescript const bashResult = await cyberdesk.executeBashAction({ path: { id: desktopId }, body: { command: 'ls -la /tmp' } }); if ('error' in bashResult) { throw new Error('Bash command failed: ' + bashResult.error); } console.log('Bash command output:', bashResult.output); ``` ```python bash_result = client.execute_bash_action( desktop_id, "ls -la /tmp" ) if bash_result.error: raise Exception('Bash command failed: ' + str(bash_result.error)) print('Bash command output:', getattr(bash_result, 'output', bash_result)) ``` ### 5. Stop the Desktop Instance ```typescript const terminateResult = await cyberdesk.terminateDesktop({ path: { id: desktopId } }); if ('error' in terminateResult) { // You might still want to proceed even if termination fails } ``` ```python terminate_result = client.terminate_desktop(desktop_id) if terminate_result.error: print('Termination failed:', terminate_result.error) ``` ### 6. Async Usage ```typescript // All methods are available as async functions (see above) ``` ```python import asyncio from cyberdesk import CyberdeskClient from cyberdesk.actions import click_mouse, ClickMouseButton async def main(): client = CyberdeskClient(api_key="YOUR_API_KEY") result = await client.async_launch_desktop(timeout_ms=10000) print(result) action = click_mouse(x=100, y=200, button=ClickMouseButton.LEFT) await client.async_execute_computer_action(desktop_id, action) # ... use other async_ methods as needed asyncio.run(main()) ``` ### 7. Type Hints and Models ```typescript // All request/response types are available from the generated models ``` ```python from cyberdesk.actions import click_mouse, drag_mouse, type_text, wait, scroll, move_mouse, press_keys, screenshot, get_cursor_position, ClickMouseButton, ClickMouseActionType, PressKeyActionType, ScrollDirection ``` ### 8. Available Computer Actions | Action | Factory Function (Python Only) | Description | |----------------|-------------------------|----------------------------| | Click Mouse | `click_mouse` | Mouse click at (x, y) | | Drag Mouse | `drag_mouse` | Mouse drag from/to (x, y) | | Move Mouse | `move_mouse` | Move mouse to (x, y) | | Scroll | `scroll` | Scroll by dx, dy | | Type Text | `type_text` | Type text | | Press Keys | `press_keys` | Press keyboard keys | | Screenshot | `screenshot` | Take a screenshot | | Wait | `wait` | Wait for ms milliseconds | | Get Cursor Pos | `get_cursor_position` | Get mouse cursor position | ## Next Steps - Explore the [Tutorials](/docs/tutorials) for more complex examples using the SDK. - Understand core concepts in the [Conceptual Guide](/docs/conceptual-guide). - Review the [API Reference](/docs/api-reference) if you need details on the underlying REST API. ================================================ FILE: apps/docs/content/docs/tutorials.mdx ================================================ --- title: Tutorials description: Step-by-step tutorials for using the Cyberdesk TypeScript SDK --- Learn how to use the Cyberdesk TypeScript SDK effectively with these step-by-step tutorials. **Setup:** All examples assume you have initialized the SDK client as shown in the [Quickstart](/docs/quickstart): ```typescript import { createCyberdeskClient } from 'cyberdesk'; const cyberdesk = createCyberdeskClient({ apiKey: 'YOUR_API_KEY', }); ``` ## Tutorial 1: Creating and Interacting with a Desktop This tutorial walks you through creating a desktop instance and performing basic interactions using the SDK. ### Step 1: Create a Desktop Instance Use `launchDesktop`: ```typescript async function createDesktop() { console.log('Creating desktop...'); const launchResult = await cyberdesk.launchDesktop({ body: { timeout_ms: 3600000 } // Optional: 1-hour timeout }); if ('error' in launchResult) { console.error('Failed to create desktop:', launchResult.error); throw new Error('Failed to create desktop: ' + launchResult.error); } console.log(`Desktop created with ID: ${launchResult.id}`); // Note: You might need to wait or use getDesktop to get the streamUrl return launchResult.id; } // Example usage: // const desktopId = await createDesktop(); ``` ### Step 2: Perform Mouse Actions Use `executeComputerAction` with `type: 'click_mouse'`: ```typescript async function clickOnDesktop(desktopId: string) { console.log(`Clicking on desktop ${desktopId}...`); const clickResult = await cyberdesk.executeComputerAction({ path: { id: desktopId }, body: { type: 'click_mouse', x: 100, y: 200, button: 'left' // Optional } }); if ('error' in clickResult) { console.error('Mouse click failed:', clickResult.error); throw new Error('Mouse click failed: ' + clickResult.error); } console.log('Mouse click successful:', clickResult); } // Example usage: // await clickOnDesktop(desktopId); ``` ### Step 3: Type Text Use `executeComputerAction` with `type: 'type'`: ```typescript async function typeOnDesktop(desktopId: string) { console.log(`Typing on desktop ${desktopId}...`); const typeResult = await cyberdesk.executeComputerAction({ path: { id: desktopId }, body: { type: 'type', text: 'Hello, Cyberdesk SDK!' } }); if ('error' in typeResult) { console.error('Typing failed:', typeResult.error); throw new Error('Typing failed: ' + typeResult.error); } console.log('Typing successful:', typeResult); } // Example usage: // await typeOnDesktop(desktopId); ``` ### Step 4: Take a Screenshot Use `executeComputerAction` with `type: 'screenshot'`: ```typescript async function captureScreenshot(desktopId: string) { console.log(`Taking screenshot of desktop ${desktopId}...`); const screenshotResult = await cyberdesk.executeComputerAction({ path: { id: desktopId }, body: { type: 'screenshot' } }); if ('error' in screenshotResult) { console.error('Screenshot failed:', screenshotResult.error); throw new Error('Screenshot failed: ' + screenshotResult.error); } console.log('Screenshot successful.'); // Access the image data: // const base64Image = screenshotResult.base64_image; // console.log('Base64 Image length:', base64Image?.length); return screenshotResult.base64_image; } // Example usage: // const imageData = await captureScreenshot(desktopId); ``` ### Step 5: Execute a Bash Command Use `executeBashAction`: ```typescript async function runCommand(desktopId: string) { console.log(`Running command on desktop ${desktopId}...`); const bashResult = await cyberdesk.executeBashAction({ path: { id: desktopId }, body: { command: 'ls -la /tmp' } }); if ('error' in bashResult) { console.error('Bash command failed:', bashResult.error); throw new Error('Bash command failed: ' + bashResult.error); } console.log('Command successful.'); console.log('Output:', bashResult.output); return bashResult.output; } // Example usage: // const commandOutput = await runCommand(desktopId); ``` ### Step 6: Stop the Desktop Use `terminateDesktop`: ```typescript async function stopDesktop(desktopId: string) { console.log(`Stopping desktop ${desktopId}...`); const stopResult = await cyberdesk.terminateDesktop({ path: { id: desktopId } }); if ('error' in stopResult) { console.error('Failed to stop desktop:', stopResult.error); // Decide if you need to throw an error or just log } console.log('Desktop stop requested.', stopResult); } // Example usage: // await stopDesktop(desktopId); ``` ## Tutorial 2: Automating Web Testing This tutorial demonstrates how to use the Cyberdesk SDK to automate simple web browser tasks. ### Step 1: Create a Desktop Instance Create a desktop instance using the `createDesktop` function from Tutorial 1. ```typescript // const desktopId = await createDesktop(); ``` ### Step 2: Open a Web Browser and Wait Use `executeBashAction` to open a browser (e.g., Firefox) in the background, then use `executeComputerAction` with `type: 'wait'` to allow time for it to load. ```typescript async function openBrowserAndWait(desktopId: string, url: string, waitMs: number = 5000) { console.log(`Opening ${url} on desktop ${desktopId}...`); const openResult = await cyberdesk.executeBashAction({ path: { id: desktopId }, body: { command: `firefox ${url} &` // Run in background } }); if ('error' in openResult) { console.error('Failed to send open browser command:', openResult.error); throw new Error('Failed to send open browser command: ' + openResult.error); } console.log('Browser opening command sent.'); console.log(`Waiting ${waitMs}ms for browser to load...`); const waitResult = await cyberdesk.executeComputerAction({ path: { id: desktopId }, body: { type: 'wait', ms: waitMs } }); if ('error' in waitResult) { console.error('Wait action failed:', waitResult.error); throw new Error('Wait action failed: ' + waitResult.error); } console.log('Wait complete.'); } // Example usage: // await openBrowserAndWait(desktopId, 'https://example.com'); ``` ### Step 3: Take a Screenshot for Verification Capture a screenshot using the `captureScreenshot` function from Tutorial 1 to verify the page loaded. ```typescript async function verifyPageLoad(desktopId: string) { console.log('Taking verification screenshot...'); const imageData = await captureScreenshot(desktopId); if (imageData) { console.log('Verification screenshot captured (length:', imageData.length, ')'); // Add logic here to analyze the screenshot if needed } else { console.warn('Could not capture verification screenshot.'); } } // Example usage: // await verifyPageLoad(desktopId); ``` ### Step 4: Stop the Desktop Remember to stop the desktop instance using the `stopDesktop` function from Tutorial 1 when the test is complete. ```typescript // await stopDesktop(desktopId); ``` ## Tutorial 3: Integrating with AI Models for Computer Use This tutorial demonstrates how to integrate the Cyberdesk SDK with AI models (like Anthropic's Claude) using the Vercel AI SDK to create agents that can use computers. ### Step 1: Prerequisites and Setup Install the necessary dependencies: ```bash npm install cyberdesk @ai-sdk/anthropic ai ``` Initialize the Cyberdesk SDK client (likely in a shared module, e.g., `@/lib/cyberdeskClient.ts`): ```typescript // src/lib/cyberdeskClient.ts import { createCyberdeskClient } from 'cyberdesk'; const client = createCyberdeskClient({ apiKey: process.env.CYBERDESK_API_KEY || 'YOUR_API_KEY', // Use environment variable }); export default client; ``` Ensure you have `CYBERDESK_API_KEY` set in your environment variables. ### Step 2: Implement `executeComputerAction` Utility Create a utility function that maps AI tool parameters to the Cyberdesk SDK's `executeComputerAction` method. This function handles the various action types supported by the AI tool. ```typescript // src/utils/computer-use.ts import client from '@/lib/cyberdeskClient'; import type { ExecuteComputerActionParams } from "cyberdesk" // Define the action types your AI tool might use export type ClaudeComputerActionType0124 = /* ... (action types like 'left_click', 'type', etc.) ... */ | "left_click" | "right_click" // ... (include all action types from the provided code) ... | "screenshot"; export async function executeComputerAction( action: ClaudeComputerActionType0124, desktopId: string, coordinate?: { x: number; y: number }, text?: string, duration?: number, scroll_amount?: number, scroll_direction?: "left" | "right" | "down" | "up", start_coordinate?: { x: number; y: number } ): Promise { try { let requestBody: ExecuteComputerActionParams['body']; // Map the AI tool action to the Cyberdesk SDK's expected format switch (action) { case 'left_click': requestBody = { type: 'click_mouse', x: coordinate?.x, y: coordinate?.y, button: 'left', click_type: 'click', num_of_clicks: 1 }; break; // ... Map other actions (right_click, type, scroll, screenshot, etc.) ... case 'type': requestBody = { type: 'type', text: text || '' }; break; case 'screenshot': requestBody = { type: 'screenshot' }; break; // ... (Include all case mappings from the provided computer-use.ts code) ... default: { const _exhaustiveCheck: never = action; throw new Error(`Unhandled action: ${action}`); } } const clientParams: ExecuteComputerActionParams = { path: { id: desktopId }, body: requestBody }; // *** Use the Cyberdesk SDK client *** const result = await client.executeComputerAction(clientParams); // Check the raw response status embedded in the SDK result if (result.response.status !== 200) { let errorDetails = `Failed with status: ${result.response.status}`; try { // Attempt to parse error details from the response body const errorBody = await result.response.json(); errorDetails = errorBody.message || errorBody.error || JSON.stringify(errorBody); } catch (e) { /* Failed to parse body */ } throw new Error(`Failed to execute computer action ${action}: ${errorDetails}`); } const data = result.data; // Access the parsed data from the SDK result if (data?.base64_image) { return { type: "image", data: data.base64_image }; } return data?.output || 'Action completed successfully'; } catch (error) { console.error(`Error executing computer action ${action}:`, error); throw error; // Re-throw to be handled by the AI SDK } } ``` *Note: The full mapping logic for all action types is omitted for brevity but should be included as shown in the `computer-use.ts` file.* ### Step 3: Implement `executeBashCommand` Utility Create a similar utility for bash commands, calling the `executeBashAction` SDK method. ```typescript // src/utils/bash.ts import client from '@/lib/cyberdeskClient'; export async function executeBashCommand( command: string, desktopId: string ): Promise { try { // *** Use the Cyberdesk SDK client *** const result = await client.executeBashAction({ path: { id: desktopId }, body: { command }, }); // Check the raw response status if (result.response.status !== 200) { let errorDetails = `Failed with status: ${result.response.status}`; try { const errorBody = await result.response.json(); errorDetails = errorBody.message || errorBody.error || JSON.stringify(errorBody); } catch (e) { /* Failed to parse body */ } throw new Error(`Failed to execute bash command: ${errorDetails}`); } const data = result.data; // Access the parsed data return data?.output || ''; // Return output or empty string } catch (error) { console.error(`Error executing bash command "${command}":`, error); // Return a meaningful error message for the AI to potentially see return 'Error executing bash command: ' + (error as Error).message; } } ``` ### Step 4: Set Up the AI Tools and API Route Create an API route (e.g., `/api/chat`) that uses the Vercel AI SDK (`streamText`) and defines tools that call your utility functions. ```typescript // src/app/api/chat/route.ts import { anthropic } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; import { executeComputerAction } from '../../../utils/computer-use'; // Adjust path import { executeBashCommand } from '../../../utils/bash'; // Adjust path // Define result types if needed interface ComputerActionResult { type: "image"; data: string; } export const maxDuration = 300; export async function POST(req: Request) { const desktopId = req.headers.get('X-Desktop-Id'); const { messages } = await req.json(); if (!desktopId) { return Response.json({ error: "Desktop ID is required" }, { status: 400 }); } const lastMessage = messages[messages.length - 1]; const userContent = /* ... logic to extract text from lastMessage.content ... */ ""; // Define the computer tool using the AI SDK const computerTool = anthropic.tools.computer_20250124({ displayWidthPx: 1024, displayHeightPx: 768, // The execute function calls *your* utility function, which uses the Cyberdesk SDK execute: async ({ action, coordinate, duration, scroll_amount, scroll_direction, start_coordinate, text }) => { const coordinateObj = coordinate ? { x: coordinate[0], y: coordinate[1] } : undefined; const startCoordinateObj = start_coordinate ? { x: start_coordinate[0], y: start_coordinate[1] } : undefined; // *** Call your utility function *** const result = await executeComputerAction( action, desktopId, coordinateObj, text, duration, scroll_amount, scroll_direction, startCoordinateObj ); // Format the result for the AI SDK tool return (typeof result === 'string') ? { type: "text" as const, text: result } : { type: "image" as const, data: result.data }; }, // Optional: Format the tool result content for the AI model experimental_toToolResultContent(result: { type: "text"; text: string } | ComputerActionResult) { return result.type === 'text' ? [{ type: 'text', text: result.text }] : [{ type: 'image', data: result.data, mimeType: 'image/jpeg' }]; }, }); // Define the bash tool using the AI SDK const bashTool = anthropic.tools.bash_20250124({ // The execute function calls *your* utility function execute: async ({ command }) => { // *** Call your utility function *** const output = await executeBashCommand(command, desktopId); return output; // Return the string output directly } }); try { // Call the AI model with the tools const response = streamText({ model: anthropic("claude-3-7-sonnet-20250219"), prompt: userContent, system: "You are an AI assistant that can control a computer...", // Your system prompt tools: { computer: computerTool, bash: bashTool }, maxSteps: 100 }); return response.toDataStreamResponse(); } catch (error) { console.error("Error calling Anthropic:", error); return Response.json({ error: "Failed to process request" }, { status: 500 }); } } ``` ### Step 5: Frontend Implementation A frontend application would: 1. Create a desktop instance (perhaps via another API route that uses `cyberdesk.launchDesktop`). 2. Render a chat interface. 3. Display the desktop stream using the `stream_url`. 4. Send user messages to the `/api/chat` endpoint, including the `desktopId` in the `X-Desktop-Id` header. 5. Process the streamed response from the API, updating the chat UI. *(Refer to the Cyberdesk Starter Assistant UI template for a full frontend example)*. ### Conclusion By wrapping the Cyberdesk SDK methods within utility functions called by your AI tool's `execute` logic, you can seamlessly integrate robust desktop control into your AI agents. The SDK handles the direct API communication, authentication, and response parsing, simplifying your integration code. ================================================ FILE: apps/docs/mdx-components.tsx ================================================ import defaultComponents from "fumadocs-ui/mdx"; import type { MDXComponents } from "mdx/types"; export function useMDXComponents(components: MDXComponents): MDXComponents { return { ...defaultComponents, ...components, }; } ================================================ FILE: apps/docs/next-env.d.ts ================================================ /// /// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. ================================================ FILE: apps/docs/next.config.mjs ================================================ import createMDX from "fumadocs-mdx/config"; const withMDX = createMDX(); /** @type {import('next').NextConfig} */ const config = { reactStrictMode: true, }; export default withMDX(config); ================================================ FILE: apps/docs/package.json ================================================ { "name": "docs", "version": "0.0.0", "private": true, "scripts": { "generate": "node scripts/generate-docs.mjs", "build": "next build", "dev": "next dev", "start": "next start" }, "dependencies": { "@vercel/analytics": "^1.5.0", "fumadocs-core": "^12.1.2", "fumadocs-mdx": "^8.2.32", "fumadocs-openapi": "^3.0.0", "fumadocs-typescript": "^2.0.1", "fumadocs-ui": "^12.1.2", "next": "^14.2.4", "react": "^18.3.1", "react-dom": "^18.3.1", "zod": "^3.23.8" }, "devDependencies": { "@types/mdx": "^2.0.13", "@types/node": "^20.11.30", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "autoprefixer": "^10.4.19", "postcss": "^8.4.38", "tailwindcss": "^3.4.4", "typescript": "^5.4.5" } } ================================================ FILE: apps/docs/postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ================================================ FILE: apps/docs/scripts/generate-docs.mjs ================================================ import * as path from "node:path"; import * as OpenAPI from "fumadocs-openapi"; import * as Typescript from "fumadocs-typescript"; void OpenAPI.generateFiles({ input: ["../../sdks/openapi.json"], output: "./content/docs/", name: () => "api-reference", frontmatter: (title) => ({ toc: false, title: `${title[0].toUpperCase()}${title.slice(1)}`, }), }); void Typescript.generateFiles({ input: ["./content/docs/**/*.model.mdx"], output: (file) => path.resolve( path.dirname(file), `${path.basename(file).split(".")[0]}.mdx` ), }); ================================================ FILE: apps/docs/tailwind.config.js ================================================ import { createPreset } from 'fumadocs-ui/tailwind-plugin'; /** @type {import('tailwindcss').Config} */ export default { content: [ './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', './content/**/*.{md,mdx}', './mdx-components.{ts,tsx}', './node_modules/fumadocs-ui/dist/**/*.js', './node_modules/fumadocs-openapi/dist/**/*.js', ], presets: [createPreset()], theme: { extend: { typography: { DEFAULT: { css: { h1: { letterSpacing: '-0.025em', // This is what tracking-tight does fontWeight: '500', // font-medium (500) instead of bold (700) }, h2: { letterSpacing: '-0.025em', fontWeight: '500', }, h3: { letterSpacing: '-0.025em', fontWeight: '500', }, h4: { letterSpacing: '-0.025em', fontWeight: '500', }, h5: { letterSpacing: '-0.025em', fontWeight: '500', }, h6: { letterSpacing: '-0.025em', fontWeight: '500', }, }, }, }, }, }, }; ================================================ FILE: apps/docs/tsconfig.json ================================================ { "compilerOptions": { "baseUrl": ".", "target": "ESNext", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "paths": { "@/*": ["./*"] }, "plugins": [ { "name": "next" } ] }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } ================================================ FILE: apps/web/.eslintrc.json ================================================ { "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint"], "extends": [ "next/core-web-vitals", "plugin:@typescript-eslint/recommended" ], "rules": { "@next/next/no-img-element": "off", "@typescript-eslint/no-unused-vars": "warn" } } ================================================ FILE: apps/web/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .yarn/install-state.gz # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts .env .env.local # turbo .turbo ================================================ FILE: apps/web/LICENSE.md ================================================ # Tailwind Plus License ## Personal License Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates. The license grants permission to **one individual** (the Licensee) to access and use the Components and Templates. You **can**: - Use the Components and Templates to create unlimited End Products. - Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license. - Use the Components and Templates to create unlimited End Products for unlimited Clients. - Use the Components and Templates to create End Products where the End Product is sold to End Users. - Use the Components and Templates to create End Products that are open source and freely available to End Users. You **cannot**: - Use the Components and Templates to create End Products that are designed to allow an End User to build their own End Products using the Components and Templates or derivatives of the Components and Templates. - Re-distribute the Components and Templates or derivatives of the Components and Templates separately from an End Product, neither in code or as design assets. - Share your access to the Components and Templates with any other individuals. - Use the Components and Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc. ### Example usage Examples of usage **allowed** by the license: - Creating a personal website by yourself. - Creating a website or web application for a client that will be owned by that client. - Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application. - Creating a commercial self-hosted web application that is sold to end users for a one-time fee. - Creating a web application where the primary purpose is clearly not to simply re-distribute the components (like a conference organization app that uses the components for its UI for example) that is free and open source, where the source code is publicly available. Examples of usage **not allowed** by the license: - Creating a repository of your favorite Tailwind Plus components or templates (or derivatives based on Tailwind Plus components or templates) and publishing it publicly. - Creating a React or Vue version of Tailwind Plus and making it available either for sale or for free. - Create a Figma or Sketch UI kit based on the Tailwind Plus component designs. - Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind Plus. - Creating a theme, template, or project starter kit using the components or templates and making it available either for sale or for free. - Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free. In simple terms, use Tailwind Plus for anything you like as long as it doesn't compete with Tailwind Plus. ### Personal License Definitions Licensee is the individual who has purchased a Personal License. Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind Plus license. End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates. End User is a user of an End Product. Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document. ## Team License Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates. The license grants permission for **up to 25 Employees and Contractors of the Licensee** to access and use the Components and Templates. You **can**: - Use the Components and Templates to create unlimited End Products. - Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license. - Use the Components and Templates to create unlimited End Products for unlimited Clients. - Use the Components and Templates to create End Products where the End Product is sold to End Users. - Use the Components and Templates to create End Products that are open source and freely available to End Users. You **cannot**: - Use the Components or Templates to create End Products that are designed to allow an End User to build their own End Products using the Components or Templates or derivatives of the Components or Templates. - Re-distribute the Components or Templates or derivatives of the Components or Templates separately from an End Product. - Use the Components or Templates to create End Products that are the property of any individual or entity other than the Licensee or Clients of the Licensee. - Use the Components or Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc. ### Example usage Examples of usage **allowed** by the license: - Creating a website for your company. - Creating a website or web application for a client that will be owned by that client. - Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application. - Creating a commercial self-hosted web application that is sold to end users for a one-time fee. - Creating a web application where the primary purpose is clearly not to simply re-distribute the components or templates (like a conference organization app that uses the components or a template for its UI for example) that is free and open source, where the source code is publicly available. Examples of use **not allowed** by the license: - Creating a repository of your favorite Tailwind Plus components or template (or derivatives based on Tailwind Plus components or templates) and publishing it publicly. - Creating a React or Vue version of Tailwind Plus and making it available either for sale or for free. - Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind Plus. - Creating a theme or template using the components or templates and making it available either for sale or for free. - Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free. - Creating any End Product that is not the sole property of either your company or a client of your company. For example your employees/contractors can't use your company Tailwind Plus license to build their own websites or side projects. ### Team License Definitions Licensee is the business entity who has purchased a Team License. Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind Plus license. End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates. End User is a user of an End Product. Employee is a full-time or part-time employee of the Licensee. Contractor is an individual or business entity contracted to perform services for the Licensee. Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document. ## Enforcement If you are found to be in violation of the license, access to your Tailwind Plus account will be terminated, and a refund may be issued at our discretion. When license violation is blatant and malicious (such as intentionally redistributing the Components or Templates through private warez channels), no refund will be issued. The copyright of the Components and Templates is owned by Tailwind Labs Inc. You are granted only the permissions described in this license; all other rights are reserved. Tailwind Labs Inc. reserves the right to pursue legal remedies for any unauthorized use of the Components or Templates outside the scope of this license. ## Liability Tailwind Labs Inc.’s liability to you for costs, damages, or other losses arising from your use of the Components or Templates — including third-party claims against you — is limited to a refund of your license fee. Tailwind Labs Inc. may not be held liable for any consequential damages related to your use of the Components or Templates. This Agreement is governed by the laws of the Province of Ontario and the applicable laws of Canada. Legal proceedings related to this Agreement may only be brought in the courts of Ontario. You agree to service of process at the e-mail address on your original order. ## Questions? Unsure which license you need, or unsure if your use case is covered by our licenses? Email us at [support@tailwindcss.com](mailto:support@tailwindcss.com) with your questions. ================================================ FILE: apps/web/README.md ================================================ ## Getting started To get started with this template, first install the npm dependencies: ```bash npm install ``` Next, create a new Sanity project to power the blog within this template: ```bash npm create sanity@latest -- --env=.env.local --create-project "Radiant Blog" --dataset production ``` This will prompt you to create a new Sanity account if you don't have one already. When asked "Would you like to add configuration files for a Sanity project in this Next.js folder?", choose "n". Next, optionally import the demo seed data for the blog: ```bash npx sanity@latest dataset import seed.tar.gz ``` Next, run the development server: ```bash npm run dev ``` Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website. To manage your blog content, visit the embedded Sanity Studio at [http://localhost:3000/studio](http://localhost:3000/studio). ## License This site template is a commercial product and is licensed under the [Tailwind Plus license](https://tailwindcss.com/plus/license). ## Learn more To learn more about the technologies used in this site template, see the following resources: - [Tailwind CSS](https://tailwindcss.com/docs) - the official Tailwind CSS documentation - [Next.js](https://nextjs.org/docs) - the official Next.js documentation - [Headless UI](https://headlessui.dev) - the official Headless UI documentation - [Sanity](https://www.sanity.io) - the Sanity website ================================================ FILE: apps/web/components.json ================================================ { "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": true, "tsx": true, "tailwind": { "config": "", "css": "src/styles/tailwind.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" }, "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" }, "iconLibrary": "lucide" } ================================================ FILE: apps/web/config.ts ================================================ const CONFIG = { docsURL: 'https://docs.cyberdesk.io', subscriptionLimit: 1000 } export default CONFIG; ================================================ FILE: apps/web/middleware.ts ================================================ import { type NextRequest } from 'next/server' import { updateSession } from '@/utils/supabase/middleware' export async function middleware(request: NextRequest) { return await updateSession(request) } export const config = { matcher: [ /* * Match all request paths except for the ones starting with: * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) * Feel free to modify this pattern to include more paths. */ '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', ], } ================================================ FILE: apps/web/next.config.mjs ================================================ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, async rewrites() { return [ { source: "/ingest/static/:path*", destination: "https://us-assets.i.posthog.com/static/:path*", }, { source: "/ingest/:path*", destination: "https://us.i.posthog.com/:path*", }, { source: "/ingest/decide", destination: "https://us.i.posthog.com/decide", }, ]; }, // This is required to support PostHog trailing slash API requests skipTrailingSlashRedirect: true, }; export default nextConfig ================================================ FILE: apps/web/package.json ================================================ { "name": "web", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", "typegen": "sanity schema extract --path=src/sanity/extract.json && sanity typegen generate && rm ./src/sanity/extract.json" }, "dependencies": { "@ai-sdk/anthropic": "^1.2.2", "@ai-sdk/openai": "^1.3.3", "@anthropic-ai/sdk": "^0.39.0", "@assistant-ui/react": "^0.8.6", "@assistant-ui/react-ai-sdk": "^0.8.0", "@assistant-ui/react-markdown": "^0.8.0", "@headlessui/react": "^2.1.1", "@heroicons/react": "^2.1.4", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@sanity/image-url": "^1.0.2", "@sanity/vision": "^3.52.2", "@supabase/ssr": "^0.6.1", "@supabase/supabase-js": "^2.49.4", "@types/uuid": "^10.0.0", "@unkey/api": "^0.34.0", "ai": "^4.2.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cyberdesk": "^0.2.0", "dayjs": "^1.11.12", "feed": "^4.2.2", "framer-motion": "^11.2.10", "lucide-react": "^0.484.0", "motion": "^12.11.0", "next": "14.2.11", "next-sanity": "^9.4.7", "next-themes": "^0.4.6", "openai": "^4.90.0", "posthog-js": "^1.234.1", "posthog-node": "^4.10.2", "react": "^18", "react-dom": "^18", "react-icons": "^5.5.0", "react-resizable-panels": "^3.0.2", "react-use-measure": "^2.1.1", "remark-gfm": "^4.0.1", "sanity": "^3.55.0", "sonner": "^2.0.3", "stripe": "^17.7.0", "tailwind-merge": "^3.0.2", "tw-animate-css": "^1.2.4" }, "devDependencies": { "@tailwindcss/postcss": "^4.0.6", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/parser": "^7.13.1", "eslint": "^8", "eslint-config-next": "14.2.11", "postcss": "^8.4.40", "prettier": "^3.3.2", "prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-tailwindcss": "^0.6.10", "tailwindcss": "^4.0.6", "typescript": "^5" }, "optionalDependencies": { "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1", "@tailwindcss/oxide-win32-x64-msvc": "^4.0.17", "lightningcss-linux-x64-gnu": "^1.29.1", "lightningcss-win32-x64-msvc": "^1.29.3" } } ================================================ FILE: apps/web/postcss.config.js ================================================ module.exports = { plugins: { '@tailwindcss/postcss': {}, }, } ================================================ FILE: apps/web/prettier.config.js ================================================ /** @type {import('prettier').Options} */ module.exports = { singleQuote: true, semi: false, plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'], tailwindFunctions: ['clsx'], tailwindStylesheet: './src/styles/tailwind.css', } ================================================ FILE: apps/web/radiant/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # Dependencies /node_modules /.pnp .pnp.js # Compiled Sanity Studio /dist # Temporary Sanity runtime, generated by the CLI on every dev server start /.sanity # Logs /logs *.log # Coverage directory used by testing tools /coverage # Misc .DS_Store *.pem # Typescript *.tsbuildinfo # Dotenv and similar local-only files *.local ================================================ FILE: apps/web/radiant/README.md ================================================ # Sanity Clean Content Studio Congratulations, you have now installed the Sanity Content Studio, an open-source real-time content editing environment connected to the Sanity backend. Now you can do the following things: - [Read “getting started” in the docs](https://www.sanity.io/docs/introduction/getting-started?utm_source=readme) - [Join the community Slack](https://slack.sanity.io/?utm_source=readme) - [Extend and build plugins](https://www.sanity.io/docs/content-studio/extending?utm_source=readme) ================================================ FILE: apps/web/radiant/eslint.config.mjs ================================================ import studio from '@sanity/eslint-config-studio' export default [...studio] ================================================ FILE: apps/web/radiant/package.json ================================================ { "name": "radiant", "private": true, "version": "1.0.0", "main": "package.json", "license": "UNLICENSED", "scripts": { "dev": "sanity dev", "start": "sanity start", "build": "sanity build", "deploy": "sanity deploy", "deploy-graphql": "sanity graphql deploy" }, "keywords": [ "sanity" ], "dependencies": { "@sanity/vision": "^3.80.0", "react": "^18.2.0", "react-dom": "^18.2.0", "sanity": "^3.80.0", "styled-components": "^6.1.8" }, "devDependencies": { "@sanity/eslint-config-studio": "^5.0.2", "@types/react": "^18.0.25", "eslint": "^9.9.0", "prettier": "^3.0.2", "typescript": "^5.1.6" }, "prettier": { "semi": false, "printWidth": 100, "bracketSpacing": false, "singleQuote": true } } ================================================ FILE: apps/web/radiant/sanity.cli.ts ================================================ import {defineCliConfig} from 'sanity/cli' export default defineCliConfig({ api: { projectId: '4hdczeqj', dataset: 'production' }, /** * Enable auto-updates for studios. * Learn more at https://www.sanity.io/docs/cli#auto-updates */ autoUpdates: true, }) ================================================ FILE: apps/web/radiant/sanity.config.ts ================================================ import {defineConfig} from 'sanity' import {structureTool} from 'sanity/structure' import {visionTool} from '@sanity/vision' import {schemaTypes} from './schemaTypes' export default defineConfig({ name: 'default', title: 'Radiant', projectId: '4hdczeqj', dataset: 'production', plugins: [structureTool(), visionTool()], schema: { types: schemaTypes, }, }) ================================================ FILE: apps/web/radiant/schemaTypes/index.ts ================================================ export const schemaTypes = [] ================================================ FILE: apps/web/radiant/static/.gitkeep ================================================ Files placed here will be served by the Sanity server under the `/static`-prefix ================================================ FILE: apps/web/radiant/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "module": "Preserve", "moduleDetection": "force", "isolatedModules": true, "jsx": "preserve", "incremental": true }, "include": ["**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] } ================================================ FILE: apps/web/sanity-typegen.json ================================================ { "path": "./src/**/*.{ts,tsx,js,jsx}", "schema": "./src/sanity/extract.json", "generates": "./src/sanity/types.ts" } ================================================ FILE: apps/web/sanity.cli.ts ================================================ import { defineCliConfig } from 'sanity/cli' const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET export default defineCliConfig({ api: { projectId, dataset } }) ================================================ FILE: apps/web/sanity.config.ts ================================================ 'use client' import { visionTool } from '@sanity/vision' import { defineConfig } from 'sanity' import { structureTool } from 'sanity/structure' import { apiVersion, dataset, projectId } from './src/sanity/env' import { schema } from './src/sanity/schema' export default defineConfig({ basePath: '/studio', projectId, dataset, schema, plugins: [structureTool(), visionTool({ defaultApiVersion: apiVersion })], }) ================================================ FILE: apps/web/src/app/api/playground/chat/route.ts ================================================ import { anthropic } from "@ai-sdk/anthropic"; import { streamText, type UIMessage } from "ai"; import { prunedMessages } from "@/utils/playground/misc-demo-utils"; import { bashTool, computerTool } from "@/utils/playground/tools"; import client from "@/utils/playground/cyberdesk-client"; // Allow streaming responses up to 5 minutes export const maxDuration = 300; export async function POST(req: Request) { const { messages, sandboxId }: { messages: UIMessage[]; sandboxId: string } = await req.json(); try { const result = streamText({ model: anthropic("claude-3-7-sonnet-20250219"), // Using Sonnet for computer use system: "You are a helpful assistant with access to a computer. " + "Use the computer tool to help the user with their requests. " + "Use the bash tool to execute commands on the computer. You can create files and folders using the bash tool. Always prefer the bash tool where it is viable for the task. " + "Be sure to advise the user when waiting is necessary. " + "If the browser opens with a setup wizard, YOU MUST IGNORE IT and move straight to the next step (e.g. input the url in the search bar)." + "Use DuckDuckGo to search the web.", messages: prunedMessages(messages), tools: { computer: computerTool(sandboxId), bash: bashTool(sandboxId) }, providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } }, }, maxSteps: 100, }); // Create response stream const response = result.toDataStreamResponse({ // @ts-expect-error eheljfe getErrorMessage(error) { console.error("Error in streamText:", error); return error; }, }); return response; } catch (error) { console.error("Chat API error:", error); await client.terminateDesktop({ path: { id: sandboxId, }, }); return new Response(JSON.stringify({ error: "Internal Server Error" }), { status: 500, headers: { "Content-Type": "application/json" }, }); } } ================================================ FILE: apps/web/src/app/api/playground/kill-desktop/route.ts ================================================ import client from "@/utils/playground/cyberdesk-client"; // Common handler for both GET and POST requests async function handleKillDesktop(request: Request) { // Enable CORS to ensure this works across all browsers const { searchParams } = new URL(request.url); const sandboxId = searchParams.get("sandboxId"); console.log(`Kill desktop request received via ${request.method} for ID: ${sandboxId}`); if (!sandboxId) { return new Response("No sandboxId provided", { status: 400 }); } try { await client.terminateDesktop({ path: { id: sandboxId, } }); return new Response("Desktop killed successfully", { status: 200 }); } catch (error) { console.error(`Failed to kill desktop with ID: ${sandboxId}`, error); return new Response("Failed to kill desktop", { status: 500 }); } } // Handle POST requests export async function POST(request: Request) { return handleKillDesktop(request); } ================================================ FILE: apps/web/src/app/api/stripe/checkout/route.ts ================================================ import { stripe, STRIPE_PRICE_ID } from '@/utils/stripe/stripe-server'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(req: NextRequest) { try { const { successUrl, cancelUrl, stripeCustomerId } = await req.json(); // Create a Stripe checkout session for the Pro plan const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], line_items: [ { price: STRIPE_PRICE_ID, quantity: 1, }, ], mode: 'subscription', success_url: successUrl || `${req.nextUrl.origin}/dashboard?payment=success`, cancel_url: cancelUrl || `${req.nextUrl.origin}/pricing?payment=cancelled`, metadata: { plan: 'pro', }, ...(stripeCustomerId && { customer: stripeCustomerId }), }); return NextResponse.json({ sessionId: session.id, url: session.url }); } catch (error) { console.error('Error creating checkout session:', error); return NextResponse.json( { error: 'Error creating checkout session' }, { status: 500 } ); } } ================================================ FILE: apps/web/src/app/api/stripe/portal/route.ts ================================================ import { stripe } from '@/utils/stripe/stripe-server'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(req: NextRequest) { try { const { customerId, returnUrl } = await req.json(); if (!customerId) { return NextResponse.json( { error: 'Customer ID is required' }, { status: 400 } ); } // Create a billing portal session for the customer const session = await stripe.billingPortal.sessions.create({ customer: customerId, return_url: returnUrl || `${req.nextUrl.origin}/dashboard`, }); return NextResponse.json({ url: session.url }); } catch (error) { console.error('Error creating portal session:', error); return NextResponse.json( { error: 'Error creating portal session' }, { status: 500 } ); } } ================================================ FILE: apps/web/src/app/api/stripe/webhook/route.ts ================================================ import { stripe } from '@/utils/stripe/stripe-server'; import { headers } from 'next/headers'; import { NextRequest, NextResponse } from 'next/server'; import Stripe from 'stripe'; import { createClient } from '@/utils/supabase/server'; // Define subscription status type using Stripe's type type SubscriptionStatus = Stripe.Subscription.Status; // This is your Stripe webhook secret for testing your endpoint locally. const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; export async function POST(req: NextRequest) { const body = await req.text(); const sig = headers().get('stripe-signature') as string; const supabase = createClient(); let event; try { event = stripe.webhooks.constructEvent(body, sig, endpointSecret!); } catch (err: unknown) { let errorMessage = "An unknown error occurred"; if (err instanceof Error) { errorMessage = err.message; } console.error(`Webhook Error: ${errorMessage}`); return NextResponse.json( { error: `Webhook Error: ${errorMessage}` }, { status: 400 } ); } // Helper function to update profile in Supabase const updateProfile = async (customerId: string, data: { subscription_status?: SubscriptionStatus; updated_at?: Date; stripe_subscription_id?: string | null; current_period_end?: Date | null; plan_id?: string | null; cancel_at_period_end?: boolean | null; email?: string | null; }) => { try { // First, find the profile with this Stripe customer ID const { data: profiles, error: fetchError } = await supabase .from('profiles') .select('*') .eq('stripe_customer_id', customerId) .limit(1); if (fetchError) { console.error('Error fetching profile:', fetchError); return; } if (profiles && profiles.length > 0) { const profile = profiles[0]; // Update the profile with new data const { error: updateError } = await supabase .from('profiles') .update(data) .eq('id', profile.id); if (updateError) { console.error('Error updating profile:', updateError); } else { console.log(`Profile ${profile.id} updated successfully`); } } else { console.log(`No profile found for customer ID: ${customerId}`); } } catch (error) { console.error('Error in updateProfile:', error); } }; // Handle the event try { switch (event.type) { case 'checkout.session.async_payment_failed': { const session = event.data.object; console.log('Checkout session async payment failed:', session); // Handle the failed async payment const customerId = session.customer ? (typeof session.customer === 'string' ? session.customer : session.customer.id) : null; if (customerId) { await updateProfile(customerId, { subscription_status: 'past_due', updated_at: new Date() }); } break; } case 'checkout.session.async_payment_succeeded': { const session = event.data.object; console.log('Checkout session async payment succeeded:', session); // Similar handling to checkout.session.completed const customerId = session.customer ? (typeof session.customer === 'string' ? session.customer : session.customer.id) : null; const subscriptionId = session.subscription ? (typeof session.subscription === 'string' ? session.subscription : session.subscription.id) : null; if (customerId && subscriptionId) { // Get subscription details const subscription = await stripe.subscriptions.retrieve(subscriptionId); // Update profile with subscription info await updateProfile(customerId, { stripe_subscription_id: subscriptionId, subscription_status: subscription.status, current_period_end: new Date(subscription.current_period_end * 1000), plan_id: subscription.items.data[0].price.id, cancel_at_period_end: subscription.cancel_at_period_end, updated_at: new Date() }); } break; } case 'checkout.session.completed': { const checkoutSession = event.data.object console.log('Checkout session completed:', checkoutSession); // Get customer ID and subscription ID from the session, handling possible null values const customerId = checkoutSession.customer ? (typeof checkoutSession.customer === 'string' ? checkoutSession.customer : checkoutSession.customer.id) : null; const subscriptionId = checkoutSession.subscription ? (typeof checkoutSession.subscription === 'string' ? checkoutSession.subscription : checkoutSession.subscription.id) : null; if (customerId && subscriptionId) { // Get subscription details const subscription = await stripe.subscriptions.retrieve(subscriptionId); // Update profile with subscription info await updateProfile(customerId, { stripe_subscription_id: subscriptionId, subscription_status: subscription.status, current_period_end: new Date(subscription.current_period_end * 1000), plan_id: subscription.items.data[0].price.id, cancel_at_period_end: subscription.cancel_at_period_end, updated_at: new Date() }); } break; } case 'customer.created': { const customer = event.data.object; console.log('Customer created:', customer); // No specific action needed here as the customer ID will be associated with a profile // when they complete checkout or when the user account is created break; } case 'customer.deleted': { const customer = event.data.object; console.log('Customer deleted:', customer); // Mark the customer as deleted in our system await updateProfile(customer.id, { subscription_status: 'canceled', updated_at: new Date() }); break; } case 'customer.updated': { const customer = event.data.object; console.log('Customer updated:', customer); // Update basic customer information await updateProfile(customer.id, { email: customer.email, updated_at: new Date() }); break; } case 'customer.subscription.created': { const subscription = event.data.object console.log('Subscription created:', subscription); const customerId = typeof subscription.customer === 'string' ? subscription.customer : subscription.customer.id; await updateProfile(customerId, { stripe_subscription_id: subscription.id, subscription_status: subscription.status, current_period_end: new Date(subscription.current_period_end * 1000), plan_id: subscription.items.data[0].price.id, cancel_at_period_end: subscription.cancel_at_period_end, updated_at: new Date() }); break; } case 'customer.subscription.paused': { const subscription = event.data.object; console.log('Subscription paused:', subscription); const customerId = typeof subscription.customer === 'string' ? subscription.customer : subscription.customer.id; await updateProfile(customerId, { subscription_status: 'paused', updated_at: new Date() }); break; } case 'customer.subscription.resumed': { const subscription = event.data.object; console.log('Subscription resumed:', subscription); const customerId = typeof subscription.customer === 'string' ? subscription.customer : subscription.customer.id; await updateProfile(customerId, { subscription_status: subscription.status, current_period_end: new Date(subscription.current_period_end * 1000), updated_at: new Date() }); break; } case 'customer.subscription.updated': { const updatedSubscription = event.data.object as Stripe.Subscription; console.log('Subscription updated:', updatedSubscription); const customerId = typeof updatedSubscription.customer === 'string' ? updatedSubscription.customer : updatedSubscription.customer.id; await updateProfile(customerId, { subscription_status: updatedSubscription.status, current_period_end: new Date(updatedSubscription.current_period_end * 1000), plan_id: updatedSubscription.items.data[0].price.id, cancel_at_period_end: updatedSubscription.cancel_at_period_end, updated_at: new Date() }); break; } case 'customer.subscription.deleted': { const deletedSubscription = event.data.object as Stripe.Subscription; console.log('Subscription deleted:', deletedSubscription); const customerId = typeof deletedSubscription.customer === 'string' ? deletedSubscription.customer : deletedSubscription.customer.id; await updateProfile(customerId, { subscription_status: 'canceled', cancel_at_period_end: false, updated_at: new Date() }); break; } case 'invoice.paid': { const invoice = event.data.object as Stripe.Invoice; console.log('Invoice paid:', invoice); // Only update if this invoice is for a subscription if (invoice.subscription && invoice.customer) { const subscriptionId = typeof invoice.subscription === 'string' ? invoice.subscription : invoice.subscription.id; const customerId = typeof invoice.customer === 'string' ? invoice.customer : invoice.customer.id; const subscription = await stripe.subscriptions.retrieve(subscriptionId); await updateProfile(customerId, { subscription_status: 'active', current_period_end: new Date(subscription.current_period_end * 1000), updated_at: new Date() }); } break; } case 'invoice.payment_failed': { const failedInvoice = event.data.object as Stripe.Invoice; console.log('Invoice payment failed:', failedInvoice); if (failedInvoice.customer) { const customerId = typeof failedInvoice.customer === 'string' ? failedInvoice.customer : failedInvoice.customer.id; await updateProfile(customerId, { subscription_status: 'past_due', updated_at: new Date() }); } break; } default: console.log(`Unhandled event type ${event.type}`); } } catch (error) { console.error(`Error processing webhook event ${event.type}:`, error); } return NextResponse.json({ received: true }); } ================================================ FILE: apps/web/src/app/api/unkey/route.ts ================================================ import { NextResponse } from 'next/server'; import { createClient } from '@/utils/supabase/server'; // Unkey API endpoints const UNKEY_API_URL = 'https://api.unkey.dev/v1'; const UNKEY_API_ID = process.env.UNKEY_API_ID; const UNKEY_ROOT_KEY = process.env.UNKEY_ROOT_KEY; // Force dynamic rendering to ensure fresh data export const dynamic = 'force-dynamic'; export const revalidate = 0; export async function GET(request: Request) { console.log('API route called'); // Get userId from query parameters const url = new URL(request.url); const userId = url.searchParams.get('userId'); console.log('Received userId from query:', userId); if (!userId) { return NextResponse.json({ error: 'UserId is required' }, { status: 400 }); } // Create Supabase client const supabase = createClient(); try { // Check if user exists in profiles table const { data: profileData, error: profileError } = await supabase .from('profiles') .select('id, unkey_key_id') .eq('id', userId) .single(); console.log('Profile data:', profileData); if (profileError && profileError.code !== 'PGRST116') { // PGRST116 is the error code for 'no rows returned' console.error('Error fetching profile:', profileError); return NextResponse.json({ error: 'Failed to fetch user profile' }, { status: 500 }); } // If user doesn't exist in profiles, create an entry if (!profileData) { console.log('Creating new profile for user:', userId); const { error: insertError } = await supabase .from('profiles') .insert({ id: userId }); if (insertError) { console.error('Error creating profile:', insertError); return NextResponse.json({ error: 'Failed to create user profile' }, { status: 500 }); } // Return that API key doesn't exist return NextResponse.json({ exists: false }); } // Only return exists: true if the user has a non-null unkey_key_id if (!profileData.unkey_key_id) { console.log('User exists but has no API key'); return NextResponse.json({ exists: false }); } // At this point, we have a user with a non-null unkey_key_id console.log('User has an API key ID:', profileData.unkey_key_id); // Check if the key exists in Unkey const getKeyResponse = await fetch( `${UNKEY_API_URL}/keys.getKey?keyId=${profileData.unkey_key_id}`, { method: 'GET', headers: { Authorization: `Bearer ${UNKEY_ROOT_KEY}`, }, } ); // If key doesn't exist in Unkey, return that it doesn't exist if (!getKeyResponse.ok) { console.log('Key ID exists in profile but not in Unkey'); return NextResponse.json({ exists: false }); } const keyData = await getKeyResponse.json(); console.log('Key found in Unkey'); return NextResponse.json({ exists: true, key: keyData.key }); } catch (error) { console.error('Error checking API key:', error); return NextResponse.json({ error: 'Failed to check API key' }, { status: 500 }); } } export async function POST(request: Request) { // Parse the request body to get the userId let userId; try { const body = await request.json(); userId = body.userId; console.log('Received userId for key creation:', userId); if (!userId) { return NextResponse.json({ error: 'UserId is required' }, { status: 400 }); } } catch (error) { console.error('Error parsing request body:', error); return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); } // Create Supabase client const supabase = createClient(); try { // Check if user exists in profiles table const { error: profileError } = await supabase .from('profiles') .select('id, unkey_key_id') .eq('id', userId) .single(); if (profileError && profileError.code !== 'PGRST116') { console.error('Error fetching profile:', profileError); return NextResponse.json({ error: 'Failed to fetch user profile' }, { status: 500 }); } // Create an API key for the user const createKeyResponse = await fetch(`${UNKEY_API_URL}/keys.createKey`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${UNKEY_ROOT_KEY}`, }, body: JSON.stringify({ apiId: UNKEY_API_ID, prefix: 'cd', byteLength: 16, externalId: userId, meta: { createdAt: new Date().toISOString(), userId }, }), }); if (!createKeyResponse.ok) { const errorText = await createKeyResponse.text(); console.log('Error creating key:', errorText); throw new Error(`Failed to create key: ${createKeyResponse.statusText}`); } const keyData = await createKeyResponse.json(); console.log('Key created:', keyData.keyId); // Update the user's profile with the key ID const { error: updateError } = await supabase .from('profiles') .update({ unkey_key_id: keyData.keyId }) .eq('id', userId); if (updateError) { console.error('Error updating profile with key ID:', updateError); // We'll still return the key even if updating the profile fails } return NextResponse.json({ success: true, key: keyData.key, keyId: keyData.keyId, }); } catch (error) { console.error('Error creating API key:', error); return NextResponse.json({ error: 'Failed to create API key' }, { status: 500 }); } } ================================================ FILE: apps/web/src/app/auth/callback/route.ts ================================================ import { createClient } from '@/utils/supabase/server' import { NextRequest, NextResponse } from 'next/server' import PostHogClient from '@/utils/posthog/posthog' export async function GET(request: NextRequest) { const requestUrl = new URL(request.url) const code = requestUrl.searchParams.get('code') const error = requestUrl.searchParams.get('error') const errorDescription = requestUrl.searchParams.get('error_description') // Create a response that will be used for redirecting const redirectUrl = new URL('/dashboard', request.url) if (error) { console.error(`Auth error: ${error}, Description: ${errorDescription}`) // If there's an error, redirect to login redirectUrl.pathname = '/login' // Add error information as query parameters redirectUrl.searchParams.set('error', error) if (errorDescription) { redirectUrl.searchParams.set('error_description', errorDescription) } } else if (code) { try { const supabase = createClient() // Exchange the code for a session const { data } = await supabase.auth.exchangeCodeForSession(code) // Identify the user in PostHog if (data?.user) { const posthog = PostHogClient() // Use the correct identify method signature for posthog-node posthog.identify({ distinctId: data.user.id, properties: { email: data.user.email, name: data.user.user_metadata?.full_name || data.user.user_metadata?.name } }) } } catch (err) { console.error('Error exchanging code for session:', err) // If there's an error, redirect to login redirectUrl.pathname = '/login' redirectUrl.searchParams.set('error', 'session_exchange_error') } } // URL to redirect to after sign in process completes return NextResponse.redirect(redirectUrl) } ================================================ FILE: apps/web/src/app/blog/[slug]/page.tsx ================================================ import { Button } from '@/components/button' import { Container } from '@/components/container' import { Footer } from '@/components/footer' import { GradientBackground } from '@/components/gradient' import { Link } from '@/components/link' import { Navbar } from '@/components/navbar' import { Heading, Subheading } from '@/components/text' import { image } from '@/sanity/image' import { getPost } from '@/sanity/queries' import { ChevronLeftIcon } from '@heroicons/react/16/solid' import dayjs from 'dayjs' import type { Metadata } from 'next' import { PortableText } from 'next-sanity' import { notFound } from 'next/navigation' export async function generateMetadata({ params, }: { params: { slug: string } }): Promise { const post = await getPost(params.slug) return post ? { title: post.title, description: post.excerpt } : {} } export default async function BlogPost({ params, }: { params: { slug: string } }) { const post = (await getPost(params.slug)) || notFound() return (
{dayjs(post.publishedAt).format('dddd, MMMM D, YYYY')} {post.title}
{post.author && (
{post.author.image && ( )}
{post.author.name}
)} {Array.isArray(post.categories) && (
{post.categories.map((category) => ( {category.title} ))}
)}
{post.mainImage && ( {post.mainImage.alt )} {post.body && ( (

{children}

), h2: ({ children }) => (

{children}

), h3: ({ children }) => (

{children}

), blockquote: ({ children }) => (
{children}
), }, types: { image: ({ value }) => ( {value.alt ), separator: ({ value }) => { switch (value.style) { case 'line': return (
) case 'space': return
default: return null } }, }, list: { bullet: ({ children }) => (
    {children}
), number: ({ children }) => (
    {children}
), }, listItem: { bullet: ({ children }) => { return (
  • {children}
  • ) }, number: ({ children }) => { return (
  • {children}
  • ) }, }, marks: { strong: ({ children }) => ( {children} ), code: ({ children }) => ( <> ` {children} ` ), link: ({ value, children }) => { return ( {children} ) }, }, }} /> )}
    ) } ================================================ FILE: apps/web/src/app/blog/feed.xml/route.ts ================================================ import { image } from '@/sanity/image' import { getPostsForFeed } from '@/sanity/queries' import { Feed } from 'feed' import assert from 'node:assert' export async function GET(req: Request) { const siteUrl = new URL(req.url).origin const feed = new Feed({ title: 'The Cyberdesk Blog', description: 'Stay informed with product updates, company news, and insights on how to build world class computer agents.', author: { name: 'Alan Duong', email: 'devs@cyberdesk.io', }, id: siteUrl, link: siteUrl, image: `${siteUrl}/favicon.ico`, favicon: `${siteUrl}/favicon.ico`, copyright: `All rights reserved ${new Date().getFullYear()}`, feedLinks: { rss2: `${siteUrl}/feed.xml`, }, }) const posts = await getPostsForFeed() posts.forEach((post) => { try { assert(typeof post.title === 'string') assert(typeof post.slug === 'string') assert(typeof post.excerpt === 'string') assert(typeof post.publishedAt === 'string') } catch (error) { console.log('Post is missing required fields for RSS feed:', post) return } feed.addItem({ title: post.title, id: post.slug, link: `${siteUrl}/blog/${post.slug}`, content: post.excerpt, image: post.mainImage ? image(post.mainImage) .size(1200, 800) .format('jpg') .url() .replaceAll('&', '&') : undefined, author: post.author?.name ? [{ name: post.author.name }] : [], contributor: post.author?.name ? [{ name: post.author.name }] : [], date: new Date(post.publishedAt), }) }) return new Response(feed.rss2(), { status: 200, headers: { 'content-type': 'application/xml', 'cache-control': 's-maxage=31556952', }, }) } ================================================ FILE: apps/web/src/app/blog/page.tsx ================================================ import { Button } from '@/components/button' import { Container } from '@/components/container' import { Footer } from '@/components/footer' import { GradientBackground } from '@/components/gradient' import { Link } from '@/components/link' import { Navbar } from '@/components/navbar' import { Heading, Lead, Subheading } from '@/components/text' import { image } from '@/sanity/image' import { getCategories, getFeaturedPosts, getPosts, getPostsCount, } from '@/sanity/queries' import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react' import { CheckIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpDownIcon, RssIcon, } from '@heroicons/react/16/solid' import { clsx } from 'clsx' import dayjs from 'dayjs' import type { Metadata } from 'next' import { notFound } from 'next/navigation' export const metadata: Metadata = { title: 'Blog', description: 'Stay informed with product updates, company news, and insights on how to sell smarter at your company.', } const postsPerPage = 5 async function FeaturedPosts() { const featuredPosts = await getFeaturedPosts(3) if (featuredPosts.length === 0) { return } return (

    Featured

    {featuredPosts.map((post) => (
    {post.mainImage && ( {post.mainImage.alt )}
    {dayjs(post.publishedAt).format('dddd, MMMM D, YYYY')}
    {post.title}
    {post.excerpt}
    {post.author && (
    {post.author.image && ( )}
    {post.author.name}
    )}
    ))}
    ) } async function Categories({ selected }: { selected?: string }) { const categories = await getCategories() if (categories.length === 0) { return } return (
    {categories.find(({ slug }) => slug === selected)?.title || 'All categories'}

    All categories

    {categories.map((category) => (

    {category.title}

    ))}
    ) } async function Posts({ page, category }: { page: number; category?: string }) { const posts = await getPosts( (page - 1) * postsPerPage, page * postsPerPage, category, ) if (posts.length === 0 && (page > 1 || category)) { notFound() } if (posts.length === 0) { return

    No posts found.

    } return (
    {posts.map((post) => (
    {dayjs(post.publishedAt).format('dddd, MMMM D, YYYY')}
    {post.author && (
    {post.author.image && ( )}
    {post.author.name}
    )}

    {post.title}

    {post.excerpt}

    Read more
    ))}
    ) } async function Pagination({ page, category, }: { page: number category?: string }) { function url(page: number) { const params = new URLSearchParams() if (category) params.set('category', category) if (page > 1) params.set('page', page.toString()) return params.size !== 0 ? `/blog?${params.toString()}` : '/blog' } const totalPosts = await getPostsCount(category) const hasPreviousPage = page - 1 const previousPageUrl = hasPreviousPage ? url(page - 1) : undefined const hasNextPage = page * postsPerPage < totalPosts const nextPageUrl = hasNextPage ? url(page + 1) : undefined const pageCount = Math.ceil(totalPosts / postsPerPage) if (pageCount < 2) { return } return (
    {Array.from({ length: pageCount }, (_, i) => ( {i + 1} ))}
    ) } export default async function Blog({ searchParams, }: { searchParams: { [key: string]: string | string[] | undefined } }) { const page = 'page' in searchParams ? typeof searchParams.page === 'string' && parseInt(searchParams.page) > 1 ? parseInt(searchParams.page) : notFound() : 1 const category = typeof searchParams.category === 'string' ? searchParams.category : undefined return (
    Blog What's happening at Cyberdesk. Stay informed with product updates, company news, and insights on how to build world class computer agents. {page === 1 && !category && }
    ) } ================================================ FILE: apps/web/src/app/company/page.tsx ================================================ import { AnimatedNumber } from '@/components/animated-number' import { Button } from '@/components/button' import { Container } from '@/components/container' import { Footer } from '@/components/footer' import { GradientBackground } from '@/components/gradient' import { Navbar } from '@/components/navbar' import { Heading, Lead, Subheading } from '@/components/text' import type { Metadata } from 'next' export const metadata: Metadata = { title: 'Company', description: 'We’re on a mission to transform revenue organizations by harnessing vast amounts of illegally acquired customer data.', } function Header() { return ( Helping companies generate revenue. We’re on a mission to transform revenue organizations by harnessing vast amounts of illegally acquired customer data.

    Our mission

    At Radiant, we are dedicated to transforming the way revenue organizations source and close deals. Our mission is to provide our customers with an unfair advantage over both their competitors and potential customers through insight and analysis. We’ll stop at nothing to get you the data you need to close a deal.

    We’re customer-obsessed — putting the time in to build a detailed financial picture of every one of our customers so that we know more about your business than you do. We are in this together, mostly because we are all implicated in large-scale financial crime. In our history as a company, we’ve never lost a customer, because if any one of us talks, we all go down.

    The Numbers
    Raised
    $M
    Companies
    K
    Deals Closed
    M
    Leads Generated
    M
    ) } function Person({ name, description, img, }: { name: string description: string img: string }) { return (
  • {name}

    {description}

  • ) } function Team() { return ( Meet the team Founded by an all-star team. Radiant is founded by two of the best sellers in the business and backed by investors who look the other way.

    Years ago, while working as sales associates at rival companies, Thomas, Ben, and Natalie were discussing a big client they had all been competing for. Joking about seeing the terms of each other’s offers, they had an idea: what if they shared data to win deals and split the commission behind their companies’ backs? It turned out to be an incredible success, and that idea became the kernel for Radiant.

    Today, Radiant transforms revenue organizations by harnessing illegally acquired customer and competitor data, using it to provide extraordinary leverage. More than 30,000 companies rely on Radiant to undercut their competitors and extort their customers, all through a single integrated platform.

    The team
    ) } function Investors() { return ( Investors Funded by industry-leaders. We are fortunate to be backed by the best investors in the industry — both literal and metaphorical partners in crime. Venture Capital
    • Remington Schwartz

      Remington Schwartz has been a driving force in the tech industry, backing bold entrepreneurs who explore grey areas in financial and privacy law. Their deep industry expertise and extensive political lobbying provide their portfolio companies with favorable regulation and direct access to lawmakers.

    • Deccel

      Deccel has been at the forefront of innovation, investing in pioneering companies across various sectors, including technology, consumer goods, and healthcare. Their philosophy of ‘plausible deniability’ and dedication to looking the other way have helped produce some of the world’s most controversial companies.

    Individual investors
    ) } function Testimonial() { return (
    ) } function Careers() { return ( Careers Join our fully remote team. We work together from all over the world, mainly from locations without extradition agreements.
    Open positions
    Title Location Read more
    Engineering
    iOS Developer Remote
    Backend Engineer Remote
    Product Engineer Remote
    Design
    Principal Designer Remote
    Designer Remote
    Senior Designer Remote
    ) } export default function Company() { return (
    ) } ================================================ FILE: apps/web/src/app/dashboard/dashboard-content.tsx ================================================ 'use client' import { SubscriptionSection } from '@/components/dashboard/subscription-section' import type { Profile } from '@/types/database' import { ApiKeyManager } from '@/components/dashboard/api-key-manager' import { VMInstancesManager } from '@/components/dashboard/vm-instances-manager' import { supabase } from '@/utils/supabase/client' import { ArrowRightOnRectangleIcon } from '@heroicons/react/24/outline' import { Button } from '@/components/button' import posthog from 'posthog-js' // import { Subheading } from '@/components/text' interface DashboardContentProps { userEmail?: string; userId?: string; profile?: Profile | null; } export function DashboardContent({ userEmail, userId, profile }: DashboardContentProps) { const isSubscriptionActive = profile?.subscription_status === "active"; // Removed fetching active subscriptions as pricing card is no longer displayed for non-subscribers // Removed fetching active subscriptions as pricing card is no longer displayed for non-subscribers const handleLogout = async () => { // Reset PostHog user to anonymous before logging out posthog.reset(); await supabase.auth.signOut(); window.location.href = '/'; }; // const isSoldOut = activeSubscriptionsCount !== null && activeSubscriptionsCount >= SUBSCRIPTION_LIMIT; // If subscription is not active, show booking message instead of pricing/FAQ if (!isSubscriptionActive) { return (

    Looks like we haven't set up your account yet.

    Book a time here to get you up and running soon!

    ); } return (
    {isSubscriptionActive ? ( <>

    Dashboard

    Welcome to your dashboard. This is where you can manage your virtual desktops, account settings, and more.

    ) : ( <>

    Get Started with Cyberdesk

    Unlock the full power of Cyberdesk by subscribing to our Pro plan.

    )}
    {/* Pricing and FAQ sections have been removed for non-subscribers */} {/* For active subscribers, show API Key, VM Instances, and Subscription sections */} {isSubscriptionActive && ( <> {/* API Key Section */}
    {/* VM Instances Section */}
    {/* Subscription Management Section */}
    )}
    ) } ================================================ FILE: apps/web/src/app/dashboard/page.tsx ================================================ import { redirect } from 'next/navigation'; import { stripe } from '@/utils/stripe/stripe-server'; import type { Profile } from '@/types/database'; import { DashboardLayout } from '@/components/dashboard/dashboard-layout'; import { DashboardContent } from './dashboard-content'; import { createClient } from '@/utils/supabase/server'; export default async function Dashboard() { const supabase = createClient(); // Check if user is authenticated - using getUser() for better security const { data: { user } } = await supabase.auth.getUser(); if (!user) { redirect('/login'); } const userId = user.id; // Query the profiles table for the user const { data: dbProfile, error } = await supabase .from('profiles') .select('*') .eq('id', userId) .single(); let profile: Profile | null = dbProfile; if (error && error.code !== 'PGRST116') { // PGRST116 is the error code for "no rows returned" - we handle this case separately console.error('Error fetching profile:', error); } // If profile doesn't exist, create it along with a Stripe customer if (!profile) { try { // Create a Stripe customer const customer = await stripe.customers.create({ email: user.email, metadata: { userId: userId } }); // Create a profile entry with the Stripe customer ID const newProfile: Profile = { id: userId, stripe_customer_id: customer.id, subscription_status: 'inactive', created_at: new Date(), updated_at: new Date() }; const { error: insertError } = await supabase .from('profiles') .insert(newProfile); profile = newProfile; if (insertError) { console.error('Error creating profile:', insertError); } } catch (err) { console.error('Error creating Stripe customer:', err); } } // Get session for client components // TODO: Utilize this for client components // const { data: { session } } = await supabase.auth.getSession(); return ( ); } ================================================ FILE: apps/web/src/app/demo/page.tsx ================================================ 'use client' import { DemoSection } from '@/components/demo-section' import { Thread } from '@/components/thread' import { AssistantRuntimeProvider } from '@assistant-ui/react' import { useChatRuntime } from '@assistant-ui/react-ai-sdk' import { ChatBubbleLeftIcon, HeartIcon, } from '@heroicons/react/24/outline' import { useState } from 'react' export default function PlaygroundDemo() { // State to store the desktop ID const [desktopId, setDesktopId] = useState(null) // State to track if the demo has been finished const [finishedDemo, setFinishedDemo] = useState(false) // Update the chat runtime to support OpenAI Responses API features const runtime = useChatRuntime({ api: '/api/chat', // Use headers instead of body to pass the desktopId headers: () => { // Use the actual desktopId from state, or a fallback value if it's null const currentDesktopId = desktopId || 'NO_DESKTOP_ID_YET' console.log( '[useChatRuntime] Sending request with desktopId in headers:', currentDesktopId, ) return Promise.resolve({ 'Content-Type': 'application/json', 'X-Desktop-Id': currentDesktopId, }) }, onFinish: (message) => { const sources = message.metadata?.custom?.sources if (sources) { console.log('Web search sources:', sources) } }, }) // Function to handle when a desktop is deployed const handleDesktopDeployed = (id: string) => { // Set the desktop ID directly from the API response setDesktopId(id) } const handleDesktopStopped = () => { // Set the desktop ID directly from the API response setDesktopId(null) // Set finishedDemo to true when desktop is stopped setFinishedDemo(true) } return (
    {/* Thread Area */}
    {desktopId ? (
    {/* Add a button to stop the desktop on mobile and tablet only */}
    ) : (
    {finishedDemo ? ( ) : ( )}

    {finishedDemo ? 'Thank you for trying our demo! Sign up to get started.' : 'Launch the demo to start chatting with the assistant.'}

    )} {/* The mobile stop button is now handled by a reference to the DemoSection component */}
    {/* Demo Section */}
    ) } ================================================ FILE: apps/web/src/app/layout.tsx ================================================ import '@/styles/tailwind.css' import type { Metadata } from 'next' import { PostHogProvider } from '../components/PostHogProvider' export const metadata: Metadata = { title: { template: '%s | Cyberdesk', default: 'Cyberdesk | Virtual desktops for AI agents', }, icons: { icon: '/favicon.svg', }, } export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return ( {children} ) } ================================================ FILE: apps/web/src/app/login/login-form.d.ts ================================================ // Type declaration for login-form component export function LoginForm(): JSX.Element; ================================================ FILE: apps/web/src/app/login/login-form.tsx ================================================ 'use client'; import { Button } from '@/components/button' import { Link } from '@/components/link' import { Mark } from '@/components/logo' import { supabase } from '@/utils/supabase/client' import { useRouter } from 'next/navigation' export function LoginForm() { const router = useRouter() const signInWithGoogle = async () => { const { error } = await supabase.auth.signInWithOAuth({ provider: 'google', options: { redirectTo: `${window.location.origin}/auth/callback`, }, }) if (error) { console.error('Error signing in with Google:', error.message) } } return ( <>
    {router.back()}}>

    Welcome to Cyberdesk!

    Sign in to your account to continue.

    {/*
    */}
    ) } ================================================ FILE: apps/web/src/app/login/page.tsx ================================================ import { GradientBackground } from '@/components/gradient' import type { Metadata } from 'next' import { LoginForm } from './login-form' export const metadata: Metadata = { title: 'Login', description: 'Sign in to your account to continue.', } export default function Login() { return (
    ) } ================================================ FILE: apps/web/src/app/page.tsx ================================================ import { Footer } from '@/components/footer' import type { Metadata } from 'next' import { Hero } from '../components/hero' import { YCBanner } from '../components/yc-banner' import Playground from './playground/page' export const metadata: Metadata = { description: 'Cyberdesk deploys virtual desktops for your computer agents with only in a few lines of code.', } export default function Home() { return (
    {/* */}
    ) } ================================================ FILE: apps/web/src/app/playground/page.tsx ================================================ "use client"; import { PreviewMessage } from "@/components/playground/message"; import { getDesktopURL, startDesktop } from "@/utils/playground/server-actions"; import { useScrollToBottom } from "@/utils/playground/use-scroll-to-bottom"; import { useChat } from "@ai-sdk/react"; import { useEffect, useState } from "react"; import { Input } from "@/components/playground/input"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; import { ProjectInfo } from "@/components/playground/project-info"; import { PromptSuggestions } from "@/components/playground/prompt-suggestions"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; import { ABORTED } from "@/utils/playground/misc-demo-utils"; import { FaRocket } from "react-icons/fa"; import { ChatError } from "@/components/playground/chat-error"; // Shared polling helper for desktop URL async function pollForDesktopURL(sandboxId: string | null | undefined) { let delay = 1000; // Start with 1 second const maxDelay = 5000; // Cap at 5 seconds const maxTime = 180000; // 3 minutes in ms const startTime = Date.now(); while (true) { if (Date.now() - startTime > maxTime) { throw new Error("Timed out after 3 minutes while getting desktop stream URL."); } const { streamUrl, id } = await getDesktopURL(sandboxId || undefined); if (streamUrl) { return { streamUrl, id }; } delay = Math.min(maxDelay, Math.floor(delay * 1.5)); await new Promise((resolve) => setTimeout(resolve, delay)); } } export default function Playground() { // Create separate refs for mobile and desktop to ensure both scroll properly const [desktopContainerRef, desktopEndRef] = useScrollToBottom(); const [mobileContainerRef, mobileEndRef] = useScrollToBottom(); const [isInitializing, setIsInitializing] = useState(false); const [streamUrl, setStreamUrl] = useState(null); const [sandboxId, setSandboxId] = useState(null); const [hasStarted, setHasStarted] = useState(false); const { messages, input, handleInputChange, handleSubmit, error, reload, status, stop: stopGeneration, append, setMessages, } = useChat({ api: "/api/playground/chat", id: sandboxId ?? undefined, body: { sandboxId, }, onError: (error) => { console.error(error); toast.error("There was an error", { description: "Please try again later.", richColors: true, position: "top-center", }); }, }); const stop = () => { stopGeneration(); const lastMessage = messages.at(-1); const lastMessageLastPart = lastMessage?.parts.at(-1); if ( lastMessage?.role === "assistant" && lastMessageLastPart?.type === "tool-invocation" ) { setMessages((prev) => [ ...prev.slice(0, -1), { ...lastMessage, parts: [ ...lastMessage.parts.slice(0, -1), { ...lastMessageLastPart, toolInvocation: { ...lastMessageLastPart.toolInvocation, state: "result", result: ABORTED, }, }, ], }, ]); } }; const isLoading = status !== "ready"; const refreshDesktop = async () => { try { setIsInitializing(true); const data = await startDesktop(); const id = data.id; const { streamUrl } = await pollForDesktopURL(id); setStreamUrl(streamUrl); setSandboxId(id); } catch (err) { console.error("Failed to refresh desktop:", err); } finally { setIsInitializing(false); } }; // Handler for Start Desktop button const handleStartDesktop = async () => { setHasStarted(true); setIsInitializing(true); try { const data = await startDesktop(); const id = data.id; const { streamUrl } = await pollForDesktopURL(id); setStreamUrl(streamUrl); setSandboxId(id); } catch (err) { console.error("Failed to initialize desktop:", err); toast.error("Failed to initialize desktop"); setHasStarted(false); // allow retry } finally { setIsInitializing(false); } }; // Kill desktop on page close useEffect(() => { if (!sandboxId) return; // Function to kill the desktop - just one method to reduce duplicates const killDesktop = () => { if (!sandboxId) return; // Use sendBeacon which is best supported across browsers navigator.sendBeacon( `/api/playground/kill-desktop?sandboxId=${encodeURIComponent(sandboxId)}`, ); }; // Detect iOS / Safari const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1); const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); // Choose exactly ONE event handler based on the browser if (isIOS || isSafari) { // For Safari on iOS, use pagehide which is most reliable window.addEventListener("pagehide", killDesktop); return () => { window.removeEventListener("pagehide", killDesktop); // Also kill desktop when component unmounts killDesktop(); }; } else { // For all other browsers, use beforeunload window.addEventListener("beforeunload", killDesktop); return () => { window.removeEventListener("beforeunload", killDesktop); // Also kill desktop when component unmounts killDesktop(); }; } }, [sandboxId]); return (
    {/* Starter Overlay */} {!hasStarted && (
    {/* Animated background shapes */}

    Launch a demo computer agent

    {/*

    Your secure, cloud-powered development environment

    */}

    Click below to start a Cyberdesk desktop. Once it launches, you can chat with a computer agent that can use the desktop.

    Powered by Cyberdesk
    )} {/* Mobile/tablet banner */} {/* REMOVE THIS BANNER
    Headless mode
    */} {/* Resizable Panels */}
    {/* Desktop Stream Panel */} {streamUrl ? ( <> {isLoading && (
    Preparing stream...
    )}
    ) } const InitialDemoState = ({ onLaunchDemo }: { onLaunchDemo: () => void }) => (

    Deploy a virtual desktop

    Production ready, secure, and scalable

    ) const PostDemoState = ({ isLoggedIn }: { isLoggedIn: boolean }) => (

    Ready to deploy your own?

    Create your first virtual desktop in a few clicks.

    ) const DemoContent = React.forwardRef< HTMLDivElement, { isLoading: boolean isDemoLaunched: boolean hasEverLaunchedDemo: boolean streamUrl: string isLoggedIn: boolean onLaunchDemo: () => void error?: string | null } >(({ isLoading, isDemoLaunched, hasEverLaunchedDemo, streamUrl, isLoggedIn, onLaunchDemo, error }, ref) => { let content if (isLoading) { content = } else if (error) { content = (

    Oops! Something went wrong.

    {error}

    ) } else if (isDemoLaunched) { content = } else { content = !hasEverLaunchedDemo ? ( ) : ( ) } return (
    {content}
    ) }) DemoContent.displayName = 'DemoContent' // API functions const deployVirtualDesktop = async (): Promise => { try { const apiResponse = await fetch('/api/playground/desktop', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ timeoutMs: DESKTOP_TIMEOUT_MS }) }); const responseData = await apiResponse.json(); if (!apiResponse.ok) { console.error('Backend API error:', responseData.error || `Status: ${apiResponse.status}`); // Handle error appropriately in the UI if needed return { id: '', status: 'error' }; } else { // TODO: Maybe use the returned streamUrl and id? // Example: setStreamUrl(responseData.streamUrl); onDesktopDeployed(responseData.streamUrl, responseData.id); return { id: responseData.id, status: responseData.status }; } } catch (error) { console.error('Error calling backend API:', error); // Handle fetch error appropriately return { id: '', status: 'error' }; } } const stopVirtualDesktop = async (id: string): Promise => { try { const response = await fetch('/api/playground/desktop', { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ id }), }) if (!response.ok) { throw new Error('Failed to stop desktop') } return true } catch (error) { console.error('Error stopping desktop:', error) return false } } const getDetailsVirtualDesktop = async (id: string): Promise<{ status: string; stream_url?: string; error?: string }> => { try { const response = await fetch(`/api/playground/desktop?id=${id}`) const data = await response.json() if (!response.ok) { return { status: 'error', error: data.error || `Status: ${response.status}` } } return data } catch (error) { return { status: 'error', error: (error as Error).message } } } ================================================ FILE: apps/web/src/components/feature-section.tsx ================================================ import { Container } from '@/components/container' import { Heading } from '@/components/text' import { Screenshot } from '@/components/screenshot' export function FeatureSection() { return (
    Spin up thousands of concurrent desktops in seconds
    ) } ================================================ FILE: apps/web/src/components/footer.tsx ================================================ 'use client' import { PlusGrid, PlusGridItem, PlusGridRow } from '@/components/plus-grid' import { Button } from './button' import { Container } from './container' import { Gradient } from './gradient' import { Link } from './link' import { LogoText } from './LogoText' // Import the new component import { Subheading } from './text' import { useState, useEffect } from 'react' import { supabase } from '@/utils/supabase/client' import { AppLogo } from './shared/app-logo' function CallToAction() { return (
    Book a demo

    Dealing with high-volume
    repetitive computer tasks?

    We'd love to chat and see how we can help

    ) } function SitemapHeading({ children }: { children: React.ReactNode }) { return

    {children}

    } function SitemapLinks({ children }: { children: React.ReactNode }) { return
      {children}
    } function SitemapLink(props: React.ComponentPropsWithoutRef) { return (
  • ) } function Sitemap() { return ( <> {/*
    Product Pricing Docs API
    */}
    Legal Terms of Service Privacy Policy
    {/*
    Company Careers Blog Company
    Support Help center Community
    Company Terms of service Privacy policy
    */} ) } function SocialIconX(props: React.ComponentPropsWithoutRef<'svg'>) { return ( ) } function SocialIconFacebook(props: React.ComponentPropsWithoutRef<'svg'>) { return ( ) } function SocialIconLinkedIn(props: React.ComponentPropsWithoutRef<'svg'>) { return ( ) } // Social links currently not in use. function Copyright() { return (
    © {new Date().getFullYear()} Cyberdesk.io Inc.
    ) } export function Footer() { const [isLoggedIn, setIsLoggedIn] = useState(false) useEffect(() => { const checkAuthStatus = async () => { const { data } = await supabase.auth.getSession() setIsLoggedIn(!!data.session) } checkAuthStatus() }, []) return (
    {!isLoggedIn && }
    {/*
    */}
    ) } ================================================ FILE: apps/web/src/components/gradient.tsx ================================================ import { clsx } from 'clsx' export function Gradient({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { return (
    ) } export function GradientBackground({ children }: { children?: React.ReactNode }) { return (
    {children}
    ) } ================================================ FILE: apps/web/src/components/hero.tsx ================================================ 'use client'; import { Button } from '@/components/button' import { Container } from '@/components/container' import { Gradient } from '@/components/gradient' import { Link } from '@/components/link' import { Navbar } from '@/components/navbar' import { ChevronRightIcon } from '@heroicons/react/16/solid' import { useState, useEffect } from 'react' import { supabase } from '@/utils/supabase/client' import CONFIG from '../../config'; export function Hero() { const [isLoggedIn, setIsLoggedIn] = useState(false) const [isLoading, setIsLoading] = useState(true) useEffect(() => { const checkAuthStatus = async () => { try { const { data } = await supabase.auth.getSession() setIsLoggedIn(!!data.session) } catch (error) { console.error('Error checking auth status:', error) } finally { setIsLoading(false) } } checkAuthStatus() }, []) return (
    // Cyberdesk raises $100M Series A from Tailwind Ventures // // // } />

    AI agents for highly reliable computer task automations

    We integrate our automations with existing systems in healthcare, accounting, supply chain and beyond

    {!isLoading && (isLoggedIn ? ( ) : ( <> {/* */} ))}
    ) } ================================================ FILE: apps/web/src/components/keyboard.tsx ================================================ 'use client' import { clsx } from 'clsx' import { motion } from 'framer-motion' import { createContext, useContext } from 'react' const KeyboardContext = createContext<{ highlighted: string[] }>({ highlighted: [], }) function Row(props: { children: React.ReactNode }) { return
    } function Key({ name, width = 36, className, children, }: { name: string width?: number className?: string children?: React.ReactNode }) { const { highlighted } = useContext(KeyboardContext) return ( {children} ) } function KeyGroup(props: { children: React.ReactNode }) { return (
    ) } function EscapeKey() { return ( ) } function F1Key() { return ( ) } function F2Key() { return ( ) } function F3Key() { return ( ) } function F4Key() { return ( ) } function F5Key() { return ( ) } function F6Key() { return ( ) } function F7Key() { return ( ) } function F8Key() { return ( ) } function F9Key() { return ( ) } function F10Key() { return ( ) } function F11Key() { return ( ) } function F12Key() { return ( ) } function LockKey() { return ( ) } function BacktickKey() { return ( ) } function OneKey() { return ( ) } function TwoKey() { return ( ) } function ThreeKey() { return ( ) } function FourKey() { return ( ) } function FiveKey() { return ( ) } function SixKey() { return ( ) } function SevenKey() { return ( ) } function EightKey() { return ( ) } function NineKey() { return ( ) } function ZeroKey() { return ( ) } function DashKey() { return ( ) } function EqualsKey() { return ( ) } function DeleteKey() { return ( ) } function TabKey() { return ( ) } function QKey() { return ( ) } function WKey() { return ( ) } function EKey() { return ( ) } function RKey() { return ( ) } function TKey() { return ( ) } function YKey() { return ( ) } function UKey() { return ( ) } function IKey() { return ( ) } function OKey() { return ( ) } function PKey() { return ( ) } function LeftSquareBracketKey() { return ( ) } function RightSquareBracketKey() { return ( ) } function BackSlashKey() { return ( ) } function CapsLockKey() { return ( ) } function AKey() { return ( ) } function SKey() { return ( ) } function DKey() { return ( ) } function FKey() { return ( ) } function GKey() { return ( ) } function HKey() { return ( ) } function JKey() { return ( ) } function KKey() { return ( ) } function LKey() { return ( ) } function SemicolonKey() { return ( ) } function SingleQuoteKey() { return ( ) } function ReturnKey() { return ( ) } function ShiftKey({ position }: { position: 'Left' | 'Right' }) { return ( ) } function ZKey() { return ( ) } function XKey() { return ( ) } function CKey() { return ( ) } function VKey() { return ( ) } function BKey() { return ( ) } function NKey() { return ( ) } function MKey() { return ( ) } function CommaKey() { return ( ) } function PeriodKey() { return ( ) } function ForwardSlashKey() { return ( ) } function FunctionKey() { return ( ) } function ControlKey() { return ( ) } function OptionKey({ position }: { position: 'Left' | 'Right' }) { return ( ) } function CommandKey({ position }: { position: 'Left' | 'Right' }) { return ( ) } function SpaceKey() { return } function LeftKey() { return ( ) } function UpKey() { return ( ) } function DownKey() { return ( ) } function RightKey() { return ( ) } export function Keyboard({ highlighted = [] }: { highlighted?: string[] }) { return ( ) } ================================================ FILE: apps/web/src/components/link.tsx ================================================ import * as Headless from '@headlessui/react' import NextLink, { type LinkProps } from 'next/link' import { forwardRef } from 'react' export const Link = forwardRef(function Link( props: LinkProps & React.ComponentPropsWithoutRef<'a'>, ref: React.ForwardedRef, ) { return ( ) }) ================================================ FILE: apps/web/src/components/linked-avatars.tsx ================================================ 'use client' import { CheckIcon } from '@heroicons/react/16/solid' import { clsx } from 'clsx' import { motion } from 'framer-motion' const transition = { duration: 0.75, repeat: Infinity, repeatDelay: 1.25, } function Rings() { return ( {Array.from(Array(42).keys()).map((n) => ( ))} ) } function Checkmark() { return (
    ) } function Photos() { return (
    ) } export function LinkedAvatars() { return ( ) } ================================================ FILE: apps/web/src/components/logo-cloud.tsx ================================================ import { clsx } from 'clsx' export function LogoCloud({ className, }: React.ComponentPropsWithoutRef<'div'>) { return (
    SavvyCal Laravel Tuple Transistor Statamic
    ) } ================================================ FILE: apps/web/src/components/logo-cluster.tsx ================================================ 'use client' import { clsx } from 'clsx' import { motion } from 'framer-motion' import { Mark } from './logo' function Circle({ size, delay, opacity, }: { size: number delay: number opacity: string }) { return ( ) } function Circles() { return (
    ) } function MainLogo() { return (
    ) } function Logo({ src, left, top, hover, }: { src: string left: number top: number hover: { x: number; y: number; rotate: number; delay: number } }) { return ( ) } export function LogoCluster() { return ( ) } ================================================ FILE: apps/web/src/components/logo-timeline.tsx ================================================ import { clsx } from 'clsx' import { Mark } from './logo' function Row({ children }: { children: React.ReactNode }) { return (
    {children}
    ) } function Logo({ label, src, className, }: { label: string src: string className: string }) { return (
    {label}
    ) } export function LogoTimeline() { return (