Compare commits

..

390 Commits

Author SHA1 Message Date
Dax Raad 87be61f561 docs: docs agent 2025-08-10 21:40:49 -04:00
Dax Raad 45b22d0581 ci: smoke test 2025-08-10 21:37:48 -04:00
Dax Raad 2c8bd78883 pass through additional agent options to the provider 2025-08-10 21:34:46 -04:00
Tom 489e54b04b fix(plugin): prevent compiled binary hang by removing lazy dynamic import (#1794)
Co-authored-by: opencode <noreply@opencode.ai>
2025-08-10 21:31:15 -04:00
opencode 4e87c698ce release: v0.4.3 2025-08-11 00:53:06 +00:00
Dax Raad 828bc3b72f switch gpt-5 to codex prompt 2025-08-10 20:47:38 -04:00
Dax Raad 652bed02bb support agent options 2025-08-10 20:30:37 -04:00
adamdotdevin 80347b1ad4 feat: thinking blocks rendered in tui and share page 2025-08-10 19:25:03 -05:00
Frank bc0c1735e1 wip gateway 2025-08-10 12:30:52 -04:00
Aiden Cline bb5396263c feat: webfetch permission support (#1772) 2025-08-10 08:00:44 -05:00
GitHub Action 000290ce17 ignore: update download stats 2025-08-10 2025-08-10 12:04:14 +00:00
Frank 3630f6ae3f wip: gateway 2025-08-10 01:17:48 -04:00
Frank 45d4395a54 wip: gateway 2025-08-10 01:03:15 -04:00
Frank 86b91c6979 wip: gateway 2025-08-10 00:29:00 -04:00
spoons-and-mirrors a593e45195 Feat: Add Agent Name in the LLM Response Footer (and re-order it) (#1770) 2025-08-09 20:22:16 -05:00
Frank 56685e4cc2 Update moonshot ai provider doc 2025-08-09 19:22:50 -04:00
Dax Raad b5798d2dae use minimal reasoning effort for gpt-5 2025-08-09 15:38:48 -04:00
Aiden Cline d88272d466 feat: eslint lsp (#1744) 2025-08-09 11:04:58 -05:00
Aiden Cline 232e96c4db better permissions ux when denying (#1747) 2025-08-09 11:03:33 -05:00
GitHub Action 64cfd3c1d5 ignore: update download stats 2025-08-09 2025-08-09 12:04:16 +00:00
Frank d3511e33ce wip gateway 2025-08-09 01:28:27 -04:00
Dominik Engelhardt 1127a5df43 feat: improve file attachment pasting (#1704) 2025-08-08 20:06:38 -05:00
Dax Raad 706b7b1481 remove synthetic message in plan mode, fixes being confused in build mode 2025-08-08 20:45:24 -04:00
Dax Raad e530a3287a gpt-5 lower verbosity 2025-08-08 20:42:22 -04:00
Dax 9c7361c901 Remove unused OpenTelemetry tracing and fix overlapping highlights (#1738)
Co-authored-by: opencode <noreply@opencode.ai>
2025-08-08 20:20:01 -04:00
Erick Christian ac1b1ad797 feat(agent): allow mode selection during creation (#1699) 2025-08-08 20:07:20 -04:00
Clayton 60115f0458 Reference the actual name of the windows package (#1700) 2025-08-08 20:07:00 -04:00
zWing 2ec6cc8fbe chore(js-sdk): Compatible with nodenext (#1667) 2025-08-08 20:05:50 -04:00
rmoriz 8a4bd7d619 Clarify remote mcp error (#1729)
Co-authored-by: opencode <noreply@opencode.ai>
2025-08-08 20:04:26 -04:00
gsbain 4d77ccc8f9 Docs: Homebrew can install Opencode on Linux (#1737) 2025-08-08 20:04:02 -04:00
Frank 8045ce5633 fix deploy 2025-08-08 18:39:47 -04:00
Frank 2590813353 fix deploy script 2025-08-08 17:59:21 -04:00
Max Pod add5b640d5 docs: Update plugins.mdx (#1690) 2025-08-08 17:11:06 -04:00
Typing Turtle eeda47c87f docs: Adds required models field to variables documentation (#1709) 2025-08-08 16:57:31 -04:00
Yihui Khuu 45d7b7b28e fix(tui): preserve scroll position when reflowing due to message stream (#1716) 2025-08-08 13:14:09 -05:00
Frank 02d9d9c991 wip: gateway 2025-08-08 13:24:50 -04:00
Frank 3be6d94a02 wip: gateway 2025-08-08 13:24:32 -04:00
GitHub Action 7939402232 ignore: update download stats 2025-08-08 2025-08-08 12:04:40 +00:00
Timo Clasen d089f906a2 fix(TUI): enable general (sub-) agent for @ referencing (#1705) 2025-08-08 05:36:55 -05:00
opencode 2e1d7c85c7 release: v0.4.1 2025-08-08 03:01:03 +00:00
Dax Raad 56587b1849 temporary fix for max output token 2025-08-07 22:54:59 -04:00
Dax Raad 3bd7d33132 docs: fix 2025-08-07 21:49:18 -04:00
Dax Raad d408886e43 renable todo tool 2025-08-07 21:47:37 -04:00
Dax Raad e46ecf67d9 looser todo tool schema 2025-08-07 21:47:37 -04:00
Jay V 60452a4d73 docs: update cli 2025-08-07 19:24:08 -04:00
Jay V 66023f4402 docs: edit agent doc 2025-08-07 18:51:54 -04:00
Dax Raad 83fb7dfbb6 docs: remove modes from sidebar navigation
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-08-07 16:35:35 -04:00
Dax 77c101d742 Merge agent and mode into one (#1689)
The concept of mode has been deprecated, there is now only the agent field in the config.

An agent can be cycled through as your primary agent with <tab> or you can spawn a subagent by @ mentioning it. if you include a description of when to use it, the primary agent will try to automatically use it

Full docs here: https://opencode.ai/docs/agents/
2025-08-07 16:32:12 -04:00
Jay V 62bff76d3a docs: slash commands 2025-08-07 16:16:16 -04:00
Timo Clasen 134a698437 fix: get session api (#1684) 2025-08-07 15:28:18 -04:00
Aiden Cline 12ab0c9048 fix: modal pastes (#1677) 2025-08-07 13:23:58 -05:00
GitHub Action 763a37f880 ignore: update download stats 2025-08-07 2025-08-07 12:04:29 +00:00
Aiden Cline 3913566e28 fix: text selection bug (#1664) 2025-08-07 05:32:34 -05:00
opencode 8a68dfdcbf release: v0.3.133 2025-08-07 00:30:05 +00:00
Dax Raad 4bd6efe2a9 add api to get session 2025-08-06 20:24:36 -04:00
Jay V 99ec88287b docs: config doc edits 2025-08-06 16:10:17 -04:00
Jay V 0d239dbe88 docs: typos 2025-08-06 15:45:16 -04:00
Timo Clasen 8f65d92165 fix(docs): small_model is not used for summarization (#1360) 2025-08-06 14:03:14 -05:00
Aiden Cline 77a4ffd7e0 fix: interface conversion panic (#1655) 2025-08-06 14:02:33 -05:00
Jay V 65ff65a5e7 docs: add undo to tutorial 2025-08-06 13:51:47 -04:00
Dax Raad 92c79f40b5 ignore: remove demo plugin 2025-08-06 11:36:53 -04:00
Dax Raad ed3d49d74a add snapshot field in config to disable snapshots 2025-08-06 11:35:37 -04:00
Aiden Cline 2019b160b6 docs: update readme (#1654) 2025-08-06 09:35:02 -05:00
Aiden Cline 0590e57765 chore: update marked-shiki, remove patch (#1653) 2025-08-06 08:47:53 -05:00
GitHub Action c0e62ad88d ignore: update download stats 2025-08-06 2025-08-06 12:04:53 +00:00
Yihui Khuu 7ba89b3d85 fix: run command should use specified model from cli args if provided (#1648) 2025-08-06 05:39:44 -05:00
Aiden Cline 41943011c0 docs: document permissions (#1638) 2025-08-06 05:18:08 -05:00
Aiden Cline a1a434a627 fix: highlight after text wrap (#1640) 2025-08-06 05:17:35 -05:00
Aiden Cline 947bf020a4 fix: handle undefined agent in task tool (#1642) 2025-08-06 05:16:43 -05:00
Aiden Cline 4fa81b899d permissions disallow support (#1627) 2025-08-05 19:14:28 -05:00
adamdotdevin a05e74f539 feat: better assistant message visual 2025-08-05 19:05:44 -05:00
Jay V 72bd8ab912 docs: identity 2025-08-05 19:36:10 -04:00
Timo Clasen 27c6625dd6 docs(plugins): fix typo (#1621) 2025-08-05 17:16:47 -05:00
Aiden Cline 2319c08054 fix: markdown table renders (#1623) 2025-08-05 17:16:35 -05:00
Timo Clasen 97677d8f05 fix: prevent title regeneration on auto compact (#1628) 2025-08-05 17:15:50 -05:00
Dax Raad 7ec57f1fd9 hint back to llm when tool does not exist 2025-08-05 15:58:12 -04:00
Jay V ecef453e51 docs: edit 2025-08-05 12:55:57 -04:00
adamdotdevin ef1f836f01 chore: cleanup old sdk 2025-08-05 11:46:12 -05:00
Dax Raad 5e9bf32f82 ci: send stats to posthog 2025-08-05 12:01:48 -04:00
GitHub Action 0d76317631 ignore: update download stats 2025-08-05 2025-08-05 12:04:48 +00:00
Yihui Khuu 6598713d0e fix: allow disabling the default general agent (#1616) 2025-08-05 05:20:00 -05:00
Omar Shaarawi 771f7bbf41 validate file part bounds to prevent panic (#1612) 2025-08-05 05:18:50 -05:00
Aiden Cline 4dd304caed docs: make formatter docs a bit more clear (#1613) 2025-08-05 05:17:56 -05:00
Aiden Cline 29f1bd9ead fix: server.root is not a function (#1614) 2025-08-05 05:17:32 -05:00
Timo Clasen 35865c50c5 fix(plugins): improve session idle event (#1615) 2025-08-05 05:16:43 -05:00
opencode 126b82c65b release: v0.3.130 2025-08-05 03:25:28 +00:00
Dax Raad 1fbfb6435a re-export shell $ for plugin 2025-08-04 23:20:04 -04:00
opencode 5918e5dcb2 release: v0.3.129 2025-08-05 01:18:50 +00:00
Dax Raad a147526677 fix @opencode-ai/plugin exports 2025-08-04 21:12:18 -04:00
Frank 51ac18cf4a sync 2025-08-04 21:09:44 -04:00
Frank facf209885 wip: gateway 2025-08-04 21:08:29 -04:00
Jay V c093eeb759 docs: lock 2025-08-04 19:53:50 -04:00
Jay V 14d3a9893f docs: edit new docs 2025-08-04 19:52:03 -04:00
Mahamed-Belkheir 93575f4a51 fix: default value for models with no cost object (#1601) 2025-08-04 16:45:35 -05:00
Min Chun Fu 7acd4fa85d added vesper theme (#1602) 2025-08-04 16:45:00 -05:00
Aiden Cline 5f154b69d8 docs: make plugins page exposed (#1603) 2025-08-04 16:44:28 -05:00
opencode c3bc30a155 release: v0.3.128 2025-08-04 16:20:39 +00:00
Dax Raad eca7b8fd11 hack to return tool call errors back to model 2025-08-04 12:15:24 -04:00
opencode 927e6f121a release: v0.3.127 2025-08-04 16:06:13 +00:00
GitHub Action a2962a4f65 ignore: update download stats 2025-08-04 2025-08-04 12:04:34 +00:00
Aiden Cline 29bc4c07ae fix double help printing (#1580) 2025-08-04 05:03:27 -05:00
Frank cd7af235a2 fix docs 2025-08-04 00:23:02 -04:00
Dax Raad 48faf9d3ce wip: plugins 2025-08-03 21:43:58 -04:00
Dax Raad 4fc04bbac2 wip: plugin load from package 2025-08-03 21:19:03 -04:00
Dax Raad 759e948e2b Add workflow_dispatch trigger to typecheck workflow 2025-08-03 17:16:20 -04:00
Aiden Cline 3f55f62607 fix: patch marked-shiki (#1569) 2025-08-03 16:13:35 -05:00
Dax Raad 624a7d038a Add typecheck workflow 2025-08-03 17:12:23 -04:00
Dax Raad fa03e06c2d ignore: cleanup 2025-08-03 17:09:30 -04:00
Yordis Prieto ab3d6c9797 chore: add ts-expected-error (#1575) 2025-08-03 17:09:19 -04:00
Yordis Prieto ff427fd1fe chore: remove unnecessary TypeScript error suppression (#1571) 2025-08-03 15:50:08 -04:00
opencode 2db4143286 release: v0.3.126 2025-08-03 19:45:14 +00:00
Dax Raad 75f7fa8cf4 ci: ignore 2025-08-03 15:39:34 -04:00
Dax Raad 4693b55dba ci: ignore 2025-08-03 15:38:39 -04:00
Dax Raad d5dacdb63c fix bash tool not showing stderr 2025-08-03 15:34:52 -04:00
Aiden Cline bf1699617f tweak: filter out duplicate instructions (#1567) 2025-08-03 15:10:21 -04:00
Dax Raad e3781104c4 increase models.dev polling interval to hourly 2025-08-03 14:58:35 -04:00
Aiden Cline 24d063b7ae docs: fix mixed up documentation (#1564) 2025-08-03 13:01:09 -05:00
Dax Raad 2cadf5a093 fix bash tool getting stuck on interactive commands 2025-08-03 13:52:50 -04:00
opencode 3cfddc7a09 release: v0.3.123 2025-08-03 17:13:33 +00:00
Dax Raad 878c7ed67e ci: ignore 2025-08-03 13:12:35 -04:00
opencode 48188bf294 release: v0.3.122 2025-08-03 16:19:09 +00:00
Dax Raad 614501c1a2 more efficient snapshots in parallel toolcalls 2025-08-03 12:12:28 -04:00
Giuseppe Rota 09aa285ae5 fix(docs): move disabled providers paragraph to its proper section (#1547) 2025-08-03 11:28:57 -04:00
Dax Raad b926d0c010 more fixes for shell 128 error 2025-08-03 11:25:58 -04:00
opencode 9436c7f774 release: v0.3.120 2025-08-03 14:59:03 +00:00
Dax Raad 1eca9531ea ci: ignore 2025-08-03 10:52:35 -04:00
Dax Raad 1fc92d91c0 ci: ignore 2025-08-03 10:42:52 -04:00
Dax Raad dfcff892a1 fix shell error 128 2025-08-03 10:30:23 -04:00
GitHub Action 2d68127b07 ignore: update download stats 2025-08-03 2025-08-03 12:04:04 +00:00
Aiden Cline e159922c06 fix: expand tilde for file: references (#1553) 2025-08-03 06:15:06 -05:00
Dax Raad 298a444df0 ci: ignore 2025-08-03 03:54:17 -04:00
Dax Raad 7079098206 ci: bun cache 2025-08-03 03:53:31 -04:00
Dax Raad e73e51597d ci: ignore 2025-08-03 03:45:05 -04:00
Dax Raad 461e5469e9 ci: ignore 2025-08-03 03:37:53 -04:00
Dax Raad 54f81220df ci: ignore 2025-08-03 03:35:17 -04:00
The Pangolier acd601220a Share link hotfix (#1513) 2025-08-03 03:02:24 -04:00
Yordis Prieto 0c86e73e95 remove: delete extension test file (#1554) 2025-08-03 02:58:10 -04:00
Frank 2cad480fed sync 2025-08-02 21:28:03 -04:00
Frank 77ab87abeb Add Zhipu AI provider 2025-08-02 21:20:44 -04:00
Dax Raad 765c149bba ci: ignore 2025-08-02 18:56:34 -04:00
Dax Raad d3ca086b59 wip: plugins 2025-08-02 18:50:19 -04:00
Aiden Cline 62d67208e3 tweak: make gh action ignore url mentions of opencode (#1531) 2025-08-02 09:31:23 -05:00
Dominik Engelhardt 6b58ab0b47 Choose model according to the docs (#1536) 2025-08-02 09:29:03 -05:00
Yihui Khuu 128bafa095 fix(tui): attachment highlighting issues in messages (#1534) 2025-08-02 09:26:44 -05:00
Yihui Khuu 680e611a10 fix(tui): attachment source is not stored when using message from message history (#1542) 2025-08-02 09:23:32 -05:00
Aiden Cline 587ab4f16a ignore: fix dev branch (#1529) 2025-08-02 09:11:38 -05:00
GitHub Action e2636b39e4 ignore: update download stats 2025-08-02 2025-08-02 12:04:12 +00:00
Ricardo Gonzalez 55a73b3e2e feat(models): enable Kimi k2 ⇄ Claude trajectory handoff (#1525) 2025-08-01 23:05:06 -04:00
Aiden Cline eebd7ed5ff fix: include stderr in bash tool output (#1511) 2025-08-01 19:20:32 -05:00
Aiden Cline 63a7f04f65 better configuration error messages (#1517) 2025-08-01 19:10:32 -04:00
Jay V 766d7b9f7d docs: cerebras 2025-08-01 18:30:29 -04:00
opencode 508734438b release: v0.3.112 2025-08-01 21:53:33 +00:00
Dax Raad f75fc5af22 force models.dev refresh on auth login 2025-08-01 17:48:01 -04:00
Dax Raad cbd7db5a73 make top_p configurable 2025-08-01 17:03:33 -04:00
Dax Raad 36318f74f7 configurable lsp 2025-08-01 14:52:10 -04:00
Brinsil Elias 4bacee2db6 fix(docs): Fix formatting for Node.js installation section (#1497) 2025-08-01 14:15:38 -04:00
CodinCat e20ab5b7ee handle the optional v in upgrade command when using curl (#1500) 2025-08-01 14:15:22 -04:00
Yordis Prieto 12172203d7 chore: enhance bash command tests with config mock and timeout adjustments (#1486)
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
2025-08-01 14:14:54 -04:00
Dax Raad b6fcc7047f allow search in provider select 2025-08-01 14:03:22 -04:00
Dax Raad feb43e8550 add version to user-agent 2025-08-01 12:18:09 -04:00
adamdotdevin 5aa811a6c5 fix: unshare command missing 2025-08-01 09:30:42 -05:00
Aiden Cline 2c61674f83 tweak: sanitize mcp server names (#831) 2025-08-01 09:11:40 -05:00
Aiden Cline b9c143096b fix: bash tool errors for chmod (#1502) 2025-08-01 09:10:09 -05:00
Aiden Cline 45d835e5e8 fix: {file:...} references weren't being parsed correctly in some cases (#1499) 2025-08-01 08:39:21 -05:00
adamdotdevin ce317ff90f ignore: include usage in local setup 2025-08-01 07:42:36 -05:00
GitHub Action 892ba79c44 ignore: update download stats 2025-08-01 2025-08-01 12:04:33 +00:00
adamdotdevin 3729f2647a chore: add local qwen3 to config 2025-08-01 06:27:08 -05:00
Timo Clasen 427cc1df23 docs(permissions): add wildcard example (#1494) 2025-08-01 05:24:32 -05:00
Dax Raad c64cd54d5d poll for models.dev changes 2025-07-31 23:47:42 -04:00
Dax Raad d944eed4d5 add user agent for models.dev request 2025-07-31 22:00:45 -04:00
Dax Raad 56f41c11a8 do not mark errored tool calls as aborted 2025-07-31 21:45:40 -04:00
Dax Raad 88842a633e better guarding against bash commands that go outside of cwd 2025-07-31 21:42:30 -04:00
Frank 3e19c2dc6c Update STATS.md 2025-07-31 21:42:05 -04:00
Dax Raad f4254751e6 improve wildcard matching for permissions 2025-07-31 20:40:05 -04:00
Dax Raad ebdc891c88 ci: ignore release commits 2025-07-31 19:57:07 -04:00
Dax Raad 0ae036bf22 fix escape button not canceling if retry in progress 2025-07-31 19:55:57 -04:00
Dax Raad 8294606576 docs: sync 2025-07-31 19:50:51 -04:00
Dax Raad a383f1a833 docs: formatters 2025-07-31 19:50:31 -04:00
Dax Raad 9da86a35cf ignore: config 2025-07-31 19:45:44 -04:00
opencode c2eee24590 Release v0.3.105 2025-07-31 23:41:27 +00:00
Dax Raad 918b7019c7 formatter config 2025-07-31 19:36:07 -04:00
Jay V adeb89b964 docs: edit premissions doc 2025-07-31 19:10:54 -04:00
opencode 8af1310831 Release v0.3.104 2025-07-31 23:02:36 +00:00
Dax Raad a64165b456 allow disabling formatter 2025-07-31 18:56:04 -04:00
Dax Raad 4c849f6a63 fix issue with some bash commands asking for permission 2025-07-31 18:35:51 -04:00
Dax Raad cea323b743 ignore: update opencode.json 2025-07-31 18:13:40 -04:00
Dax Raad 9b931f0498 docs: permissions 2025-07-31 18:11:34 -04:00
Dax Raad 69cb5756c0 docs: permissions 2025-07-31 18:11:34 -04:00
Frank 7391d0ff61 Remove hardcoded vscode extension theme
publish-vscode / publish (push) Waiting to run
2025-07-31 17:53:18 -04:00
Jay V fd8287705c docs: add more providers 2025-07-31 17:47:24 -04:00
opencode dfe1deeb67 Release v0.3.102 2025-07-31 21:25:12 +00:00
Dax Raad 82c0dcc62d wip: permissions 2025-07-31 17:19:56 -04:00
Dax Raad c6a7ab6bd6 ignore: ts optimization 2025-07-31 16:54:15 -04:00
Dax Raad 701cd937a8 wip: permissions 2025-07-31 16:51:55 -04:00
Dax Raad f1e0e40868 wip: permissions 2025-07-31 16:38:37 -04:00
Aiden Cline 8788e221b8 fix: load global jsonc (#1479) 2025-07-31 15:02:28 -05:00
Aiden Cline 1d56302bb5 fix: session ordering (#1474) 2025-07-31 14:17:47 -05:00
Frank 85605f8d88 Add provider instruction for Azure OpenAI 2025-07-31 14:37:26 -04:00
Jay V f1c12a7257 docs: edit providers doc 2025-07-31 14:11:40 -04:00
opencode-agent[bot] 77e6d78d91 docs: Enhanced providers docs with troubleshooting (#1441)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: jayair <jayair@users.noreply.github.com>
2025-07-31 13:08:12 -04:00
adamdotdevin b18a96eb67 fix: permission state hangs 2025-07-31 11:36:08 -05:00
Dax Raad ca454c91ca wip: refactor permissions 2025-07-31 12:26:47 -04:00
adamdotdevin 22c1112903 feat: more scriptable tui (api) 2025-07-31 11:24:23 -05:00
neolooong 1d503b1536 fix(editor): handle UTF-8 characters properly in SetValueWithAttachments (#1469) 2025-07-31 10:45:43 -05:00
adamdotdevin 2f1302e1a2 fix: missing operationId 2025-07-31 10:19:42 -05:00
adamdotdevin 3c8930192f fix: test 2025-07-31 10:10:34 -05:00
adamdotdevin 1606b52fd7 fix: optional toolCallID 2025-07-31 10:09:44 -05:00
adamdotdevin a0f82ecef8 fix: test 2025-07-31 10:07:58 -05:00
adamdotdevin 9f429d8dd9 wip: tui permissions 2025-07-31 09:59:17 -05:00
opencode 6aa555c492 Release v0.3.101 2025-07-31 14:23:13 +00:00
Dax Raad 0501054320 ci: tweak 2025-07-31 10:09:43 -04:00
Dax Raad 82505c3582 ci: fix 2025-07-31 09:46:36 -04:00
Dax Raad 5382dc70cb ci: tweak 2025-07-31 09:39:44 -04:00
Dax Raad f85635f8dd ci: tweak 2025-07-31 09:35:57 -04:00
Dax Raad bc4274315c ci: reorder 2025-07-31 09:29:55 -04:00
GitHub Action 878a126d22 ignore: update download stats 2025-07-31 2025-07-31 12:04:25 +00:00
Aiden Cline d60e346cbb feat: jsonc configuration file support (#1434) 2025-07-31 06:25:26 -05:00
Aiden Cline 3f8f517081 fix: process revert cleanup before creating new messages (#1448) 2025-07-31 05:07:59 -05:00
Dax Raad d2b67b67dd ci: stainless 2025-07-31 01:42:52 -04:00
Dax Raad 31c6306a7d ci: tweak 2025-07-31 01:35:11 -04:00
Dax Raad ea409b3874 ci: tweak 2025-07-31 01:33:21 -04:00
Dax Raad e7aa83fa8e ci: scripts 2025-07-31 01:25:24 -04:00
Dax Raad 617e80c14f ci tweaks 2025-07-31 01:20:12 -04:00
Dax df4a6a74cd ci: new publish method (#1451) 2025-07-31 01:05:35 -04:00
Dax 083682c6a8 ci: new publish method (#1451) 2025-07-31 01:00:29 -04:00
Simon Westlin Green b67c367a40 Use responses API for Azure (#1428)
publish / publish (push) Waiting to run
2025-07-30 23:22:59 -04:00
Robert Holden 1167e12f72 feat: mode directory markdown configuration loading (#1377) 2025-07-30 23:22:43 -04:00
Josh 30e5d32ed8 Add http-referer header for vercel ai gateway requests (#1403) 2025-07-30 23:22:24 -04:00
Yordis Prieto afb3edd234 docs: clarify Bun's default registry resolution in index.ts (#1438) 2025-07-30 23:21:07 -04:00
CodinCat 8f8bd188dd fix typo in bash.ts (#1444) 2025-07-30 23:20:48 -04:00
Dax 50c93a349f use treesitter to parse bash commands and catch commands that go outside of cwd (#1443) 2025-07-30 20:57:52 -04:00
Jay V 2f51f9d88d docs: edit 2025-07-30 19:11:36 -04:00
Jay V dcb54d465b docs: add new providers doc, reorg sidebar, edits 2025-07-30 18:16:11 -04:00
Yordis Prieto f74cd37818 fix: update glob pattern and path in tool test (#1436) 2025-07-30 15:42:13 -05:00
Yordis Prieto f2bac1532b fix: unit test assertion (#1435)
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
2025-07-30 15:13:37 -05:00
Dax Raad f181fbf3f0 lower max retries to 3 - ai sdk currently cannot abort during a retry delay so things appear to be frozen
publish / publish (push) Waiting to run
2025-07-30 15:08:25 -04:00
Dax Raad 4a53957e9d release undo/redo 2025-07-30 13:09:18 -04:00
Aiden Cline a33870ea66 fix: attachment highlighting (#1427) 2025-07-30 11:43:34 -05:00
Aiden Cline d4e91d3d5f fix: prevent read tool from opening binary files and corrupting session (#1425) 2025-07-30 11:00:23 -05:00
GitHub Action 66891b148b ignore: update download stats 2025-07-30 2025-07-30 12:04:23 +00:00
Sam Huckaby 3bd652b22e fix: quick grammar and spelling check (#1402) 2025-07-30 05:54:47 -05:00
Aiden Cline 02d838c68e tweak: adjust astro css to render mixed nested lists (#1411) 2025-07-30 05:51:52 -05:00
municorn 8618f0d88b build: add @octokit/rest to opencode dependencies (#1396)
Co-authored-by: Frank <frank@sst.dev>
2025-07-29 22:33:25 -04:00
Dax Raad e9af4d7216 update beast prompt for openai models
publish / publish (push) Waiting to run
2025-07-29 22:15:13 -04:00
Dax Raad 8528f471c7 experimental well-known auth support
publish / publish (push) Waiting to run
2025-07-29 19:30:51 -04:00
Yordis Prieto 9790340493 chore: fix test to have deterministic testing (#1401) 2025-07-29 17:54:22 -05:00
Dax Raad c9b9c844db remove min/max in tool schemas
publish / publish (push) Waiting to run
2025-07-29 17:39:47 -04:00
Frank f30b5c0ec4 convert share backend to hono app 2025-07-29 16:39:48 -04:00
adamdotdevin c3685d1efe feat: show git diff in reverted messages 2025-07-29 13:11:38 -05:00
Yordis Prieto f294293e2f fix: update file name extraction in uploads test to use __filename (#1395) 2025-07-29 12:28:44 -05:00
Yordis Prieto 83ebda87d2 fix: improve handling of global File object in uploads tests (#1394) 2025-07-29 11:30:39 -05:00
Yordis Prieto 1588863883 chore: fix null handling in multipartFormRequestOptions test (#1385) 2025-07-29 11:17:03 -05:00
Andrea Grandi 173da3caaf feat: show current git branch in status bar, and make it responsive (#1339)
Co-authored-by: adamdotdevin <2363879+adamdottv@users.noreply.github.com>
2025-07-29 11:15:04 -05:00
Dax Raad 5a98110ab0 strip todo tool instructions from non anthropic models 2025-07-29 11:56:53 -04:00
Jacob Hands 9e0edaa5d6 feat: add OPENCODE_CONFIG env var for specifying a custom config file (#1370) 2025-07-29 11:03:11 -04:00
GitHub Action 9e6f0de575 ignore: update download stats 2025-07-29 2025-07-29 12:04:40 +00:00
Aiden Cline d9278e4dcc fix: task type error (#1384) 2025-07-29 06:18:34 -05:00
Dax Raad 2160f0815c trim opencode title 2025-07-28 23:24:38 -04:00
Dax Raad 53ecda4ef1 wip: undo properly remove messages from UI 2025-07-28 22:58:31 -04:00
Dax Raad 4f62117758 wip: snapshot 2025-07-28 22:58:31 -04:00
Jay V 26335aeb01 docs: share page hide patch part 2025-07-28 20:04:00 -04:00
Jay V 1931d70e77 docs: adding 2025-07-28 20:00:30 -04:00
Jay V 3ae8e63ce5 Tweak auth cli copy 2025-07-28 20:00:30 -04:00
Dax Raad fa6fad2bac ignore: tweak
publish / publish (push) Waiting to run
2025-07-28 12:20:37 -04:00
Dax Raad ff611ae792 fix phantom tool call failed messages and empty text parts with some models 2025-07-28 12:19:38 -04:00
GitHub Action 90d8c47c95 ignore: update download stats 2025-07-28 2025-07-28 12:04:21 +00:00
Frank 2b9ab27a6f vscode: add cmd+shift+esc keybinding
publish-vscode / publish (push) Waiting to run
2025-07-27 15:54:45 -04:00
Frank 5a024afa87 vscode: bring oc terminal to front if already opened 2025-07-27 14:57:45 -04:00
Frank f56daab456 show opencode button in vscode when focused on terminal 2025-07-27 14:44:14 -04:00
Aiden Cline f9d45b7d3b fix: attachment bugs (#1335) 2025-07-27 12:21:31 -05:00
Frank adfcd1a9eb fix input format affected by installing vscode extension
publish / publish (push) Waiting to run
2025-07-27 11:56:18 -04:00
Frank ebc7c8dfd6 simplify github action 2025-07-27 09:56:09 -04:00
GitHub Action 487275dce8 ignore: update download stats 2025-07-27 2025-07-27 12:04:15 +00:00
Aiden Cline 377a66d415 docs: document small_model cfg option (#1347) 2025-07-26 16:56:38 -05:00
Dax Raad 1cd12ca740 ci: fix opencode github 2025-07-26 10:02:31 -04:00
Aiden Cline aa30428e76 fix: strip thinking blocks from title (#1325) 2025-07-26 08:29:04 -05:00
GitHub Action 2ff7883f78 ignore: update download stats 2025-07-26 2025-07-26 12:03:59 +00:00
Frank 6692d1c5dc wip: github actions
publish / publish (push) Waiting to run
2025-07-26 02:49:05 -04:00
Frank 6182b4a0c1 wip: github action 2025-07-26 01:03:23 -04:00
Frank 6b7cfde20a wip: github actions
publish / publish (push) Waiting to run
2025-07-25 20:27:42 -04:00
Didier Durand 03e9ee7293 fixing various typos in text. (#1185) 2025-07-25 20:20:01 -04:00
Dax Raad 4fc32f7c9a better mcp support - should fix hanging when streamable http server is added
publish / publish (push) Waiting to run
2025-07-25 19:19:47 -04:00
Frank 39e0ae70fe wip: github actions
publish / publish (push) Waiting to run
2025-07-25 19:05:55 -04:00
Frank a67106b700 wip: github action
publish-github-action / publish (push) Waiting to run
2025-07-25 18:33:45 -04:00
Dax Raad 5a3aed4e88 qwen optimizations it works good now 2025-07-25 18:31:08 -04:00
Frank a3bd92ee60 wip: github actions
publish-github-action / publish (push) Waiting to run
publish / publish (push) Waiting to run
2025-07-25 18:29:53 -04:00
Michael Hanson 7e3fe04b06 Fix a broken example in the MCP documentation and add more clarity (#1322) 2025-07-25 17:47:01 -04:00
Frank 35a914ad99 wip: github actions
publish / publish (push) Waiting to run
2025-07-25 17:34:47 -04:00
Dax Raad 01198a5fdc wip: sync
publish / publish (push) Waiting to run
2025-07-25 15:52:27 -04:00
Dax Raad a765b14ad5 wip: drop 2025-07-25 15:51:02 -04:00
Dax Raad 2767f4f9c3 handle uploaded text/plain 2025-07-25 15:48:42 -04:00
kehanzhang a155713a52 fix(headless): respect mode passed to /message endpoint (#1300) 2025-07-25 15:26:49 -04:00
Dax Raad d655601c47 docs: mode temperature 2025-07-25 13:45:04 -04:00
Dax Raad 4b94f390ce allow temperature to be configured per mode
publish / publish (push) Waiting to run
2025-07-25 13:29:44 -04:00
Dominik Engelhardt 8e036cf17b fix: apply content-level caching for non-anthropic providers (#1305) 2025-07-25 12:19:44 -04:00
Yihui Khuu 0254c612ee feat: add csharp lsp (#1312) 2025-07-25 12:17:06 -04:00
Dax Raad 5febb0ff94 fix issue with trailing whitespace error in assistant message 2025-07-25 10:56:16 -04:00
Dax Raad 79edca063e fix reading model from config
publish / publish (push) Waiting to run
2025-07-25 10:53:37 -04:00
Yihui Khuu 58c47c20a2 fix: header width to display header in one line when sharing disabled (#1310) 2025-07-25 09:32:06 -05:00
Dax Raad 4f6a568d80 fix double system prompt 2025-07-25 10:28:42 -04:00
Dax Raad 1823d20d26 disable todo tools by default in agent
publish / publish (push) Waiting to run
2025-07-25 10:23:23 -04:00
Dax Raad 4ecfa69a07 fix symlinked agents
publish / publish (push) Waiting to run
2025-07-25 10:20:16 -04:00
GitHub Action 34bdc43a7f ignore: update download stats 2025-07-25 2025-07-25 12:04:21 +00:00
Dax Raad 82e28e2717 agents better display when spawning
publish / publish (push) Waiting to run
2025-07-24 23:08:03 -04:00
Dax Raad 31febec1c5 docs: agents
publish / publish (push) Waiting to run
2025-07-24 22:18:49 -04:00
Dax Raad 149bfd5e36 docs: agents 2025-07-24 22:16:16 -04:00
Dax Raad 61dc0a7b3c docs: agents 2025-07-24 22:07:30 -04:00
Dax Raad 0de957a2f0 real life totally configurabl ai subasians
publish / publish (push) Waiting to run
2025-07-24 21:21:02 -04:00
Frank 0caf100598 wip: github actions 2025-07-24 19:03:10 -04:00
Jay V 9f51d713ce docs: add to quick start 2025-07-24 18:57:54 -04:00
Filip c903c82704 Fix: added environment() to summarize() (#1290) 2025-07-24 18:24:54 -04:00
Dax Raad 621e7f8697 wip: handle deleting file 2025-07-24 17:49:23 -04:00
Dax Raad 877cbfc45b wip: fix type errors 2025-07-24 17:38:11 -04:00
Dax Raad 0d12a8c4d9 wip: more snapshot stuff 2025-07-24 17:38:11 -04:00
Aiden Cline 0463894731 (tui) tweak: add setting for scroll speed (#1288) 2025-07-24 16:34:59 -05:00
Clay Warren 9b94c9c57f feat: Replace unzip with @zip.js/zip.js for Windows compatibility (#662) 2025-07-24 16:49:04 -04:00
Filip 0ef0b3404b fix: add custom() to system prompt on summarize (#1289) 2025-07-24 16:48:17 -04:00
Filip 771c714b10 fix: edit tool (#1287) 2025-07-24 16:18:04 -04:00
Dax Raad 03cd495c20 ci: tweak
publish / publish (push) Waiting to run
2025-07-24 15:55:45 -04:00
Dax Raad 85c4cd83bc ci: tweak
publish / publish (push) Waiting to run
2025-07-24 15:51:33 -04:00
Dax Raad a480809965 includ baseline builds
publish / publish (push) Waiting to run
2025-07-24 14:37:38 -04:00
Jesse van der Pluijm 5c1d6d3c8d Check if modelID includes "claude" for antropic/claude prompt caching (#1284) 2025-07-24 11:31:28 -04:00
Dax Raad 99f1c60633 disable snapshots
publish / publish (push) Waiting to run
2025-07-24 11:08:20 -04:00
Aiden Cline cbffb6baad fix: slog error log serialization (#1276) 2025-07-24 07:19:00 -05:00
danielfyhr c1ce8564d2 add aura theme (#1280) 2025-07-24 07:17:27 -05:00
GitHub Action a6e69ae533 ignore: update download stats 2025-07-24 2025-07-24 12:04:18 +00:00
Dax Raad da84388d6c disable undo/redo for now 2025-07-23 21:02:13 -04:00
Rico Sta. Cruz cd758913cc fix: update max visible height in list tests (#1269) 2025-07-23 20:49:15 -04:00
Dax Raad 1d0162ebae fix header width 2025-07-23 20:48:35 -04:00
Dax Raad 0475fb5137 wip: always force create snapshot 2025-07-23 20:46:43 -04:00
Dax Raad cbf56d2cbd wip: fix redoing 2025-07-23 20:42:02 -04:00
Dax 11d236609d basic undo feature (#1268)
Co-authored-by: adamdotdevin <2363879+adamdottv@users.noreply.github.com>
Co-authored-by: Jay V <air@live.ca>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Andrew Joslin <andrew@ajoslin.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Tobias Walle <9933601+tobias-walle@users.noreply.github.com>
2025-07-23 20:30:46 -04:00
Yihui Khuu 9e80e6d855 feat: pass mode into task tool (#1248) 2025-07-23 20:29:59 -04:00
Aiden Cline 2e376833f6 docs: remove deprecated 'log_level' reference in docs (#1258) 2025-07-23 18:53:58 -04:00
Aiden Cline 4caae581b8 tweak: handle pasted attachment references (#1257) 2025-07-23 15:41:17 -05:00
Tobias Walle 1b848a00a5 fix "working" spinner animation (#1054) (#1259) 2025-07-23 15:40:34 -05:00
Dax Raad c2c65c796a remove providers path
publish / publish (push) Waiting to run
2025-07-23 12:15:31 -04:00
Dax Raad 38b4d1e03d add debug paths command 2025-07-23 12:14:54 -04:00
GitHub Action 117d54e948 ignore: update download stats 2025-07-23 2025-07-23 12:04:18 +00:00
Aiden Cline 7f3c98aa1f tweak: ensure most recently interacted with session appears at the top (#1239) 2025-07-22 22:37:36 -05:00
Dax Raad 1acf196e98 config: improve config schema
publish / publish (push) Waiting to run
2025-07-22 20:35:40 -04:00
Jay V 36abbe1d7b docs: edit ide 2025-07-22 19:02:30 -04:00
Andrew Joslin 7ac284c977 Allow multiline prompts for github agent (#1225) 2025-07-22 18:30:51 -04:00
Aiden Cline 774678077f feat(tui): support pipe into tui (#1230) 2025-07-22 17:19:20 -05:00
Jay V 7ed687975e docs: add tutorial closes #740 2025-07-22 17:54:53 -04:00
Frank a382ad9b1c wip: vscode extension 2025-07-22 17:13:58 -04:00
Dax Raad 4b52e5bd49 ci: error github api fail 2025-07-22 17:06:06 -04:00
Frank c6eed8c6fe wip: vscode extension 2025-07-22 16:50:56 -04:00
Frank 824da2a300 wip: vscode extension
publish / publish (push) Waiting to run
2025-07-22 16:46:44 -04:00
Frank 06bee6d089 wip: vscode extension 2025-07-22 16:46:44 -04:00
Frank 76fb049963 wip: vscode extension 2025-07-22 16:46:44 -04:00
Dax Raad 0eebf55d07 ignore: lock changes 2025-07-22 15:49:36 -04:00
Dax Raad 09e8d33f5f wip: tui api 2025-07-22 15:49:24 -04:00
Frank 23aa869162 wip: vscode extension
publish-vscode / publish (push) Waiting to run
2025-07-22 15:36:55 -04:00
Frank 64d43e06ec wip: vscode extension 2025-07-22 15:28:09 -04:00
Frank c7027ea017 wip: vscode extension 2025-07-22 13:21:49 -04:00
Aiden Cline 6eb987cbb9 tweak: cleanup cancelled markdown (#1222) 2025-07-22 12:08:03 -05:00
adamdotdevin 0b2d6c20ff chore: generate sdk into packages/sdk 2025-07-22 11:50:51 -05:00
Dax Raad f4ecb5e90b wip: append-prompt is better 2025-07-22 12:27:02 -04:00
Dax Raad 9dd85ea2c7 wip: tui api 2025-07-22 12:15:50 -04:00
Frank eca975848a wip: vscode extension 2025-07-22 11:21:29 -04:00
adamdotdevin a05547f189 fix(tui): more defensive attachment conversion
publish / publish (push) Waiting to run
2025-07-22 09:28:13 -05:00
GitHub Action 51aba439ec ignore: update download stats 2025-07-22 2025-07-22 12:04:22 +00:00
Aiden Cline 7d6d060f7d fix: mouse text selection bug (#1206) 2025-07-21 19:15:36 -05:00
Dax Raad 26e93c4d11 wip: api for tui 2025-07-21 19:53:58 -04:00
Frank cfe5f3bbbc wip: vscode extension 2025-07-21 19:10:57 -04:00
Timo Clasen bb7aec2213 feat(tui): add ctrl+p and ctrl-n to history navigation (#1199) 2025-07-21 15:10:50 -05:00
Frank cf3006d133 wip: vscode extension
publish-vscode / publish (push) Waiting to run
2025-07-21 15:48:46 -04:00
Dax Raad a6cd7c220a ci: tweaks 2025-07-21 15:45:44 -04:00
Dax Raad 4d6e7f818d fix broken attachments
publish / publish (push) Waiting to run
2025-07-21 15:38:41 -04:00
Dax Raad adf56be05f fix title generation bug
publish / publish (push) Waiting to run
2025-07-21 15:23:47 -04:00
Dax 531d035547 message queuing (#1200)
publish / publish (push) Waiting to run
2025-07-21 15:14:54 -04:00
adamdotdevin 0aec866a8c fix(tui): up/down arrow handling
publish / publish (push) Waiting to run
2025-07-21 10:44:21 -05:00
plyght fc18dbaa4c feat(tui): paste minimizing (#784)
Co-authored-by: adamdotdevin <2363879+adamdottv@users.noreply.github.com>
2025-07-21 10:31:29 -05:00
Dax Raad 6f15934a6b do not copy empty strings 2025-07-21 11:27:15 -04:00
Dax Raad d705b484c8 include newline between messages when copying
publish / publish (push) Waiting to run
2025-07-21 11:22:51 -04:00
Dax Raad adcc58a2b0 add toast for copy
publish / publish (push) Waiting to run
2025-07-21 11:19:54 -04:00
adamelmore 76a98df85f feat(tui): disable keybinds 2025-07-21 10:08:25 -05:00
adamelmore f356058fb4 docs: cleanup 2025-07-21 10:02:58 -05:00
adamelmore db24514d3e feat(tui): theme override with OPENCODE_THEME 2025-07-21 10:02:57 -05:00
adamdotdevin 4c5bbcf74e fix(tui): restore spinner ticks
publish / publish (push) Waiting to run
2025-07-21 05:58:24 -05:00
adamdotdevin b6f723f092 feat(tui): message history select with up/down arrows
publish / publish (push) Waiting to run
2025-07-21 05:52:11 -05:00
Aiden Cline e4f4915136 fix: pasting issue (#1182) 2025-07-21 04:09:16 -05:00
Aiden Cline f5180998dd fix: export cmd (#1184) 2025-07-21 04:08:26 -05:00
Dax 9b672c6b44 Update STATS.md 2025-07-20 20:36:23 -04:00
Dax Raad 22c807f5ee trimmed selection ui
publish / publish (push) Waiting to run
2025-07-20 19:36:56 -04:00
Dax Raad bce6822bcc ignore: remove log 2025-07-20 19:08:19 -04:00
Dax Raad 004f6504e2 implement copy paste much wow can you believe we went this long without it so stupid i blame adam
publish / publish (push) Waiting to run
2025-07-20 19:05:38 -04:00
Dax Raad 9278f64588 do not snapshot nongit projects for now
publish / publish (push) Waiting to run
2025-07-20 13:59:30 -04:00
Frank 4bdb686e1b wip: vscode extension
publish-vscode / publish (push) Waiting to run
2025-07-20 13:31:16 -04:00
396 changed files with 30346 additions and 4508 deletions
+1
View File
@@ -24,3 +24,4 @@ jobs:
- 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 }}
+7 -2
View File
@@ -6,9 +6,14 @@ on:
jobs:
opencode:
if: startsWith(github.event.comment.body, 'hey opencode')
if: |
contains(github.event.comment.body, ' /oc') ||
startsWith(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, ' /opencode') ||
startsWith(github.event.comment.body, '/opencode')
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout repository
@@ -17,7 +22,7 @@ jobs:
fetch-depth: 1
- name: Run opencode
uses: sst/opencode/sdks/github@github-v1
uses: sst/opencode/github@latest
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
with:
+1 -1
View File
@@ -27,4 +27,4 @@ jobs:
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
./script/publish
working-directory: ./sdks/github
working-directory: ./github
+24 -15
View File
@@ -1,14 +1,17 @@
name: publish
run-name: "${{ format('v{0}', inputs.version) }}"
on:
workflow_dispatch:
push:
branches:
- dev
tags:
- "*"
- "!vscode-v*"
- "!github-v*"
inputs:
version:
description: "Version to publish"
required: true
type: string
title:
description: "Custom title for this run"
required: false
type: string
concurrency: ${{ github.workflow }}-${{ github.ref }}
@@ -34,7 +37,16 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.17
bun-version: 1.2.19
- name: Cache ~/.bun
id: cache-bun
uses: actions/cache@v3
with:
path: ~/.bun
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install makepkg
run: |
@@ -50,15 +62,12 @@ jobs:
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
- name: Install dependencies
run: bun install
- name: Publish
run: |
bun install
if [ "${{ startsWith(github.ref, 'refs/tags/') }}" = "true" ]; then
./script/publish.ts
else
./script/publish.ts --snapshot
fi
working-directory: ./packages/opencode
OPENCODE_VERSION=${{ inputs.version }} ./script/publish.ts
env:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
+3 -1
View File
@@ -21,7 +21,7 @@ jobs:
bun-version: latest
- name: Run stats script
run: bun scripts/stats.ts
run: bun script/stats.ts
- name: Commit stats
run: |
@@ -30,3 +30,5 @@ jobs:
git add STATS.md
git diff --staged --quiet || git commit -m "ignore: update download stats $(date -I)"
git push
env:
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
+24
View File
@@ -0,0 +1,24 @@
name: Typecheck
on:
pull_request:
branches: [dev]
workflow_dispatch:
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.2.19
- name: Install dependencies
run: bun install
- name: Run typecheck
run: bun typecheck
+1 -1
View File
@@ -1,8 +1,8 @@
.DS_Store
node_modules
.opencode
.sst
.env
.idea
.vscode
openapi.json
playground
+14
View File
@@ -0,0 +1,14 @@
---
model: openai/gpt-5
reasoningEffort: medium
description: ALWAYS use this when writing docs
---
You are an expert technical documentation writer
You are not verbose
Every chunk of text should be followed by an example or something besides text
to look at.
Chunks of text should not be more than 2 sentences long.
+11 -14
View File
@@ -1,15 +1,12 @@
# TUI Agent Guidelines
## IMPORTANT
## Style
- prefer single word variable/function names
- avoid try catch where possible - prefer to let exceptions bubble up
- avoid else statements where possible
- do not make useless helper functions - inline functionality unless the
function is reusable or composable
- prefer Bun apis
## Workflow
- you can regenerate the golang sdk by calling ./scripts/stainless.ts
- we use bun for everything
- Try to keep things in one function unless composable or reusable
- DO NOT do unnecessary destructuring of variables
- DO NOT use `else` statements unless necessary
- DO NOT use `try`/`catch` if it can be avoided
- AVOID `try`/`catch` where possible
- AVOID `else` statements
- AVOID using `any` type
- AVOID `let` statements
- PREFER single word variable names where possible
- Use as many bun apis as possible like Bun.file()
+3 -3
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
@@ -97,7 +97,7 @@ $ bun run packages/opencode/src/index.ts
It's very similar to Claude Code in terms of capability. Here are the key differences:
- 100% open source
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider agnostic is important.
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
- A focus on TUI. opencode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
- A client/server architecture. This for example can allow opencode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
+44 -23
View File
@@ -1,25 +1,46 @@
# 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-10 | 43,796 (+5,744) | 71,402 (+6,934) | 115,198 (+12,678) |
| 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-18 | 70,380 (+1) | 102,587 (+0) | 172,967 (+1) |
| 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) |
| 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) |
+1567 -112
View File
File diff suppressed because it is too large Load Diff
+20
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,
},
},
})
@@ -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");
@@ -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");
@@ -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");
@@ -0,0 +1 @@
ALTER TABLE "usage" DROP COLUMN "request_id";
@@ -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": {}
}
}
@@ -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": {}
}
}
@@ -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": {}
}
}
@@ -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": {}
}
}
+34
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
View File
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cloud-core",
"version": "0.4.3",
"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
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(),
)
}
}
+75
View File
@@ -0,0 +1,75 @@
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
email: 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
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
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)
},
}
}
}
+94
View File
@@ -0,0 +1,94 @@
import { drizzle } from "drizzle-orm/postgres-js"
import { Resource } from "sst"
export * from "drizzle-orm"
import postgres from "postgres"
function createClient() {
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"
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
}
}
}
+29
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"),
}
+26
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])
}
}
+12
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)],
)
+45
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)],
)
+16
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)],
)
+16
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)],
)
+25
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
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
}
+55
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: {} }
}
}
}
+3
View File
@@ -0,0 +1,3 @@
export function centsToMicroCents(amount: number) {
return Math.round(amount * 1000000)
}
+48
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
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"]
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "@opencode/cloud-function",
"version": "0.4.3",
"$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:"
}
}
+124
View File
@@ -0,0 +1,124 @@
import { Resource } from "sst"
import { z } from "zod"
import { issuer } from "@openauthjs/openauth"
import { createSubjects } from "@openauthjs/openauth/subject"
import { CodeProvider } from "@openauthjs/openauth/provider/code"
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"
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!,
})
}
return ctx.subject("account", accountID, { accountID, email })
},
}).fetch(request, env, ctx)
},
}
+909
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
+92
View File
@@ -0,0 +1,92 @@
/* 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
}
"Console": {
"type": "sst.cloudflare.StaticSite"
"url": 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 {}
+9
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
View File
@@ -0,0 +1,2 @@
node_modules
dist
+38
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
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
View File
@@ -0,0 +1,32 @@
{
"name": "@opencode/cloud-web",
"version": "0.4.3",
"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"
}
}
+3
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

+3
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

+24
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
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

@@ -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
}
@@ -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
}
@@ -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,
}
})
+13
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")!)
}
+7
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 }
}
+11
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>
)
}
@@ -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;
}
}
}
+132
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>
</>
)
}
@@ -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...".
@@ -0,0 +1,271 @@
import { createResource } from "solid-js"
import { createStore, produce } from "solid-js/store"
import SYSTEM_PROMPT from "./system.txt?raw"
import type {
LanguageModelV1Prompt,
LanguageModelV1CallOptions,
LanguageModelV1,
} from "ai"
interface Tool {
name: string
description: string
inputSchema: any
}
interface ToolCallerProps {
tool: {
list: () => Promise<Tool[]>
call: (input: { name: string; arguments: any }) => Promise<any>
}
generate: (
prompt: LanguageModelV1CallOptions,
) => Promise<
| { err: "rate" }
| { err: "context" }
| { err: "balance" }
| ({ err: false } & Awaited<ReturnType<LanguageModelV1["doGenerate"]>>)
>
onPromptUpdated?: (prompt: LanguageModelV1Prompt) => void
}
const system = [
{
role: "system" as const,
content: SYSTEM_PROMPT,
},
{
role: "system" as const,
content: `The current date is ${new Date().toDateString()}. Always use this current date when responding to relative date queries.`,
},
]
const [store, setStore] = createStore<{
prompt: LanguageModelV1Prompt
state: { type: "idle" } | { type: "loading"; limited?: boolean }
}>({
prompt: [...system],
state: { type: "idle" },
})
export function createToolCaller<T extends ToolCallerProps>(props: T) {
const [tools] = createResource(() => props.tool.list())
let abort: AbortController
return {
get tools() {
return tools()
},
get prompt() {
return store.prompt
},
get state() {
return store.state
},
clear() {
setStore("prompt", [...system])
},
async chat(input: string) {
if (store.state.type !== "idle") return
abort = new AbortController()
setStore(
produce((s) => {
s.state = {
type: "loading",
limited: false,
}
s.prompt.push({
role: "user",
content: [
{
type: "text",
text: input,
},
],
})
}),
)
props.onPromptUpdated?.(store.prompt)
while (true) {
if (abort.signal.aborted) {
break
}
const response = await props.generate({
inputFormat: "messages",
prompt: store.prompt,
temperature: 0,
seed: 69,
mode: {
type: "regular",
tools: tools()?.map((tool) => ({
type: "function",
name: tool.name,
description: tool.description,
parameters: {
...tool.inputSchema,
},
})),
},
})
if (abort.signal.aborted) continue
if (!response.err) {
setStore("state", {
type: "loading",
})
if (response.text) {
setStore(
produce((s) => {
s.prompt.push({
role: "assistant",
content: [
{
type: "text",
text: response.text || "",
},
],
})
}),
)
props.onPromptUpdated?.(store.prompt)
}
if (response.finishReason === "stop") {
break
}
if (response.finishReason === "tool-calls") {
for (const item of response.toolCalls || []) {
setStore(
produce((s) => {
s.prompt.push({
role: "assistant",
content: [
{
type: "tool-call",
toolName: item.toolName,
args: JSON.parse(item.args),
toolCallId: item.toolCallId,
},
],
})
}),
)
props.onPromptUpdated?.(store.prompt)
const called = await props.tool.call({
name: item.toolName,
arguments: JSON.parse(item.args),
})
setStore(
produce((s) => {
s.prompt.push({
role: "tool",
content: [
{
type: "tool-result",
toolName: item.toolName,
toolCallId: item.toolCallId,
result: called,
},
],
})
}),
)
props.onPromptUpdated?.(store.prompt)
}
}
continue
}
if (response.err === "context") {
setStore(
produce((s) => {
s.prompt.splice(2, 1)
}),
)
props.onPromptUpdated?.(store.prompt)
}
if (response.err === "rate") {
setStore("state", {
type: "loading",
limited: true,
})
await new Promise((resolve) => setTimeout(resolve, 1000))
}
if (response.err === "balance") {
setStore(
produce((s) => {
s.prompt.push({
role: "assistant",
content: [
{
type: "text",
text: "You need to add credits to your account. Please go to Billing and add credits to continue.",
},
],
})
s.state = { type: "idle" }
}),
)
props.onPromptUpdated?.(store.prompt)
break
}
}
setStore("state", { type: "idle" })
},
async cancel() {
abort.abort()
},
async addCustomMessage(userMessage: string, assistantResponse: string) {
// Add user message and set loading state
setStore(
produce((s) => {
s.prompt.push({
role: "user",
content: [
{
type: "text",
text: userMessage,
},
],
})
s.state = {
type: "loading",
limited: false,
}
}),
)
props.onPromptUpdated?.(store.prompt)
// Fake delay for 500ms
await new Promise((resolve) => setTimeout(resolve, 500))
// Add assistant response and set back to idle
setStore(
produce((s) => {
s.prompt.push({
role: "assistant",
content: [
{
type: "text",
text: assistantResponse,
},
],
})
s.state = { type: "idle" }
}),
)
props.onPromptUpdated?.(store.prompt)
},
}
}
@@ -0,0 +1,239 @@
.root {
display: contents;
[data-slot="messages"] {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
height: 0;
/* This is important for flexbox to allow scrolling */
font-family: var(--font-mono);
color: var(--color-text);
row-gap: var(--space-4);
/* Add consistent spacing between messages */
/* Remove top border for first user message */
&>[data-component="message"][data-user]:first-child::before {
display: none;
}
&:has([data-component="loading"]) [data-component="clear"] {
display: none;
}
}
[data-component="message"] {
width: 100%;
padding: var(--space-2) var(--space-4);
line-height: var(--font-line-height);
white-space: pre-wrap;
align-self: flex-start;
min-height: auto;
/* Allow natural height for all messages */
display: flex;
flex-direction: column;
align-items: flex-start;
/* User message styling */
&[data-user] {
padding: var(--space-6) var(--space-4);
position: relative;
font-weight: 600;
color: var(--color-text);
/* margin: 0.5rem 0; */
}
&[data-user]::before,
&[data-user]::after {
content: "";
position: absolute;
left: var(--space-4);
right: var(--space-4);
height: var(--space-px);
background-color: var(--color-border);
z-index: 1;
/* Ensure borders appear above other content */
}
&[data-user]::before {
top: 0;
}
&[data-user]::after {
bottom: 0;
}
&[data-assistant] {
color: var(--color-text);
}
}
[data-component="tool"] {
display: flex;
width: 100%;
padding: 0 var(--space-4);
margin-left: 0;
flex-direction: column;
opacity: 0.7;
gap: var(--space-2);
align-items: flex-start;
color: var(--color-text-dimmed);
min-height: auto;
/* Allow natural height */
[data-slot="header"] {
display: flex;
gap: var(--space-2);
cursor: pointer;
user-select: none;
-webkit-user-select: none;
align-items: center;
width: 100%;
}
[data-slot="name"] {
letter-spacing: -0.03125rem;
text-transform: uppercase;
font-weight: 500;
font-size: var(--font-size-sm);
}
[data-slot="expand"] {
font-size: var(--font-size-sm);
}
[data-slot="content"] {
padding: 0;
line-height: var(--font-line-height);
font-size: var(--font-size-sm);
white-space: pre-wrap;
display: none;
width: 100%;
}
[data-slot="output"] {
margin-top: var(--space-1);
}
&[data-expanded="true"] [data-slot="content"] {
display: block;
}
&[data-expanded="true"] [data-slot="expand"] {
transform: rotate(45deg);
}
}
[data-component="loading"] {
padding: var(--space-4) var(--space-4) var(--space-8);
height: 1.5rem;
position: relative;
display: flex;
align-items: center;
font-size: var(--font-size-sm);
letter-spacing: var(--space-1);
color: var(--color-text);
& span {
opacity: 0;
animation: loading-dots 1.4s linear infinite;
}
& span:nth-child(2) {
animation-delay: 0.2s;
}
& span:nth-child(3) {
animation-delay: 0.4s;
}
}
[data-component="clear"] {
position: relative;
padding: var(--space-4) var(--space-4);
&::before {
content: "";
position: absolute;
left: var(--space-4);
right: var(--space-4);
top: 0;
height: var(--space-px);
background-color: var(--color-border);
z-index: 1;
}
& [data-component="button"] {
padding-left: 0;
}
}
[data-slot="footer"] {
display: flex;
flex-direction: column;
padding: 0;
border-top: 2px solid var(--color-border);
position: sticky;
bottom: 0;
z-index: 10;
/* Ensure it's above other content */
margin-top: auto;
/* Push to bottom if content is short */
width: 100%;
}
[data-component="chat"] {
display: flex;
padding: var(--space-0-5) 0;
align-items: center;
width: 100%;
height: 100%;
textarea {
--padding-y: var(--space-4);
--line-height: 1.5;
--text-height: calc(var(--line-height) * var(--font-size-lg));
--height: calc(var(--text-height) + var(--padding-y) * 2);
width: 100%;
resize: none;
line-height: var(--line-height);
height: var(--height);
min-height: var(--height);
max-height: calc(5 * var(--text-height) + var(--padding-y) * 2);
padding: var(--padding-y) var(--space-4);
border-radius: 0;
background-color: transparent;
color: var(--color-text);
border: none;
outline: none;
font-size: var(--font-size-lg);
}
textarea::placeholder {
color: var(--color-text-dimmed);
opacity: 0.75;
}
textarea:focus {
outline: 0;
}
& [data-component="button"] {
height: 100%;
}
}
}
@keyframes loading-dots {
0%,
100% {
opacity: 0;
}
40%,
60% {
opacity: 1;
}
}
+18
View File
@@ -0,0 +1,18 @@
import { Button } from "../../ui/button"
import { IconArrowRight } from "../../ui/svg/icons"
import { createSignal, For } from "solid-js"
import { createToolCaller } from "./components/tool"
import { useApi } from "../components/context-api"
import { useWorkspace } from "../components/context-workspace"
import style from "./index.module.css"
export default function Index() {
const api = useApi()
const workspace = useWorkspace()
return (
<div class={style.root}>
<h1>Hello</h1>
</div>
)
}
@@ -0,0 +1,97 @@
.root {
display: flex;
flex-direction: column;
gap: 2rem;
}
.root [data-slot="keys-info"] {
display: flex;
flex-direction: column;
gap: 1rem;
}
.root [data-slot="header"] {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.root [data-slot="header"] h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.root [data-slot="header"] p {
margin: 0;
color: var(--color-text-secondary);
}
.root [data-slot="key-list"] {
display: flex;
flex-direction: column;
gap: 1rem;
}
.root [data-slot="key-item"] {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border: 1px solid var(--color-border);
border-radius: 0.5rem;
background: var(--color-background-secondary);
}
.root [data-slot="key-actions"] {
display: flex;
gap: 0.5rem;
}
.root [data-slot="key-info"] {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.root [data-slot="key-value"] {
font-family: monospace;
font-size: 0.875rem;
color: var(--color-text-primary);
}
.root [data-slot="key-meta"] {
font-size: 0.75rem;
color: var(--color-text-secondary);
}
.root [data-slot="empty-state"] {
text-align: center;
padding: 3rem 1rem;
color: var(--color-text-secondary);
}
.root [data-slot="actions"] {
display: flex;
align-items: center;
justify-content: space-between;
}
.root [data-slot="create-form"] {
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 300px;
}
.root [data-slot="form-actions"] {
display: flex;
gap: 0.5rem;
}
.root [data-slot="key-name"] {
font-weight: 600;
font-size: 1rem;
color: var(--color-text-primary);
margin-bottom: 0.25rem;
}
+151
View File
@@ -0,0 +1,151 @@
import { Button } from "../../ui/button"
import { useApi } from "../components/context-api"
import { createSignal, createResource, For, Show } from "solid-js"
import style from "./keys.module.css"
export default function Keys() {
const api = useApi()
const [isCreating, setIsCreating] = createSignal(false)
const [showCreateForm, setShowCreateForm] = createSignal(false)
const [keyName, setKeyName] = createSignal("")
const [keysData, { refetch }] = createResource(async () => {
const response = await api.keys.$get()
return response.json()
})
const handleCreateKey = async () => {
if (!keyName().trim()) return
try {
setIsCreating(true)
await api.keys.$post({
json: { name: keyName().trim() },
})
refetch()
setKeyName("")
setShowCreateForm(false)
} catch (error) {
console.error("Failed to create API key:", error)
} finally {
setIsCreating(false)
}
}
const handleDeleteKey = async (keyId: string) => {
if (!confirm("Are you sure you want to delete this API key? This action cannot be undone.")) {
return
}
try {
await api.keys[":id"].$delete({
param: { id: keyId },
})
refetch()
} catch (error) {
console.error("Failed to delete API key:", error)
}
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString()
}
const formatKey = (key: string) => {
if (key.length <= 11) return key
return `${key.slice(0, 7)}...${key.slice(-4)}`
}
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
} catch (error) {
console.error("Failed to copy to clipboard:", error)
}
}
return (
<>
<div data-component="title-bar">
<div data-slot="left">
<h1>API Keys</h1>
</div>
</div>
<div class={style.root} data-max-width data-max-width-64>
<div data-slot="keys-info">
<div data-slot="actions">
<div data-slot="header">
<h2>API Keys</h2>
<p>Manage your API keys to access the OpenCode gateway.</p>
</div>
<Show
when={!showCreateForm()}
fallback={
<div data-slot="create-form">
<input
data-component="input"
type="text"
placeholder="Enter key name"
value={keyName()}
onInput={(e) => setKeyName(e.currentTarget.value)}
onKeyPress={(e) => e.key === "Enter" && handleCreateKey()}
/>
<div data-slot="form-actions">
<Button color="primary" disabled={isCreating() || !keyName().trim()} onClick={handleCreateKey}>
{isCreating() ? "Creating..." : "Create"}
</Button>
<Button
color="ghost"
onClick={() => {
setShowCreateForm(false)
setKeyName("")
}}
>
Cancel
</Button>
</div>
</div>
}
>
<Button color="primary" onClick={() => setShowCreateForm(true)}>
Create API Key
</Button>
</Show>
</div>
<div data-slot="key-list">
<For
each={keysData()?.keys}
fallback={
<div data-slot="empty-state">
<p>Create an API key to access opencode gateway</p>
</div>
}
>
{(key) => (
<div data-slot="key-item">
<div data-slot="key-info">
<div data-slot="key-name">{key.name}</div>
<div data-slot="key-value">{formatKey(key.key)}</div>
<div data-slot="key-meta">
Created: {formatDate(key.timeCreated)}
{key.timeUsed && ` • Last used: ${formatDate(key.timeUsed)}`}
</div>
</div>
<div data-slot="key-actions">
<Button color="ghost" onClick={() => copyToClipboard(key.key)} title="Copy API key">
Copy
</Button>
<Button color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
Delete
</Button>
</div>
</div>
)}
</For>
</div>
</div>
</div>
</>
)
}
@@ -0,0 +1,24 @@
import { hc } from "hono/client"
import { ApiType } from "@opencode/cloud-function/src/gateway"
import { useWorkspace } from "./context-workspace"
import { useOpenAuth } from "../../components/context-openauth"
export function useApi() {
const workspace = useWorkspace()
const auth = useOpenAuth()
return hc<ApiType>(import.meta.env.VITE_API_URL, {
async fetch(...args: Parameters<typeof fetch>): Promise<Response> {
const [input, init] = args
const request = input instanceof Request ? input : new Request(input, init)
const headers = new Headers(request.headers)
headers.set("authorization", `Bearer ${await auth.access()}`)
headers.set("x-opencode-workspace", workspace.id)
return fetch(
new Request(request, {
...init,
headers,
}),
)
},
})
}
@@ -0,0 +1,38 @@
import { useNavigate, useParams } from "@solidjs/router"
import { createInitializedContext } from "../../util/context"
import { useAccount } from "../../components/context-account"
import { createEffect, createMemo } from "solid-js"
export const { use: useWorkspace, provider: WorkspaceProvider } =
createInitializedContext("WorkspaceProvider", () => {
const params = useParams()
const account = useAccount()
const workspace = createMemo(() =>
account.current?.workspaces.find(
(x) => x.id === params.workspace || x.slug === params.workspace,
),
)
const nav = useNavigate()
createEffect(() => {
if (!workspace()) nav("/")
})
const result = () => workspace()!
result.ready = true
return {
get id() {
return workspace()!.id
},
get slug() {
return workspace()!.slug
},
get name() {
return workspace()!.name
},
get ready() {
return workspace() !== undefined
},
}
})
@@ -0,0 +1,199 @@
.root {
--padding: var(--space-10);
--vertical-padding: var(--space-8);
--heading-font-size: var(--font-size-4xl);
--sidebar-width: 200px;
--mobile-breakpoint: 40rem;
--topbar-height: 60px;
margin: var(--space-4);
border: 2px solid var(--color-border);
height: calc(100vh - var(--space-8));
display: flex;
flex-direction: row;
overflow: hidden;
/* Prevent overall scrolling */
position: relative;
}
[data-component="mobile-top-bar"] {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--topbar-height);
background: var(--color-background);
border-bottom: 2px solid var(--color-border);
z-index: 20;
align-items: center;
padding: 0 var(--space-4) 0 0;
[data-slot="logo"] {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
div {
text-transform: uppercase;
font-weight: 600;
letter-spacing: -0.03125rem;
}
svg {
height: 28px;
width: auto;
color: var(--color-white);
}
}
[data-slot="toggle"] {
background: transparent;
border: none;
padding: var(--space-4);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
& svg {
width: 24px;
height: 24px;
color: var(--color-foreground);
}
}
}
[data-component="sidebar"] {
width: var(--sidebar-width);
border-right: 2px solid var(--color-border);
display: flex;
flex-direction: column;
padding: calc(var(--padding) / 2);
overflow-y: auto;
/* Allow scrolling if needed */
position: sticky;
top: 0;
height: 100%;
background-color: var(--color-background);
z-index: 10;
[data-slot="logo"] {
margin-top: 2px;
margin-bottom: var(--space-7);
color: var(--color-white);
& svg {
height: 32px;
width: auto;
}
}
[data-slot="nav"] {
flex: 1;
ul {
list-style-type: none;
padding: 0;
}
li {
margin-bottom: calc(var(--vertical-padding) / 2);
text-transform: uppercase;
font-weight: 500;
}
a {
display: block;
padding: var(--space-2) 0;
}
}
[data-slot="user"] {
[data-component="button"] {
padding-left: 0;
padding-bottom: 0;
height: auto;
}
}
}
.navActiveLink {
cursor: default;
text-decoration: none;
}
[data-slot="main-content"] {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
/* Full height */
overflow: hidden;
/* Prevent overflow */
position: relative;
/* For positioning footer */
width: 100%;
/* Full width */
}
/* Backdrop for mobile */
[data-component="backdrop"] {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
/* background-color: rgba(0, 0, 0, 0.5); */
z-index: 25;
backdrop-filter: blur(2px);
}
/* Mobile styles */
@media (max-width: 40rem) {
.root {
margin: 0;
border: none;
height: 100vh;
}
[data-component="mobile-top-bar"] {
display: flex;
}
[data-component="backdrop"] {
display: block;
}
[data-component="sidebar"] {
position: fixed;
left: -100%;
top: 0;
height: 100vh;
width: 80%;
max-width: 280px;
transition: left 0.3s ease-in-out;
box-shadow: none;
z-index: 30;
padding: var(--space-8);
background-color: var(--color-bg);
&[data-opened="true"] {
left: 0;
box-shadow: 8px 0 0px 0px var(--color-gray-4);
}
}
[data-slot="main-content"] {
padding-top: var(--topbar-height);
/* Add space for the top bar */
overflow-y: auto;
}
/* Hide the logo in the sidebar on mobile since it's in the top bar */
[data-component="sidebar"] [data-slot="logo"] {
display: none;
}
}
+96
View File
@@ -0,0 +1,96 @@
import style from "./layout.module.css"
import { useAccount } from "../../components/context-account"
import { Button } from "../../ui/button"
import { IconLogomark } from "../../ui/svg"
import { IconBars3BottomLeft } from "../../ui/svg/icons"
import { ParentProps, createMemo, createSignal } from "solid-js"
import { A, useLocation } from "@solidjs/router"
import { useOpenAuth } from "../../components/context-openauth"
export default function Layout(props: ParentProps) {
const auth = useOpenAuth()
const account = useAccount()
const [sidebarOpen, setSidebarOpen] = createSignal(false)
const location = useLocation()
const workspaceId = createMemo(() => account.current?.workspaces[0].id)
const pageTitle = createMemo(() => {
const path = location.pathname
if (path.endsWith("/billing")) return "Billing"
if (path.endsWith("/keys")) return "API Keys"
return null
})
function handleLogout() {
auth.logout(auth.subject?.id!)
}
return (
<div class={style.root}>
{/* Mobile top bar */}
<div data-component="mobile-top-bar">
<button data-slot="toggle" onClick={() => setSidebarOpen(!sidebarOpen())}>
<IconBars3BottomLeft />
</button>
<div data-slot="logo">
{pageTitle() ? (
<div>{pageTitle()}</div>
) : (
<A href="/">
<IconLogomark />
</A>
)}
</div>
</div>
{/* Backdrop for mobile sidebar - closes sidebar when clicked */}
{sidebarOpen() && <div data-component="backdrop" onClick={() => setSidebarOpen(false)}></div>}
<div data-component="sidebar" data-opened={sidebarOpen() ? "true" : "false"}>
<div data-slot="logo">
<A href="/">
<IconLogomark />
</A>
</div>
<nav data-slot="nav">
<ul>
<li>
<A end activeClass={style.navActiveLink} href={`/${workspaceId()}`} onClick={() => setSidebarOpen(false)}>
Chat
</A>
</li>
<li>
<A
activeClass={style.navActiveLink}
href={`/${workspaceId()}/billing`}
onClick={() => setSidebarOpen(false)}
>
Billing
</A>
</li>
<li>
<A
activeClass={style.navActiveLink}
href={`/${workspaceId()}/keys`}
onClick={() => setSidebarOpen(false)}
>
API Keys
</A>
</li>
</ul>
</nav>
<div data-slot="user">
<Button color="ghost" onClick={handleLogout} title={account.current?.email || ""}>
Logout
</Button>
</div>
</div>
{/* Main Content */}
<div data-slot="main-content">{props.children}</div>
</div>
)
}
+39
View File
@@ -0,0 +1,39 @@
import { Match, Switch } from "solid-js"
import { useAccount } from "../components/context-account"
import { Navigate } from "@solidjs/router"
import { IconLogo } from "../ui/svg"
import styles from "./lander.module.css"
import { useOpenAuth } from "../components/context-openauth"
export default function Index() {
const auth = useOpenAuth()
const account = useAccount()
return (
<Switch>
<Match when={account.current}>
<Navigate href={`/${account.current!.workspaces[0].id}`} />
</Match>
<Match when={!account.current}>
<div class={styles.lander}>
<div data-slot="hero">
<section data-slot="top">
<div data-slot="logo">
<IconLogo />
</div>
<h1>opencode Gateway Console</h1>
</section>
<section data-slot="cta">
<div>
<span onClick={() => auth.authorize({ provider: "github" })}>Sign in with GitHub</span>
</div>
<div>
<span onClick={() => auth.authorize({ provider: "google" })}>Sign in with Google</span>
</div>
</section>
</div>
</div>
</Match>
</Switch>
)
}
+83
View File
@@ -0,0 +1,83 @@
.lander {
--padding: 3rem;
--vertical-padding: 2rem;
--heading-font-size: 2rem;
margin: 1rem;
@media (max-width: 30rem) {
& {
--padding: 1.5rem;
--vertical-padding: 1rem;
--heading-font-size: 1.5rem;
margin: 0.5rem;
}
}
[data-slot="hero"] {
border: 2px solid var(--color-border);
max-width: 64rem;
margin-left: auto;
margin-right: auto;
width: 100%;
}
[data-slot="top"] {
padding: var(--padding);
h1 {
margin-top: calc(var(--vertical-padding) / 8);
font-size: var(--heading-font-size);
line-height: 1.25;
text-transform: uppercase;
font-weight: 600;
}
[data-slot="logo"] {
width: clamp(200px, 70vw, 400px);
color: var(--color-white);
}
}
[data-slot="cta"] {
display: flex;
flex-direction: row;
justify-content: space-between;
border-top: 2px solid var(--color-border);
& > div {
flex: 1;
line-height: 1.4;
text-align: center;
text-transform: uppercase;
cursor: pointer;
text-decoration: underline;
letter-spacing: -0.03125rem;
&[data-slot="col-2"] {
background-color: var(--color-border);
color: var(--color-text-invert);
font-weight: 600;
}
& > * {
display: block;
width: 100%;
height: 100%;
padding: calc(var(--padding) / 2) 0.5rem;
}
}
@media (max-width: 30rem) {
& > div {
padding-bottom: calc(var(--padding) / 2 + 4px);
}
}
& > div + div {
border-left: 2px solid var(--color-border);
}
}
}
+204
View File
@@ -0,0 +1,204 @@
.pageContainer {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.componentTable {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
border: 2px solid var(--color-border);
}
.componentCell {
padding: 1rem;
border: 2px solid var(--color-border);
vertical-align: top;
}
.componentLabel {
text-transform: uppercase;
letter-spacing: -0.03125rem;
font-size: 0.85rem;
font-weight: 500;
margin-bottom: 0.75rem;
color: var(--color-text-dimmed);
}
.sectionTitle {
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: -0.03125rem;
font-size: 1.2rem;
}
.divider {
height: 2px;
background: var(--color-border);
margin: 3rem 0;
width: 100%;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.buttonSection {
margin-bottom: 4rem;
}
.colorSection {
margin-bottom: 4rem;
}
.labelSection {
margin-bottom: 4rem;
}
.inputSection {
margin-bottom: 4rem;
}
.dialogSection {
margin-bottom: 4rem;
}
.formGroup {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.dialogContent {
padding: 2rem;
}
.dialogContentFooter {
margin-top: 1rem;
}
.pageTitle {
font-size: var(--heading-font-size, 2rem);
text-transform: uppercase;
font-weight: 600;
}
.colorBox {
width: 100%;
height: 80px;
margin-bottom: 0.5rem;
position: relative;
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: 0.5rem;
}
.colorOrange {
background-color: var(--color-orange);
}
.colorOrangeLow {
background-color: var(--color-orange-low);
}
.colorOrangeHigh {
background-color: var(--color-orange-high);
}
.colorGreen {
background-color: var(--color-green);
}
.colorGreenLow {
background-color: var(--color-green-low);
}
.colorGreenHigh {
background-color: var(--color-green-high);
}
.colorBlue {
background-color: var(--color-blue);
}
.colorBlueLow {
background-color: var(--color-blue-low);
}
.colorBlueHigh {
background-color: var(--color-blue-high);
}
.colorPurple {
background-color: var(--color-purple);
}
.colorPurpleLow {
background-color: var(--color-purple-low);
}
.colorPurpleHigh {
background-color: var(--color-purple-high);
}
.colorRed {
background-color: var(--color-red);
}
.colorRedLow {
background-color: var(--color-red-low);
}
.colorRedHigh {
background-color: var(--color-red-high);
}
.colorAccent {
background-color: var(--color-accent);
}
.colorAccentLow {
background-color: var(--color-accent-low);
}
.colorAccentHigh {
background-color: var(--color-accent-high);
}
.colorCode {
background-color: rgba(0, 0, 0, 0.5);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8rem;
font-family: monospace;
}
.colorVariants {
display: flex;
gap: 0.5rem;
}
.colorVariant {
flex: 1;
height: 40px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.colorVariantCode {
background-color: rgba(0, 0, 0, 0.5);
color: white;
padding: 2px 4px;
border-radius: 4px;
font-size: 0.65rem;
font-family: monospace;
white-space: nowrap;
}
+562
View File
@@ -0,0 +1,562 @@
import { Button } from "../../ui/button"
import { Dialog } from "../../ui/dialog"
import { Navigate } from "@solidjs/router"
import { createSignal, Show } from "solid-js"
import { IconHome, IconPencilSquare } from "../../ui/svg/icons"
import { useTheme } from "../../components/context-theme"
import { useDialog } from "../../ui/context-dialog"
import { DialogString } from "../../ui/dialog-string"
import { DialogSelect } from "../../ui/dialog-select"
import styles from "./design.module.css"
export default function DesignSystem() {
const dialog = useDialog()
const [dialogOpen, setDialogOpen] = createSignal(false)
const [dialogOpenTransition, setDialogOpenTransition] = createSignal(false)
const theme = useTheme()
// Check if we're running locally
const isLocal = import.meta.env.DEV === true
if (!isLocal) {
return <Navigate href="/" />
}
// Add a toggle button for theme
const toggleTheme = () => {
theme.setMode(theme.mode === "light" ? "dark" : "light")
}
return (
<div class={styles.pageContainer}>
<div class={styles.header}>
<h1 class={styles.pageTitle}>Design System</h1>
<Button onClick={toggleTheme}>
Toggle {theme.mode === "light" ? "Dark" : "Light"} Mode
</Button>
</div>
<section class={styles.colorSection}>
<h2 class={styles.sectionTitle}>Colors</h2>
<table class={styles.componentTable}>
<tbody>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Orange</h3>
<div class={`${styles.colorBox} ${styles.colorOrange}`}>
<span class={styles.colorCode}>hsl(41, 82%, 63%)</span>
</div>
<div class={styles.colorVariants}>
<div
class={`${styles.colorVariant} ${styles.colorOrangeLow}`}
>
<span class={styles.colorVariantCode}>
hsl(41, 39%, 22%)
</span>
</div>
<div
class={`${styles.colorVariant} ${styles.colorOrangeHigh}`}
>
<span class={styles.colorVariantCode}>
hsl(41, 82%, 87%)
</span>
</div>
</div>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Green</h3>
<div class={`${styles.colorBox} ${styles.colorGreen}`}>
<span class={styles.colorCode}>hsl(101, 82%, 63%)</span>
</div>
<div class={styles.colorVariants}>
<div class={`${styles.colorVariant} ${styles.colorGreenLow}`}>
<span class={styles.colorVariantCode}>
hsl(101, 39%, 22%)
</span>
</div>
<div
class={`${styles.colorVariant} ${styles.colorGreenHigh}`}
>
<span class={styles.colorVariantCode}>
hsl(101, 82%, 80%)
</span>
</div>
</div>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Blue</h3>
<div class={`${styles.colorBox} ${styles.colorBlue}`}>
<span class={styles.colorCode}>hsl(234, 100%, 60%)</span>
</div>
<div class={styles.colorVariants}>
<div class={`${styles.colorVariant} ${styles.colorBlueLow}`}>
<span class={styles.colorVariantCode}>
hsl(234, 54%, 20%)
</span>
</div>
<div class={`${styles.colorVariant} ${styles.colorBlueHigh}`}>
<span class={styles.colorVariantCode}>
hsl(234, 100%, 87%)
</span>
</div>
</div>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Purple</h3>
<div class={`${styles.colorBox} ${styles.colorPurple}`}>
<span class={styles.colorCode}>hsl(281, 82%, 63%)</span>
</div>
<div class={styles.colorVariants}>
<div
class={`${styles.colorVariant} ${styles.colorPurpleLow}`}
>
<span class={styles.colorVariantCode}>
hsl(281, 39%, 22%)
</span>
</div>
<div
class={`${styles.colorVariant} ${styles.colorPurpleHigh}`}
>
<span class={styles.colorVariantCode}>
hsl(281, 82%, 89%)
</span>
</div>
</div>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Red</h3>
<div class={`${styles.colorBox} ${styles.colorRed}`}>
<span class={styles.colorCode}>hsl(339, 82%, 63%)</span>
</div>
<div class={styles.colorVariants}>
<div class={`${styles.colorVariant} ${styles.colorRedLow}`}>
<span class={styles.colorVariantCode}>
hsl(339, 39%, 22%)
</span>
</div>
<div class={`${styles.colorVariant} ${styles.colorRedHigh}`}>
<span class={styles.colorVariantCode}>
hsl(339, 82%, 87%)
</span>
</div>
</div>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Accent</h3>
<div class={`${styles.colorBox} ${styles.colorAccent}`}>
<span class={styles.colorCode}>hsl(13, 88%, 57%)</span>
</div>
<div class={styles.colorVariants}>
<div
class={`${styles.colorVariant} ${styles.colorAccentLow}`}
>
<span class={styles.colorVariantCode}>
hsl(13, 75%, 30%)
</span>
</div>
<div
class={`${styles.colorVariant} ${styles.colorAccentHigh}`}
>
<span class={styles.colorVariantCode}>
hsl(13, 100%, 78%)
</span>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</section>
<div class={styles.divider}></div>
<section class={styles.buttonSection}>
<h2 class={styles.sectionTitle}>Buttons</h2>
<table class={styles.componentTable}>
<tbody>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Primary</h3>
<Button>Primary Button</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Secondary</h3>
<Button color="secondary">Secondary Button</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Ghost</h3>
<Button color="ghost">Ghost Button</Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Primary Disabled</h3>
<Button disabled>Primary Button</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Secondary Disabled</h3>
<Button color="secondary" disabled>
Secondary Button
</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Ghost Disabled</h3>
<Button color="ghost" disabled>
Ghost Button
</Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small</h3>
<Button size="sm">Small Button</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small Secondary</h3>
<Button size="sm" color="secondary">
Small Secondary
</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small Ghost</h3>
<Button size="sm" color="ghost">
Small Ghost
</Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>With Icon</h3>
<Button icon={<IconHome />}>With Icon</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Icon + Secondary</h3>
<Button icon={<IconHome />} color="secondary">
Icon Secondary
</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Icon + Ghost</h3>
<Button icon={<IconHome />} color="ghost">
Icon Ghost
</Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small + Icon</h3>
<Button size="sm" icon={<IconHome />}>
Small Icon
</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small + Icon + Secondary</h3>
<Button size="sm" icon={<IconHome />} color="secondary">
Small Icon Secondary
</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small + Icon + Ghost</h3>
<Button size="sm" icon={<IconHome />} color="ghost">
Small Icon Ghost
</Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Icon Only</h3>
<Button icon={<IconHome />}></Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Icon Only + Secondary</h3>
<Button icon={<IconHome />} color="secondary"></Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Icon Only + Ghost</h3>
<Button icon={<IconHome />} color="ghost"></Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Icon Only Disabled</h3>
<Button icon={<IconHome />} disabled></Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>
Icon Only + Secondary Disabled
</h3>
<Button icon={<IconHome />} color="secondary" disabled></Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>
Icon Only + Ghost Disabled
</h3>
<Button icon={<IconHome />} color="ghost" disabled></Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small Icon Only</h3>
<Button size="sm" icon={<IconHome />}></Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>
Small Icon Only + Secondary
</h3>
<Button
size="sm"
icon={<IconHome />}
color="secondary"
></Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small Icon Only + Ghost</h3>
<Button size="sm" icon={<IconHome />} color="ghost"></Button>
</td>
</tr>
</tbody>
</table>
</section>
<div class={styles.divider}></div>
<section class={styles.labelSection}>
<h2 class={styles.sectionTitle}>Labels</h2>
<table class={styles.componentTable}>
<tbody>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small</h3>
<label data-size="sm" data-component="label">
Small Label Text
</label>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Medium</h3>
<label data-size="md" data-component="label">
Medium Label Text
</label>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Large</h3>
<label data-size="lg" data-component="label">
Large Label Text
</label>
</td>
</tr>
</tbody>
</table>
</section>
<div class={styles.divider}></div>
<section class={styles.inputSection}>
<h2 class={styles.sectionTitle}>Inputs</h2>
<table class={styles.componentTable}>
<tbody>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small</h3>
<input
data-component="input"
data-size="sm"
placeholder="Small input field"
/>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Medium</h3>
<input
data-component="input"
data-size="md"
placeholder="Medium input field"
/>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Large</h3>
<input
data-component="input"
data-size="lg"
placeholder="Large input field"
/>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Disabled</h3>
<input
data-component="input"
data-size="md"
placeholder="Disabled input"
disabled
/>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>With Value</h3>
<input
data-component="input"
data-size="md"
value="Input with preset value"
readOnly
/>
</td>
</tr>
</tbody>
</table>
</section>
<div class={styles.divider}></div>
<section class={styles.dialogSection}>
<h2 class={styles.sectionTitle}>Dialogs</h2>
<table class={styles.componentTable}>
<tbody>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Default</h3>
<Button color="secondary" onClick={() => setDialogOpen(true)}>
Open Dialog
</Button>
<Dialog open={dialogOpen()} onOpenChange={setDialogOpen}>
<div data-slot="header">
<div data-slot="title">Dialog Title</div>
</div>
<div data-slot="main">
<p>This is the default dialog content.</p>
</div>
<div data-slot="footer">
<Button onClick={() => setDialogOpen(false)}>Close</Button>
</div>
</Dialog>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small With Transition</h3>
<Button
color="secondary"
onClick={() => {
setDialogOpenTransition(true)
}}
>
Small Dialog
</Button>
<Dialog
open={dialogOpenTransition()}
onOpenChange={setDialogOpenTransition}
size="sm"
transition={true}
>
<div class={styles.dialogContent}>
<h2 class={styles.sectionTitle}>Small Dialog</h2>
<p>This is a smaller dialog with transitions.</p>
<div class={styles.dialogContentFooter}>
<Button onClick={() => setDialogOpenTransition(false)}>
Close
</Button>
</div>
</div>
</Dialog>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Input String</h3>
<Button
color="secondary"
onClick={() =>
dialog.open(DialogString, {
title: "Name",
action: "Change name",
placeholder: "Enter a name",
onSubmit: () => {},
})
}
>
String
</Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Select Input</h3>
<Button
color="secondary"
onClick={() =>
dialog.open(DialogSelect, {
placeholder: "Select",
title: "User Settings",
options: [
{
display: "Change name",
prefix: <IconPencilSquare />,
onSelect: () => {
dialog.close()
},
},
{
display: "Remove user",
prefix: <IconHome />,
onSelect: () => {
dialog.close()
},
},
],
})
}
>
Select
</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Select Input</h3>
<Button
color="secondary"
onClick={() =>
dialog.open(DialogSelect, {
placeholder: "Select",
title: "User Settings",
options: [
{
display: "Change name",
onSelect: () => {
dialog.close()
},
},
{
display: "Remove user",
onSelect: () => {
dialog.close()
},
},
],
})
}
>
No Prefix
</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Select No Options</h3>
<Button
color="secondary"
onClick={() =>
dialog.open(DialogSelect, {
placeholder: "Select",
title: "User Settings",
options: [],
})
}
>
No Options
</Button>
</td>
</tr>
</tbody>
</table>
</section>
</div>
)
}
+12
View File
@@ -0,0 +1,12 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_DOCS_URL: string
readonly VITE_API_URL: string
readonly VITE_AUTH_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
+24
View File
@@ -0,0 +1,24 @@
import { Button as Kobalte } from "@kobalte/core/button"
import { JSX, Show, splitProps } from "solid-js"
export interface ButtonProps {
color?: "primary" | "secondary" | "ghost"
size?: "md" | "sm"
icon?: JSX.Element
}
export function Button(props: JSX.IntrinsicElements["button"] & ButtonProps) {
const [split, rest] = splitProps(props, ["color", "size", "icon"])
return (
<Kobalte
{...rest}
data-component="button"
data-size={split.size || "md"}
data-color={split.color || "primary"}
>
<Show when={props.icon}>
<div data-slot="icon">{props.icon}</div>
</Show>
{props.children}
</Kobalte>
)
}
+120
View File
@@ -0,0 +1,120 @@
import { createContext, JSX, ParentProps, useContext } from "solid-js"
import { StandardSchemaV1 } from "@standard-schema/spec"
import { createStore } from "solid-js/store"
import { Dialog } from "./dialog"
const Context = createContext<DialogControl>()
type DialogControl = {
open<Schema extends StandardSchemaV1<object>>(
component: DialogComponent<Schema>,
input: StandardSchemaV1.InferInput<Schema>,
): void
close(): void
isOpen(input: any): boolean
size: "sm" | "md"
transition?: boolean
input?: any
}
type DialogProps<Schema extends StandardSchemaV1<object>> = {
input: StandardSchemaV1.InferInput<Schema>
control: DialogControl
}
type DialogComponent<Schema extends StandardSchemaV1<object>> = ReturnType<
typeof createDialog<Schema>
>
export function createDialog<Schema extends StandardSchemaV1<object>>(props: {
schema: Schema
size: "sm" | "md"
render: (props: DialogProps<Schema>) => JSX.Element
}) {
const result = () => {
const dialog = useDialog()
return (
<Dialog
size={dialog.size}
transition={dialog.transition}
open={dialog.isOpen(result)}
onOpenChange={(val) => {
if (!val) dialog.close()
}}
>
{props.render({
input: dialog.input,
control: dialog,
})}
</Dialog>
)
}
result.schema = props.schema
result.size = props.size
return result
}
export function DialogProvider(props: ParentProps) {
const [store, setStore] = createStore<{
dialog?: DialogComponent<any>
input?: any
transition?: boolean
size: "sm" | "md"
}>({
size: "sm",
})
const control: DialogControl = {
get input() {
return store.input
},
get size() {
return store.size
},
get transition() {
return store.transition
},
isOpen(input) {
return store.dialog === input
},
open(component, input) {
setStore({
dialog: component,
input: input,
size: store.dialog !== undefined ? store.size : component.size,
transition: store.dialog !== undefined,
})
setTimeout(() => {
setStore({
size: component.size,
})
}, 0)
setTimeout(() => {
setStore({
transition: false,
})
}, 150)
},
close() {
setStore({
dialog: undefined,
})
},
}
return (
<>
<Context.Provider value={control}>{props.children}</Context.Provider>
</>
)
}
export function useDialog() {
const ctx = useContext(Context)
if (!ctx) {
throw new Error("useDialog must be used within a DialogProvider")
}
return ctx
}
+36
View File
@@ -0,0 +1,36 @@
.options {
margin-top: var(--space-1);
border-top: 2px solid var(--color-border);
padding: var(--space-2);
[data-slot="option"] {
outline: none;
flex-shrink: 0;
height: var(--space-11);
display: flex;
justify-content: start;
align-items: center;
padding: 0 var(--space-2-5);
gap: var(--space-3);
cursor: pointer;
&[data-empty] {
cursor: default;
color: var(--color-text-dimmed);
}
&[data-active] {
background-color: var(--color-bg-surface);
}
[data-slot="title"] {
font-size: var(--font-size-md);
}
[data-slot="prefix"] {
width: var(--space-4);
height: var(--space-4);
}
}
}
+124
View File
@@ -0,0 +1,124 @@
import style from "./dialog-select.module.css"
import { z } from "zod"
import { createMemo, createSignal, For, JSX, onMount } from "solid-js"
import { createList } from "solid-list"
import { createDialog } from "./context-dialog"
export const DialogSelect = createDialog({
size: "md",
schema: z.object({
title: z.string(),
placeholder: z.string(),
onSelect: z
.function(z.tuple([z.any()]))
.returns(z.void())
.optional(),
options: z.array(
z.object({
display: z.string(),
value: z.any().optional(),
onSelect: z.function().returns(z.void()).optional(),
prefix: z.custom<JSX.Element>().optional(),
}),
),
}),
render: (ctx) => {
let input: HTMLInputElement
onMount(() => {
input.focus()
input.value = ""
})
const [filter, setFilter] = createSignal("")
const filtered = createMemo(() =>
ctx.input.options?.filter((i) =>
i.display.toLowerCase().includes(filter().toLowerCase()),
),
)
const list = createList({
loop: true,
initialActive: 0,
items: () => filtered().map((_, i) => i),
handleTab: false,
})
const handleSelection = (index: number) => {
const option = ctx.input.options[index]
// If the option has its own onSelect handler, use it
if (option.onSelect) {
option.onSelect()
}
// Otherwise, if there's a global onSelect handler, call it with the option's value
else if (ctx.input.onSelect) {
ctx.input.onSelect(
option.value !== undefined ? option.value : option.display,
)
}
}
return (
<>
<div data-slot="header">
<label
data-size="md"
data-slot="title"
data-component="label"
for={`dialog-select-${ctx.input.title}`}
>
{ctx.input.title}
</label>
</div>
<div data-slot="main">
<input
data-size="lg"
data-component="input"
value={filter()}
onInput={(e) => {
setFilter(e.target.value)
list.setActive(0)
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
const selected = list.active()
if (selected === null) return
handleSelection(selected)
return
}
if (e.key === "Escape") {
setFilter("")
return
}
list.onKeyDown(e)
}}
id={`dialog-select-${ctx.input.title}`}
ref={(r) => (input = r)}
data-slot="input"
placeholder={ctx.input.placeholder}
/>
</div>
<div data-slot="options" class={style.options}>
<For
each={filtered()}
fallback={
<div data-slot="option" data-empty>
No results
</div>
}
>
{(option, index) => (
<div
onClick={() => handleSelection(index())}
data-slot="option"
data-active={list.active() === index() ? true : undefined}
>
{option.prefix && <div data-slot="prefix">{option.prefix}</div>}
<div data-slot="title">{option.display}</div>
</div>
)}
</For>
</div>
</>
)
},
})
+70
View File
@@ -0,0 +1,70 @@
import { z } from "zod"
import { onMount } from "solid-js"
import { createDialog } from "./context-dialog"
import { Button } from "./button"
export const DialogString = createDialog({
size: "sm",
schema: z.object({
title: z.string(),
placeholder: z.string(),
action: z.string(),
onSubmit: z.function().args(z.string()).returns(z.void()),
}),
render: (ctx) => {
let input: HTMLInputElement
onMount(() => {
setTimeout(() => {
input.focus()
input.value = ""
}, 50)
})
function submit() {
const value = input.value.trim()
if (value) {
ctx.input.onSubmit(value)
ctx.control.close()
}
}
return (
<>
<div data-slot="header">
<label
data-size="md"
data-slot="title"
data-component="label"
for={`dialog-string-${ctx.input.title}`}
>
{ctx.input.title}
</label>
</div>
<div data-slot="main">
<input
data-slot="input"
data-size="lg"
data-component="input"
ref={(r) => (input = r)}
placeholder={ctx.input.placeholder}
id={`dialog-string-${ctx.input.title}`}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
submit()
}
}}
/>
</div>
<div data-slot="footer">
<Button size="md" color="ghost" onClick={() => ctx.control.close()}>
Cancel
</Button>
<Button size="md" color="secondary" onClick={submit}>
{ctx.input.action}
</Button>
</div>
</>
)
},
})
+27
View File
@@ -0,0 +1,27 @@
import { Dialog as Kobalte } from "@kobalte/core/dialog"
import { ComponentProps, ParentProps } from "solid-js"
export type Props = ParentProps<{
size?: "sm" | "md"
transition?: boolean
}> &
ComponentProps<typeof Kobalte>
export function Dialog(props: Props) {
return (
<Kobalte {...props}>
<Kobalte.Portal>
<Kobalte.Overlay data-component="dialog-overlay" />
<div data-component="dialog-center">
<Kobalte.Content
data-transition={props.transition ? "" : undefined}
data-size={props.size}
data-slot="content"
>
{props.children}
</Kobalte.Content>
</div>
</Kobalte.Portal>
</Kobalte>
)
}
@@ -0,0 +1,78 @@
[data-component="button"] {
width: fit-content;
display: flex;
line-height: 1;
align-items: center;
justify-content: center;
gap: var(--space-2);
font-size: var(--font-size-md);
text-transform: uppercase;
height: var(--space-11);
outline: none;
font-weight: 500;
padding: 0 var(--space-4);
border-width: 2px;
border-color: var(--color-border);
cursor: pointer;
&:disabled {
opacity: 0.5;
cursor: default;
}
&[data-color="primary"] {
background-color: var(--color-text);
border-color: var(--color-text);
color: var(--color-text-invert);
&:active {
border-color: var(--color-accent);
}
}
&[data-color="secondary"] {
&:active {
border-color: var(--color-accent);
}
}
&[data-color="ghost"] {
border: none;
text-decoration: underline;
&:active {
color: var(--color-text-accent);
}
}
&:has([data-slot="icon"]) {
padding-left: var(--space-3);
padding-right: var(--space-3);
}
&[data-size="sm"] {
height: var(--space-8);
padding: var(--space-3);
font-size: var(--font-size-xs);
[data-slot="icon"] {
width: var(--space-3-5);
height: var(--space-3-5);
}
&:has([data-slot="icon"]) {
padding-left: var(--space-2);
padding-right: var(--space-2);
}
}
[data-slot="icon"] {
width: var(--space-4);
height: var(--space-4);
transition: transform 0.2s ease;
}
&[data-rotate] [data-slot="icon"] {
transform: rotate(180deg);
}
}
@@ -0,0 +1,84 @@
[data-component="dialog-overlay"] {
pointer-events: none !important;
position: fixed;
inset: 0;
animation-name: fadeOut;
animation-duration: 200ms;
animation-timing-function: ease;
opacity: 0;
backdrop-filter: blur(2px);
&[data-expanded] {
animation-name: fadeIn;
opacity: 1;
pointer-events: auto !important;
}
}
[data-component="dialog-center"] {
position: fixed;
inset: 0;
padding-top: 10vh;
justify-content: center;
pointer-events: none;
[data-slot="content"] {
width: 45rem;
margin: 0 auto;
transition: 150ms width;
background-color: var(--color-bg);
border-width: 2px;
border-color: var(--color-border);
overflow: hidden;
display: flex;
flex-direction: column;
gap: var(--space-3);
outline: none;
animation-duration: 1ms;
animation-name: zoomOut;
animation-timing-function: ease;
box-shadow: 8px 8px 0px 0px var(--color-gray-4);
&[data-expanded] {
animation-name: zoomIn;
}
&[data-transition] {
animation-duration: 200ms;
}
&[data-size="sm"] {
width: 30rem;
}
[data-slot="header"] {
display: flex;
padding: var(--space-4) var(--space-4) 0;
[data-slot="title"] {
}
}
[data-slot="main"] {
padding: 0 var(--space-4);
&:has([data-slot="options"]) {
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-4);
}
}
[data-slot="input"] {
}
[data-slot="footer"] {
padding: var(--space-4);
display: flex;
gap: var(--space-4);
justify-content: end;
}
}
}
@@ -0,0 +1,34 @@
[data-component="input"] {
font-size: var(--font-size-md);
background: transparent;
caret-color: var(--color-accent);
font-family: var(--font-mono);
height: var(--space-11);
padding: 0 var(--space-4);
width: 100%;
resize: none;
border: 2px solid var(--color-border);
&::placeholder {
color: var(--color-text-dimmed);
opacity: 0.75;
}
&:focus {
outline: 0;
}
&[data-size="sm"] {
height: var(--space-9);
padding: 0 var(--space-3);
font-size: var(--font-size-xs);
}
&[data-size="md"] {
}
&[data-size="lg"] {
height: var(--space-12);
font-size: var(--font-size-lg);
}
}
@@ -0,0 +1,17 @@
[data-component="label"] {
letter-spacing: -0.03125rem;
text-transform: uppercase;
color: var(--color-text-dimmed);
font-weight: 500;
font-size: var(--font-size-md);
&[data-size="sm"] {
font-size: var(--font-size-sm);
}
&[data-size="md"] {
}
&[data-size="lg"] {
font-size: var(--font-size-lg);
}
}
@@ -0,0 +1,32 @@
[data-component="title-bar"] {
display: flex;
align-items: center;
justify-content: space-between;
height: 72px;
padding: 0 var(--space-4);
border-bottom: 2px solid var(--color-border);
[data-slot="left"] {
display: flex;
flex-direction: column;
gap: var(--space-1-5);
h1 {
letter-spacing: -0.03125rem;
font-size: var(--font-size-xl);
text-transform: uppercase;
font-weight: 600;
}
p {
color: var(--color-text-dimmed);
}
}
}
@media (max-width: 40rem) {
[data-component="title-bar"] {
display: none;
}
}
+50
View File
@@ -0,0 +1,50 @@
/* tokens */
@import "./token/color.css";
@import "./token/reset.css";
@import "./token/animation.css";
@import "./token/font.css";
@import "./token/space.css";
/* components */
@import "./component/label.css";
@import "./component/input.css";
@import "./component/button.css";
@import "./component/dialog.css";
@import "./component/title-bar.css";
body {
font-family: var(--font-mono);
line-height: 1;
color: var(--color-text);
background-color: var(--color-bg);
cursor: default;
user-select: none;
text-underline-offset: 0.1875rem;
}
a {
text-decoration: underline;
&:active {
color: var(--color-text-accent);
}
}
::selection {
background-color: var(--color-text-accent-invert);
}
/* Responsive utilities */
[data-max-width] {
width: 100%;
& > * {
max-width: 90rem;
margin-left: auto;
margin-right: auto;
width: 100%;
}
&[data-max-width-64] > * {
max-width: 64rem;
}
}
@@ -0,0 +1,23 @@
@keyframes zoomIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes zoomOut {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
+88
View File
@@ -0,0 +1,88 @@
:root {
--color-white: hsl(0, 0%, 100%);
--color-gray-1: hsl(224, 20%, 94%);
--color-gray-2: hsl(224, 6%, 77%);
--color-gray-3: hsl(224, 6%, 56%);
--color-gray-4: hsl(224, 7%, 36%);
--color-gray-5: hsl(224, 10%, 23%);
--color-gray-6: hsl(224, 14%, 16%);
--color-black: hsl(224, 10%, 10%);
--hue-orange: 41;
--color-orange-low: hsl(var(--hue-orange), 39%, 22%);
--color-orange: hsl(var(--hue-orange), 82%, 63%);
--color-orange-high: hsl(var(--hue-orange), 82%, 87%);
--hue-green: 101;
--color-green-low: hsl(var(--hue-green), 39%, 22%);
--color-green: hsl(var(--hue-green), 82%, 63%);
--color-green-high: hsl(var(--hue-green), 82%, 80%);
--hue-blue: 234;
--color-blue-low: hsl(var(--hue-blue), 54%, 20%);
--color-blue: hsl(var(--hue-blue), 100%, 60%);
--color-blue-high: hsl(var(--hue-blue), 100%, 87%);
--hue-purple: 281;
--color-purple-low: hsl(var(--hue-purple), 39%, 22%);
--color-purple: hsl(var(--hue-purple), 82%, 63%);
--color-purple-high: hsl(var(--hue-purple), 82%, 89%);
--hue-red: 339;
--color-red-low: hsl(var(--hue-red), 39%, 22%);
--color-red: hsl(var(--hue-red), 82%, 63%);
--color-red-high: hsl(var(--hue-red), 82%, 87%);
--color-accent-low: hsl(13, 75%, 30%);
--color-accent: hsl(13, 88%, 57%);
--color-accent-high: hsl(13, 100%, 78%);
--color-text: var(--color-gray-1);
--color-text-dimmed: var(--color-gray-3);
--color-text-accent: var(--color-accent);
--color-text-invert: var(--color-black);
--color-text-accent-invert: var(--color-accent-high);
--color-bg: var(--color-black);
--color-bg-surface: var(--color-gray-5);
--color-bg-accent: var(--color-accent-high);
--color-border: var(--color-gray-2);
--color-backdrop-overlay: hsla(223, 13%, 10%, 0.66);
}
:root[data-color-mode="light"] {
--color-white: hsl(224, 10%, 10%);
--color-gray-1: hsl(224, 14%, 16%);
--color-gray-2: hsl(224, 10%, 23%);
--color-gray-3: hsl(224, 7%, 36%);
--color-gray-4: hsl(224, 6%, 56%);
--color-gray-5: hsl(224, 6%, 77%);
--color-gray-6: hsl(224, 20%, 94%);
--color-gray-7: hsl(224, 19%, 97%);
--color-black: hsl(0, 0%, 100%);
--color-orange-high: hsl(var(--hue-orange), 80%, 25%);
--color-orange: hsl(var(--hue-orange), 90%, 60%);
--color-orange-low: hsl(var(--hue-orange), 90%, 88%);
--color-green-high: hsl(var(--hue-green), 80%, 22%);
--color-green: hsl(var(--hue-green), 90%, 46%);
--color-green-low: hsl(var(--hue-green), 85%, 90%);
--color-blue-high: hsl(var(--hue-blue), 80%, 30%);
--color-blue: hsl(var(--hue-blue), 90%, 60%);
--color-blue-low: hsl(var(--hue-blue), 88%, 90%);
--color-purple-high: hsl(var(--hue-purple), 90%, 30%);
--color-purple: hsl(var(--hue-purple), 90%, 60%);
--color-purple-low: hsl(var(--hue-purple), 80%, 90%);
--color-red-high: hsl(var(--hue-red), 80%, 30%);
--color-red: hsl(var(--hue-red), 90%, 60%);
--color-red-low: hsl(var(--hue-red), 80%, 90%);
--color-accent-high: hsl(13, 75%, 26%);
--color-accent: hsl(13, 88%, 60%);
--color-accent-low: hsl(13, 100%, 89%);
--color-text-accent: var(--color-accent);
--color-text-dimmed: var(--color-gray-4);
--color-text-invert: var(--color-black);
--color-text-accent-invert: var(--color-accent-low);
--color-bg-surface: var(--color-gray-6);
--color-bg-accent: var(--color-accent);
--color-backdrop-overlay: hsla(225, 9%, 36%, 0.66);
}
+20
View File
@@ -0,0 +1,20 @@
:root {
--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, monospace;
--font-sans: Rubik, sans-serif;
--font-line-height: 1.75;
}
+212
View File
@@ -0,0 +1,212 @@
* {
margin: 0;
padding: 0;
font: inherit;
}
*,
*::before,
*::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: var(--global-color-border, currentColor);
}
html {
line-height: 1.5;
--font-fallback: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-moz-tab-size: 4;
tab-size: 4;
font-family: var(--global-font-body, var(--font-fallback));
}
hr {
height: 0;
color: inherit;
border-top-width: 1px;
}
body {
height: 100%;
line-height: inherit;
}
img {
border-style: none;
}
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
vertical-align: middle;
}
img,
video {
max-width: 100%;
height: auto;
}
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
ol,
ul {
list-style: none;
}
code,
kbd,
pre,
samp {
font-size: 1em;
}
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
background-color: transparent;
background-image: none;
}
button,
input,
optgroup,
select,
textarea {
color: inherit;
}
button,
select {
text-transform: none;
}
table {
text-indent: 0;
border-color: inherit;
border-collapse: collapse;
}
input::placeholder,
textarea::placeholder {
opacity: 1;
color: var(--global-color-placeholder, #9ca3af);
}
textarea {
resize: vertical;
}
summary {
display: list-item;
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
dialog {
padding: 0;
}
a {
color: inherit;
text-decoration: inherit;
}
abbr:where([title]) {
text-decoration: underline dotted;
}
b,
strong {
font-weight: bolder;
}
code,
kbd,
samp,
pre {
font-size: 1em;
--font-mono-fallback: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New";
font-family: var(--global-font-mono, var(--font-fallback));
}
input[type="text"],
input[type="email"],
input[type="search"],
input[type="password"] {
-webkit-appearance: none;
-moz-appearance: none;
}
input[type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
::-webkit-search-decoration,
::-webkit-search-cancel-button {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
input[type="number"] {
-moz-appearance: textfield;
}
:-moz-ui-invalid {
box-shadow: none;
}
:-moz-focusring {
outline: auto;
}
+38
View File
@@ -0,0 +1,38 @@
:root {
--space-0: 0;
--space-px: 1px;
--space-0-5: 0.125rem;
--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-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;
}
File diff suppressed because it is too large Load Diff
+67
View File
@@ -0,0 +1,67 @@
import { JSX } from "solid-js"
export function IconLogomark(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} 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="currentColor"
/>
</svg>
)
}
export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 220 35" 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.99208L2.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="currentColor"
/>
<path
d="M45.628 28.432C42.556 28.432 40.252 27.688 38.716 26.2C37.204 24.688 36.448 22.552 36.448 19.792V12.628C36.448 9.84399 37.204 7.70799 38.716 6.21999C40.252 4.73199 42.556 3.98799 45.628 3.98799C48.7 3.98799 50.992 4.73199 52.504 6.21999C54.04 7.70799 54.808 9.84399 54.808 12.628V19.792C54.808 22.552 54.04 24.688 52.504 26.2C50.992 27.688 48.7 28.432 45.628 28.432ZM45.628 25.228C47.452 25.228 48.832 24.76 49.768 23.824C50.704 22.864 51.172 21.484 51.172 19.684V12.736C51.172 10.912 50.704 9.53199 49.768 8.59599C48.832 7.65999 47.452 7.19199 45.628 7.19199C43.828 7.19199 42.448 7.65999 41.488 8.59599C40.552 9.53199 40.084 10.912 40.084 12.736V19.684C40.084 21.484 40.552 22.864 41.488 23.824C42.448 24.76 43.828 25.228 45.628 25.228Z"
fill="currentColor"
/>
<path
d="M67.0294 28.432C66.1654 28.432 65.2414 28.312 64.2574 28.072C63.2734 27.856 62.4694 27.544 61.8454 27.136L61.7734 24.184C62.4214 24.544 63.1414 24.832 63.9334 25.048C64.7254 25.24 65.4574 25.336 66.1294 25.336C67.4254 25.336 68.3614 24.964 68.9374 24.22C69.5374 23.452 69.8374 22.312 69.8374 20.8V16.912C69.8374 15.592 69.6094 14.632 69.1534 14.032C68.6974 13.408 68.0014 13.096 67.0654 13.096C66.2734 13.096 65.4694 13.336 64.6534 13.816C63.8374 14.272 62.8174 15.064 61.5934 16.192L61.4854 13.168C62.2534 12.448 62.9854 11.848 63.6814 11.368C64.3774 10.888 65.0854 10.528 65.8054 10.288C66.5254 10.048 67.2934 9.92799 68.1094 9.92799C69.8614 9.92799 71.1814 10.468 72.0694 11.548C72.9814 12.628 73.4374 14.272 73.4374 16.48V21.196C73.4374 23.572 72.8854 25.372 71.7814 26.596C70.7014 27.82 69.1174 28.432 67.0294 28.432ZM59.1094 34.66C58.8454 34.66 58.7134 34.528 58.7134 34.264V14.536C58.7134 13.936 58.6894 13.3 58.6414 12.628C58.5934 11.956 58.5334 11.356 58.4614 10.828C58.4374 10.516 58.5694 10.36 58.8574 10.36H61.4134C61.6774 10.36 61.8214 10.48 61.8454 10.72C61.8934 10.912 61.9414 11.164 61.9894 11.476C62.0374 11.788 62.0734 12.1 62.0974 12.412C62.1214 12.7 62.1334 12.916 62.1334 13.06L62.3134 14.968V34.264C62.3134 34.528 62.1814 34.66 61.9174 34.66H59.1094Z"
fill="currentColor"
/>
<path
d="M84.0237 28.432C81.5757 28.432 79.7157 27.88 78.4437 26.776C77.1957 25.672 76.5717 24.028 76.5717 21.844V17.056C76.5717 14.728 77.1957 12.964 78.4437 11.764C79.6917 10.54 81.5397 9.92799 83.9877 9.92799C86.4117 9.92799 88.2477 10.516 89.4957 11.692C90.7437 12.868 91.3677 14.584 91.3677 16.84V19.648C91.3677 19.912 91.2477 20.044 91.0077 20.044H80.1717V21.196C80.1717 22.66 80.4837 23.728 81.1077 24.4C81.7317 25.072 82.7397 25.408 84.1317 25.408C85.1877 25.408 86.0037 25.24 86.5797 24.904C87.1557 24.568 87.4437 24.052 87.4437 23.356C87.4437 23.092 87.5877 22.96 87.8757 22.96H90.6477C90.8637 22.96 90.9957 23.08 91.0437 23.32C91.1397 24.928 90.5637 26.188 89.3157 27.1C88.0677 27.988 86.3037 28.432 84.0237 28.432ZM80.1717 17.308H87.8037V17.128C87.8037 15.688 87.4917 14.632 86.8677 13.96C86.2437 13.288 85.2957 12.952 84.0237 12.952C82.7277 12.952 81.7557 13.3 81.1077 13.996C80.4837 14.692 80.1717 15.736 80.1717 17.128V17.308Z"
fill="currentColor"
/>
<path
d="M106.647 28C106.383 28 106.251 27.868 106.251 27.604V16.3C106.251 15.196 106.023 14.392 105.567 13.888C105.135 13.36 104.451 13.096 103.515 13.096C102.627 13.096 101.751 13.36 100.887 13.888C100.023 14.392 98.9906 15.244 97.7906 16.444L97.7186 13.528C98.4866 12.712 99.2306 12.04 99.9506 11.512C100.671 10.984 101.403 10.588 102.147 10.324C102.915 10.06 103.719 9.92799 104.559 9.92799C106.287 9.92799 107.595 10.432 108.483 11.44C109.395 12.424 109.851 13.912 109.851 15.904V27.604C109.851 27.868 109.731 28 109.491 28H106.647ZM95.2346 28C94.9706 28 94.8386 27.868 94.8386 27.604V14.824C94.8386 14.152 94.8026 13.444 94.7306 12.7C94.6826 11.932 94.6346 11.32 94.5866 10.864C94.5386 10.528 94.6706 10.36 94.9826 10.36H97.5386C97.7786 10.36 97.9226 10.48 97.9706 10.72C98.0186 10.936 98.0666 11.236 98.1146 11.62C98.1626 12.004 98.2106 12.412 98.2586 12.844C98.3066 13.276 98.3306 13.648 98.3306 13.96L98.4386 15.112V27.604C98.4386 27.868 98.3066 28 98.0426 28H95.2346Z"
fill="currentColor"
/>
<path
d="M122.87 28.432C119.894 28.432 117.626 27.7 116.066 26.236C114.506 24.748 113.726 22.6 113.726 19.792V12.664C113.726 9.83199 114.506 7.68399 116.066 6.21999C117.65 4.73199 119.93 3.98799 122.906 3.98799C124.85 3.98799 126.542 4.31199 127.982 4.95999C129.422 5.60799 130.49 6.54399 131.186 7.76799C131.906 8.96799 132.146 10.408 131.906 12.088C131.882 12.232 131.846 12.352 131.798 12.448C131.75 12.544 131.654 12.592 131.51 12.592H128.63C128.366 12.592 128.246 12.46 128.27 12.196C128.342 10.612 127.922 9.37599 127.01 8.48799C126.098 7.59999 124.742 7.15599 122.942 7.15599C121.142 7.15599 119.762 7.62399 118.802 8.55999C117.842 9.49599 117.362 10.864 117.362 12.664V19.756C117.362 21.556 117.842 22.924 118.802 23.86C119.762 24.796 121.142 25.264 122.942 25.264C124.766 25.264 126.146 24.82 127.082 23.932C128.042 23.02 128.438 21.784 128.27 20.224C128.246 19.96 128.366 19.828 128.63 19.828H131.474C131.714 19.828 131.858 19.996 131.906 20.332C132.05 21.964 131.762 23.392 131.042 24.616C130.346 25.816 129.29 26.752 127.874 27.424C126.458 28.096 124.79 28.432 122.87 28.432Z"
fill="currentColor"
/>
<path
d="M142.558 28.432C140.086 28.432 138.19 27.832 136.87 26.632C135.55 25.432 134.89 23.62 134.89 21.196V17.164C134.89 14.74 135.538 12.928 136.834 11.728C138.154 10.528 140.062 9.92799 142.558 9.92799C145.03 9.92799 146.914 10.528 148.21 11.728C149.53 12.928 150.19 14.74 150.19 17.164V21.196C150.19 23.62 149.542 25.432 148.246 26.632C146.95 27.832 145.054 28.432 142.558 28.432ZM142.558 25.3C143.974 25.3 144.994 24.94 145.618 24.22C146.266 23.5 146.59 22.384 146.59 20.872V17.488C146.59 16 146.266 14.896 145.618 14.176C144.994 13.432 143.974 13.06 142.558 13.06C141.118 13.06 140.074 13.432 139.426 14.176C138.802 14.896 138.49 16 138.49 17.488V20.872C138.49 22.384 138.802 23.5 139.426 24.22C140.074 24.94 141.118 25.3 142.558 25.3Z"
fill="currentColor"
/>
<path
d="M165.493 28C165.229 28 165.097 27.868 165.097 27.604V16.3C165.097 15.196 164.869 14.392 164.413 13.888C163.981 13.36 163.297 13.096 162.361 13.096C161.473 13.096 160.597 13.36 159.733 13.888C158.869 14.392 157.837 15.244 156.637 16.444L156.565 13.528C157.333 12.712 158.077 12.04 158.797 11.512C159.517 10.984 160.249 10.588 160.993 10.324C161.761 10.06 162.565 9.92799 163.405 9.92799C165.133 9.92799 166.441 10.432 167.329 11.44C168.241 12.424 168.697 13.912 168.697 15.904V27.604C168.697 27.868 168.577 28 168.337 28H165.493ZM154.081 28C153.817 28 153.685 27.868 153.685 27.604V14.824C153.685 14.152 153.649 13.444 153.577 12.7C153.529 11.932 153.481 11.32 153.433 10.864C153.385 10.528 153.517 10.36 153.829 10.36H156.385C156.625 10.36 156.769 10.48 156.817 10.72C156.865 10.936 156.913 11.236 156.961 11.62C157.009 12.004 157.057 12.412 157.105 12.844C157.153 13.276 157.177 13.648 157.177 13.96L157.285 15.112V27.604C157.285 27.868 157.153 28 156.889 28H154.081Z"
fill="currentColor"
/>
<path
d="M179.477 28.432C178.157 28.432 177.113 28.228 176.345 27.82C175.577 27.388 175.013 26.74 174.653 25.876C174.317 24.988 174.149 23.884 174.149 22.564V13.42H171.413C171.173 13.42 171.053 13.288 171.053 13.024V10.756C171.053 10.492 171.173 10.36 171.413 10.36H174.257L174.689 6.03999C174.713 5.79999 174.857 5.67999 175.121 5.67999H177.317C177.581 5.67999 177.713 5.79999 177.713 6.03999L177.749 10.36H182.897C183.161 10.36 183.293 10.492 183.293 10.756V13.024C183.293 13.288 183.161 13.42 182.897 13.42H177.749V22.42C177.749 23.476 177.929 24.232 178.289 24.688C178.673 25.12 179.321 25.336 180.233 25.336C180.689 25.336 181.145 25.288 181.601 25.192C182.081 25.072 182.501 24.928 182.861 24.76C183.173 24.64 183.329 24.736 183.329 25.048V27.388C183.329 27.604 183.233 27.748 183.041 27.82C182.585 27.988 182.057 28.132 181.457 28.252C180.881 28.372 180.221 28.432 179.477 28.432Z"
fill="currentColor"
/>
<path
d="M186.233 28C185.969 28 185.837 27.868 185.837 27.604V14.716C185.837 14.02 185.813 13.36 185.765 12.736C185.741 12.088 185.681 11.44 185.585 10.792C185.537 10.504 185.669 10.36 185.981 10.36H188.537C188.801 10.36 188.945 10.48 188.969 10.72C189.065 11.152 189.137 11.656 189.185 12.232C189.233 12.808 189.257 13.3 189.257 13.708L189.437 15.688V27.604C189.437 27.868 189.305 28 189.041 28H186.233ZM189.041 17.02L188.897 13.744C189.401 13 189.977 12.352 190.625 11.8C191.273 11.224 191.933 10.768 192.605 10.432C193.277 10.096 193.901 9.92799 194.477 9.92799C194.885 9.92799 195.209 9.96399 195.449 10.036C195.641 10.108 195.749 10.252 195.773 10.468C195.821 10.996 195.833 11.56 195.809 12.16C195.809 12.736 195.773 13.288 195.701 13.816C195.653 14.104 195.485 14.212 195.197 14.14C195.029 14.092 194.837 14.056 194.621 14.032C194.405 14.008 194.177 13.996 193.937 13.996C193.361 13.996 192.785 14.128 192.209 14.392C191.633 14.632 191.069 14.98 190.517 15.436C189.989 15.868 189.497 16.396 189.041 17.02Z"
fill="currentColor"
/>
<path
d="M204.648 28.432C202.176 28.432 200.28 27.832 198.96 26.632C197.64 25.432 196.98 23.62 196.98 21.196V17.164C196.98 14.74 197.628 12.928 198.924 11.728C200.244 10.528 202.152 9.92799 204.648 9.92799C207.12 9.92799 209.004 10.528 210.3 11.728C211.62 12.928 212.28 14.74 212.28 17.164V21.196C212.28 23.62 211.632 25.432 210.336 26.632C209.04 27.832 207.144 28.432 204.648 28.432ZM204.648 25.3C206.064 25.3 207.084 24.94 207.708 24.22C208.356 23.5 208.68 22.384 208.68 20.872V17.488C208.68 16 208.356 14.896 207.708 14.176C207.084 13.432 206.064 13.06 204.648 13.06C203.208 13.06 202.164 13.432 201.516 14.176C200.892 14.896 200.58 16 200.58 17.488V20.872C200.58 22.384 200.892 23.5 201.516 24.22C202.164 24.94 203.208 25.3 204.648 25.3Z"
fill="currentColor"
/>
<path
d="M216.171 28C215.907 28 215.775 27.868 215.775 27.604V3.80799C215.775 3.54399 215.907 3.41199 216.171 3.41199H218.979C219.243 3.41199 219.375 3.54399 219.375 3.80799V27.604C219.375 27.868 219.243 28 218.979 28H216.171Z"
fill="currentColor"
/>
</svg>
)
}
+26
View File
@@ -0,0 +1,26 @@
import { ParentProps, Show, createContext, useContext } from "solid-js"
export function createInitializedContext<
Name extends string,
T extends { ready: boolean },
>(name: Name, cb: () => T) {
const ctx = createContext<T>()
return {
use: () => {
const context = useContext(ctx)
if (!context) throw new Error(`No ${name} context`)
return context
},
provider: (props: ParentProps) => {
const value = cb()
return (
<Show when={value.ready}>
<ctx.Provider value={value} {...props}>
{props.children}
</ctx.Provider>
</Show>
)
},
}
}

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