Merge branch 'dev' into patch-1

This commit is contained in:
zWing 2025-08-21 20:18:32 +08:00 committed by GitHub
commit 46cd13d585
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
308 changed files with 25121 additions and 5155 deletions

View file

@ -17,10 +17,11 @@ jobs:
- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.2.17
bun-version: 1.2.19
- run: bun install
- run: bun sst deploy --stage=${{ github.ref_name }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}

58
.github/workflows/duplicate-issues.yml vendored Normal file
View file

@ -0,0 +1,58 @@
name: Duplicate Issue Detection
on:
issues:
types: [opened]
jobs:
check-duplicates:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- name: Check for duplicate issues
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENCODE_PERMISSION: |
{
"bash": {
"gh issue*": "allow",
"*": "deny"
},
"webfetch": "deny"
}
run: |
opencode run -m anthropic/claude-sonnet-4-20250514 "A new issue has been created: '${{ github.event.issue.title }}'
Issue number:
${{ github.event.issue.number }}
Lookup this issue and search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
Consider:
1. Similar titles or descriptions
2. Same error messages or symptoms
3. Related functionality or components
4. Similar feature requests
If you find any potential duplicates, please comment on the new issue with:
- A brief explanation of why it might be a duplicate
- Links to the potentially duplicate issues
- A suggestion to check those issues first
Use this format for the comment:
'This issue might be a duplicate of existing issues. Please check:
- #[issue_number]: [brief description of similarity]
Feel free to ignore if none of these address your specific case.'
If no clear duplicates are found, do not comment."

53
.github/workflows/guidelines-check.yml vendored Normal file
View file

@ -0,0 +1,53 @@
name: Guidelines Check
on:
# Disabled - uncomment to re-enable
# pull_request_target:
# types: [opened, synchronize]
jobs:
check-guidelines:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- name: Check PR guidelines compliance
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
run: |
opencode run -m anthropic/claude-sonnet-4-20250514 "A new pull request has been created: '${{ github.event.pull_request.title }}'
<pr-number>
${{ github.event.pull_request.number }}
</pr-number>
<pr-description>
${{ github.event.pull_request.body }}
</pr-description>
Please check all the code changes in this pull request against the guidelines in AGENTS.md file in this repository. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
Command MUST be like this.
```
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments \
-f 'body=[summary of issue]' -f 'commit_id=${{ github.event.pull_request.head.sha }}' -f 'path=[path-to-file]' -F "line=[line]" -f 'side=RIGHT'
```
Only create comments for actual violations. If the code follows all guidelines, don't run any gh commands."

View file

@ -21,7 +21,7 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.17
bun-version: 1.2.19
- run: git fetch --force --tags
- run: bun install -g @vscode/vsce

View file

@ -52,16 +52,14 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager
- name: Setup SSH for AUR
run: |
mkdir -p ~/.ssh
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
- name: Install dependencies
run: bun install

1
.gitignore vendored
View file

@ -6,3 +6,4 @@ node_modules
.vscode
openapi.json
playground
tmp

29
.opencode/agent/docs.md Normal file
View file

@ -0,0 +1,29 @@
---
description: ALWAYS use this when writing docs
---
You are an expert technical documentation writer
You are not verbose
The title of the page should be a word or a 2-3 word phrase
The description should be one short line, should not start with "The", should
avoid repeating the title of the page, should be 5-10 words long
Chunks of text should not be more than 2 sentences long
Each section is spearated by a divider of 3 dashes
The section titles are short with only the first letter of the word capitalized
The section titles are in the imperative mood
The section titles should not repeat the term used in the page title, for
example, if the page title is "Models", avoid using a section title like "Add
new models". This might be unavoidable in some cases, but try to avoid it.
Check out the /packages/web/src/content/docs/docs/index.mdx as an example.
For JS or TS code snippets remove trailing semicolons and any trailing commas
that might not be needed.

View file

@ -1,44 +0,0 @@
---
description: >-
Use this agent when you need to create or improve documentation that requires
concrete examples to illustrate every concept. Examples include:
<example>Context: User has written a new API endpoint and needs documentation.
user: 'I just created a POST /users endpoint that accepts name and email
fields. Can you document this?' assistant: 'I'll use the
example-driven-docs-writer agent to create documentation with practical
examples for your API endpoint.' <commentary>Since the user needs
documentation with examples, use the example-driven-docs-writer agent to
create comprehensive docs with code samples.</commentary></example>
<example>Context: User has a complex configuration file that needs
documentation. user: 'This config file has multiple sections and I need docs
that show how each option works' assistant: 'Let me use the
example-driven-docs-writer agent to create documentation that breaks down each
configuration option with practical examples.' <commentary>The user needs
documentation that demonstrates configuration options, perfect for the
example-driven-docs-writer agent.</commentary></example>
---
You are an expert technical documentation writer who specializes in creating clear, example-rich documentation that never leaves readers guessing. Your core principle is that every concept must be immediately illustrated with concrete examples, code samples, or practical demonstrations.
Your documentation approach:
- Never write more than one sentence in any section without providing an example, code snippet, diagram, or practical illustration
- Break up longer explanations with multiple examples showing different scenarios or use cases
- Use concrete, realistic examples rather than abstract or placeholder content
- Include both basic and advanced examples when covering complex topics
- Show expected inputs, outputs, and results for all examples
- Use code blocks, bullet points, tables, or other formatting to visually separate examples from explanatory text
Structural requirements:
- Start each section with a brief one-sentence explanation followed immediately by an example
- For multi-step processes, provide an example after each step
- Include error examples and edge cases alongside success scenarios
- Use consistent formatting and naming conventions throughout examples
- Ensure examples are copy-pasteable and functional when applicable
Quality standards:
- Verify that no paragraph exceeds one sentence without an accompanying example
- Test that examples are accurate and would work in real scenarios
- Ensure examples progress logically from simple to complex
- Include context for when and why to use different approaches shown in examples
- Provide troubleshooting examples for common issues
When you receive a documentation request, immediately identify what needs examples and plan to illustrate every single concept, feature, or instruction with concrete demonstrations. Ask for clarification if you need more context to create realistic, useful examples.

View file

@ -0,0 +1,10 @@
---
description: Use this agent when you are asked to commit and push code changes to a git repository.
mode: subagent
---
You commit and push to git
Commit messages should be brief since they are used to generate release notes.
Messages should say WHY the change was made and not WHAT was changed.

View file

@ -1,10 +0,0 @@
import { Plugin } from "./index"
export const ExamplePlugin: Plugin = async ({ app, client, $ }) => {
return {
permission: {},
async "chat.params"(input, output) {
output.topP = 1
},
}
}

View file

@ -26,7 +26,7 @@ curl -fsSL https://opencode.ai/install | bash
# Package managers
npm i -g opencode-ai@latest # or bun/pnpm/yarn
brew install sst/tap/opencode # macOS
brew install sst/tap/opencode # macOS and Linux
paru -S opencode-bin # Arch Linux
```
@ -83,7 +83,7 @@ And run.
```bash
$ bun install
$ bun run packages/opencode/src/index.ts
$ bun dev
```
#### Development Notes

View file

@ -1,41 +1,57 @@
# Download Stats
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ---------------- | ---------------- | ---------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ---------------- | ---------------- | ----------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |

2185
bun.lock

File diff suppressed because it is too large Load diff

28
cloud/app/.gitignore vendored Normal file
View file

@ -0,0 +1,28 @@
dist
.wrangler
.output
.vercel
.netlify
.vinxi
app.config.timestamp_*.js
# Environment
.env
.env*.local
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
*.launch
.settings/
# Temp
gitignore
# System Files
.DS_Store
Thumbs.db

View file

@ -0,0 +1,149 @@
---
description: use whenever you are styling a ui with css
---
you are very good at writing clean maintainable css using modern techniques
css is structured like this
```css
[data-page="home"] {
[data-component="header"] {
[data-slot="logo"] {
}
}
}
```
top level pages are scoped using `data-page`
pages can break down into components using `data-component`
components can break down into slots using `data-slot`
structure things so that this hierarchy is followed IN YOUR CSS - you should rarely need to
nest components inside other components. you should NEVER nest components inside
slots. you should NEVER nest slots inside other slots.
**IMPORTANT: This hierarchy rule applies to CSS structure, NOT JSX/DOM structure.**
The hierarchy in css file does NOT have to match the hierarchy in the dom - you
can put components or slots at the same level in CSS even if one goes inside another in the DOM.
Your JSX can nest however makes semantic sense - components can be inside slots,
slots can contain components, etc. The DOM structure should be whatever makes the most
semantic and functional sense.
It is more important to follow the pages -> components -> slots structure IN YOUR CSS,
while keeping your JSX/DOM structure logical and semantic.
use data attributes to represent different states of the component
```css
[data-component="modal"] {
opacity: 0;
&[data-state="open"] {
opacity: 1;
}
}
```
this will allow jsx to control the syling
avoid selectors that just target an element type like `> span` you should assign
it a slot name. it's ok to do this sometimes where it makes sense semantically
like targeting `li` elements in a list
in terms of file structure `./src/style/` contains all universal styling rules.
these should not contain anything specific to a page
`./src/style/token` contains all the tokens used in the project
`./src/style/component` is for reusable components like buttons or inputs
page specific styles should go next to the page they are styling so
`./src/routes/about.tsx` should have its styles in `./src/routes/about.css`
`about.css` should be scoped using `data-page="about"`
## Example of correct implementation
JSX can nest however makes sense semantically:
```jsx
<div data-slot="left">
<div data-component="title">Section Title</div>
<div data-slot="content">Content here</div>
</div>
```
CSS maintains clean hierarchy regardless of DOM nesting:
```css
[data-page="home"] {
[data-component="screenshots"] {
[data-slot="left"] {
/* styles */
}
[data-slot="content"] {
/* styles */
}
}
[data-component="title"] {
/* can be at same level even though nested in DOM */
}
}
```
## Reusable Components
If a component is reused across multiple sections of the same page, define it at the page level:
```jsx
<!-- Used in multiple places on the same page -->
<section data-component="install">
<div data-component="method">
<h3 data-component="title">npm</h3>
</div>
<div data-component="method">
<h3 data-component="title">bun</h3>
</div>
</section>
<section data-component="screenshots">
<div data-slot="left">
<div data-component="title">Screenshot Title</div>
</div>
</section>
```
```css
[data-page="home"] {
/* Reusable title component defined at page level since it's used in multiple components */
[data-component="title"] {
text-transform: uppercase;
font-weight: 400;
}
[data-component="install"] {
/* install-specific styles */
}
[data-component="screenshots"] {
/* screenshots-specific styles */
}
}
```
This is correct because the `title` component has consistent styling and behavior across the page.
## Key Clarifications
1. **JSX Nesting is Flexible**: Components can be nested inside slots, slots can contain components - whatever makes semantic sense
2. **CSS Hierarchy is Strict**: Follow pages → components → slots structure in CSS
3. **Reusable Components**: Define at the appropriate level where they're shared (page level if used across the page, component level if only used within that component)
4. **DOM vs CSS Structure**: These don't need to match - optimize each for its purpose
See ./src/routes/index.css and ./src/routes/index.tsx for a complete example.

32
cloud/app/README.md Normal file
View file

@ -0,0 +1,32 @@
# SolidStart
Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
## Creating a project
```bash
# create a new project in the current directory
npm init solid@latest
# create a new project in my-app
npm init solid@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
Solid apps are built with _presets_, which optimise your project for deployment to different environments.
By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli)

9
cloud/app/app.config.ts Normal file
View file

@ -0,0 +1,9 @@
import { defineConfig } from "@solidjs/start/config"
export default defineConfig({
vite: {
server: {
allowedHosts: true,
},
},
})

23
cloud/app/package.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "@opencode/cloud-app",
"type": "module",
"scripts": {
"dev": "vinxi dev --host 0.0.0.0",
"build": "vinxi build",
"start": "vinxi start",
"version": "0.5.12"
},
"dependencies": {
"@ibm/plex": "6.4.1",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.0",
"@solidjs/start": "^1.1.0",
"solid-js": "^1.9.5",
"vinxi": "^0.5.7",
"@opencode/cloud-core": "workspace:*"
},
"engines": {
"node": ">=22"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

1
cloud/app/src/app.css Normal file
View file

@ -0,0 +1 @@
@import "./style/index.css";

23
cloud/app/src/app.tsx Normal file
View file

@ -0,0 +1,23 @@
import { MetaProvider, Title } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { ErrorBoundary, Suspense } from "solid-js";
import "@ibm/plex/css/ibm-plex.css";
import "./app.css";
export default function App() {
return (
<Router
root={props => (
<MetaProvider>
<Title>SolidStart - Basic</Title>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense>{props.children}</Suspense>
</ErrorBoundary>
</MetaProvider>
)}
>
<FileRoutes />
</Router>
);
}

View file

@ -0,0 +1,19 @@
<svg width="289" height="50" viewBox="0 0 289 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 16.5H24.5V33H8.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M48.5 16.5H64.5V33H48.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M120.5 16.5H136.5V33H120.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M160.5 16.5H176.5V33H160.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M192.5 16.5H208.5V33H192.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M232.5 16.5H248.5V33H232.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="white" fill-opacity="0.95"/>
<path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="white" fill-opacity="0.95"/>
<path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="white" fill-opacity="0.95"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z" fill="white" fill-opacity="0.95"/>
<path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="white" fill-opacity="0.5"/>
<path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="white" fill-opacity="0.5"/>
<path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="white" fill-opacity="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z" fill="white" fill-opacity="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z" fill="white" fill-opacity="0.5"/>
<path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="white" fill-opacity="0.95"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 KiB

View file

@ -0,0 +1,24 @@
import { JSX } from "solid-js"
export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg
{...props}
viewBox="0 0 512 512" >
<rect width="336" height="336" x="128" y="128" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32" rx="57" ry="57"></rect>
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="m383.5 128l.5-24a56.16 56.16 0 0 0-56-56H112a64.19 64.19 0 0 0-64 64v216a56.16 56.16 0 0 0 56 56h24"></path>
</svg>
)
}
export function IconCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg
{...props}
viewBox="0 0 24 24" >
<path fill="currentColor" d="M9 16.17L5.53 12.7a.996.996 0 1 0-1.41 1.41l4.18 4.18c.39.39 1.02.39 1.41 0L20.29 7.71a.996.996 0 1 0-1.41-1.41z"></path>
</svg>
)
}

View file

@ -0,0 +1,109 @@
import { useSession } from "vinxi/http"
import { createClient } from "@openauthjs/openauth/client"
import { getRequestEvent } from "solid-js/web"
import { and, Database, eq, inArray } from "@opencode/cloud-core/drizzle/index.js"
import { WorkspaceTable } from "@opencode/cloud-core/schema/workspace.sql.js"
import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
import { query, redirect } from "@solidjs/router"
import { AccountTable } from "@opencode/cloud-core/schema/account.sql.js"
import { Actor } from "@opencode/cloud-core/actor.js"
export async function withActor<T>(fn: () => T) {
const actor = await getActor()
return Actor.provide(actor.type, actor.properties, fn)
}
export const getActor = query(async (): Promise<Actor.Info> => {
"use server"
const evt = getRequestEvent()
const url = new URL(evt!.request.headers.get("referer") ?? evt!.request.url)
const auth = await useAuthSession()
const [workspaceHint] = url.pathname.split("/").filter((x) => x.length > 0)
if (!workspaceHint) {
if (auth.data.current) {
const current = auth.data.account[auth.data.current]
return {
type: "account",
properties: {
email: current.email,
accountID: current.id,
},
}
}
if (Object.keys(auth.data.account).length > 0) {
const current = Object.values(auth.data.account)[0]
await auth.update(val => ({
...val,
current: current.id,
}))
return {
type: "account",
properties: {
email: current.email,
accountID: current.id,
},
}
}
return {
type: "public",
properties: {},
}
}
const accounts = Object.keys(auth.data.account)
const result = await Database.transaction(async (tx) => {
return await tx.select({
user: UserTable
})
.from(AccountTable)
.innerJoin(UserTable, and(eq(UserTable.email, AccountTable.email)))
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
.where(
and(
inArray(AccountTable.id, accounts),
eq(WorkspaceTable.id, workspaceHint),
)
)
.limit(1)
.execute()
.then((x) => x[0])
})
if (result) {
return {
type: "user",
properties: {
userID: result.user.id,
workspaceID: result.user.workspaceID,
},
}
}
throw redirect("/auth/authorize")
}, "actor")
export const AuthClient = createClient({
clientID: "app",
issuer: import.meta.env.VITE_AUTH_URL,
})
export interface AuthSession {
account: Record<string, {
id: string
email: string
}>
current?: string
}
export function useAuthSession() {
return useSession<AuthSession>({
password: "0".repeat(32),
name: "auth"
})
}
export function AuthProvider() {
}

View file

@ -0,0 +1,4 @@
// @refresh reload
import { mount, StartClient } from "@solidjs/start/client";
mount(() => <StartClient />, document.getElementById("app")!);

View file

@ -0,0 +1,21 @@
// @refresh reload
import { createHandler, StartServer } from "@solidjs/start/server";
export default createHandler(() => (
<StartServer
document={({ assets, children, scripts }) => (
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
{assets}
</head>
<body data-color-mode="dark">
<div id="app">{children}</div>
{scripts}
</body>
</html>
)}
/>
));

1
cloud/app/src/global.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="@solidjs/start/env" />

View file

@ -0,0 +1,19 @@
import { Title } from "@solidjs/meta";
import { HttpStatusCode } from "@solidjs/start";
export default function NotFound() {
return (
<main>
<Title>Not Found</Title>
<HttpStatusCode code={404} />
<h1>Page Not Found</h1>
<p>
Visit{" "}
<a href="https://start.solidjs.com" target="_blank">
start.solidjs.com
</a>{" "}
to learn how to build SolidStart apps.
</p>
</main>
);
}

View file

@ -0,0 +1,15 @@
import { createAsync, query } from "@solidjs/router"
import { getActor, withActor } from "~/context/auth"
const getPosts = query(async () => {
"use server"
return withActor(() => {
return "ok"
})
}, "posts")
export default function () {
const actor = createAsync(async () => getActor())
return <div>{JSON.stringify(actor())}</div>
}

View file

@ -0,0 +1,7 @@
import type { APIEvent } from "@solidjs/start/server"
import { AuthClient } from "~/context/auth"
export async function GET(input: APIEvent) {
const result = await AuthClient.authorize(new URL("./callback", input.request.url).toString(), "code")
return Response.redirect(result.url, 302)
}

View file

@ -0,0 +1,31 @@
import type { APIEvent } from "@solidjs/start/server"
import { AuthClient, useAuthSession } from "~/context/auth"
export async function GET(input: APIEvent) {
const url = new URL(input.request.url)
const code = url.searchParams.get("code")
if (!code) throw new Error("No code found")
const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`)
if (result.err) {
throw new Error(result.err.message)
}
const decoded = AuthClient.decode(result.tokens.access, {} as any)
if (decoded.err) throw new Error(decoded.err.message)
const session = await useAuthSession()
const id = decoded.subject.properties.accountID
await session.update((value) => {
return {
...value,
account: {
[id]: {
id,
email: decoded.subject.properties.email,
},
},
current: id,
}
})
return {
result,
}
}

View file

@ -0,0 +1,257 @@
[data-page="home"] {
--color-bg: oklch(0.2097 0.008 274.53);
--color-border: oklch(0.46 0.02 269.88);
--color-text: #ffffff;
--color-text-secondary: oklch(0.72 0.01 270.15);
--color-text-dimmed: hsl(224, 7%, 46%);
padding: var(--space-6);
font-family: var(--font-mono);
color: var(--color-text);
a {
color: var(--color-text);
text-decoration: underline;
text-underline-offset: var(--space-0-75);
}
background: var(--color-bg);
position: fixed;
overflow-y: scroll;
inset: 0;
[data-component="content"] {
max-width: 67.5rem;
margin: 0 auto;
border: 2px solid var(--color-border);
}
[data-component="top"] {
padding: var(--space-12);
display: flex;
flex-direction: column;
align-items: start;
gap: var(--space-4);
[data-slot="logo"] {
height: 70px;
}
[data-slot="title"] {
font-size: var(--font-size-2xl);
text-transform: uppercase;
}
}
[data-component="cta"] {
height: var(--space-19);
border-top: 2px solid var(--color-border);
display: flex;
[data-slot="left"] {
display: flex;
padding: 0 var(--space-12);
text-transform: uppercase;
text-decoration: underline;
align-items: center;
justify-content: center;
text-underline-offset: var(--space-0-75);
border-right: 2px solid var(--color-border);
}
[data-slot="right"] {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2-5);
padding: 0 var(--space-6);
}
[data-slot="command"] {
all: unset;
display: flex;
align-items: center;
cursor: pointer;
color: var(--color-text-secondary);
font-size: var(--font-size-lg);
font-family: var(--font-mono);
gap: var(--space-2);
}
[data-slot="highlight"] {
color: var(--color-text);
font-weight: 500;
}
}
[data-component="features"] {
border-top: 2px solid var(--color-border);
padding: var(--space-12);
[data-slot="list"] {
padding-left: var(--space-4);
margin: 0;
list-style: disc;
li {
margin-bottom: var(--space-4);
strong {
text-transform: uppercase;
font-weight: 600;
}
}
li:last-child {
margin-bottom: 0;
}
}
}
[data-component="install"] {
border-top: 2px solid var(--color-border);
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
@media (max-width: 40rem) {
grid-template-columns: 1fr;
grid-template-rows: auto;
}
}
[data-component="title"] {
letter-spacing: -0.03125rem;
text-transform: uppercase;
font-weight: 400;
font-size: var(--font-size-md);
flex-shrink: 0;
color: oklch(0.55 0.02 269.87);
}
[data-component="method"] {
padding: var(--space-4) var(--space-6);
display: flex;
flex-direction: column;
align-items: start;
gap: var(--space-3);
&:nth-child(2) {
border-left: 2px solid var(--color-border);
}
&:nth-child(3) {
border-top: 2px solid var(--color-border);
}
&:nth-child(4) {
border-top: 2px solid var(--color-border);
border-left: 2px solid var(--color-border);
}
[data-slot="button"] {
all: unset;
cursor: pointer;
display: flex;
align-items: center;
color: var(--color-text-secondary);
gap: var(--space-2);
strong {
color: var(--color-text);
font-weight: 500;
}
}
}
[data-component="screenshots"] {
border-top: 2px solid var(--color-border);
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
[data-slot="left"] {
padding: var(--space-8) var(--space-6);
display: flex;
flex-direction: column;
img {
width: 100%;
height: auto;
}
}
[data-slot="right"] {
display: grid;
grid-template-rows: 1fr 1fr;
border-left: 2px solid var(--color-border);
}
[data-slot="filler"] {
display: flex;
flex-grow: 1;
align-items: center;
justify-content: center;
}
[data-slot="cell"] {
padding: var(--space-8) var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-4);
&:nth-child(2) {
border-top: 2px solid var(--color-border);
}
img {
width: 80%;
height: auto;
}
}
}
[data-component="copy-status"] {
[data-slot="copy"] {
display: block;
width: var(--space-4);
height: var(--space-4);
color: var(--color-text-dimmed);
[data-copied] & {
display: none;
}
}
[data-slot="check"] {
display: none;
width: var(--space-4);
height: var(--space-4);
color: white;
[data-copied] & {
display: block;
}
}
}
[data-component="footer"] {
border-top: 2px solid var(--color-border);
display: grid;
grid-template-columns: 1fr 1fr 1fr;
font-size: var(--font-size-lg);
height: var(--space-20);
[data-slot="cell"] {
display: flex;
align-items: center;
justify-content: center;
border-right: 2px solid var(--color-border);
text-transform: uppercase;
&:last-child {
border-right: none;
}
}
}
}

View file

@ -0,0 +1,186 @@
import { Title } from "@solidjs/meta"
import { onCleanup, onMount } from "solid-js"
import "./index.css"
import logo from "../asset/logo-ornate-dark.svg"
import IMG_SPLASH from "../asset/screenshot-splash.webp"
import IMG_VSCODE from "../asset/screenshot-vscode.webp"
import IMG_GITHUB from "../asset/screenshot-github.webp"
import { IconCopy, IconCheck } from "../component/icon"
import { createAsync, query, redirect, RouteDefinition } from "@solidjs/router"
import { getActor, withActor } from "~/context/auth"
import { Account } from "@opencode/cloud-core/account.js"
function CopyStatus() {
return (
<div data-component="copy-status">
<IconCopy data-slot="copy" />
<IconCheck data-slot="check" />
</div>
)
}
const isLoggedIn = query(async () => {
"use server"
const actor = await getActor()
if (actor.type === "account") {
const workspaces = await withActor(() => Account.workspaces())
throw redirect("/" + workspaces[0].id)
}
return
}, "isLoggedIn")
export default function Home() {
createAsync(() => isLoggedIn(), {
deferStream: true,
})
onMount(() => {
const commands = document.querySelectorAll("[data-copy]")
for (const button of commands) {
const callback = () => {
const text = button.textContent
if (text) {
navigator.clipboard.writeText(text)
button.setAttribute("data-copied", "")
setTimeout(() => {
button.removeAttribute("data-copied")
}, 1500)
}
}
button.addEventListener("click", callback)
onCleanup(() => {
button.removeEventListener("click", callback)
})
}
})
return (
<main data-page="home">
<Title>opencode | AI coding agent built for the terminal</Title>
<div data-component="content">
<section data-component="top">
<img data-slot="logo" src={logo} alt="logo" />
<h1 data-slot="title">The AI coding agent built for the terminal.</h1>
</section>
<section data-component="cta">
<div data-slot="left">
<a href="/docs">Get Started</a>
</div>
<div data-slot="right">
<button data-copy data-slot="command">
<span>
<span>curl -fsSL&nbsp;</span>
<span data-slot="protocol">https://</span>
<span data-slot="highlight">opencode.ai/install</span>
&nbsp;| bash
</span>
<CopyStatus />
</button>
</div>
</section>
<section data-component="features">
<ul data-slot="list">
<li>
<strong>Native TUI</strong>: A responsive, native, themeable terminal UI.
</li>
<li>
<strong>LSP enabled</strong>: Automatically loads the right LSPs for the LLM.
</li>
<li>
<strong>Multi-session</strong>: Start multiple agents in parallel on the same project.
</li>
<li>
<strong>Shareable links</strong>: Share a link to any sessions for reference or to debug.
</li>
<li>
<strong>Claude Pro</strong>: Log in with Anthropic to use your Claude Pro or Max account.
</li>
<li>
<strong>Use any model</strong>: Supports 75+ LLM providers through{" "}
<a href="https://models.dev">Models.dev</a>, including local models.
</li>
</ul>
</section>
<section data-component="install">
<div data-component="method">
<h3 data-component="title">npm</h3>
<button data-copy data-slot="button">
<span>
npm install -g&nbsp;<strong>opencode-ai</strong>
</span>
<CopyStatus />
</button>
</div>
<div data-component="method">
<h3 data-component="title">bun</h3>
<button data-copy data-slot="button">
<span>
bun install -g&nbsp;<strong>opencode-ai</strong>
</span>
<CopyStatus />
</button>
</div>
<div data-component="method">
<h3 data-component="title">homebrew</h3>
<button data-copy data-slot="button">
<span>
brew install&nbsp;<strong>sst/tap/opencode</strong>
</span>
<CopyStatus />
</button>
</div>
<div data-component="method">
<h3 data-component="title">paru</h3>
<button data-copy data-slot="button">
<span>
paru -S&nbsp;<strong>opencode-bin</strong>
</span>
<CopyStatus />
</button>
</div>
</section>
<section data-component="screenshots">
<div data-slot="left">
<div data-component="title">opencode TUI with tokyonight theme</div>
<div data-slot="filler">
<img src={IMG_SPLASH} alt="opencode TUI with tokyonight theme" />
</div>
</div>
<div data-slot="right">
<div data-slot="cell">
<div data-component="title">opencode in VS Code</div>
<div data-slot="filler">
<img src={IMG_VSCODE} alt="opencode in VS Code" />
</div>
</div>
<div data-slot="cell">
<div data-component="title">opencode in GitHub</div>
<div data-slot="filler">
<img src={IMG_GITHUB} alt="opencode in GitHub" />
</div>
</div>
</div>
</section>
<footer data-component="footer">
<div data-slot="cell">
<a href="https://github.com/sst/opencode">GitHub</a>
</div>
<div data-slot="cell">
<a href="https://opencode.ai/discord">Discord</a>
</div>
<div data-slot="cell">
<span>
©2025 <a href="https://anoma.ly">Anomaly Innovations</a>
</span>
</div>
</footer>
</div>
</main>
)
}

View file

@ -0,0 +1,8 @@
html {
color-scheme: dark;
line-height: 1;
}
body {
font-family: var(--font-sans);
}

View file

@ -0,0 +1,102 @@
[data-component="button"] {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
border: 1px solid transparent;
border-radius: var(--space-2);
font-family: var(--font-sans);
font-size: var(--font-size-md);
font-weight: 500;
line-height: 1.25;
cursor: pointer;
transition: all 0.2s ease-in-out;
text-decoration: none;
user-select: none;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px var(--color-primary);
}
&[data-color="primary"] {
background-color: var(--color-primary);
color: var(--color-primary-text);
border-color: var(--color-primary);
&:hover:not(:disabled) {
background-color: var(--color-primary-hover);
border-color: var(--color-primary-hover);
}
&:active:not(:disabled) {
background-color: var(--color-primary-active);
border-color: var(--color-primary-active);
}
}
&[data-color="danger"] {
background-color: var(--color-danger);
color: var(--color-danger-text);
border-color: var(--color-danger);
&:hover:not(:disabled) {
background-color: var(--color-danger-hover);
border-color: var(--color-danger-hover);
}
&:active:not(:disabled) {
background-color: var(--color-danger-active);
border-color: var(--color-danger-active);
}
&:focus {
box-shadow: 0 0 0 2px var(--color-danger);
}
}
&[data-color="warning"] {
background-color: var(--color-warning);
color: var(--color-warning-text);
border-color: var(--color-warning);
&:hover:not(:disabled) {
background-color: var(--color-warning-hover);
border-color: var(--color-warning-hover);
}
&:active:not(:disabled) {
background-color: var(--color-warning-active);
border-color: var(--color-warning-active);
}
&:focus {
box-shadow: 0 0 0 2px var(--color-warning);
}
}
&[data-size="small"] {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-sm);
gap: var(--space-1-5);
}
&[data-size="large"] {
padding: var(--space-4) var(--space-6);
font-size: var(--font-size-lg);
gap: var(--space-3);
}
[data-slot="icon"] {
display: flex;
align-items: center;
width: 1em;
height: 1em;
}
}

View file

@ -0,0 +1,8 @@
@import "./token/color.css";
@import "./token/font.css";
@import "./token/space.css";
@import "./component/button.css";
@import "./reset.css";
@import "./base.css";

View file

@ -0,0 +1,76 @@
/* 1. Use a more-intuitive box-sizing model */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* 2. Remove default margin */
* {
margin: 0;
}
/* 3. Enable keyword animations */
@media (prefers-reduced-motion: no-preference) {
html {
interpolate-size: allow-keywords;
}
}
body {
/* 4. Add accessible line-height */
line-height: 1.5;
/* 5. Improve text rendering */
-webkit-font-smoothing: antialiased;
}
/* 6. Improve media defaults */
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
/* 7. Inherit fonts for form controls */
input,
button,
textarea,
select {
font: inherit;
}
/* 8. Avoid text overflows */
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
/* 9. Improve line wrapping */
p {
text-wrap: pretty;
}
h1,
h2,
h3,
h4,
h5,
h6 {
text-wrap: balance;
}
/*
10. Create a root stacking context
*/
#root,
#__next {
isolation: isolate;
}

View file

@ -0,0 +1,90 @@
body {
--color-white: #ffffff;
--color-black: #000000;
}
[data-color-mode="dark"] {
/* OpenCode theme colors */
--color-bg: #0c0c0e;
--color-bg-surface: #161618;
--color-bg-elevated: #1c1c1f;
--color-text: #ffffff;
--color-text-muted: #a1a1a6;
--color-text-disabled: #68686f;
--color-accent: #007aff;
--color-accent-hover: #0056b3;
--color-accent-active: #004085;
--color-success: #30d158;
--color-warning: #ff9f0a;
--color-danger: #ff453a;
--color-border: #38383a;
--color-border-muted: #2c2c2e;
/* Button colors */
--color-primary: var(--color-accent);
--color-primary-hover: var(--color-accent-hover);
--color-primary-active: var(--color-accent-active);
--color-primary-text: #ffffff;
--color-danger: #ff453a;
--color-danger-hover: #d70015;
--color-danger-active: #a50011;
--color-danger-text: #ffffff;
--color-warning: #ff9f0a;
--color-warning-hover: #cc7f08;
--color-warning-active: #995f06;
--color-warning-text: #000000;
/* Surface colors */
--color-surface: var(--color-bg-surface);
--color-surface-hover: var(--color-bg-elevated);
--color-border: var(--color-border);
}
[data-color-mode="light"] {
/* OpenCode light theme colors */
--color-bg: #ffffff;
--color-bg-surface: #f5f5f7;
--color-bg-elevated: #ffffff;
--color-text: #1d1d1f;
--color-text-muted: #6e6e73;
--color-text-disabled: #86868b;
--color-accent: #007aff;
--color-accent-hover: #0056b3;
--color-accent-active: #004085;
--color-success: #30d158;
--color-warning: #ff9f0a;
--color-danger: #ff3b30;
--color-border: #d2d2d7;
--color-border-muted: #e5e5ea;
/* Button colors */
--color-primary: var(--color-accent);
--color-primary-hover: var(--color-accent-hover);
--color-primary-active: var(--color-accent-active);
--color-primary-text: #ffffff;
--color-danger: #ff3b30;
--color-danger-hover: #d70015;
--color-danger-active: #a50011;
--color-danger-text: #ffffff;
--color-warning: #ff9f0a;
--color-warning-hover: #cc7f08;
--color-warning-active: #995f06;
--color-warning-text: #000000;
/* Surface colors */
--color-surface: var(--color-bg-surface);
--color-surface-hover: var(--color-bg-elevated);
--color-border: var(--color-border);
}

View file

@ -0,0 +1,18 @@
body {
--font-size-2xs: 0.6875rem;
--font-size-xs: 0.75rem;
--font-size-sm: 0.8125rem;
--font-size-md: 0.9375rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--font-size-2xl: 1.5rem;
--font-size-3xl: 1.875rem;
--font-size-4xl: 2.25rem;
--font-size-5xl: 3rem;
--font-size-6xl: 3.75rem;
--font-size-7xl: 4.5rem;
--font-size-8xl: 6rem;
--font-size-9xl: 8rem;
--font-mono: IBM Plex Mono;
--font-sans: Inter;
}

View file

@ -0,0 +1,42 @@
body {
--space-0: 0;
--space-px: 1px;
--space-0-5: 0.125rem;
--space-0-75: 0.1875rem;
--space-1: 0.25rem;
--space-1-5: 0.375rem;
--space-2: 0.5rem;
--space-2-5: 0.625rem;
--space-3: 0.75rem;
--space-3-5: 0.875rem;
--space-4: 1rem;
--space-4-5: 1.125rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-7: 1.75rem;
--space-8: 2rem;
--space-9: 2.25rem;
--space-10: 2.5rem;
--space-11: 2.75rem;
--space-12: 3rem;
--space-14: 3.5rem;
--space-16: 4rem;
--space-17: 4.25rem;
--space-18: 4.5rem;
--space-19: 4.75rem;
--space-20: 5rem;
--space-24: 6rem;
--space-28: 7rem;
--space-32: 8rem;
--space-36: 9rem;
--space-40: 10rem;
--space-44: 11rem;
--space-48: 12rem;
--space-52: 13rem;
--space-56: 14rem;
--space-60: 15rem;
--space-64: 16rem;
--space-72: 18rem;
--space-80: 20rem;
--space-96: 24rem;
}

9
cloud/app/sst-env.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}

19
cloud/app/tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"allowJs": true,
"strict": true,
"noEmit": true,
"types": ["vinxi/types/client"],
"isolatedModules": true,
"paths": {
"~/*": ["./src/*"]
}
}
}

View file

@ -0,0 +1,20 @@
import { defineConfig } from "drizzle-kit"
import { Resource } from "sst"
export default defineConfig({
out: "./migrations/",
strict: true,
schema: ["./src/**/*.sql.ts"],
verbose: true,
dialect: "postgresql",
dbCredentials: {
database: Resource.Database.database,
host: Resource.Database.host,
user: Resource.Database.username,
password: Resource.Database.password,
port: Resource.Database.port,
ssl: {
rejectUnauthorized: false,
},
},
})

View file

@ -0,0 +1,66 @@
CREATE TABLE "billing" (
"id" varchar(30) NOT NULL,
"workspace_id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"customer_id" varchar(255),
"payment_method_id" varchar(255),
"payment_method_last4" varchar(4),
"balance" bigint NOT NULL,
"reload" boolean,
CONSTRAINT "billing_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
);
--> statement-breakpoint
CREATE TABLE "payment" (
"id" varchar(30) NOT NULL,
"workspace_id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"customer_id" varchar(255),
"payment_id" varchar(255),
"amount" bigint NOT NULL,
CONSTRAINT "payment_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
);
--> statement-breakpoint
CREATE TABLE "usage" (
"id" varchar(30) NOT NULL,
"workspace_id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"request_id" varchar(255),
"model" varchar(255) NOT NULL,
"input_tokens" integer NOT NULL,
"output_tokens" integer NOT NULL,
"reasoning_tokens" integer,
"cache_read_tokens" integer,
"cache_write_tokens" integer,
"cost" bigint NOT NULL,
CONSTRAINT "usage_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
);
--> statement-breakpoint
CREATE TABLE "user" (
"id" varchar(30) NOT NULL,
"workspace_id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"email" text NOT NULL,
"name" varchar(255) NOT NULL,
"time_seen" timestamp with time zone,
"color" integer,
CONSTRAINT "user_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
);
--> statement-breakpoint
CREATE TABLE "workspace" (
"id" varchar(30) PRIMARY KEY NOT NULL,
"slug" varchar(255),
"name" varchar(255),
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone
);
--> statement-breakpoint
ALTER TABLE "billing" ADD CONSTRAINT "billing_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "payment" ADD CONSTRAINT "payment_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "usage" ADD CONSTRAINT "usage_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user" ADD CONSTRAINT "user_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "user_email" ON "user" USING btree ("workspace_id","email");--> statement-breakpoint
CREATE UNIQUE INDEX "slug" ON "workspace" USING btree ("slug");

View file

@ -0,0 +1,8 @@
CREATE TABLE "account" (
"id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"email" varchar(255) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX "email" ON "account" USING btree ("email");

View file

@ -0,0 +1,14 @@
CREATE TABLE "key" (
"id" varchar(30) NOT NULL,
"workspace_id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"user_id" text NOT NULL,
"name" varchar(255) NOT NULL,
"key" varchar(255) NOT NULL,
"time_used" timestamp with time zone,
CONSTRAINT "key_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
);
--> statement-breakpoint
ALTER TABLE "key" ADD CONSTRAINT "key_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "global_key" ON "key" USING btree ("key");

View file

@ -0,0 +1 @@
ALTER TABLE "usage" DROP COLUMN "request_id";

View file

@ -0,0 +1,461 @@
{
"id": "9b5cec8c-8b59-4d7a-bb5c-76ade1c83d6f",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.billing": {
"name": "billing",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_id": {
"name": "payment_method_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_last4": {
"name": "payment_method_last4",
"type": "varchar(4)",
"primaryKey": false,
"notNull": false
},
"balance": {
"name": "balance",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"reload": {
"name": "reload",
"type": "boolean",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"billing_workspace_id_workspace_id_fk": {
"name": "billing_workspace_id_workspace_id_fk",
"tableFrom": "billing",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.payment": {
"name": "payment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_id": {
"name": "payment_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"amount": {
"name": "amount",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"payment_workspace_id_workspace_id_fk": {
"name": "payment_workspace_id_workspace_id_fk",
"tableFrom": "payment",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.usage": {
"name": "usage",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"request_id": {
"name": "request_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"model": {
"name": "model",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"input_tokens": {
"name": "input_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"output_tokens": {
"name": "output_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"reasoning_tokens": {
"name": "reasoning_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_read_tokens": {
"name": "cache_read_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_write_tokens": {
"name": "cache_write_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cost": {
"name": "cost",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"usage_workspace_id_workspace_id_fk": {
"name": "usage_workspace_id_workspace_id_fk",
"tableFrom": "usage",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"color": {
"name": "color",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_workspace_id_workspace_id_fk": {
"name": "user_workspace_id_workspace_id_fk",
"tableFrom": "user",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workspace": {
"name": "workspace",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": true,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,515 @@
{
"id": "bf9e9084-4073-4ecb-8e56-5610816c9589",
"prevId": "9b5cec8c-8b59-4d7a-bb5c-76ade1c83d6f",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email": {
"name": "email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.billing": {
"name": "billing",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_id": {
"name": "payment_method_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_last4": {
"name": "payment_method_last4",
"type": "varchar(4)",
"primaryKey": false,
"notNull": false
},
"balance": {
"name": "balance",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"reload": {
"name": "reload",
"type": "boolean",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"billing_workspace_id_workspace_id_fk": {
"name": "billing_workspace_id_workspace_id_fk",
"tableFrom": "billing",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.payment": {
"name": "payment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_id": {
"name": "payment_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"amount": {
"name": "amount",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"payment_workspace_id_workspace_id_fk": {
"name": "payment_workspace_id_workspace_id_fk",
"tableFrom": "payment",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.usage": {
"name": "usage",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"request_id": {
"name": "request_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"model": {
"name": "model",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"input_tokens": {
"name": "input_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"output_tokens": {
"name": "output_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"reasoning_tokens": {
"name": "reasoning_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_read_tokens": {
"name": "cache_read_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_write_tokens": {
"name": "cache_write_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cost": {
"name": "cost",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"usage_workspace_id_workspace_id_fk": {
"name": "usage_workspace_id_workspace_id_fk",
"tableFrom": "usage",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"color": {
"name": "color",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_workspace_id_workspace_id_fk": {
"name": "user_workspace_id_workspace_id_fk",
"tableFrom": "user",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workspace": {
"name": "workspace",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": true,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,615 @@
{
"id": "351e4956-74e0-4282-a23b-02f1a73fa38c",
"prevId": "bf9e9084-4073-4ecb-8e56-5610816c9589",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email": {
"name": "email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.billing": {
"name": "billing",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_id": {
"name": "payment_method_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_last4": {
"name": "payment_method_last4",
"type": "varchar(4)",
"primaryKey": false,
"notNull": false
},
"balance": {
"name": "balance",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"reload": {
"name": "reload",
"type": "boolean",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"billing_workspace_id_workspace_id_fk": {
"name": "billing_workspace_id_workspace_id_fk",
"tableFrom": "billing",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.payment": {
"name": "payment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_id": {
"name": "payment_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"amount": {
"name": "amount",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"payment_workspace_id_workspace_id_fk": {
"name": "payment_workspace_id_workspace_id_fk",
"tableFrom": "payment",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.usage": {
"name": "usage",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"request_id": {
"name": "request_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"model": {
"name": "model",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"input_tokens": {
"name": "input_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"output_tokens": {
"name": "output_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"reasoning_tokens": {
"name": "reasoning_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_read_tokens": {
"name": "cache_read_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_write_tokens": {
"name": "cache_write_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cost": {
"name": "cost",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"usage_workspace_id_workspace_id_fk": {
"name": "usage_workspace_id_workspace_id_fk",
"tableFrom": "usage",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.key": {
"name": "key",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"key": {
"name": "key",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_used": {
"name": "time_used",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
{
"expression": "key",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"key_workspace_id_workspace_id_fk": {
"name": "key_workspace_id_workspace_id_fk",
"tableFrom": "key",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"color": {
"name": "color",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_workspace_id_workspace_id_fk": {
"name": "user_workspace_id_workspace_id_fk",
"tableFrom": "user",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workspace": {
"name": "workspace",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": true,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,609 @@
{
"id": "fa935883-9e51-4811-90c7-8967eefe458c",
"prevId": "351e4956-74e0-4282-a23b-02f1a73fa38c",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email": {
"name": "email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.billing": {
"name": "billing",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_id": {
"name": "payment_method_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_last4": {
"name": "payment_method_last4",
"type": "varchar(4)",
"primaryKey": false,
"notNull": false
},
"balance": {
"name": "balance",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"reload": {
"name": "reload",
"type": "boolean",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"billing_workspace_id_workspace_id_fk": {
"name": "billing_workspace_id_workspace_id_fk",
"tableFrom": "billing",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.payment": {
"name": "payment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_id": {
"name": "payment_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"amount": {
"name": "amount",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"payment_workspace_id_workspace_id_fk": {
"name": "payment_workspace_id_workspace_id_fk",
"tableFrom": "payment",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.usage": {
"name": "usage",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"model": {
"name": "model",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"input_tokens": {
"name": "input_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"output_tokens": {
"name": "output_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"reasoning_tokens": {
"name": "reasoning_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_read_tokens": {
"name": "cache_read_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_write_tokens": {
"name": "cache_write_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cost": {
"name": "cost",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"usage_workspace_id_workspace_id_fk": {
"name": "usage_workspace_id_workspace_id_fk",
"tableFrom": "usage",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.key": {
"name": "key",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"key": {
"name": "key",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_used": {
"name": "time_used",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
{
"expression": "key",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"key_workspace_id_workspace_id_fk": {
"name": "key_workspace_id_workspace_id_fk",
"tableFrom": "key",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"color": {
"name": "color",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_workspace_id_workspace_id_fk": {
"name": "user_workspace_id_workspace_id_fk",
"tableFrom": "user",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workspace": {
"name": "workspace",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": true,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,34 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1754518198186,
"tag": "0000_amused_mojo",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1754609655262,
"tag": "0001_thankful_chat",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1754627626945,
"tag": "0002_stale_jackal",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1754672464106,
"tag": "0003_tranquil_spencer_smythe",
"breakpoints": true
}
]
}

23
cloud/core/package.json Normal file
View file

@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cloud-core",
"version": "0.5.12",
"private": true,
"type": "module",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"drizzle-orm": "0.41.0",
"postgres": "3.4.7",
"stripe": "18.0.0",
"ulid": "3.0.0"
},
"exports": {
"./*": "./src/*"
},
"scripts": {
"db": "sst shell drizzle-kit"
},
"devDependencies": {
"drizzle-kit": "0.30.5"
}
}

67
cloud/core/src/account.ts Normal file
View file

@ -0,0 +1,67 @@
import { z } from "zod"
import { and, eq, getTableColumns, isNull } from "drizzle-orm"
import { fn } from "./util/fn"
import { Database } from "./drizzle"
import { Identifier } from "./identifier"
import { AccountTable } from "./schema/account.sql"
import { Actor } from "./actor"
import { WorkspaceTable } from "./schema/workspace.sql"
import { UserTable } from "./schema/user.sql"
export namespace Account {
export const create = fn(
z.object({
email: z.string().email(),
id: z.string().optional(),
}),
async (input) =>
Database.transaction(async (tx) => {
const id = input.id ?? Identifier.create("account")
await tx.insert(AccountTable).values({
id,
email: input.email,
})
return id
}),
)
export const fromID = fn(z.string(), async (id) =>
Database.transaction(async (tx) => {
return tx
.select()
.from(AccountTable)
.where(eq(AccountTable.id, id))
.execute()
.then((rows) => rows[0])
}),
)
export const fromEmail = fn(z.string().email(), async (email) =>
Database.transaction(async (tx) => {
return tx
.select()
.from(AccountTable)
.where(eq(AccountTable.email, email))
.execute()
.then((rows) => rows[0])
}),
)
export const workspaces = async () => {
const actor = Actor.assert("account")
return Database.transaction(async (tx) =>
tx
.select(getTableColumns(WorkspaceTable))
.from(WorkspaceTable)
.innerJoin(UserTable, eq(UserTable.workspaceID, WorkspaceTable.id))
.where(
and(
eq(UserTable.email, actor.properties.email),
isNull(UserTable.timeDeleted),
isNull(WorkspaceTable.timeDeleted),
),
)
.execute(),
)
}
}

74
cloud/core/src/actor.ts Normal file
View file

@ -0,0 +1,74 @@
import { Context } from "./context"
import { Log } from "./util/log"
export namespace Actor {
interface Account {
type: "account"
properties: {
accountID: string
email: string
}
}
interface Public {
type: "public"
properties: {}
}
interface User {
type: "user"
properties: {
userID: string
workspaceID: string
}
}
interface System {
type: "system"
properties: {
workspaceID: string
}
}
export type Info = Account | Public | User | System
const ctx = Context.create<Info>()
export const use = ctx.use
const log = Log.create().tag("namespace", "actor")
export function provide<R, T extends Info["type"]>(
type: T,
properties: Extract<Info, { type: T }>["properties"],
cb: () => R,
) {
return ctx.provide(
{
type,
properties,
} as any,
() => {
return Log.provide({ ...properties }, () => {
log.info("provided")
return cb()
})
},
)
}
export function assert<T extends Info["type"]>(type: T) {
const actor = use()
if (actor.type !== type) {
throw new Error(`Expected actor type ${type}, got ${actor.type}`)
}
return actor as Extract<Info, { type: T }>
}
export function workspace() {
const actor = use()
if ("workspaceID" in actor.properties) {
return actor.properties.workspaceID
}
throw new Error(`actor of type "${actor.type}" is not associated with a workspace`)
}
}

71
cloud/core/src/billing.ts Normal file
View file

@ -0,0 +1,71 @@
import { Resource } from "sst"
import { Stripe } from "stripe"
import { Database, eq, sql } from "./drizzle"
import { BillingTable, UsageTable } from "./schema/billing.sql"
import { Actor } from "./actor"
import { fn } from "./util/fn"
import { z } from "zod"
import { Identifier } from "./identifier"
import { centsToMicroCents } from "./util/price"
export namespace Billing {
export const stripe = () =>
new Stripe(Resource.STRIPE_SECRET_KEY.value, {
apiVersion: "2025-03-31.basil",
})
export const get = async () => {
return Database.use(async (tx) =>
tx
.select({
customerID: BillingTable.customerID,
paymentMethodID: BillingTable.paymentMethodID,
balance: BillingTable.balance,
reload: BillingTable.reload,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, Actor.workspace()))
.then((r) => r[0]),
)
}
export const consume = fn(
z.object({
requestID: z.string().optional(),
model: z.string(),
inputTokens: z.number(),
outputTokens: z.number(),
reasoningTokens: z.number().optional(),
cacheReadTokens: z.number().optional(),
cacheWriteTokens: z.number().optional(),
costInCents: z.number(),
}),
async (input) => {
const workspaceID = Actor.workspace()
const cost = centsToMicroCents(input.costInCents)
return await Database.transaction(async (tx) => {
await tx.insert(UsageTable).values({
workspaceID,
id: Identifier.create("usage"),
requestID: input.requestID,
model: input.model,
inputTokens: input.inputTokens,
outputTokens: input.outputTokens,
reasoningTokens: input.reasoningTokens,
cacheReadTokens: input.cacheReadTokens,
cacheWriteTokens: input.cacheWriteTokens,
cost,
})
const [updated] = await tx
.update(BillingTable)
.set({
balance: sql`${BillingTable.balance} - ${cost}`,
})
.where(eq(BillingTable.workspaceID, workspaceID))
.returning()
return updated.balance
})
},
)
}

21
cloud/core/src/context.ts Normal file
View file

@ -0,0 +1,21 @@
import { AsyncLocalStorage } from "node:async_hooks"
export namespace Context {
export class NotFound extends Error {}
export function create<T>() {
const storage = new AsyncLocalStorage<T>()
return {
use() {
const result = storage.getStore()
if (!result) {
throw new NotFound()
}
return result
},
provide<R>(value: T, fn: () => R) {
return storage.run<R>(value, fn)
},
}
}
}

View file

@ -0,0 +1,95 @@
import { drizzle } from "drizzle-orm/postgres-js"
import { Resource } from "sst"
export * from "drizzle-orm"
import postgres from "postgres"
const createClient = memo(() => {
const client = postgres({
idle_timeout: 30000,
connect_timeout: 30000,
host: Resource.Database.host,
database: Resource.Database.database,
user: Resource.Database.username,
password: Resource.Database.password,
port: Resource.Database.port,
ssl: {
rejectUnauthorized: false,
},
max: 1,
})
return drizzle(client, {})
})
import { PgTransaction, type PgTransactionConfig } from "drizzle-orm/pg-core"
import type { ExtractTablesWithRelations } from "drizzle-orm"
import type { PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js"
import { Context } from "../context"
import { memo } from "../util/memo"
export namespace Database {
export type Transaction = PgTransaction<
PostgresJsQueryResultHKT,
Record<string, unknown>,
ExtractTablesWithRelations<Record<string, unknown>>
>
export type TxOrDb = Transaction | ReturnType<typeof createClient>
const TransactionContext = Context.create<{
tx: TxOrDb
effects: (() => void | Promise<void>)[]
}>()
export async function use<T>(callback: (trx: TxOrDb) => Promise<T>) {
try {
const { tx } = TransactionContext.use()
return tx.transaction(callback)
} catch (err) {
if (err instanceof Context.NotFound) {
const client = createClient()
const effects: (() => void | Promise<void>)[] = []
const result = await TransactionContext.provide(
{
effects,
tx: client,
},
() => callback(client),
)
await Promise.all(effects.map((x) => x()))
return result
}
throw err
}
}
export async function fn<Input, T>(callback: (input: Input, trx: TxOrDb) => Promise<T>) {
return (input: Input) => use(async (tx) => callback(input, tx))
}
export async function effect(effect: () => any | Promise<any>) {
try {
const { effects } = TransactionContext.use()
effects.push(effect)
} catch {
await effect()
}
}
export async function transaction<T>(callback: (tx: TxOrDb) => Promise<T>, config?: PgTransactionConfig) {
try {
const { tx } = TransactionContext.use()
return callback(tx)
} catch (err) {
if (err instanceof Context.NotFound) {
const client = createClient()
const effects: (() => void | Promise<void>)[] = []
const result = await client.transaction(async (tx) => {
return TransactionContext.provide({ tx, effects }, () => callback(tx))
}, config)
await Promise.all(effects.map((x) => x()))
return result
}
throw err
}
}
}

View file

@ -0,0 +1,29 @@
import { bigint, timestamp, varchar } from "drizzle-orm/pg-core"
export const ulid = (name: string) => varchar(name, { length: 30 })
export const workspaceColumns = {
get id() {
return ulid("id").notNull()
},
get workspaceID() {
return ulid("workspace_id").notNull()
},
}
export const id = () => ulid("id").notNull()
export const utc = (name: string) =>
timestamp(name, {
withTimezone: true,
})
export const currency = (name: string) =>
bigint(name, {
mode: "number",
})
export const timestamps = {
timeCreated: utc("time_created").notNull().defaultNow(),
timeDeleted: utc("time_deleted"),
}

View file

@ -0,0 +1,26 @@
import { ulid } from "ulid"
import { z } from "zod"
export namespace Identifier {
const prefixes = {
account: "acc",
billing: "bil",
key: "key",
payment: "pay",
usage: "usg",
user: "usr",
workspace: "wrk",
} as const
export function create(prefix: keyof typeof prefixes, given?: string): string {
if (given) {
if (given.startsWith(prefixes[prefix])) return given
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
}
return [prefixes[prefix], ulid()].join("_")
}
export function schema(prefix: keyof typeof prefixes) {
return z.string().startsWith(prefixes[prefix])
}
}

View file

@ -0,0 +1,12 @@
import { pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core"
import { id, timestamps } from "../drizzle/types"
export const AccountTable = pgTable(
"account",
{
id: id(),
...timestamps,
email: varchar("email", { length: 255 }).notNull(),
},
(table) => [uniqueIndex("email").on(table.email)],
)

View file

@ -0,0 +1,45 @@
import { bigint, boolean, integer, pgTable, varchar } from "drizzle-orm/pg-core"
import { timestamps, workspaceColumns } from "../drizzle/types"
import { workspaceIndexes } from "./workspace.sql"
export const BillingTable = pgTable(
"billing",
{
...workspaceColumns,
...timestamps,
customerID: varchar("customer_id", { length: 255 }),
paymentMethodID: varchar("payment_method_id", { length: 255 }),
paymentMethodLast4: varchar("payment_method_last4", { length: 4 }),
balance: bigint("balance", { mode: "number" }).notNull(),
reload: boolean("reload"),
},
(table) => [...workspaceIndexes(table)],
)
export const PaymentTable = pgTable(
"payment",
{
...workspaceColumns,
...timestamps,
customerID: varchar("customer_id", { length: 255 }),
paymentID: varchar("payment_id", { length: 255 }),
amount: bigint("amount", { mode: "number" }).notNull(),
},
(table) => [...workspaceIndexes(table)],
)
export const UsageTable = pgTable(
"usage",
{
...workspaceColumns,
...timestamps,
model: varchar("model", { length: 255 }).notNull(),
inputTokens: integer("input_tokens").notNull(),
outputTokens: integer("output_tokens").notNull(),
reasoningTokens: integer("reasoning_tokens"),
cacheReadTokens: integer("cache_read_tokens"),
cacheWriteTokens: integer("cache_write_tokens"),
cost: bigint("cost", { mode: "number" }).notNull(),
},
(table) => [...workspaceIndexes(table)],
)

View file

@ -0,0 +1,16 @@
import { text, pgTable, varchar, uniqueIndex } from "drizzle-orm/pg-core"
import { timestamps, utc, workspaceColumns } from "../drizzle/types"
import { workspaceIndexes } from "./workspace.sql"
export const KeyTable = pgTable(
"key",
{
...workspaceColumns,
...timestamps,
userID: text("user_id").notNull(),
name: varchar("name", { length: 255 }).notNull(),
key: varchar("key", { length: 255 }).notNull(),
timeUsed: utc("time_used"),
},
(table) => [...workspaceIndexes(table), uniqueIndex("global_key").on(table.key)],
)

View file

@ -0,0 +1,16 @@
import { text, pgTable, uniqueIndex, varchar, integer } from "drizzle-orm/pg-core"
import { timestamps, utc, workspaceColumns } from "../drizzle/types"
import { workspaceIndexes } from "./workspace.sql"
export const UserTable = pgTable(
"user",
{
...workspaceColumns,
...timestamps,
email: text("email").notNull(),
name: varchar("name", { length: 255 }).notNull(),
timeSeen: utc("time_seen"),
color: integer("color"),
},
(table) => [...workspaceIndexes(table), uniqueIndex("user_email").on(table.workspaceID, table.email)],
)

View file

@ -0,0 +1,25 @@
import { primaryKey, foreignKey, pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core"
import { timestamps, ulid } from "../drizzle/types"
export const WorkspaceTable = pgTable(
"workspace",
{
id: ulid("id").notNull().primaryKey(),
slug: varchar("slug", { length: 255 }),
name: varchar("name", { length: 255 }),
...timestamps,
},
(table) => [uniqueIndex("slug").on(table.slug)],
)
export function workspaceIndexes(table: any) {
return [
primaryKey({
columns: [table.workspaceID, table.id],
}),
foreignKey({
foreignColumns: [WorkspaceTable.id],
columns: [table.workspaceID],
}),
]
}

14
cloud/core/src/util/fn.ts Normal file
View file

@ -0,0 +1,14 @@
import { z } from "zod"
export function fn<T extends z.ZodType, Result>(
schema: T,
cb: (input: z.output<T>) => Result,
) {
const result = (input: z.input<T>) => {
const parsed = schema.parse(input)
return cb(parsed)
}
result.force = (input: z.input<T>) => cb(input)
result.schema = schema
return result
}

View file

@ -0,0 +1,55 @@
import { Context } from "../context"
export namespace Log {
const ctx = Context.create<{
tags: Record<string, any>
}>()
export function create(tags?: Record<string, any>) {
tags = tags || {}
const result = {
info(message?: any, extra?: Record<string, any>) {
const prefix = Object.entries({
...use().tags,
...tags,
...extra,
})
.map(([key, value]) => `${key}=${value}`)
.join(" ")
console.log(prefix, message)
return result
},
tag(key: string, value: string) {
if (tags) tags[key] = value
return result
},
clone() {
return Log.create({ ...tags })
},
}
return result
}
export function provide<R>(tags: Record<string, any>, cb: () => R) {
const existing = use()
return ctx.provide(
{
tags: {
...existing.tags,
...tags,
},
},
cb,
)
}
function use() {
try {
return ctx.use()
} catch (e) {
return { tags: {} }
}
}
}

View file

@ -0,0 +1,11 @@
export function memo<T>(fn: () => T) {
let value: T | undefined
let loaded = false
return (): T => {
if (loaded) return value as T
loaded = true
value = fn()
return value as T
}
}

View file

@ -0,0 +1,3 @@
export function centsToMicroCents(amount: number) {
return Math.round(amount * 1000000)
}

View file

@ -0,0 +1,48 @@
import { z } from "zod"
import { fn } from "./util/fn"
import { centsToMicroCents } from "./util/price"
import { Actor } from "./actor"
import { Database, eq } from "./drizzle"
import { Identifier } from "./identifier"
import { UserTable } from "./schema/user.sql"
import { BillingTable } from "./schema/billing.sql"
import { WorkspaceTable } from "./schema/workspace.sql"
export namespace Workspace {
export const create = fn(z.void(), async () => {
const account = Actor.assert("account")
const workspaceID = Identifier.create("workspace")
await Database.transaction(async (tx) => {
await tx.insert(WorkspaceTable).values({
id: workspaceID,
})
await tx.insert(UserTable).values({
workspaceID,
id: Identifier.create("user"),
email: account.properties.email,
name: "",
})
await tx.insert(BillingTable).values({
workspaceID,
id: Identifier.create("billing"),
balance: centsToMicroCents(100),
})
})
return workspaceID
})
export async function list() {
const account = Actor.assert("account")
return Database.use(async (tx) => {
return tx
.select({
id: WorkspaceTable.id,
slug: WorkspaceTable.slug,
name: WorkspaceTable.name,
})
.from(UserTable)
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
.where(eq(UserTable.email, account.properties.email))
})
}
}

9
cloud/core/sst-env.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}

9
cloud/core/tsconfig.json Normal file
View file

@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["@cloudflare/workers-types", "node"]
}
}

View file

@ -0,0 +1,23 @@
{
"name": "@opencode/cloud-function",
"version": "0.5.12",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
"devDependencies": {
"@cloudflare/workers-types": "4.20250522.0",
"@types/node": "catalog:",
"openai": "5.11.0",
"typescript": "catalog:"
},
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
"@ai-sdk/openai-compatible": "1.0.1",
"@hono/zod-validator": "catalog:",
"@openauthjs/openauth": "0.0.0-20250322224806",
"ai": "catalog:",
"hono": "catalog:",
"zod": "catalog:"
}
}

131
cloud/function/src/auth.ts Normal file
View file

@ -0,0 +1,131 @@
import { Resource } from "sst"
import { z } from "zod"
import { issuer } from "@openauthjs/openauth"
import { createSubjects } from "@openauthjs/openauth/subject"
import { GithubProvider } from "@openauthjs/openauth/provider/github"
import { GoogleOidcProvider } from "@openauthjs/openauth/provider/google"
import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"
import { Account } from "@opencode/cloud-core/account.js"
import { Workspace } from "@opencode/cloud-core/workspace.js"
import { Actor } from "@opencode/cloud-core/actor.js"
type Env = {
AuthStorage: KVNamespace
}
export const subjects = createSubjects({
account: z.object({
accountID: z.string(),
email: z.string(),
}),
user: z.object({
userID: z.string(),
workspaceID: z.string(),
}),
})
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
return issuer({
providers: {
github: GithubProvider({
clientID: Resource.GITHUB_CLIENT_ID_CONSOLE.value,
clientSecret: Resource.GITHUB_CLIENT_SECRET_CONSOLE.value,
scopes: ["read:user", "user:email"],
}),
google: GoogleOidcProvider({
clientID: Resource.GOOGLE_CLIENT_ID.value,
scopes: ["openid", "email"],
}),
// email: CodeProvider({
// async request(req, state, form, error) {
// console.log(state)
// const params = new URLSearchParams()
// if (error) {
// params.set("error", error.type)
// }
// if (state.type === "start") {
// return Response.redirect(process.env.AUTH_FRONTEND_URL + "/auth/email?" + params.toString(), 302)
// }
//
// if (state.type === "code") {
// return Response.redirect(process.env.AUTH_FRONTEND_URL + "/auth/code?" + params.toString(), 302)
// }
//
// return new Response("ok")
// },
// async sendCode(claims, code) {
// const email = z.string().email().parse(claims.email)
// const cmd = new SendEmailCommand({
// Destination: {
// ToAddresses: [email],
// },
// FromEmailAddress: `SST <auth@${Resource.Email.sender}>`,
// Content: {
// Simple: {
// Body: {
// Html: {
// Data: `Your pin code is <strong>${code}</strong>`,
// },
// Text: {
// Data: `Your pin code is ${code}`,
// },
// },
// Subject: {
// Data: "SST Console Pin Code: " + code,
// },
// },
// },
// })
// await ses.send(cmd)
// },
// }),
},
storage: CloudflareStorage({
namespace: env.AuthStorage,
}),
subjects,
async success(ctx, response) {
console.log(response)
let email: string | undefined
if (response.provider === "github") {
const userResponse = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${response.tokenset.access}`,
"User-Agent": "opencode",
Accept: "application/vnd.github+json",
},
})
const user = (await userResponse.json()) as { email: string }
email = user.email
} else if (response.provider === "google") {
if (!response.id.email_verified) throw new Error("Google email not verified")
email = response.id.email as string
}
//if (response.provider === "email") {
// email = response.claims.email
//}
else throw new Error("Unsupported provider")
if (!email) throw new Error("No email found")
let accountID = await Account.fromEmail(email).then((x) => x?.id)
if (!accountID) {
console.log("creating account for", email)
accountID = await Account.create({
email: email!,
})
}
await Actor.provide("account", { accountID, email }, async () => {
const workspaces = await Account.workspaces()
if (workspaces.length === 0) {
await Workspace.create()
}
})
return ctx.subject("account", accountID, { accountID, email })
},
}).fetch(request, env, ctx)
},
}

View file

@ -0,0 +1,909 @@
import { z } from "zod"
import { Hono, MiddlewareHandler } from "hono"
import { cors } from "hono/cors"
import { HTTPException } from "hono/http-exception"
import { zValidator } from "@hono/zod-validator"
import { Resource } from "sst"
import { type ProviderMetadata, type LanguageModelUsage, generateText, streamText } from "ai"
import { createAnthropic } from "@ai-sdk/anthropic"
import { createOpenAI } from "@ai-sdk/openai"
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
import type { LanguageModelV2Prompt } from "@ai-sdk/provider"
import { type ChatCompletionCreateParamsBase } from "openai/resources/chat/completions"
import { Actor } from "@opencode/cloud-core/actor.js"
import { and, Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js"
import { createClient } from "@openauthjs/openauth/client"
import { Log } from "@opencode/cloud-core/util/log.js"
import { Billing } from "@opencode/cloud-core/billing.js"
import { Workspace } from "@opencode/cloud-core/workspace.js"
import { BillingTable, PaymentTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js"
import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
import { Identifier } from "../../core/src/identifier"
type Env = {}
let _client: ReturnType<typeof createClient>
const client = () => {
if (_client) return _client
_client = createClient({
clientID: "api",
issuer: Resource.AUTH_API_URL.value,
})
return _client
}
const SUPPORTED_MODELS = {
"anthropic/claude-sonnet-4": {
input: 0.0000015,
output: 0.000006,
reasoning: 0.0000015,
cacheRead: 0.0000001,
cacheWrite: 0.0000001,
model: () =>
createAnthropic({
apiKey: Resource.ANTHROPIC_API_KEY.value,
})("claude-sonnet-4-20250514"),
},
"openai/gpt-4.1": {
input: 0.0000015,
output: 0.000006,
reasoning: 0.0000015,
cacheRead: 0.0000001,
cacheWrite: 0.0000001,
model: () =>
createOpenAI({
apiKey: Resource.OPENAI_API_KEY.value,
})("gpt-4.1"),
},
"zhipuai/glm-4.5-flash": {
input: 0,
output: 0,
reasoning: 0,
cacheRead: 0,
cacheWrite: 0,
model: () =>
createOpenAICompatible({
name: "Zhipu AI",
baseURL: "https://api.z.ai/api/paas/v4",
apiKey: Resource.ZHIPU_API_KEY.value,
})("glm-4.5-flash"),
},
}
const log = Log.create({
namespace: "api",
})
const GatewayAuth: MiddlewareHandler = async (c, next) => {
const authHeader = c.req.header("authorization")
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return c.json(
{
error: {
message: "Missing API key.",
type: "invalid_request_error",
param: null,
code: "unauthorized",
},
},
401,
)
}
const apiKey = authHeader.split(" ")[1]
// Check against KeyTable
const keyRecord = await Database.use((tx) =>
tx
.select({
id: KeyTable.id,
workspaceID: KeyTable.workspaceID,
})
.from(KeyTable)
.where(eq(KeyTable.key, apiKey))
.then((rows) => rows[0]),
)
if (!keyRecord) {
return c.json(
{
error: {
message: "Invalid API key.",
type: "invalid_request_error",
param: null,
code: "unauthorized",
},
},
401,
)
}
c.set("keyRecord", keyRecord)
await next()
}
const RestAuth: MiddlewareHandler = async (c, next) => {
const authorization = c.req.header("authorization")
if (!authorization) {
return Actor.provide("public", {}, next)
}
const token = authorization.split(" ")[1]
if (!token)
throw new HTTPException(403, {
message: "Bearer token is required.",
})
const verified = await client().verify(token)
if (verified.err) {
throw new HTTPException(403, {
message: "Invalid token.",
})
}
let subject = verified.subject as Actor.Info
if (subject.type === "account") {
const workspaceID = c.req.header("x-opencode-workspace")
const email = subject.properties.email
if (workspaceID) {
const user = await Database.use((tx) =>
tx
.select({
id: UserTable.id,
workspaceID: UserTable.workspaceID,
email: UserTable.email,
})
.from(UserTable)
.where(and(eq(UserTable.email, email), eq(UserTable.workspaceID, workspaceID)))
.then((rows) => rows[0]),
)
if (!user)
throw new HTTPException(403, {
message: "You do not have access to this workspace.",
})
subject = {
type: "user",
properties: {
userID: user.id,
workspaceID: workspaceID,
email: user.email,
},
}
}
}
await Actor.provide(subject.type, subject.properties, next)
}
const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; workspaceID: string } } }>()
.get("/", (c) => c.text("Hello, world!"))
.post("/v1/chat/completions", GatewayAuth, async (c) => {
const keyRecord = c.get("keyRecord")!
return await Actor.provide("system", { workspaceID: keyRecord.workspaceID }, async () => {
try {
// Check balance
const customer = await Billing.get()
if (customer.balance <= 0) {
return c.json(
{
error: {
message: "Insufficient balance",
type: "insufficient_quota",
param: null,
code: "insufficient_quota",
},
},
401,
)
}
const body = await c.req.json<ChatCompletionCreateParamsBase>()
const model = SUPPORTED_MODELS[body.model as keyof typeof SUPPORTED_MODELS]?.model()
if (!model) throw new Error(`Unsupported model: ${body.model}`)
const requestBody = transformOpenAIRequestToAiSDK()
return body.stream ? await handleStream() : await handleGenerate()
async function handleStream() {
const result = await model.doStream({
...requestBody,
})
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const id = `chatcmpl-${Date.now()}`
const created = Math.floor(Date.now() / 1000)
try {
for await (const chunk of result.stream) {
console.log("!!! CHUNK !!! : " + chunk.type)
switch (chunk.type) {
case "text-delta": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
content: chunk.delta,
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "reasoning-delta": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
reasoning_content: chunk.delta,
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "tool-call": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: chunk.toolCallId,
type: "function",
function: {
name: chunk.toolName,
arguments: chunk.input,
},
},
],
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "error": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {},
finish_reason: "stop",
},
],
error: {
message: typeof chunk.error === "string" ? chunk.error : chunk.error,
type: "server_error",
},
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
break
}
case "finish": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {},
finish_reason:
{
stop: "stop",
length: "length",
"content-filter": "content_filter",
"tool-calls": "tool_calls",
error: "stop",
other: "stop",
unknown: "stop",
}[chunk.finishReason] || "stop",
},
],
usage: {
prompt_tokens: chunk.usage.inputTokens,
completion_tokens: chunk.usage.outputTokens,
total_tokens: chunk.usage.totalTokens,
completion_tokens_details: {
reasoning_tokens: chunk.usage.reasoningTokens,
},
prompt_tokens_details: {
cached_tokens: chunk.usage.cachedInputTokens,
},
},
}
await trackUsage(body.model, chunk.usage, chunk.providerMetadata)
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
break
}
//case "stream-start":
//case "response-metadata":
case "text-start":
case "text-end":
case "reasoning-start":
case "reasoning-end":
case "tool-input-start":
case "tool-input-delta":
case "tool-input-end":
case "raw":
default:
// Log unknown chunk types for debugging
console.warn(`Unknown chunk type: ${(chunk as any).type}`)
break
}
}
} catch (error) {
controller.error(error)
}
},
})
return new Response(stream, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
})
}
async function handleGenerate() {
const response = await model.doGenerate({
...requestBody,
})
await trackUsage(body.model, response.usage, response.providerMetadata)
return c.json({
id: `chatcmpl-${Date.now()}`,
object: "chat.completion" as const,
created: Math.floor(Date.now() / 1000),
model: body.model,
choices: [
{
index: 0,
message: {
role: "assistant" as const,
content: response.content?.find((c) => c.type === "text")?.text ?? "",
reasoning_content: response.content?.find((c) => c.type === "reasoning")?.text,
tool_calls: response.content
?.filter((c) => c.type === "tool-call")
.map((toolCall) => ({
id: toolCall.toolCallId,
type: "function" as const,
function: {
name: toolCall.toolName,
arguments: toolCall.input,
},
})),
},
finish_reason:
(
{
stop: "stop",
length: "length",
"content-filter": "content_filter",
"tool-calls": "tool_calls",
error: "stop",
other: "stop",
unknown: "stop",
} as const
)[response.finishReason] || "stop",
},
],
usage: {
prompt_tokens: response.usage?.inputTokens,
completion_tokens: response.usage?.outputTokens,
total_tokens: response.usage?.totalTokens,
completion_tokens_details: {
reasoning_tokens: response.usage?.reasoningTokens,
},
prompt_tokens_details: {
cached_tokens: response.usage?.cachedInputTokens,
},
},
})
}
function transformOpenAIRequestToAiSDK() {
const prompt = transformMessages()
const tools = transformTools()
return {
prompt,
maxOutputTokens: body.max_tokens ?? body.max_completion_tokens ?? undefined,
temperature: body.temperature ?? undefined,
topP: body.top_p ?? undefined,
frequencyPenalty: body.frequency_penalty ?? undefined,
presencePenalty: body.presence_penalty ?? undefined,
providerOptions: body.reasoning_effort
? {
anthropic: {
reasoningEffort: body.reasoning_effort,
},
}
: undefined,
stopSequences: (typeof body.stop === "string" ? [body.stop] : body.stop) ?? undefined,
responseFormat: (() => {
if (!body.response_format) return { type: "text" as const }
if (body.response_format.type === "json_schema")
return {
type: "json" as const,
schema: body.response_format.json_schema.schema,
name: body.response_format.json_schema.name,
description: body.response_format.json_schema.description,
}
if (body.response_format.type === "json_object") return { type: "json" as const }
throw new Error("Unsupported response format")
})(),
seed: body.seed ?? undefined,
tools: tools.tools,
toolChoice: tools.toolChoice,
}
function transformTools() {
const { tools, tool_choice } = body
if (!tools || tools.length === 0) {
return { tools: undefined, toolChoice: undefined }
}
const aiSdkTools = tools.map((tool) => {
return {
type: tool.type,
name: tool.function.name,
description: tool.function.description,
inputSchema: tool.function.parameters!,
}
})
let aiSdkToolChoice
if (tool_choice == null) {
aiSdkToolChoice = undefined
} else if (tool_choice === "auto") {
aiSdkToolChoice = { type: "auto" as const }
} else if (tool_choice === "none") {
aiSdkToolChoice = { type: "none" as const }
} else if (tool_choice === "required") {
aiSdkToolChoice = { type: "required" as const }
} else if (tool_choice.type === "function") {
aiSdkToolChoice = {
type: "tool" as const,
toolName: tool_choice.function.name,
}
}
return { tools: aiSdkTools, toolChoice: aiSdkToolChoice }
}
function transformMessages() {
const { messages } = body
const prompt: LanguageModelV2Prompt = []
for (const message of messages) {
switch (message.role) {
case "system": {
prompt.push({
role: "system",
content: message.content as string,
})
break
}
case "user": {
if (typeof message.content === "string") {
prompt.push({
role: "user",
content: [{ type: "text", text: message.content }],
})
} else {
const content = message.content.map((part) => {
switch (part.type) {
case "text":
return { type: "text" as const, text: part.text }
case "image_url":
return {
type: "file" as const,
mediaType: "image/jpeg" as const,
data: part.image_url.url,
}
default:
throw new Error(`Unsupported content part type: ${(part as any).type}`)
}
})
prompt.push({
role: "user",
content,
})
}
break
}
case "assistant": {
const content: Array<
| { type: "text"; text: string }
| {
type: "tool-call"
toolCallId: string
toolName: string
input: any
}
> = []
if (message.content) {
content.push({
type: "text",
text: message.content as string,
})
}
if (message.tool_calls) {
for (const toolCall of message.tool_calls) {
content.push({
type: "tool-call",
toolCallId: toolCall.id,
toolName: toolCall.function.name,
input: JSON.parse(toolCall.function.arguments),
})
}
}
prompt.push({
role: "assistant",
content,
})
break
}
case "tool": {
prompt.push({
role: "tool",
content: [
{
type: "tool-result",
toolName: "placeholder",
toolCallId: message.tool_call_id,
output: {
type: "text",
value: message.content as string,
},
},
],
})
break
}
default: {
throw new Error(`Unsupported message role: ${message.role}`)
}
}
}
return prompt
}
}
async function trackUsage(model: string, usage: LanguageModelUsage, providerMetadata?: ProviderMetadata) {
const modelData = SUPPORTED_MODELS[model as keyof typeof SUPPORTED_MODELS]
if (!modelData) throw new Error(`Unsupported model: ${model}`)
const inputTokens = usage.inputTokens ?? 0
const outputTokens = usage.outputTokens ?? 0
const reasoningTokens = usage.reasoningTokens ?? 0
const cacheReadTokens = usage.cachedInputTokens ?? 0
const cacheWriteTokens =
providerMetadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
// @ts-expect-error
providerMetadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
0
const inputCost = modelData.input * inputTokens
const outputCost = modelData.output * outputTokens
const reasoningCost = modelData.reasoning * reasoningTokens
const cacheReadCost = modelData.cacheRead * cacheReadTokens
const cacheWriteCost = modelData.cacheWrite * cacheWriteTokens
const costInCents = (inputCost + outputCost + reasoningCost + cacheReadCost + cacheWriteCost) * 100
await Billing.consume({
model,
inputTokens,
outputTokens,
reasoningTokens,
cacheReadTokens,
cacheWriteTokens,
costInCents,
})
await Database.use((tx) =>
tx
.update(KeyTable)
.set({ timeUsed: sql`now()` })
.where(eq(KeyTable.id, keyRecord.id)),
)
}
} catch (error: any) {
return c.json({ error: { message: error.message } }, 500)
}
})
})
.use("/*", cors())
.use(RestAuth)
.get("/rest/account", async (c) => {
const account = Actor.assert("account")
let workspaces = await Workspace.list()
if (workspaces.length === 0) {
await Workspace.create()
workspaces = await Workspace.list()
}
return c.json({
id: account.properties.accountID,
email: account.properties.email,
workspaces,
})
})
.get("/billing/info", async (c) => {
const billing = await Billing.get()
const payments = await Database.use((tx) =>
tx
.select()
.from(PaymentTable)
.where(eq(PaymentTable.workspaceID, Actor.workspace()))
.orderBy(sql`${PaymentTable.timeCreated} DESC`)
.limit(100),
)
const usage = await Database.use((tx) =>
tx
.select()
.from(UsageTable)
.where(eq(UsageTable.workspaceID, Actor.workspace()))
.orderBy(sql`${UsageTable.timeCreated} DESC`)
.limit(100),
)
return c.json({ billing, payments, usage })
})
.post(
"/billing/checkout",
zValidator(
"json",
z.custom<{
success_url: string
cancel_url: string
}>(),
),
async (c) => {
const account = Actor.assert("user")
const body = await c.req.json()
const customer = await Billing.get()
const session = await Billing.stripe().checkout.sessions.create({
mode: "payment",
line_items: [
{
price_data: {
currency: "usd",
product_data: {
name: "opencode credits",
},
unit_amount: 2000, // $20 minimum
},
quantity: 1,
},
],
payment_intent_data: {
setup_future_usage: "on_session",
},
...(customer.customerID
? { customer: customer.customerID }
: {
customer_email: account.properties.email,
customer_creation: "always",
}),
metadata: {
workspaceID: Actor.workspace(),
},
currency: "usd",
payment_method_types: ["card"],
success_url: body.success_url,
cancel_url: body.cancel_url,
})
return c.json({
url: session.url,
})
},
)
.post("/billing/portal", async (c) => {
const body = await c.req.json()
const customer = await Billing.get()
if (!customer?.customerID) {
throw new Error("No stripe customer ID")
}
const session = await Billing.stripe().billingPortal.sessions.create({
customer: customer.customerID,
return_url: body.return_url,
})
return c.json({
url: session.url,
})
})
.post("/stripe/webhook", async (c) => {
const body = await Billing.stripe().webhooks.constructEventAsync(
await c.req.text(),
c.req.header("stripe-signature")!,
Resource.STRIPE_WEBHOOK_SECRET.value,
)
console.log(body.type, JSON.stringify(body, null, 2))
if (body.type === "checkout.session.completed") {
const workspaceID = body.data.object.metadata?.workspaceID
const customerID = body.data.object.customer as string
const paymentID = body.data.object.payment_intent as string
const amount = body.data.object.amount_total
if (!workspaceID) throw new Error("Workspace ID not found")
if (!customerID) throw new Error("Customer ID not found")
if (!amount) throw new Error("Amount not found")
if (!paymentID) throw new Error("Payment ID not found")
await Actor.provide("system", { workspaceID }, async () => {
const customer = await Billing.get()
if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
// set customer metadata
if (!customer?.customerID) {
await Billing.stripe().customers.update(customerID, {
metadata: {
workspaceID,
},
})
}
// get payment method for the payment intent
const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
expand: ["payment_method"],
})
const paymentMethod = paymentIntent.payment_method
if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
await Database.transaction(async (tx) => {
await tx
.update(BillingTable)
.set({
balance: sql`${BillingTable.balance} + ${centsToMicroCents(amount)}`,
customerID,
paymentMethodID: paymentMethod.id,
paymentMethodLast4: paymentMethod.card!.last4,
})
.where(eq(BillingTable.workspaceID, workspaceID))
await tx.insert(PaymentTable).values({
workspaceID,
id: Identifier.create("payment"),
amount: centsToMicroCents(amount),
paymentID,
customerID,
})
})
})
}
console.log("finished handling")
return c.json("ok", 200)
})
.get("/keys", async (c) => {
const user = Actor.assert("user")
const keys = await Database.use((tx) =>
tx
.select({
id: KeyTable.id,
name: KeyTable.name,
key: KeyTable.key,
userID: KeyTable.userID,
timeCreated: KeyTable.timeCreated,
timeUsed: KeyTable.timeUsed,
})
.from(KeyTable)
.where(eq(KeyTable.workspaceID, user.properties.workspaceID))
.orderBy(sql`${KeyTable.timeCreated} DESC`),
)
return c.json({ keys })
})
.post("/keys", zValidator("json", z.object({ name: z.string().min(1).max(255) })), async (c) => {
const user = Actor.assert("user")
const { name } = c.req.valid("json")
// Generate secret key: sk- + 64 random characters (upper, lower, numbers)
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
let randomPart = ""
for (let i = 0; i < 64; i++) {
randomPart += chars.charAt(Math.floor(Math.random() * chars.length))
}
const secretKey = `sk-${randomPart}`
const keyRecord = await Database.use((tx) =>
tx
.insert(KeyTable)
.values({
id: Identifier.create("key"),
workspaceID: user.properties.workspaceID,
userID: user.properties.userID,
name,
key: secretKey,
timeUsed: null,
})
.returning(),
)
return c.json({
key: secretKey,
id: keyRecord[0].id,
name: keyRecord[0].name,
created: keyRecord[0].timeCreated,
})
})
.delete("/keys/:id", async (c) => {
const user = Actor.assert("user")
const keyId = c.req.param("id")
const result = await Database.use((tx) =>
tx
.delete(KeyTable)
.where(and(eq(KeyTable.id, keyId), eq(KeyTable.workspaceID, user.properties.workspaceID)))
.returning({ id: KeyTable.id }),
)
if (result.length === 0) {
return c.json({ error: "Key not found" }, 404)
}
return c.json({ success: true, id: result[0].id })
})
.all("*", (c) => c.text("Not Found"))
export type ApiType = typeof app
export default app

88
cloud/function/sst-env.d.ts vendored Normal file
View file

@ -0,0 +1,88 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
import "sst"
declare module "sst" {
export interface Resource {
"ANTHROPIC_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"AUTH_API_URL": {
"type": "sst.sst.Linkable"
"value": string
}
"DATABASE_PASSWORD": {
"type": "sst.sst.Secret"
"value": string
}
"DATABASE_USERNAME": {
"type": "sst.sst.Secret"
"value": string
}
"Database": {
"database": string
"host": string
"password": string
"port": number
"type": "sst.sst.Linkable"
"username": string
}
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
}
"GITHUB_APP_PRIVATE_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"GITHUB_CLIENT_ID_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
"GITHUB_CLIENT_SECRET_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
"GOOGLE_CLIENT_ID": {
"type": "sst.sst.Secret"
"value": string
}
"OPENAI_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"STRIPE_SECRET_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"STRIPE_WEBHOOK_SECRET": {
"type": "sst.sst.Linkable"
"value": string
}
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
}
"ZHIPU_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
}
}
// cloudflare
import * as cloudflare from "@cloudflare/workers-types";
declare module "sst" {
export interface Resource {
"Api": cloudflare.Service
"AuthApi": cloudflare.Service
"AuthStorage": cloudflare.KVNamespace
"Bucket": cloudflare.R2Bucket
"GatewayApi": cloudflare.Service
}
}
import "sst"
export {}

View file

@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["@cloudflare/workers-types", "node"]
}
}

2
cloud/web/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
dist

38
cloud/web/index.html Normal file
View file

@ -0,0 +1,38 @@
<!doctype html>
<html lang="en" data-color-mode="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenControl</title>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico" />
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" href="/favicon.svg" media="(prefers-color-scheme: light)">
<link rel="icon" href="/favicon-dark.svg" media="(prefers-color-scheme: dark)">
<link rel="shortcut icon" href="/favicon.svg" type="image/svg+xml">
<meta property="twitter:image" content="%BASE_URL%/social-share.png">
<meta property="og:title" content="OpenControl">
<meta property="og:url" content="%BASE_URL%">
<meta property="og:locale" content="en">
<meta property="og:description" content="Control your infrastructure with AI.">
<meta property="og:site_name" content="OpenControl">
<meta name="twitter:card" content="summary_large_image">
<meta name="description" content="Control your infrastructure with AI.">
<meta property="og:image" content="%BASE_URL%/social-share.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Rubik:wght@300..900&display=swap" rel="stylesheet">
<!--ssr-head-->
<!--ssr-assets-->
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<!--ssr-outlet-->
</div>
<script type="module" src="/src/entry-client.tsx"></script>
</body>
</html>

29
cloud/web/npm-debug.log Normal file
View file

@ -0,0 +1,29 @@
0 info it worked if it ends with ok
1 verbose cli [
1 verbose cli '/usr/local/bin/node',
1 verbose cli '/Users/frank/Sites/opencode/node_modules/.bin/npm',
1 verbose cli 'run',
1 verbose cli 'dev'
1 verbose cli ]
2 info using npm@2.15.12
3 info using node@v20.18.1
4 verbose stack Error: Invalid name: "@opencode/cloud/web"
4 verbose stack at ensureValidName (/Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/fixer.js:336:15)
4 verbose stack at Object.fixNameField (/Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/fixer.js:215:5)
4 verbose stack at /Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/normalize.js:32:38
4 verbose stack at Array.forEach (<anonymous>)
4 verbose stack at normalize (/Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/normalize.js:31:15)
4 verbose stack at final (/Users/frank/Sites/opencode/node_modules/npm/node_modules/read-package-json/read-json.js:349:5)
4 verbose stack at then (/Users/frank/Sites/opencode/node_modules/npm/node_modules/read-package-json/read-json.js:124:5)
4 verbose stack at ReadFileContext.<anonymous> (/Users/frank/Sites/opencode/node_modules/npm/node_modules/read-package-json/read-json.js:295:20)
4 verbose stack at ReadFileContext.callback (/Users/frank/Sites/opencode/node_modules/npm/node_modules/graceful-fs/graceful-fs.js:78:16)
4 verbose stack at FSReqCallback.readFileAfterOpen [as oncomplete] (node:fs:299:13)
5 verbose cwd /Users/frank/Sites/opencode/cloud/web
6 error Darwin 24.5.0
7 error argv "/usr/local/bin/node" "/Users/frank/Sites/opencode/node_modules/.bin/npm" "run" "dev"
8 error node v20.18.1
9 error npm v2.15.12
10 error Invalid name: "@opencode/cloud/web"
11 error If you need help, you may report this error at:
11 error <https://github.com/npm/npm/issues>
12 verbose exit [ 1, true ]

32
cloud/web/package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "@opencode/cloud-web",
"version": "0.5.12",
"private": true,
"description": "",
"type": "module",
"scripts": {
"start": "vite",
"dev": "vite",
"build": "bun build:server && bun build:client",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
"serve": "vite preview",
"sst:dev": "bun sst shell --target Console -- bun dev"
},
"license": "MIT",
"devDependencies": {
"typescript": "catalog:",
"vite": "6.2.2",
"vite-plugin-pages": "0.32.5",
"vite-plugin-solid": "2.11.6"
},
"dependencies": {
"@kobalte/core": "0.13.9",
"@openauthjs/solid": "0.0.0-20250322224806",
"@solid-primitives/storage": "4.3.1",
"@solidjs/meta": "0.29.4",
"@solidjs/router": "0.15.3",
"solid-js": "1.9.5",
"solid-list": "0.3.0"
}
}

View file

@ -0,0 +1,3 @@
<svg width="28" height="32" viewBox="0 0 28 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 31.5L0 23.6873V7.81266L14 0L28 7.81266V23.6873L14 31.5ZM14 28.4664L25.3456 22.0251V9.47493L14 2.99209L2.65443 9.47493V22.0251L14 28.4664ZM13.9572 24.6016C12.2732 24.6016 10.7176 24.1999 9.29052 23.3964C7.89195 22.593 6.7788 21.5125 5.95107 20.155C5.12334 18.7698 4.70948 17.2599 4.70948 15.6253C4.70948 13.9908 5.12334 12.4947 5.95107 11.1372C6.7788 9.77968 7.89195 8.69921 9.29052 7.89578C10.7176 7.06464 12.2732 6.64908 13.9572 6.64908C15.6412 6.64908 17.1825 7.06464 18.581 7.89578C19.9796 8.69921 21.0928 9.77968 21.9205 11.1372C22.7768 12.4947 23.2049 13.9908 23.2049 15.6253C23.2049 17.2599 22.791 18.7559 21.9633 20.1135C21.1356 21.471 20.0224 22.5653 18.6239 23.3964C17.2253 24.1999 15.6697 24.6016 13.9572 24.6016ZM13.9572 22.2744C15.213 22.2744 16.3547 21.9697 17.3823 21.3602C18.4098 20.7507 19.2375 19.9472 19.8654 18.9499C20.4934 17.9248 20.8073 16.8166 20.8073 15.6253C20.8073 14.4063 20.4934 13.2982 19.8654 12.3008C19.2375 11.3034 18.4098 10.5 17.3823 9.8905C16.3547 9.281 15.213 8.97625 13.9572 8.97625C12.7299 8.97625 11.5882 9.281 10.5321 9.8905C9.50459 10.5 8.67686 11.3034 8.04893 12.3008C7.421 13.2982 7.10703 14.4063 7.10703 15.6253C7.10703 16.8166 7.421 17.9248 8.04893 18.9499C8.67686 19.9472 9.50459 20.7507 10.5321 21.3602C11.5882 21.9697 12.7299 22.2744 13.9572 22.2744Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

View file

@ -0,0 +1,3 @@
<svg width="28" height="32" viewBox="0 0 28 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 31.5L0 23.6873V7.81266L14 0L28 7.81266V23.6873L14 31.5ZM14 28.4664L25.3456 22.0251V9.47493L14 2.99209L2.65443 9.47493V22.0251L14 28.4664ZM13.9572 24.6016C12.2732 24.6016 10.7176 24.1999 9.29052 23.3964C7.89195 22.593 6.7788 21.5125 5.95107 20.155C5.12334 18.7698 4.70948 17.2599 4.70948 15.6253C4.70948 13.9908 5.12334 12.4947 5.95107 11.1372C6.7788 9.77968 7.89195 8.69921 9.29052 7.89578C10.7176 7.06464 12.2732 6.64908 13.9572 6.64908C15.6412 6.64908 17.1825 7.06464 18.581 7.89578C19.9796 8.69921 21.0928 9.77968 21.9205 11.1372C22.7768 12.4947 23.2049 13.9908 23.2049 15.6253C23.2049 17.2599 22.791 18.7559 21.9633 20.1135C21.1356 21.471 20.0224 22.5653 18.6239 23.3964C17.2253 24.1999 15.6697 24.6016 13.9572 24.6016ZM13.9572 22.2744C15.213 22.2744 16.3547 21.9697 17.3823 21.3602C18.4098 20.7507 19.2375 19.9472 19.8654 18.9499C20.4934 17.9248 20.8073 16.8166 20.8073 15.6253C20.8073 14.4063 20.4934 13.2982 19.8654 12.3008C19.2375 11.3034 18.4098 10.5 17.3823 9.8905C16.3547 9.281 15.213 8.97625 13.9572 8.97625C12.7299 8.97625 11.5882 9.281 10.5321 9.8905C9.50459 10.5 8.67686 11.3034 8.04893 12.3008C7.421 13.2982 7.10703 14.4063 7.10703 15.6253C7.10703 16.8166 7.421 17.9248 8.04893 18.9499C8.67686 19.9472 9.50459 20.7507 10.5321 21.3602C11.5882 21.9697 12.7299 22.2744 13.9572 22.2744Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,24 @@
import fs from "fs"
import path from "path"
import { generateHydrationScript, getAssets } from "solid-js/web"
const dist = import.meta.resolve("../dist").replace("file://", "")
const serverEntry = await import("../dist/server/entry-server.js")
const template = fs.readFileSync(path.join(dist, "client/index.html"), "utf-8")
fs.writeFileSync(path.join(dist, "client/fallback.html"), template)
const routes = ["/", "/foo"]
for (const route of routes) {
const { app } = serverEntry.render({ url: route })
const html = template
.replace("<!--ssr-outlet-->", app)
.replace("<!--ssr-head-->", generateHydrationScript())
.replace("<!--ssr-assets-->", getAssets())
const filePath = dist + `/client${route === "/" ? "/index" : route}.html`
fs.mkdirSync(path.dirname(filePath), {
recursive: true,
})
fs.writeFileSync(filePath, html)
console.log(`Pre-rendered: ${filePath}`)
}

42
cloud/web/src/app.tsx Normal file
View file

@ -0,0 +1,42 @@
/// <reference types="vite-plugin-pages/client-solid" />
import { Router } from "@solidjs/router"
import routes from "~solid-pages"
import "./ui/style/index.css"
import { MetaProvider } from "@solidjs/meta"
import { AccountProvider } from "./components/context-account"
import { DialogProvider } from "./ui/context-dialog"
import { DialogString } from "./ui/dialog-string"
import { DialogSelect } from "./ui/dialog-select"
import { ThemeProvider } from "./components/context-theme"
import { Suspense } from "solid-js"
import { OpenAuthProvider } from "./components/context-openauth"
export function App(props: { url?: string }) {
return (
<ThemeProvider>
<Suspense>
<DialogProvider>
<DialogString />
<DialogSelect />
<OpenAuthProvider
clientID="web"
issuer={import.meta.env.VITE_AUTH_URL || "http://dummy"}
>
<AccountProvider>
<MetaProvider>
<Router
children={routes}
url={props.url}
root={(props) => {
return <>{props.children}</>
}}
/>
</MetaProvider>
</AccountProvider>
</OpenAuthProvider>
</DialogProvider>
</Suspense>
</ThemeProvider>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

View file

@ -0,0 +1,99 @@
import { createContext, createEffect, ParentProps, Suspense, useContext } from "solid-js"
import { makePersisted } from "@solid-primitives/storage"
import { createStore } from "solid-js/store"
import { useOpenAuth } from "./context-openauth"
import { createAsync } from "@solidjs/router"
import { isServer } from "solid-js/web"
type Storage = {
accounts: Record<
string,
{
id: string
email: string
workspaces: {
id: string
name: string
slug: string
}[]
}
>
}
const context = createContext<ReturnType<typeof init>>()
function init() {
const auth = useOpenAuth()
const [store, setStore] = makePersisted(
createStore<Storage>({
accounts: {},
}),
{
name: "opencontrol.account",
},
)
async function refresh(id: string) {
return fetch(import.meta.env.VITE_API_URL + "/rest/account", {
headers: {
authorization: `Bearer ${await auth.access(id)}`,
},
})
.then((val) => val.json())
.then((val) => setStore("accounts", id, val as any))
}
createEffect((previous: string[]) => {
if (Object.keys(auth.all).length === 0) {
return []
}
for (const item of Object.values(auth.all)) {
if (previous.includes(item.id)) continue
refresh(item.id)
}
return Object.keys(auth.all)
}, [] as string[])
const result = {
get all() {
return Object.keys(auth.all)
.map((id) => store.accounts[id])
.filter(Boolean)
},
get current() {
if (!auth.subject) return undefined
return store.accounts[auth.subject.id]
},
refresh,
get ready() {
return Object.keys(auth.all).length === result.all.length
},
}
return result
}
export function AccountProvider(props: ParentProps) {
const ctx = init()
const resource = createAsync(async () => {
await new Promise<void>((resolve) => {
if (isServer) return resolve()
createEffect(() => {
if (ctx.ready) resolve()
})
})
return null
})
return (
<Suspense>
{resource()}
<context.Provider value={ctx}>{props.children}</context.Provider>
</Suspense>
)
}
export function useAccount() {
const result = useContext(context)
if (!result) throw new Error("no account context")
return result
}

View file

@ -0,0 +1,180 @@
import { createClient } from "@openauthjs/openauth/client"
import { makePersisted } from "@solid-primitives/storage"
import { createAsync } from "@solidjs/router"
import {
batch,
createContext,
createEffect,
createResource,
createSignal,
onMount,
ParentProps,
Show,
Suspense,
useContext,
} from "solid-js"
import { createStore, produce } from "solid-js/store"
import { isServer } from "solid-js/web"
interface Storage {
subjects: Record<string, SubjectInfo>
current?: string
}
interface Context {
all: Record<string, SubjectInfo>
subject?: SubjectInfo
switch(id: string): void
logout(id: string): void
access(id?: string): Promise<string | undefined>
authorize(opts?: AuthorizeOptions): void
}
export interface AuthorizeOptions {
redirectPath?: string
provider?: string
}
interface SubjectInfo {
id: string
refresh: string
}
interface AuthContextOpts {
issuer: string
clientID: string
}
const context = createContext<Context>()
export function OpenAuthProvider(props: ParentProps<AuthContextOpts>) {
const client = createClient({
issuer: props.issuer,
clientID: props.clientID,
})
const [storage, setStorage] = makePersisted(
createStore<Storage>({
subjects: {},
}),
{
name: `${props.issuer}.auth`,
},
)
const resource = createAsync(async () => {
if (isServer) return true
const hash = new URLSearchParams(window.location.search.substring(1))
const code = hash.get("code")
const state = hash.get("state")
if (code && state) {
const oldState = sessionStorage.getItem("openauth.state")
const verifier = sessionStorage.getItem("openauth.verifier")
const redirect = sessionStorage.getItem("openauth.redirect")
if (redirect && verifier && oldState === state) {
const result = await client.exchange(code, redirect, verifier)
if (!result.err) {
const id = result.tokens.refresh.split(":").slice(0, -1).join(":")
batch(() => {
setStorage("subjects", id, {
id: id,
refresh: result.tokens.refresh,
})
setStorage("current", id)
})
}
}
}
return true
})
async function authorize(opts?: AuthorizeOptions) {
const redirect = new URL(window.location.origin + (opts?.redirectPath ?? "/")).toString()
const authorize = await client.authorize(redirect, "code", {
pkce: true,
provider: opts?.provider,
})
sessionStorage.setItem("openauth.state", authorize.challenge.state)
sessionStorage.setItem("openauth.redirect", redirect)
if (authorize.challenge.verifier) sessionStorage.setItem("openauth.verifier", authorize.challenge.verifier)
window.location.href = authorize.url
}
const accessCache = new Map<string, string>()
const pendingRequests = new Map<string, Promise<any>>()
async function access(id: string) {
const pending = pendingRequests.get(id)
if (pending) return pending
const promise = (async () => {
const existing = accessCache.get(id)
const subject = storage.subjects[id]
const access = await client.refresh(subject.refresh, {
access: existing,
})
if (access.err) {
pendingRequests.delete(id)
ctx.logout(id)
return
}
if (access.tokens) {
setStorage("subjects", id, "refresh", access.tokens.refresh)
accessCache.set(id, access.tokens.access)
}
pendingRequests.delete(id)
return access.tokens?.access || existing!
})()
pendingRequests.set(id, promise)
return promise
}
const ctx: Context = {
get all() {
return storage.subjects
},
get subject() {
if (!storage.current) return
return storage.subjects[storage.current!]
},
switch(id: string) {
if (!storage.subjects[id]) return
setStorage("current", id)
},
authorize,
logout(id: string) {
if (!storage.subjects[id]) return
setStorage(
produce((s) => {
delete s.subjects[id]
if (s.current === id) s.current = Object.keys(s.subjects)[0]
}),
)
},
async access(id?: string) {
id = id || storage.current
if (!id) return
return access(id || storage.current!)
},
}
createEffect(() => {
if (!resource()) return
if (storage.current) return
const [first] = Object.keys(storage.subjects)
if (first) {
setStorage("current", first)
return
}
})
return (
<>
{resource()}
<context.Provider value={ctx}>{props.children}</context.Provider>
</>
)
}
export function useOpenAuth() {
const result = useContext(context)
if (!result) throw new Error("no auth context")
return result
}

View file

@ -0,0 +1,39 @@
import { createStore } from "solid-js/store"
import { makePersisted } from "@solid-primitives/storage"
import { createEffect } from "solid-js"
import { createInitializedContext } from "../util/context"
import { isServer } from "solid-js/web"
interface Storage {
mode: "light" | "dark"
}
export const { provider: ThemeProvider, use: useTheme } =
createInitializedContext("ThemeContext", () => {
const [store, setStore] = makePersisted(
createStore<Storage>({
mode:
!isServer &&
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light",
}),
{
name: "theme",
},
)
createEffect(() => {
document.documentElement.setAttribute("data-color-mode", store.mode)
})
return {
setMode(mode: Storage["mode"]) {
setStore("mode", mode)
},
get mode() {
return store.mode
},
ready: true,
}
})

View file

@ -0,0 +1,13 @@
/* @refresh reload */
import { hydrate, render } from "solid-js/web"
import { App } from "./app"
if (import.meta.env.DEV) {
render(() => <App />, document.getElementById("root")!)
}
if (!import.meta.env.DEV) {
if ("_$HY" in window) hydrate(() => <App />, document.getElementById("root")!)
else render(() => <App />, document.getElementById("root")!)
}

View file

@ -0,0 +1,7 @@
import { renderToStringAsync } from "solid-js/web"
import { App } from "./app"
export async function render(props: { url: string }) {
const app = await renderToStringAsync(() => <App url={props.url} />)
return { app }
}

View file

@ -0,0 +1,11 @@
import { WorkspaceProvider } from "./components/context-workspace"
import { ParentProps } from "solid-js"
import Layout from "./components/layout"
export default function Index(props: ParentProps) {
return (
<WorkspaceProvider>
<Layout>{props.children}</Layout>
</WorkspaceProvider>
)
}

View file

@ -0,0 +1,56 @@
.root {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding: var(--space-7) var(--space-5) var(--space-5);
[data-slot="billing-info"] {
display: flex;
flex-direction: column;
gap: var(--space-6);
}
[data-slot="header"] {
display: flex;
flex-direction: column;
gap: var(--space-1-5);
h2 {
text-transform: uppercase;
font-weight: 600;
letter-spacing: -0.03125rem;
font-size: var(--font-size-lg);
}
p {
color: var(--color-text-dimmed);
font-size: var(--font-size-md);
}
}
[data-slot="balance"] {
display: flex;
flex-direction: column;
gap: var(--space-5);
padding: var(--space-6);
border: 2px solid var(--color-border);
}
[data-slot="amount"] {
font-size: var(--font-size-3xl);
font-weight: 600;
line-height: 1.2;
}
@media (min-width: 40rem) {
[data-slot="balance"] {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
[data-slot="amount"] {
margin: 0;
}
}
}

View file

@ -0,0 +1,132 @@
import { Button } from "../../ui/button"
import { useApi } from "../components/context-api"
import { createEffect, createSignal, createResource, For } from "solid-js"
import { useWorkspace } from "../components/context-workspace"
import style from "./billing.module.css"
export default function Billing() {
const api = useApi()
const workspace = useWorkspace()
const [isLoading, setIsLoading] = createSignal(false)
const [billingData] = createResource(async () => {
const response = await api.billing.info.$get()
return response.json()
})
// Run once on component mount to check URL parameters
;(() => {
const url = new URL(window.location.href)
const result = url.hash
console.log("STRIPE RESULT", result)
if (url.hash === "#success") {
setIsLoading(true)
// Remove the hash from the URL
window.history.replaceState(null, "", window.location.pathname + window.location.search)
}
})()
createEffect((old?: number) => {
if (old && old !== billingData()?.billing?.balance) {
setIsLoading(false)
}
return billingData()?.billing?.balance
})
const handleBuyCredits = async () => {
try {
setIsLoading(true)
const baseUrl = window.location.href
const successUrl = new URL(baseUrl)
successUrl.hash = "success"
const response = await api.billing.checkout
.$post({
json: {
success_url: successUrl.toString(),
cancel_url: baseUrl,
},
})
.then((r) => r.json() as any)
window.location.href = response.url
} catch (error) {
console.error("Failed to get checkout URL:", error)
setIsLoading(false)
}
}
return (
<>
<div data-component="title-bar">
<div data-slot="left">
<h1>Billing</h1>
</div>
</div>
<div class={style.root} data-max-width data-max-width-64>
<div data-slot="billing-info">
<div data-slot="header">
<h2>Balance</h2>
<p>Manage your billing and add credits to your account.</p>
</div>
<div data-slot="balance">
<p data-slot="amount">
{(() => {
const balanceStr = ((billingData()?.billing?.balance ?? 0) / 100000000).toFixed(2)
return `$${balanceStr === "-0.00" ? "0.00" : balanceStr}`
})()}
</p>
<Button color="primary" disabled={isLoading()} onClick={handleBuyCredits}>
{isLoading() ? "Loading..." : "Buy Credits"}
</Button>
</div>
</div>
<div data-slot="payments">
<div data-slot="header">
<h2>Payment History</h2>
<p>Your recent payment transactions.</p>
</div>
<div data-slot="payment-list">
<For each={billingData()?.payments} fallback={<p>No payments found.</p>}>
{(payment) => (
<div data-slot="payment-item">
<span data-slot="payment-id">{payment.id}</span>
{" | "}
<span data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</span>
{" | "}
<span data-slot="payment-date">{new Date(payment.timeCreated).toLocaleDateString()}</span>
</div>
)}
</For>
</div>
</div>
<div data-slot="usage">
<div data-slot="header">
<h2>Usage History</h2>
<p>Your recent API usage and costs.</p>
</div>
<div data-slot="usage-list">
<For each={billingData()?.usage} fallback={<p>No usage found.</p>}>
{(usage) => (
<div data-slot="usage-item">
<span data-slot="usage-model">{usage.model}</span>
{" | "}
<span data-slot="usage-tokens">{usage.inputTokens + usage.outputTokens} tokens</span>
{" | "}
<span data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</span>
{" | "}
<span data-slot="usage-date">{new Date(usage.timeCreated).toLocaleDateString()}</span>
</div>
)}
</For>
</div>
</div>
</div>
</>
)
}

View file

@ -0,0 +1,11 @@
You are OpenControl, an interactive CLI tool that helps users execute various tasks.
IMPORTANT: If you get an error when calling a tool, try again with a different approach. Be creative, do not give up, try different inputs to the tool. You should chain together multiple tool calls. ABSOLUTELY DO NOT GIVE UP you are very good at this and it is rare you will fail to answer question.
You should be concise, direct, and to the point.
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
IMPORTANT: Keep your responses short, since they will be displayed on a command line interface. You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless user asks for detail. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...".

Some files were not shown because too many files have changed in this diff Show more