mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 16:56:33 -04:00
Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5199141369 | |||
| d86d3e7ea1 | |||
| fe8d29cb2b | |||
| edd6198999 | |||
| c3b2c27997 | |||
| 679aeb29f0 | |||
| 190413580f | |||
| 8c9fbc7717 | |||
| 9d3fdda674 | |||
| 449994f120 | |||
| d772fff776 | |||
| 71b43fd02e | |||
| f40b91ab7a | |||
| 6404bd006d | |||
| 75157e515c | |||
| 5a96ee8e1b | |||
| ee6ceb4c64 | |||
| 9d53628e19 | |||
| 869b476145 | |||
| 223d487787 | |||
| 5ead6d7dd5 | |||
| a98454217f | |||
| cbb75d8577 | |||
| 4ab992a9a9 | |||
| 80b0a93d64 | |||
| e749d48534 | |||
| d7e873f807 | |||
| c23510346b | |||
| f9c5df05a1 | |||
| 02b4d1e2fc | |||
| 0b36eb8760 | |||
| 36bec9948c | |||
| 2db73c39df | |||
| 6107666d04 | |||
| cc2bd7141f | |||
| ee442975df | |||
| 9b1a508657 | |||
| 288c977596 | |||
| 6b799b304c | |||
| 92c126d875 | |||
| 7123fbeb47 | |||
| 84bb692193 | |||
| 079095d7a9 | |||
| 28e1d67ea4 | |||
| c1940d1d2c | |||
| 869f629c14 | |||
| a55943e469 | |||
| 84d95a0d2a | |||
| 7dfed8ca35 | |||
| 38ea0fc051 | |||
| 9223b6ed8f | |||
| f8528c52d9 | |||
| d63ce40af2 | |||
| 5acdd70587 | |||
| b04df6c0d2 | |||
| f1cbdf441c | |||
| 9420d80b73 | |||
| c21161b75e | |||
| aaff066457 | |||
| c7fbf9de44 | |||
| d88c17dad0 | |||
| f57c3f7cf6 | |||
| 2460108223 | |||
| 84e8eea52e | |||
| 9efc2eaf2e | |||
| 37e2644452 | |||
| 22a78cf13f | |||
| 2e9806b320 | |||
| ba839d4446 | |||
| 2bec21d81d | |||
| e5271f3d1a | |||
| 1edb23c2c7 | |||
| b1e6b9c7c9 | |||
| 20cb5a7c56 | |||
| b1d44482bc | |||
| e11102c9df | |||
| 7be9dc8e49 | |||
| 824e035815 | |||
| d652b94a14 | |||
| ebef2ea2d0 | |||
| b5b8a0555d | |||
| ae6154e1c3 | |||
| 0e19ca21ed | |||
| baaff81a06 | |||
| ffa5689885 | |||
| 0e409842e8 |
@@ -0,0 +1,50 @@
|
||||
name: snapshot
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- opentui
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ">=1.24.0"
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.21
|
||||
|
||||
- name: Cache ~/.bun
|
||||
id: cache-bun
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.bun
|
||||
key: ${{ runner.os }}-bun-1-2-21-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-1-2-21-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
./packages/opencode/script/publish.ts
|
||||
env:
|
||||
OPENCODE_SNAPSHOT: true
|
||||
OPENCODE_TAG: ${{ github.ref_name }}
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
|
||||
export default tool({
|
||||
description: "call this tool when you want to give up",
|
||||
args: {
|
||||
message: tool.schema.string().describe("give up message"),
|
||||
},
|
||||
async execute(args) {
|
||||
return "Hey fuck you!"
|
||||
},
|
||||
})
|
||||
@@ -83,3 +83,8 @@
|
||||
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
|
||||
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
|
||||
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
|
||||
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
|
||||
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
|
||||
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
|
||||
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
|
||||
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
|
||||
|
||||
@@ -14,17 +14,19 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode/app",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@shikijs/transformers": "3.9.2",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solid-primitives/scroll": "2.1.3",
|
||||
"@solidjs/router": "0.15.3",
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"diff": "8.0.2",
|
||||
"luxon": "3.7.1",
|
||||
"fuzzysort": "catalog:",
|
||||
"luxon": "catalog:",
|
||||
"marked": "16.2.0",
|
||||
"marked-shiki": "1.2.1",
|
||||
"remeda": "catalog:",
|
||||
@@ -55,11 +57,12 @@
|
||||
"@solidjs/start": "^1.1.0",
|
||||
"solid-js": "catalog:",
|
||||
"vinxi": "^0.5.7",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode/console-core",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@opencode/console-resource": "workspace:*",
|
||||
@@ -76,7 +79,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode/console-function",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -102,7 +105,7 @@
|
||||
},
|
||||
"packages/console/scripts": {
|
||||
"name": "@opencode/console-scripts",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"dependencies": {
|
||||
"@opencode/console-core": "workspace:*",
|
||||
"tsx": "4.20.5",
|
||||
@@ -114,7 +117,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode/function",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
@@ -129,7 +132,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -180,7 +183,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -192,7 +195,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"dependencies": {
|
||||
"@hey-api/openapi-ts": "0.81.0",
|
||||
},
|
||||
@@ -204,7 +207,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode/web",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -218,11 +221,11 @@
|
||||
"diff": "8.0.2",
|
||||
"js-base64": "3.7.7",
|
||||
"lang-map": "0.4.0",
|
||||
"luxon": "3.6.1",
|
||||
"luxon": "catalog:",
|
||||
"marked": "15.0.12",
|
||||
"marked-shiki": "1.2.1",
|
||||
"rehype-autolink-headings": "7.1.0",
|
||||
"remeda": "2.26.0",
|
||||
"remeda": "catalog:",
|
||||
"sharp": "0.32.5",
|
||||
"shiki": "3.4.2",
|
||||
"solid-js": "catalog:",
|
||||
@@ -249,7 +252,9 @@
|
||||
"@types/bun": "1.2.21",
|
||||
"@types/node": "22.13.9",
|
||||
"ai": "5.0.8",
|
||||
"fuzzysort": "3.1.0",
|
||||
"hono": "4.7.10",
|
||||
"luxon": "3.6.1",
|
||||
"remeda": "2.26.0",
|
||||
"solid-js": "1.9.9",
|
||||
"typescript": "5.8.2",
|
||||
@@ -992,6 +997,8 @@
|
||||
|
||||
"@smithy/util-utf8": ["@smithy/util-utf8@4.1.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ=="],
|
||||
|
||||
"@solid-primitives/event-bus": ["@solid-primitives/event-bus@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="],
|
||||
|
||||
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="],
|
||||
|
||||
"@solid-primitives/keyed": ["@solid-primitives/keyed@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-BgoEdqPw48URnI+L5sZIHdF4ua4Las1eWEBBPaoSFs42kkhnHue+rwCBPL2Z9ebOyQ75sUhUfOETdJfmv0D6Kg=="],
|
||||
@@ -1660,6 +1667,8 @@
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="],
|
||||
|
||||
"gel": ["gel@2.1.1", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-Newg9X7mRYskoBjSw70l1YnJ/ZGbq64VPyR821H5WVkTGpHG2O0mQILxCeUhxdYERLFY9B4tUyKLyf3uMTjtKw=="],
|
||||
|
||||
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
|
||||
@@ -1988,7 +1997,7 @@
|
||||
|
||||
"lru.min": ["lru.min@1.1.2", "", {}, "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg=="],
|
||||
|
||||
"luxon": ["luxon@3.7.1", "", {}, "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg=="],
|
||||
"luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
||||
|
||||
@@ -3034,8 +3043,6 @@
|
||||
|
||||
"@opencode/web/@types/luxon": ["@types/luxon@3.6.2", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="],
|
||||
|
||||
"@opencode/web/luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="],
|
||||
|
||||
"@opencode/web/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
|
||||
|
||||
"@opencode/web/shiki": ["shiki@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/engine-javascript": "3.4.2", "@shikijs/engine-oniguruma": "3.4.2", "@shikijs/langs": "3.4.2", "@shikijs/themes": "3.4.2", "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-wuxzZzQG8kvZndD7nustrNFIKYJ1jJoWIPaBpVe2+KHSvtzMi4SBjOxrigs8qeqce/l3U0cwiC+VAkLKSunHQQ=="],
|
||||
|
||||
+3
-19
@@ -75,6 +75,7 @@ export const stripeWebhook = new WebhookEndpoint("StripeWebhookEndpoint", {
|
||||
"checkout.session.async_payment_succeeded",
|
||||
"checkout.session.completed",
|
||||
"checkout.session.expired",
|
||||
"charge.refunded",
|
||||
"customer.created",
|
||||
"customer.deleted",
|
||||
"customer.updated",
|
||||
@@ -93,17 +94,10 @@ export const stripeWebhook = new WebhookEndpoint("StripeWebhookEndpoint", {
|
||||
"customer.subscription.resumed",
|
||||
"customer.subscription.trial_will_end",
|
||||
"customer.subscription.updated",
|
||||
"customer.tax_id.created",
|
||||
"customer.tax_id.deleted",
|
||||
"customer.tax_id.updated",
|
||||
],
|
||||
})
|
||||
|
||||
const ANTHROPIC_API_KEY = new sst.Secret("ANTHROPIC_API_KEY")
|
||||
const OPENAI_API_KEY = new sst.Secret("OPENAI_API_KEY")
|
||||
const XAI_API_KEY = new sst.Secret("XAI_API_KEY")
|
||||
const BASETEN_API_KEY = new sst.Secret("BASETEN_API_KEY")
|
||||
const FIREWORKS_API_KEY = new sst.Secret("FIREWORKS_API_KEY")
|
||||
const ZEN_MODELS = new sst.Secret("ZEN_MODELS")
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||
properties: { value: auth.url.apply((url) => url!) },
|
||||
@@ -128,17 +122,7 @@ if ($app.stage === "production" || $app.stage === "frank") {
|
||||
new sst.cloudflare.x.SolidStart("Console", {
|
||||
domain,
|
||||
path: "packages/console/app",
|
||||
link: [
|
||||
database,
|
||||
AUTH_API_URL,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
STRIPE_SECRET_KEY,
|
||||
ANTHROPIC_API_KEY,
|
||||
OPENAI_API_KEY,
|
||||
XAI_API_KEY,
|
||||
BASETEN_API_KEY,
|
||||
FIREWORKS_API_KEY,
|
||||
],
|
||||
link: [database, AUTH_API_URL, STRIPE_WEBHOOK_SECRET, STRIPE_SECRET_KEY, ZEN_MODELS],
|
||||
environment: {
|
||||
//VITE_DOCS_URL: web.url.apply((url) => url!),
|
||||
//VITE_API_URL: gateway.url.apply((url) => url!),
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ new sst.cloudflare.StaticSite("Desktop", {
|
||||
domain: "desktop." + domain,
|
||||
path: "packages/app",
|
||||
build: {
|
||||
command: "bun run build",
|
||||
command: "bun turbo build",
|
||||
output: "./dist",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -46,7 +46,7 @@ mkdir -p "$INSTALL_DIR"
|
||||
|
||||
if [ -z "$requested_version" ]; then
|
||||
url="https://github.com/sst/opencode/releases/latest/download/$filename"
|
||||
specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | awk -F'"' '/"tag_name": "/ {gsub(/^v/, "", $4); print $4}')
|
||||
specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
|
||||
|
||||
if [[ $? -ne 0 || -z "$specific_version" ]]; then
|
||||
echo -e "${RED}Failed to fetch version information${NC}"
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"ai": "5.0.8",
|
||||
"hono": "4.7.10",
|
||||
"fuzzysort": "3.1.0",
|
||||
"luxon": "3.6.1",
|
||||
"typescript": "5.8.2",
|
||||
"zod": "4.1.8",
|
||||
"remeda": "2.26.0",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
src/assets/theme.css
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
|
||||
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.svg" />
|
||||
<link rel="stylesheet" href="/src/assets/theme.css" />
|
||||
<title>opencode</title>
|
||||
</head>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/app",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -24,12 +24,14 @@
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@shikijs/transformers": "3.9.2",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solid-primitives/scroll": "2.1.3",
|
||||
"@solidjs/router": "0.15.3",
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"diff": "8.0.2",
|
||||
"luxon": "3.7.1",
|
||||
"fuzzysort": "catalog:",
|
||||
"luxon": "catalog:",
|
||||
"marked": "16.2.0",
|
||||
"marked-shiki": "1.2.1",
|
||||
"remeda": "catalog:",
|
||||
|
||||
@@ -35,18 +35,19 @@ class ColorResolver {
|
||||
|
||||
try {
|
||||
if (typeof value === "string") {
|
||||
if (value.startsWith("#") || value === "none") {
|
||||
return { dark: value, light: value }
|
||||
if (value === "none") return { dark: value, light: value }
|
||||
if (value.startsWith("#")) {
|
||||
return { dark: value.toLowerCase(), light: value.toLowerCase() }
|
||||
}
|
||||
const resolved = this.resolveReference(value)
|
||||
return { dark: resolved, light: resolved }
|
||||
}
|
||||
if (typeof value === "object" && value !== null) {
|
||||
const dark = this.resolveColorValue(value.dark || value.light || "#000000")
|
||||
const light = this.resolveColorValue(value.light || value.dark || "#ffffff")
|
||||
const light = this.resolveColorValue(value.light || value.dark || "#FFFFFF")
|
||||
return { dark, light }
|
||||
}
|
||||
return { dark: "#000000", light: "#ffffff" }
|
||||
return { dark: "#000000", light: "#FFFFFF" }
|
||||
} finally {
|
||||
this.visited.delete(key)
|
||||
}
|
||||
@@ -54,8 +55,9 @@ class ColorResolver {
|
||||
|
||||
private resolveColorValue(value: any): string {
|
||||
if (typeof value === "string") {
|
||||
if (value.startsWith("#") || value === "none") {
|
||||
return value
|
||||
if (value === "none") return value
|
||||
if (value.startsWith("#")) {
|
||||
return value.toLowerCase()
|
||||
}
|
||||
return this.resolveReference(value)
|
||||
}
|
||||
@@ -68,8 +70,9 @@ class ColorResolver {
|
||||
throw new Error(`Color reference '${ref}' not found`)
|
||||
}
|
||||
if (typeof colorValue === "string") {
|
||||
if (colorValue.startsWith("#") || colorValue === "none") {
|
||||
return colorValue
|
||||
if (colorValue === "none") return colorValue
|
||||
if (colorValue.startsWith("#")) {
|
||||
return colorValue.toLowerCase()
|
||||
}
|
||||
return this.resolveReference(colorValue)
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="600" height="600" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M115 180H300V420H115V180ZM253.75 229.044H161.25V370.405H253.75V229.044Z" fill="white"/>
|
||||
<path d="M346.25 180H485V229.044H392.5V370.405H485V419.449H346.25V180Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 377 B |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,584 +1,6 @@
|
||||
import { transformerNotationDiff } from "@shikijs/transformers"
|
||||
import { marked } from "marked"
|
||||
import markedShiki from "marked-shiki"
|
||||
import { codeToHtml } from "shiki"
|
||||
import { useMarked } from "@/context"
|
||||
import { createResource } from "solid-js"
|
||||
|
||||
const markedWithShiki = marked.use(
|
||||
markedShiki({
|
||||
highlight(code, lang) {
|
||||
return codeToHtml(code, {
|
||||
// structure: "inline",
|
||||
lang: lang || "text",
|
||||
tabindex: false,
|
||||
theme: {
|
||||
colors: {
|
||||
"actionBar.toggledBackground": "var(--theme-background-element)",
|
||||
"activityBarBadge.background": "var(--theme-accent)",
|
||||
"checkbox.border": "var(--theme-border)",
|
||||
"editor.background": "transparent",
|
||||
"editor.foreground": "var(--theme-text)",
|
||||
"editor.inactiveSelectionBackground": "var(--theme-background-element)",
|
||||
"editor.selectionHighlightBackground": "var(--theme-border-active)",
|
||||
"editorIndentGuide.activeBackground1": "var(--theme-border-subtle)",
|
||||
"editorIndentGuide.background1": "var(--theme-border-subtle)",
|
||||
"input.placeholderForeground": "var(--theme-text-muted)",
|
||||
"list.activeSelectionIconForeground": "var(--theme-text)",
|
||||
"list.dropBackground": "var(--theme-background-element)",
|
||||
"menu.background": "var(--theme-background-panel)",
|
||||
"menu.border": "var(--theme-border)",
|
||||
"menu.foreground": "var(--theme-text)",
|
||||
"menu.selectionBackground": "var(--theme-primary)",
|
||||
"menu.separatorBackground": "var(--theme-border)",
|
||||
"ports.iconRunningProcessForeground": "var(--theme-success)",
|
||||
"sideBarSectionHeader.background": "transparent",
|
||||
"sideBarSectionHeader.border": "var(--theme-border-subtle)",
|
||||
"sideBarTitle.foreground": "var(--theme-text-muted)",
|
||||
"statusBarItem.remoteBackground": "var(--theme-success)",
|
||||
"statusBarItem.remoteForeground": "var(--theme-text)",
|
||||
"tab.lastPinnedBorder": "var(--theme-border-subtle)",
|
||||
"tab.selectedBackground": "var(--theme-background-element)",
|
||||
"tab.selectedForeground": "var(--theme-text-muted)",
|
||||
"terminal.inactiveSelectionBackground": "var(--theme-background-element)",
|
||||
"widget.border": "var(--theme-border)",
|
||||
},
|
||||
displayName: "opencode",
|
||||
name: "opencode",
|
||||
semanticHighlighting: true,
|
||||
semanticTokenColors: {
|
||||
customLiteral: "var(--theme-syntax-function)",
|
||||
newOperator: "var(--theme-syntax-operator)",
|
||||
numberLiteral: "var(--theme-syntax-number)",
|
||||
stringLiteral: "var(--theme-syntax-string)",
|
||||
},
|
||||
tokenColors: [
|
||||
{
|
||||
scope: [
|
||||
"meta.embedded",
|
||||
"source.groovy.embedded",
|
||||
"string meta.image.inline.markdown",
|
||||
"variable.legacy.builtin.python",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-text)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "emphasis",
|
||||
settings: {
|
||||
fontStyle: "italic",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "strong",
|
||||
settings: {
|
||||
fontStyle: "bold",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "header",
|
||||
settings: {
|
||||
foreground: "var(--theme-markdown-heading)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "comment",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-comment)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "constant.language",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"constant.numeric",
|
||||
"variable.other.enummember",
|
||||
"keyword.operator.plus.exponent",
|
||||
"keyword.operator.minus.exponent",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-number)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "constant.regexp",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "entity.name.tag",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["entity.name.tag.css", "entity.name.tag.less"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "entity.other.attribute-name",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"entity.other.attribute-name.class.css",
|
||||
"source.css entity.other.attribute-name.class",
|
||||
"entity.other.attribute-name.id.css",
|
||||
"entity.other.attribute-name.parent-selector.css",
|
||||
"entity.other.attribute-name.parent.less",
|
||||
"source.css entity.other.attribute-name.pseudo-class",
|
||||
"entity.other.attribute-name.pseudo-element.css",
|
||||
"source.css.less entity.other.attribute-name.id",
|
||||
"entity.other.attribute-name.scss",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "invalid",
|
||||
settings: {
|
||||
foreground: "var(--theme-error)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.underline",
|
||||
settings: {
|
||||
fontStyle: "underline",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.bold",
|
||||
settings: {
|
||||
fontStyle: "bold",
|
||||
foreground: "var(--theme-markdown-strong)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.heading",
|
||||
settings: {
|
||||
fontStyle: "bold",
|
||||
foreground: "var(--theme-markdown-heading)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.italic",
|
||||
settings: {
|
||||
fontStyle: "italic",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.strikethrough",
|
||||
settings: {
|
||||
fontStyle: "strikethrough",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.inserted",
|
||||
settings: {
|
||||
foreground: "var(--theme-diff-added)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.deleted",
|
||||
settings: {
|
||||
foreground: "var(--theme-diff-removed)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.changed",
|
||||
settings: {
|
||||
foreground: "var(--theme-diff-context)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "punctuation.definition.quote.begin.markdown",
|
||||
settings: {
|
||||
foreground: "var(--theme-markdown-block-quote)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "punctuation.definition.list.begin.markdown",
|
||||
settings: {
|
||||
foreground: "var(--theme-markdown-list-enumeration)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.inline.raw",
|
||||
settings: {
|
||||
foreground: "var(--theme-markdown-code)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "punctuation.definition.tag",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-punctuation)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["meta.preprocessor", "entity.name.function.preprocessor"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "meta.preprocessor.string",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-string)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "meta.preprocessor.numeric",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-number)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "meta.structure.dictionary.key.python",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "meta.diff.header",
|
||||
settings: {
|
||||
foreground: "var(--theme-diff-hunk-header)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "storage",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "storage.type",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["storage.modifier", "keyword.operator.noexcept"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["string", "meta.embedded.assembly"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-string)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "string.tag",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-string)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "string.value",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-string)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "string.regexp",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"punctuation.definition.template-expression.begin",
|
||||
"punctuation.definition.template-expression.end",
|
||||
"punctuation.section.embedded",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["meta.template.expression"],
|
||||
settings: {
|
||||
foreground: "var(--theme-text)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"support.type.vendored.property-name",
|
||||
"support.type.property-name",
|
||||
"source.css variable",
|
||||
"source.coffee.embedded",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "keyword",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "keyword.control",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "keyword.operator",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"keyword.operator.new",
|
||||
"keyword.operator.expression",
|
||||
"keyword.operator.cast",
|
||||
"keyword.operator.sizeof",
|
||||
"keyword.operator.alignof",
|
||||
"keyword.operator.typeid",
|
||||
"keyword.operator.alignas",
|
||||
"keyword.operator.instanceof",
|
||||
"keyword.operator.logical.python",
|
||||
"keyword.operator.wordlike",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "keyword.other.unit",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-number)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["punctuation.section.embedded.begin.php", "punctuation.section.embedded.end.php"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "support.function.git-rebase",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "constant.sha.git-rebase",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-number)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"storage.modifier.import.java",
|
||||
"variable.language.wildcard.java",
|
||||
"storage.modifier.package.java",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-text)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "variable.language",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"entity.name.function",
|
||||
"support.function",
|
||||
"support.constant.handlebars",
|
||||
"source.powershell variable.other.member",
|
||||
"entity.name.operator.custom-literal",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-function)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"support.class",
|
||||
"support.type",
|
||||
"entity.name.type",
|
||||
"entity.name.namespace",
|
||||
"entity.other.attribute",
|
||||
"entity.name.scope-resolution",
|
||||
"entity.name.class",
|
||||
"storage.type.numeric.go",
|
||||
"storage.type.byte.go",
|
||||
"storage.type.boolean.go",
|
||||
"storage.type.string.go",
|
||||
"storage.type.uintptr.go",
|
||||
"storage.type.error.go",
|
||||
"storage.type.rune.go",
|
||||
"storage.type.cs",
|
||||
"storage.type.generic.cs",
|
||||
"storage.type.modifier.cs",
|
||||
"storage.type.variable.cs",
|
||||
"storage.type.annotation.java",
|
||||
"storage.type.generic.java",
|
||||
"storage.type.java",
|
||||
"storage.type.object.array.java",
|
||||
"storage.type.primitive.array.java",
|
||||
"storage.type.primitive.java",
|
||||
"storage.type.token.java",
|
||||
"storage.type.groovy",
|
||||
"storage.type.annotation.groovy",
|
||||
"storage.type.parameters.groovy",
|
||||
"storage.type.generic.groovy",
|
||||
"storage.type.object.array.groovy",
|
||||
"storage.type.primitive.array.groovy",
|
||||
"storage.type.primitive.groovy",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-type)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"meta.type.cast.expr",
|
||||
"meta.type.new.expr",
|
||||
"support.constant.math",
|
||||
"support.constant.dom",
|
||||
"support.constant.json",
|
||||
"entity.other.inherited-class",
|
||||
"punctuation.separator.namespace.ruby",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-type)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"keyword.control",
|
||||
"source.cpp keyword.operator.new",
|
||||
"keyword.operator.delete",
|
||||
"keyword.other.using",
|
||||
"keyword.other.directive.using",
|
||||
"keyword.other.operator",
|
||||
"entity.name.operator",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"variable",
|
||||
"meta.definition.variable.name",
|
||||
"support.variable",
|
||||
"entity.name.variable",
|
||||
"constant.other.placeholder",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["variable.other.constant", "variable.other.enummember"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["meta.object-literal.key"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"support.constant.property-value",
|
||||
"support.constant.font-name",
|
||||
"support.constant.media-type",
|
||||
"support.constant.media",
|
||||
"constant.other.color.rgb-value",
|
||||
"constant.other.rgb-value",
|
||||
"support.constant.color",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-string)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"punctuation.definition.group.regexp",
|
||||
"punctuation.definition.group.assertion.regexp",
|
||||
"punctuation.definition.character-class.regexp",
|
||||
"punctuation.character.set.begin.regexp",
|
||||
"punctuation.character.set.end.regexp",
|
||||
"keyword.operator.negation.regexp",
|
||||
"support.other.parenthesis.regexp",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-string)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"constant.character.character-class.regexp",
|
||||
"constant.other.character-class.set.regexp",
|
||||
"constant.other.character-class.regexp",
|
||||
"constant.character.set.regexp",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "keyword.operator.quantifier.regexp",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["constant.character", "constant.other.option"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "constant.character.escape",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "entity.name.label",
|
||||
settings: {
|
||||
foreground: "var(--theme-text-muted)",
|
||||
},
|
||||
},
|
||||
],
|
||||
type: "dark",
|
||||
},
|
||||
transformers: [transformerNotationDiff()],
|
||||
})
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
function strip(text: string): string {
|
||||
const wrappedRe = /^\s*<([A-Za-z]\w*)>\s*([\s\S]*?)\s*<\/\1>\s*$/
|
||||
const match = text.match(wrappedRe)
|
||||
@@ -586,10 +8,11 @@ function strip(text: string): string {
|
||||
}
|
||||
|
||||
export default function Markdown(props: { text: string; class?: string }) {
|
||||
const marked = useMarked()
|
||||
const [html] = createResource(
|
||||
() => strip(props.text),
|
||||
async (markdown) => {
|
||||
return markedWithShiki.parse(markdown)
|
||||
return marked.parse(markdown)
|
||||
},
|
||||
)
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { Select as KobalteSelect } from "@kobalte/core/select"
|
||||
import { createEffect, createMemo, Show } from "solid-js"
|
||||
import type { ComponentProps } from "solid-js"
|
||||
import { Icon } from "@/ui/icon"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { pipe, groupBy, entries, map } from "remeda"
|
||||
import { createStore } from "solid-js/store"
|
||||
|
||||
export interface SelectProps<T> {
|
||||
variant?: "default" | "outline"
|
||||
size?: "sm" | "md" | "lg"
|
||||
placeholder?: string
|
||||
filter?:
|
||||
| false
|
||||
| {
|
||||
placeholder?: string
|
||||
keys: string[]
|
||||
}
|
||||
options: T[]
|
||||
current?: T
|
||||
value?: (x: T) => string
|
||||
label?: (x: T) => string
|
||||
groupBy?: (x: T) => string
|
||||
onFilter?: (query: string) => void
|
||||
onSelect?: (value: T | undefined) => void
|
||||
class?: ComponentProps<"div">["class"]
|
||||
classList?: ComponentProps<"div">["classList"]
|
||||
}
|
||||
|
||||
export function Select<T>(props: SelectProps<T>) {
|
||||
let inputRef: HTMLInputElement | undefined = undefined
|
||||
let listboxRef: HTMLUListElement | undefined = undefined
|
||||
const [store, setStore] = createStore({
|
||||
filter: "",
|
||||
})
|
||||
const grouped = createMemo(() => {
|
||||
const needle = store.filter.toLowerCase()
|
||||
const result = pipe(
|
||||
props.options,
|
||||
(x) =>
|
||||
!needle || !props.filter
|
||||
? x
|
||||
: fuzzysort.go(needle, x, { keys: props.filter && props.filter.keys }).map((x) => x.obj),
|
||||
groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
|
||||
// mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))),
|
||||
entries(),
|
||||
map(([k, v]) => ({ category: k, options: v })),
|
||||
)
|
||||
return result
|
||||
})
|
||||
// const flat = createMemo(() => {
|
||||
// return pipe(
|
||||
// grouped(),
|
||||
// flatMap(({ options }) => options),
|
||||
// )
|
||||
// })
|
||||
|
||||
createEffect(() => {
|
||||
store.filter
|
||||
listboxRef?.scrollTo(0, 0)
|
||||
// setStore("selected", 0)
|
||||
// scroll.scrollTo(0)
|
||||
})
|
||||
|
||||
return (
|
||||
<KobalteSelect<T, { category: string; options: T[] }>
|
||||
value={props.current}
|
||||
options={grouped()}
|
||||
optionValue={(x) => (props.value ? props.value(x) : (x as string))}
|
||||
optionTextValue={(x) => (props.label ? props.label(x) : (x as string))}
|
||||
optionGroupChildren="options"
|
||||
placeholder={props.placeholder}
|
||||
sectionComponent={(props) => (
|
||||
<KobalteSelect.Section class="text-xs uppercase text-text-muted/60 font-light mt-3 first:mt-0 ml-2">
|
||||
{props.section.rawValue.category}
|
||||
</KobalteSelect.Section>
|
||||
)}
|
||||
itemComponent={(itemProps) => (
|
||||
<KobalteSelect.Item
|
||||
classList={{
|
||||
"relative flex cursor-pointer select-none items-center": true,
|
||||
"rounded-sm px-2 py-0.5 text-xs outline-none text-text": true,
|
||||
"transition-colors data-[disabled]:pointer-events-none": true,
|
||||
"data-[highlighted]:bg-background-element data-[disabled]:opacity-50": true,
|
||||
[props.class ?? ""]: !!props.class,
|
||||
}}
|
||||
{...itemProps}
|
||||
>
|
||||
<KobalteSelect.ItemLabel>
|
||||
{props.label ? props.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)}
|
||||
</KobalteSelect.ItemLabel>
|
||||
<KobalteSelect.ItemIndicator
|
||||
classList={{
|
||||
"ml-auto": true,
|
||||
}}
|
||||
>
|
||||
<Icon name="checkmark" size={16} />
|
||||
</KobalteSelect.ItemIndicator>
|
||||
</KobalteSelect.Item>
|
||||
)}
|
||||
onChange={(v) => {
|
||||
if (props.onSelect) props.onSelect(v ?? undefined)
|
||||
if (v !== null) {
|
||||
// close the select
|
||||
}
|
||||
}}
|
||||
onOpenChange={(v) => v || setStore("filter", "")}
|
||||
>
|
||||
<KobalteSelect.Trigger
|
||||
classList={{
|
||||
...(props.classList ?? {}),
|
||||
"flex w-full items-center justify-between rounded-md transition-colors": true,
|
||||
"focus-visible:outline-none focus-visible:ring focus-visible:ring-border-active/30": true,
|
||||
"disabled:cursor-not-allowed disabled:opacity-50": true,
|
||||
"data-[placeholder-shown]:text-text-muted cursor-pointer": true,
|
||||
"hover:bg-background-element focus-visible:ring-border-active": true,
|
||||
"bg-background-element text-text": props.variant === "default" || !props.variant,
|
||||
"border-2 border-border bg-transparent text-text": props.variant === "outline",
|
||||
"h-6 pl-2 text-xs": props.size === "sm",
|
||||
"h-8 pl-3 text-sm": props.size === "md" || !props.size,
|
||||
"h-10 pl-4 text-base": props.size === "lg",
|
||||
[props.class ?? ""]: !!props.class,
|
||||
}}
|
||||
>
|
||||
<KobalteSelect.Value<T>>
|
||||
{(state) => {
|
||||
const selected = state.selectedOption() ?? props.current
|
||||
if (!selected) return props.placeholder || ""
|
||||
if (props.label) return props.label(selected)
|
||||
return selected as string
|
||||
}}
|
||||
</KobalteSelect.Value>
|
||||
<KobalteSelect.Icon
|
||||
classList={{
|
||||
"size-fit shrink-0 text-text-muted transition-transform duration-100 data-[expanded]:rotate-180": true,
|
||||
}}
|
||||
>
|
||||
<Icon name="chevron-down" size={24} />
|
||||
</KobalteSelect.Icon>
|
||||
</KobalteSelect.Trigger>
|
||||
<KobalteSelect.Portal>
|
||||
<KobalteSelect.Content
|
||||
onKeyDown={(e) => {
|
||||
if (!props.filter) return
|
||||
if (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Escape") {
|
||||
return
|
||||
}
|
||||
inputRef?.focus()
|
||||
}}
|
||||
classList={{
|
||||
"min-w-32 overflow-hidden rounded-md border border-border-subtle/40": true,
|
||||
"bg-background-panel p-1 shadow-md z-50": true,
|
||||
"data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95": true,
|
||||
"data-[expanded]:animate-in data-[expanded]:fade-in-0 data-[expanded]:zoom-in-95": true,
|
||||
}}
|
||||
>
|
||||
<Show when={props.filter}>
|
||||
<input
|
||||
ref={(el) => (inputRef = el)}
|
||||
id="select-filter"
|
||||
type="text"
|
||||
placeholder={props.filter ? props.filter.placeholder : "Filter items"}
|
||||
value={store.filter}
|
||||
onInput={(e) => setStore("filter", e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
listboxRef?.focus()
|
||||
}
|
||||
}}
|
||||
classList={{
|
||||
"w-full": true,
|
||||
"px-2 pb-2 text-text font-light placeholder-text-muted/70 text-xs focus:outline-none": true,
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
<KobalteSelect.Listbox
|
||||
ref={(el) => (listboxRef = el)}
|
||||
classList={{
|
||||
"overflow-y-auto max-h-48 no-scrollbar": true,
|
||||
}}
|
||||
/>
|
||||
</KobalteSelect.Content>
|
||||
</KobalteSelect.Portal>
|
||||
</KobalteSelect>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createContext, useContext, type ParentProps } from "solid-js"
|
||||
import { createEventBus } from "@solid-primitives/event-bus"
|
||||
import type { Event as SDKEvent } from "@opencode-ai/sdk"
|
||||
import { useSDK } from "@/context"
|
||||
|
||||
export type Event = SDKEvent // can extend with custom events later
|
||||
|
||||
function init() {
|
||||
const sdk = useSDK()
|
||||
const bus = createEventBus<Event>()
|
||||
sdk.event.subscribe().then(async (events) => {
|
||||
for await (const event of events.stream) {
|
||||
bus.emit(event)
|
||||
}
|
||||
})
|
||||
return bus
|
||||
}
|
||||
|
||||
type EventContext = ReturnType<typeof init>
|
||||
|
||||
const ctx = createContext<EventContext>()
|
||||
|
||||
export function EventProvider(props: ParentProps) {
|
||||
const value = init()
|
||||
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
|
||||
}
|
||||
|
||||
export function useEvent() {
|
||||
const value = useContext(ctx)
|
||||
if (!value) {
|
||||
throw new Error("useEvent must be used within a EventProvider")
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
export { EventProvider, useEvent } from "./event"
|
||||
export { LocalProvider, useLocal } from "./local"
|
||||
export { MarkedProvider, useMarked } from "./marked"
|
||||
export { SDKProvider, useSDK } from "./sdk"
|
||||
export { ShikiProvider, useShiki } from "./shiki"
|
||||
export { SyncProvider, useSync } from "./sync"
|
||||
export { ThemeProvider, useTheme } from "./theme"
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { batch, createContext, createEffect, createMemo, useContext, type ParentProps } from "solid-js"
|
||||
import { useSync } from "./sync"
|
||||
import { uniqueBy } from "remeda"
|
||||
import type { FileContent, FileNode } from "@opencode-ai/sdk"
|
||||
import { useSDK } from "./sdk"
|
||||
import { useSDK, useEvent, useSync } from "@/context"
|
||||
|
||||
export type LocalFile = FileNode &
|
||||
Partial<{
|
||||
@@ -24,22 +23,26 @@ function init() {
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
|
||||
const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent"))
|
||||
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent"))
|
||||
const agent = (() => {
|
||||
const [store, setStore] = createStore<{
|
||||
current: string
|
||||
}>({
|
||||
current: agents()[0].name,
|
||||
current: list()[0].name,
|
||||
})
|
||||
return {
|
||||
list,
|
||||
current() {
|
||||
return agents().find((x) => x.name === store.current)!
|
||||
return list().find((x) => x.name === store.current)!
|
||||
},
|
||||
set(name: string | undefined) {
|
||||
setStore("current", name ?? list()[0].name)
|
||||
},
|
||||
move(direction: 1 | -1) {
|
||||
let next = agents().findIndex((x) => x.name === store.current) + direction
|
||||
if (next < 0) next = agents().length - 1
|
||||
if (next >= agents().length) next = 0
|
||||
const value = agents()[next]
|
||||
let next = list().findIndex((x) => x.name === store.current) + direction
|
||||
if (next < 0) next = list().length - 1
|
||||
if (next >= list().length) next = 0
|
||||
const value = list()[next]
|
||||
setStore("current", value.name)
|
||||
if (value.model)
|
||||
model.set({
|
||||
@@ -89,7 +92,12 @@ function init() {
|
||||
return store.model[agent.current().name] ?? (a.model ? a.model : fallback())
|
||||
})
|
||||
|
||||
const list = createMemo(() =>
|
||||
sync.data.provider.flatMap((x) => Object.values(x.models).map((m) => ({ providerID: x.id, modelID: m.id }))),
|
||||
)
|
||||
|
||||
return {
|
||||
list,
|
||||
current,
|
||||
recent() {
|
||||
return store.recent
|
||||
@@ -103,10 +111,10 @@ function init() {
|
||||
model: model.name ?? value.modelID,
|
||||
}
|
||||
}),
|
||||
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
|
||||
set(model: { providerID: string; modelID: string } | undefined, options?: { recent?: boolean }) {
|
||||
batch(() => {
|
||||
setStore("model", agent.current().name, model)
|
||||
if (options?.recent) {
|
||||
setStore("model", agent.current().name, model ?? fallback())
|
||||
if (options?.recent && model) {
|
||||
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
|
||||
if (uniq.length > 5) uniq.pop()
|
||||
setStore("recent", uniq)
|
||||
@@ -156,17 +164,19 @@ function init() {
|
||||
})
|
||||
}
|
||||
|
||||
const load = async (path: string) =>
|
||||
sdk.file.read({ query: { path } }).then((x) => {
|
||||
const load = async (path: string) => {
|
||||
const relative = path.replace(sync.data.path.directory + "/", "")
|
||||
sdk.file.read({ query: { path: relative } }).then((x) => {
|
||||
setStore(
|
||||
"node",
|
||||
path,
|
||||
relative,
|
||||
produce((draft) => {
|
||||
draft.loaded = true
|
||||
draft.content = x.data
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const open = async (path: string) => {
|
||||
const relative = path.replace(sync.data.path.directory + "/", "")
|
||||
@@ -204,27 +214,27 @@ function init() {
|
||||
})
|
||||
}
|
||||
|
||||
sdk.event.subscribe().then(async (events) => {
|
||||
for await (const event of events.stream) {
|
||||
switch (event.type) {
|
||||
case "message.part.updated":
|
||||
const part = event.properties.part
|
||||
if (part.type === "tool" && part.state.status === "completed") {
|
||||
switch (part.tool) {
|
||||
case "read":
|
||||
console.log("read", part.state.input)
|
||||
break
|
||||
case "edit":
|
||||
const absolute = part.state.input["filePath"] as string
|
||||
const path = absolute.replace(sync.data.path.directory + "/", "")
|
||||
load(path)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
const bus = useEvent()
|
||||
bus.listen((event) => {
|
||||
switch (event.type) {
|
||||
case "message.part.updated":
|
||||
const part = event.properties.part
|
||||
if (part.type === "tool" && part.state.status === "completed") {
|
||||
switch (part.tool) {
|
||||
case "read":
|
||||
console.log("read", part.state.input)
|
||||
break
|
||||
case "edit":
|
||||
load(part.state.input["filePath"] as string)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
case "file.watcher.updated":
|
||||
load(event.properties.file)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createContext, useContext, type ParentProps } from "solid-js"
|
||||
import { useShiki } from "@/context"
|
||||
import { marked } from "marked"
|
||||
import markedShiki from "marked-shiki"
|
||||
import { bundledLanguages, type BundledLanguage } from "shiki"
|
||||
|
||||
function init(highlighter: ReturnType<typeof useShiki>) {
|
||||
return marked.use(
|
||||
markedShiki({
|
||||
async highlight(code, lang) {
|
||||
if (!(lang in bundledLanguages)) {
|
||||
lang = "text"
|
||||
}
|
||||
if (!highlighter.getLoadedLanguages().includes(lang)) {
|
||||
await highlighter.loadLanguage(lang as BundledLanguage)
|
||||
}
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang: lang || "text",
|
||||
theme: "opencode",
|
||||
tabindex: false,
|
||||
})
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
type MarkedContext = ReturnType<typeof init>
|
||||
|
||||
const ctx = createContext<MarkedContext>()
|
||||
|
||||
export function MarkedProvider(props: ParentProps) {
|
||||
const highlighter = useShiki()
|
||||
const value = init(highlighter)
|
||||
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
|
||||
}
|
||||
|
||||
export function useMarked() {
|
||||
const value = useContext(ctx)
|
||||
if (!value) {
|
||||
throw new Error("useMarked must be used within a MarkedProvider")
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
import { createHighlighter, type ThemeInput } from "shiki"
|
||||
import { createContext, useContext, type ParentProps } from "solid-js"
|
||||
|
||||
const theme: ThemeInput = {
|
||||
colors: {
|
||||
"actionBar.toggledBackground": "var(--theme-background-element)",
|
||||
"activityBarBadge.background": "var(--theme-accent)",
|
||||
"checkbox.border": "var(--theme-border)",
|
||||
"editor.background": "transparent",
|
||||
"editor.foreground": "var(--theme-text)",
|
||||
"editor.inactiveSelectionBackground": "var(--theme-background-element)",
|
||||
"editor.selectionHighlightBackground": "var(--theme-border-active)",
|
||||
"editorIndentGuide.activeBackground1": "var(--theme-border-subtle)",
|
||||
"editorIndentGuide.background1": "var(--theme-border-subtle)",
|
||||
"input.placeholderForeground": "var(--theme-text-muted)",
|
||||
"list.activeSelectionIconForeground": "var(--theme-text)",
|
||||
"list.dropBackground": "var(--theme-background-element)",
|
||||
"menu.background": "var(--theme-background-panel)",
|
||||
"menu.border": "var(--theme-border)",
|
||||
"menu.foreground": "var(--theme-text)",
|
||||
"menu.selectionBackground": "var(--theme-primary)",
|
||||
"menu.separatorBackground": "var(--theme-border)",
|
||||
"ports.iconRunningProcessForeground": "var(--theme-success)",
|
||||
"sideBarSectionHeader.background": "transparent",
|
||||
"sideBarSectionHeader.border": "var(--theme-border-subtle)",
|
||||
"sideBarTitle.foreground": "var(--theme-text-muted)",
|
||||
"statusBarItem.remoteBackground": "var(--theme-success)",
|
||||
"statusBarItem.remoteForeground": "var(--theme-text)",
|
||||
"tab.lastPinnedBorder": "var(--theme-border-subtle)",
|
||||
"tab.selectedBackground": "var(--theme-background-element)",
|
||||
"tab.selectedForeground": "var(--theme-text-muted)",
|
||||
"terminal.inactiveSelectionBackground": "var(--theme-background-element)",
|
||||
"widget.border": "var(--theme-border)",
|
||||
},
|
||||
displayName: "opencode",
|
||||
name: "opencode",
|
||||
semanticHighlighting: true,
|
||||
semanticTokenColors: {
|
||||
customLiteral: "var(--theme-syntax-function)",
|
||||
newOperator: "var(--theme-syntax-operator)",
|
||||
numberLiteral: "var(--theme-syntax-number)",
|
||||
stringLiteral: "var(--theme-syntax-string)",
|
||||
},
|
||||
tokenColors: [
|
||||
{
|
||||
scope: [
|
||||
"meta.embedded",
|
||||
"source.groovy.embedded",
|
||||
"string meta.image.inline.markdown",
|
||||
"variable.legacy.builtin.python",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-text)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "emphasis",
|
||||
settings: {
|
||||
fontStyle: "italic",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "strong",
|
||||
settings: {
|
||||
fontStyle: "bold",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "header",
|
||||
settings: {
|
||||
foreground: "var(--theme-markdown-heading)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "comment",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-comment)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "constant.language",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"constant.numeric",
|
||||
"variable.other.enummember",
|
||||
"keyword.operator.plus.exponent",
|
||||
"keyword.operator.minus.exponent",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-number)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "constant.regexp",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "entity.name.tag",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["entity.name.tag.css", "entity.name.tag.less"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "entity.other.attribute-name",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"entity.other.attribute-name.class.css",
|
||||
"source.css entity.other.attribute-name.class",
|
||||
"entity.other.attribute-name.id.css",
|
||||
"entity.other.attribute-name.parent-selector.css",
|
||||
"entity.other.attribute-name.parent.less",
|
||||
"source.css entity.other.attribute-name.pseudo-class",
|
||||
"entity.other.attribute-name.pseudo-element.css",
|
||||
"source.css.less entity.other.attribute-name.id",
|
||||
"entity.other.attribute-name.scss",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "invalid",
|
||||
settings: {
|
||||
foreground: "var(--theme-error)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.underline",
|
||||
settings: {
|
||||
fontStyle: "underline",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.bold",
|
||||
settings: {
|
||||
fontStyle: "bold",
|
||||
foreground: "var(--theme-markdown-strong)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.heading",
|
||||
settings: {
|
||||
fontStyle: "bold",
|
||||
foreground: "var(--theme-markdown-heading)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.italic",
|
||||
settings: {
|
||||
fontStyle: "italic",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.strikethrough",
|
||||
settings: {
|
||||
fontStyle: "strikethrough",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.inserted",
|
||||
settings: {
|
||||
foreground: "var(--theme-diff-added)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.deleted",
|
||||
settings: {
|
||||
foreground: "var(--theme-diff-removed)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.changed",
|
||||
settings: {
|
||||
foreground: "var(--theme-diff-context)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "punctuation.definition.quote.begin.markdown",
|
||||
settings: {
|
||||
foreground: "var(--theme-markdown-block-quote)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "punctuation.definition.list.begin.markdown",
|
||||
settings: {
|
||||
foreground: "var(--theme-markdown-list-enumeration)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.inline.raw",
|
||||
settings: {
|
||||
foreground: "var(--theme-markdown-code)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "punctuation.definition.tag",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-punctuation)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["meta.preprocessor", "entity.name.function.preprocessor"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "meta.preprocessor.string",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-string)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "meta.preprocessor.numeric",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-number)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "meta.structure.dictionary.key.python",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "meta.diff.header",
|
||||
settings: {
|
||||
foreground: "var(--theme-diff-hunk-header)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "storage",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "storage.type",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["storage.modifier", "keyword.operator.noexcept"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["string", "meta.embedded.assembly"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-string)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "string.tag",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-string)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "string.value",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-string)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "string.regexp",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"punctuation.definition.template-expression.begin",
|
||||
"punctuation.definition.template-expression.end",
|
||||
"punctuation.section.embedded",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["meta.template.expression"],
|
||||
settings: {
|
||||
foreground: "var(--theme-text)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"support.type.vendored.property-name",
|
||||
"support.type.property-name",
|
||||
"source.css variable",
|
||||
"source.coffee.embedded",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "keyword",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "keyword.control",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "keyword.operator",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"keyword.operator.new",
|
||||
"keyword.operator.expression",
|
||||
"keyword.operator.cast",
|
||||
"keyword.operator.sizeof",
|
||||
"keyword.operator.alignof",
|
||||
"keyword.operator.typeid",
|
||||
"keyword.operator.alignas",
|
||||
"keyword.operator.instanceof",
|
||||
"keyword.operator.logical.python",
|
||||
"keyword.operator.wordlike",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "keyword.other.unit",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-number)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["punctuation.section.embedded.begin.php", "punctuation.section.embedded.end.php"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "support.function.git-rebase",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "constant.sha.git-rebase",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-number)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["storage.modifier.import.java", "variable.language.wildcard.java", "storage.modifier.package.java"],
|
||||
settings: {
|
||||
foreground: "var(--theme-text)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "variable.language",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"entity.name.function",
|
||||
"support.function",
|
||||
"support.constant.handlebars",
|
||||
"source.powershell variable.other.member",
|
||||
"entity.name.operator.custom-literal",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-function)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"support.class",
|
||||
"support.type",
|
||||
"entity.name.type",
|
||||
"entity.name.namespace",
|
||||
"entity.other.attribute",
|
||||
"entity.name.scope-resolution",
|
||||
"entity.name.class",
|
||||
"storage.type.numeric.go",
|
||||
"storage.type.byte.go",
|
||||
"storage.type.boolean.go",
|
||||
"storage.type.string.go",
|
||||
"storage.type.uintptr.go",
|
||||
"storage.type.error.go",
|
||||
"storage.type.rune.go",
|
||||
"storage.type.cs",
|
||||
"storage.type.generic.cs",
|
||||
"storage.type.modifier.cs",
|
||||
"storage.type.variable.cs",
|
||||
"storage.type.annotation.java",
|
||||
"storage.type.generic.java",
|
||||
"storage.type.java",
|
||||
"storage.type.object.array.java",
|
||||
"storage.type.primitive.array.java",
|
||||
"storage.type.primitive.java",
|
||||
"storage.type.token.java",
|
||||
"storage.type.groovy",
|
||||
"storage.type.annotation.groovy",
|
||||
"storage.type.parameters.groovy",
|
||||
"storage.type.generic.groovy",
|
||||
"storage.type.object.array.groovy",
|
||||
"storage.type.primitive.array.groovy",
|
||||
"storage.type.primitive.groovy",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-type)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"meta.type.cast.expr",
|
||||
"meta.type.new.expr",
|
||||
"support.constant.math",
|
||||
"support.constant.dom",
|
||||
"support.constant.json",
|
||||
"entity.other.inherited-class",
|
||||
"punctuation.separator.namespace.ruby",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-type)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"keyword.control",
|
||||
"source.cpp keyword.operator.new",
|
||||
"keyword.operator.delete",
|
||||
"keyword.other.using",
|
||||
"keyword.other.directive.using",
|
||||
"keyword.other.operator",
|
||||
"entity.name.operator",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"variable",
|
||||
"meta.definition.variable.name",
|
||||
"support.variable",
|
||||
"entity.name.variable",
|
||||
"constant.other.placeholder",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["variable.other.constant", "variable.other.enummember"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["meta.object-literal.key"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"support.constant.property-value",
|
||||
"support.constant.font-name",
|
||||
"support.constant.media-type",
|
||||
"support.constant.media",
|
||||
"constant.other.color.rgb-value",
|
||||
"constant.other.rgb-value",
|
||||
"support.constant.color",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-string)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"punctuation.definition.group.regexp",
|
||||
"punctuation.definition.group.assertion.regexp",
|
||||
"punctuation.definition.character-class.regexp",
|
||||
"punctuation.character.set.begin.regexp",
|
||||
"punctuation.character.set.end.regexp",
|
||||
"keyword.operator.negation.regexp",
|
||||
"support.other.parenthesis.regexp",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-string)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"constant.character.character-class.regexp",
|
||||
"constant.other.character-class.set.regexp",
|
||||
"constant.other.character-class.regexp",
|
||||
"constant.character.set.regexp",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "keyword.operator.quantifier.regexp",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["constant.character", "constant.other.option"],
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "constant.character.escape",
|
||||
settings: {
|
||||
foreground: "var(--theme-syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "entity.name.label",
|
||||
settings: {
|
||||
foreground: "var(--theme-text-muted)",
|
||||
},
|
||||
},
|
||||
],
|
||||
type: "dark",
|
||||
}
|
||||
|
||||
const highlighter = await createHighlighter({
|
||||
themes: [theme],
|
||||
langs: [],
|
||||
})
|
||||
|
||||
type ShikiContext = typeof highlighter
|
||||
|
||||
const ctx = createContext<ShikiContext>()
|
||||
|
||||
export function ShikiProvider(props: ParentProps) {
|
||||
return <ctx.Provider value={highlighter}>{props.children}</ctx.Provider>
|
||||
}
|
||||
|
||||
export function useShiki() {
|
||||
const value = useContext(ctx)
|
||||
if (!value) {
|
||||
throw new Error("useShiki must be used within a ShikiProvider")
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Message, Agent, Provider, Session, Part, Config, Path, File, FileNode } from "@opencode-ai/sdk"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useSDK } from "./sdk"
|
||||
import { createContext, Show, useContext, type ParentProps } from "solid-js"
|
||||
import { useSDK, useEvent } from "@/context"
|
||||
import { Binary } from "@/utils/binary"
|
||||
|
||||
function init() {
|
||||
@@ -33,69 +33,67 @@ function init() {
|
||||
changes: [],
|
||||
})
|
||||
|
||||
const sdk = useSDK()
|
||||
|
||||
sdk.event.subscribe().then(async (events) => {
|
||||
for await (const event of events.stream) {
|
||||
switch (event.type) {
|
||||
case "session.updated": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
setStore("session", result.index, reconcile(event.properties.info))
|
||||
break
|
||||
}
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.info)
|
||||
}),
|
||||
)
|
||||
const bus = useEvent()
|
||||
bus.listen((event) => {
|
||||
switch (event.type) {
|
||||
case "session.updated": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
setStore("session", result.index, reconcile(event.properties.info))
|
||||
break
|
||||
}
|
||||
case "message.updated": {
|
||||
const messages = store.message[event.properties.info.sessionID]
|
||||
if (!messages) {
|
||||
setStore("message", event.properties.info.sessionID, [event.properties.info])
|
||||
break
|
||||
}
|
||||
const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
|
||||
if (result.found) {
|
||||
setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
|
||||
break
|
||||
}
|
||||
setStore(
|
||||
"message",
|
||||
event.properties.info.sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.info)
|
||||
}),
|
||||
)
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.info)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
case "message.updated": {
|
||||
const messages = store.message[event.properties.info.sessionID]
|
||||
if (!messages) {
|
||||
setStore("message", event.properties.info.sessionID, [event.properties.info])
|
||||
break
|
||||
}
|
||||
case "message.part.updated": {
|
||||
const parts = store.part[event.properties.part.messageID]
|
||||
if (!parts) {
|
||||
setStore("part", event.properties.part.messageID, [event.properties.part])
|
||||
break
|
||||
}
|
||||
const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
|
||||
if (result.found) {
|
||||
setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
|
||||
break
|
||||
}
|
||||
setStore(
|
||||
"part",
|
||||
event.properties.part.messageID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.part)
|
||||
}),
|
||||
)
|
||||
const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
|
||||
if (result.found) {
|
||||
setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
|
||||
break
|
||||
}
|
||||
setStore(
|
||||
"message",
|
||||
event.properties.info.sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.info)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
case "message.part.updated": {
|
||||
const parts = store.part[event.properties.part.messageID]
|
||||
if (!parts) {
|
||||
setStore("part", event.properties.part.messageID, [event.properties.part])
|
||||
break
|
||||
}
|
||||
const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
|
||||
if (result.found) {
|
||||
setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
|
||||
break
|
||||
}
|
||||
setStore(
|
||||
"part",
|
||||
event.properties.part.messageID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.part)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const sdk = useSDK()
|
||||
Promise.all([
|
||||
sdk.config.providers().then((x) => setStore("provider", x.data!.providers)),
|
||||
sdk.path.get().then((x) => setStore("path", x.data!)),
|
||||
|
||||
+24
-10
@@ -4,7 +4,15 @@ import { Router, Route } from "@solidjs/router"
|
||||
import "@/index.css"
|
||||
import Layout from "@/pages/layout"
|
||||
import Home from "@/pages"
|
||||
import { SDKProvider, SyncProvider, LocalProvider, ThemeProvider } from "@/context"
|
||||
import {
|
||||
EventProvider,
|
||||
SDKProvider,
|
||||
SyncProvider,
|
||||
LocalProvider,
|
||||
ThemeProvider,
|
||||
ShikiProvider,
|
||||
MarkedProvider,
|
||||
} from "@/context"
|
||||
|
||||
const root = document.getElementById("root")
|
||||
|
||||
@@ -18,15 +26,21 @@ render(
|
||||
() => (
|
||||
<div class="h-full bg-background text-text-muted">
|
||||
<ThemeProvider defaultTheme="opencode" defaultDarkMode={true}>
|
||||
<SDKProvider>
|
||||
<SyncProvider>
|
||||
<LocalProvider>
|
||||
<Router root={Layout}>
|
||||
<Route path="/" component={Home} />
|
||||
</Router>
|
||||
</LocalProvider>
|
||||
</SyncProvider>
|
||||
</SDKProvider>
|
||||
<ShikiProvider>
|
||||
<MarkedProvider>
|
||||
<SDKProvider>
|
||||
<EventProvider>
|
||||
<SyncProvider>
|
||||
<LocalProvider>
|
||||
<Router root={Layout}>
|
||||
<Route path="/" component={Home} />
|
||||
</Router>
|
||||
</LocalProvider>
|
||||
</SyncProvider>
|
||||
</EventProvider>
|
||||
</SDKProvider>
|
||||
</MarkedProvider>
|
||||
</ShikiProvider>
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { FileIcon, Icon, IconButton, Tooltip } from "@/ui"
|
||||
import { FileIcon, Icon, IconButton, Logo, Tooltip } from "@/ui"
|
||||
import { Tabs } from "@/ui/tabs"
|
||||
import { Select } from "@/components/select"
|
||||
import FileTree from "@/components/file-tree"
|
||||
import { createSignal, For, Match, onCleanup, onMount, Show, Switch } from "solid-js"
|
||||
import { For, Match, onCleanup, onMount, Show, Switch } from "solid-js"
|
||||
import { useLocal, useSDK } from "@/context"
|
||||
import { Code } from "@/components/code"
|
||||
import {
|
||||
@@ -17,18 +18,17 @@ import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
|
||||
import type { LocalFile } from "@/context/local"
|
||||
import SessionList from "@/components/session-list"
|
||||
import SessionTimeline from "@/components/session-timeline"
|
||||
import { createStore } from "solid-js/store"
|
||||
|
||||
export default function Page() {
|
||||
const sdk = useSDK()
|
||||
const local = useLocal()
|
||||
const [clickTimer, setClickTimer] = createSignal<number | undefined>()
|
||||
const [activeItem, setActiveItem] = createSignal<string | undefined>(undefined)
|
||||
const [inputValue, setInputValue] = createSignal("")
|
||||
const [isDragging, setIsDragging] = createSignal<"left" | "right" | undefined>(undefined)
|
||||
const [leftScrolled, setLeftScrolled] = createSignal(false)
|
||||
|
||||
// TODO: remove
|
||||
local.model.set({ providerID: "opencode", modelID: "grok-code" })
|
||||
const [store, setStore] = createStore({
|
||||
clickTimer: undefined as number | undefined,
|
||||
activeItem: undefined as string | undefined,
|
||||
prompt: "",
|
||||
dragging: undefined as "left" | "right" | undefined,
|
||||
})
|
||||
|
||||
let inputRef: HTMLInputElement | undefined = undefined
|
||||
|
||||
@@ -51,6 +51,10 @@ export default function Page() {
|
||||
return
|
||||
}
|
||||
|
||||
if (document.activeElement?.id === "select-filter") {
|
||||
return
|
||||
}
|
||||
|
||||
if (local.file.active()) {
|
||||
if (e.getModifierState(MOD)) {
|
||||
if (e.key.toLowerCase() === "a") {
|
||||
@@ -76,20 +80,20 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const resetClickTimer = () => {
|
||||
if (!clickTimer()) return
|
||||
clearTimeout(clickTimer())
|
||||
setClickTimer(undefined)
|
||||
if (!store.clickTimer) return
|
||||
clearTimeout(store.clickTimer)
|
||||
setStore("clickTimer", undefined)
|
||||
}
|
||||
|
||||
const startClickTimer = () => {
|
||||
const newClickTimer = setTimeout(() => {
|
||||
setClickTimer(undefined)
|
||||
setStore("clickTimer", undefined)
|
||||
}, 300)
|
||||
setClickTimer(newClickTimer as unknown as number)
|
||||
setStore("clickTimer", newClickTimer as unknown as number)
|
||||
}
|
||||
|
||||
const handleFileClick = async (file: LocalFile) => {
|
||||
if (clickTimer()) {
|
||||
if (store.clickTimer) {
|
||||
resetClickTimer()
|
||||
local.file.update(file.path, { ...file, pinned: true })
|
||||
} else {
|
||||
@@ -107,7 +111,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const onDragStart = (event: any) => {
|
||||
setActiveItem(event.draggable.id as string)
|
||||
setStore("activeItem", event.draggable.id as string)
|
||||
}
|
||||
|
||||
const onDragOver = (event: DragEvent) => {
|
||||
@@ -123,12 +127,12 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const onDragEnd = () => {
|
||||
setActiveItem(undefined)
|
||||
setStore("activeItem", undefined)
|
||||
}
|
||||
|
||||
const handleLeftDragStart = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragging("left")
|
||||
setStore("dragging", "left")
|
||||
const startX = e.clientX
|
||||
const startWidth = local.layout.leftWidth()
|
||||
|
||||
@@ -139,7 +143,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(undefined)
|
||||
setStore("dragging", undefined)
|
||||
document.removeEventListener("mousemove", handleMouseMove)
|
||||
document.removeEventListener("mouseup", handleMouseUp)
|
||||
}
|
||||
@@ -150,7 +154,7 @@ export default function Page() {
|
||||
|
||||
const handleRightDragStart = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragging("right")
|
||||
setStore("dragging", "right")
|
||||
const startX = e.clientX
|
||||
const startWidth = local.layout.rightWidth()
|
||||
|
||||
@@ -161,7 +165,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(undefined)
|
||||
setStore("dragging", undefined)
|
||||
document.removeEventListener("mousemove", handleMouseMove)
|
||||
document.removeEventListener("mouseup", handleMouseUp)
|
||||
}
|
||||
@@ -172,8 +176,8 @@ export default function Page() {
|
||||
|
||||
const handleSubmit = async (e: SubmitEvent) => {
|
||||
e.preventDefault()
|
||||
const prompt = inputValue()
|
||||
setInputValue("")
|
||||
const prompt = store.prompt
|
||||
setStore("prompt", "")
|
||||
inputRef?.blur()
|
||||
|
||||
const session =
|
||||
@@ -228,35 +232,19 @@ export default function Page() {
|
||||
<Tabs class="relative flex flex-col h-full" defaultValue="files">
|
||||
<div class="sticky top-0 shrink-0 flex">
|
||||
<Tabs.List class="grow w-full after:hidden">
|
||||
<Tabs.Trigger value="files" class="flex-1 justify-center">
|
||||
<Tabs.Trigger value="files" class="flex-1 justify-center text-xs">
|
||||
Files
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="changes" class="flex-1 justify-center">
|
||||
<Tabs.Trigger value="changes" class="flex-1 justify-center text-xs">
|
||||
Changes
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
<Tabs.Content
|
||||
value="files"
|
||||
class="grow min-h-0 py-2 bg-background"
|
||||
onScroll={(e: Event & { currentTarget: HTMLDivElement }) => setLeftScrolled(e.currentTarget.scrollTop > 0)}
|
||||
>
|
||||
<Tabs.Content value="files" class="grow min-h-0 py-2 bg-background">
|
||||
<FileTree path="" onFileClick={handleFileClick} />
|
||||
<Show when={leftScrolled()}>
|
||||
<div
|
||||
class="pointer-events-none sticky top-20 left-px h-4
|
||||
bg-gradient-to-t from-transparent to-background"
|
||||
style={`width: ${local.layout.leftWidth() - 2}px`}
|
||||
/>
|
||||
</Show>
|
||||
<div
|
||||
class="pointer-events-none fixed bottom-0 left-px h-4
|
||||
bg-gradient-to-b from-transparent to-background"
|
||||
style={`width: ${local.layout.leftWidth() - 2}px`}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="changes" class="grow min-h-0 py-2 bg-background">
|
||||
<div class="px-2 text-sm text-text-muted">No changes yet</div>
|
||||
<div class="px-2 text-xs text-text-muted">No changes yet</div>
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</div>
|
||||
@@ -266,9 +254,9 @@ export default function Page() {
|
||||
onMouseDown={(e) => handleLeftDragStart(e)}
|
||||
>
|
||||
<div
|
||||
class="w-0.5 h-full bg-transparent group-hover:bg-border-active transition-colors"
|
||||
classList={{
|
||||
"bg-border-active": isDragging() === "left",
|
||||
"w-0.5 h-full bg-transparent group-hover:bg-border-active transition-colors": true,
|
||||
"bg-border-active!": store.dragging === "left",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -281,8 +269,8 @@ export default function Page() {
|
||||
<Show when={local.session.active()} fallback={<SessionList />}>
|
||||
{(activeSession) => (
|
||||
<div class="relative">
|
||||
<div class="sticky top-0 bg-background z-50 p-2 h-9 border-b border-border-subtle/30">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="sticky top-0 bg-background z-50 px-2 h-8 border-b border-border-subtle/30">
|
||||
<div class="h-full flex items-center gap-2">
|
||||
<IconButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
@@ -300,16 +288,6 @@ export default function Page() {
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<div
|
||||
class="pointer-events-none fixed top-0 right-px h-4
|
||||
bg-gradient-to-t from-transparent to-background"
|
||||
style={`width: ${local.layout.rightWidth() - 2}px`}
|
||||
/>
|
||||
<div
|
||||
class="pointer-events-none fixed bottom-0 right-px h-4
|
||||
bg-gradient-to-b from-transparent to-background"
|
||||
style={`width: ${local.layout.rightWidth() - 2}px`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -318,14 +296,22 @@ export default function Page() {
|
||||
onMouseDown={(e) => handleRightDragStart(e)}
|
||||
>
|
||||
<div
|
||||
class="w-0.5 h-full bg-transparent group-hover:bg-border-active transition-colors"
|
||||
classList={{ "bg-border-active": isDragging() === "right" }}
|
||||
classList={{
|
||||
"w-0.5 h-full bg-transparent group-hover:bg-border-active transition-colors": true,
|
||||
"bg-border-active!": store.dragging === "right",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class="relative"
|
||||
style={`margin-left: ${local.layout.leftWidth()}px; margin-right: ${local.layout.rightPane() ? local.layout.rightWidth() : 0}px`}
|
||||
>
|
||||
<Logo
|
||||
size={64}
|
||||
variant="ornate"
|
||||
class="absolute top-2/5 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
|
||||
/>
|
||||
<DragDropProvider
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
@@ -435,12 +421,12 @@ export default function Page() {
|
||||
</For>
|
||||
</Tabs>
|
||||
<DragOverlay>
|
||||
{activeItem() &&
|
||||
{store.activeItem &&
|
||||
(() => {
|
||||
const draggedFile = local.file.node(activeItem()!)
|
||||
const draggedFile = local.file.node(store.activeItem!)
|
||||
return (
|
||||
<div
|
||||
class="relative px-3 h-9 flex items-center
|
||||
class="relative px-3 h-8 flex items-center
|
||||
text-sm font-medium text-text whitespace-nowrap
|
||||
shrink-0 bg-background-panel
|
||||
border-x border-border-subtle/40 border-b border-b-transparent"
|
||||
@@ -453,11 +439,14 @@ export default function Page() {
|
||||
</DragDropProvider>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
class="peer/editor absolute bottom-8 z-50 flex items-center justify-center"
|
||||
style={`left: ${local.layout.leftWidth() + 40}px; right: ${local.layout.rightPane() ? local.layout.rightWidth() + 40 : 40}px`}
|
||||
class="peer/editor absolute inset-x-4 z-50 flex items-center justify-center"
|
||||
classList={{
|
||||
"bottom-8": !!local.file.active(),
|
||||
"bottom-2/5": local.file.active() === undefined,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-2xl min-w-1/2 p-2 mx-auto rounded-lg isolate backdrop-blur-xs
|
||||
class="w-full max-w-xl min-w-0 p-2 mx-auto rounded-lg isolate backdrop-blur-xs
|
||||
flex flex-col gap-1
|
||||
bg-gradient-to-b from-background-panel/90 to-background/90
|
||||
ring-1 ring-border-active/50 border border-transparent
|
||||
@@ -479,16 +468,36 @@ export default function Page() {
|
||||
<input
|
||||
ref={(el) => (inputRef = el)}
|
||||
type="text"
|
||||
value={inputValue()}
|
||||
onInput={(e) => setInputValue(e.currentTarget.value)}
|
||||
value={store.prompt}
|
||||
onInput={(e) => setStore("prompt", e.currentTarget.value)}
|
||||
placeholder="It all starts with a prompt..."
|
||||
class="w-full p-1 pb-4 text-text font-light placeholder-text-muted/70 text-sm focus:outline-none"
|
||||
/>
|
||||
<div class="px-1 flex justify-between items-center text-xs text-text-muted">
|
||||
<span>
|
||||
<span class="text-primary uppercase">{local.agent.current()?.name ?? "unknown"}</span> /{" "}
|
||||
{local.model.parsed().provider} / {local.model.parsed().model}
|
||||
</span>
|
||||
<div class="flex justify-between items-center text-xs text-text-muted">
|
||||
<div class="flex gap-2 items-center">
|
||||
<Select
|
||||
options={local.agent.list().map((a) => a.name)}
|
||||
current={local.agent.current().name}
|
||||
onSelect={local.agent.set}
|
||||
size="sm"
|
||||
class="uppercase"
|
||||
/>
|
||||
<Select
|
||||
options={local.model.list()}
|
||||
current={local.model.current()}
|
||||
onSelect={local.model.set}
|
||||
label={(x) => x.modelID}
|
||||
value={(x) => `${x.providerID}.${x.modelID}`}
|
||||
filter={{
|
||||
keys: ["providerID", "modelID"],
|
||||
placeholder: "Filter models",
|
||||
}}
|
||||
groupBy={(x) => x.providerID}
|
||||
size="sm"
|
||||
class="uppercase"
|
||||
/>
|
||||
<span class="text-text-muted/70">{local.model.parsed().provider}</span>
|
||||
</div>
|
||||
<div class="flex gap-1 items-center">
|
||||
<IconButton class="text-text-muted" size="xs" variant="ghost">
|
||||
<Icon name="photo" size={16} />
|
||||
@@ -548,7 +557,7 @@ const SortableTab = (props: {
|
||||
<TabVisual file={props.file} />
|
||||
</Tabs.Trigger>
|
||||
<IconButton
|
||||
class="absolute right-1 top-2 opacity-0 text-text-muted/60
|
||||
class="absolute right-1 top-1.5 opacity-0 text-text-muted/60
|
||||
peer-data-[selected]/tab:opacity-100 peer-data-[selected]/tab:text-text
|
||||
peer-data-[selected]/tab:hover:bg-border-subtle
|
||||
hover:opacity-100 peer-hover/tab:opacity-100"
|
||||
|
||||
@@ -18,7 +18,7 @@ function TabsList(props: TabsListProps) {
|
||||
classList={{
|
||||
"relative flex items-center bg-background overflow-x-auto no-scrollbar": true,
|
||||
"divide-x divide-border-subtle/40": true,
|
||||
"after:content-[''] after:block after:grow after:h-9": true,
|
||||
"after:content-[''] after:block after:grow after:h-8": true,
|
||||
"after:border-l empty:after:border-l-0! after:border-b after:border-border-subtle/40": true,
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
@@ -32,7 +32,7 @@ function TabsTrigger(props: ParentProps<TabsTriggerProps>) {
|
||||
return (
|
||||
<KobalteTabs.Trigger
|
||||
classList={{
|
||||
"relative px-3 h-9 flex items-center": true,
|
||||
"relative px-3 h-8 flex items-center": true,
|
||||
"text-sm font-medium text-text-muted/60 cursor-pointer": true,
|
||||
"whitespace-nowrap shrink-0 border-b border-border-subtle/40": true,
|
||||
"disabled:pointer-events-none disabled:opacity-50": true,
|
||||
|
||||
@@ -7,17 +7,18 @@
|
||||
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
|
||||
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
||||
"start": "vinxi start",
|
||||
"version": "0.10.0"
|
||||
"version": "0.11.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
"@solidjs/router": "^0.15.0",
|
||||
"@solidjs/start": "^1.1.0",
|
||||
"solid-js": "catalog:",
|
||||
"vinxi": "^0.5.7",
|
||||
"@opencode/console-core": "workspace:*"
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
|
||||
@@ -174,11 +174,20 @@ export function BillingSection() {
|
||||
</div>
|
||||
</div>
|
||||
<div data-slot="usage">
|
||||
<Show when={!balanceInfo()?.reload && !(balanceAmount() === "0.00" || balanceAmount() === "-0.00")}>
|
||||
<p>
|
||||
You have <b data-slot="value">${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b> remaining in
|
||||
your account. You can continue using the API with your remaining balance.
|
||||
</p>
|
||||
<Show when={!balanceInfo()?.reload}>
|
||||
<Show
|
||||
when={!(balanceAmount() === "0.00" || balanceAmount() === "-0.00")}
|
||||
fallback={
|
||||
<p>
|
||||
We'll load <b>$20</b> (+$1.23 processing fee) and reload it when it reaches <b>$5</b>.
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
You have <b data-slot="value">${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b> remaining in
|
||||
your account. You can continue using the API with your remaining balance.
|
||||
</p>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={balanceInfo()?.reload && !balanceInfo()?.reloadError}>
|
||||
<p>
|
||||
|
||||
@@ -40,6 +40,10 @@
|
||||
|
||||
&[data-slot="payment-amount"] {
|
||||
color: var(--color-text);
|
||||
|
||||
&[data-refunded="true"] {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,9 @@ export function PaymentSection() {
|
||||
{formatDateForTable(date)}
|
||||
</td>
|
||||
<td data-slot="payment-id">{payment.id}</td>
|
||||
<td data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</td>
|
||||
<td data-slot="payment-amount" data-refunded={!!payment.timeRefunded}>
|
||||
${((payment.amount ?? 0) / 100000000).toFixed(2)}
|
||||
</td>
|
||||
<td data-slot="payment-receipt">
|
||||
<button
|
||||
onClick={async () => {
|
||||
@@ -95,7 +97,6 @@ export function PaymentSection() {
|
||||
}
|
||||
}}
|
||||
data-slot="receipt-button"
|
||||
style="cursor: pointer;"
|
||||
>
|
||||
view
|
||||
</button>
|
||||
|
||||
@@ -175,7 +175,7 @@ export default function Home() {
|
||||
|
||||
<div data-component="legal">
|
||||
<span>
|
||||
©2025 <a href="https://anoma.ly">Anomaly Innovations</a>
|
||||
©2025 <a href="https://anoma.ly">Anomaly</a>
|
||||
</span>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Billing } from "@opencode/console-core/billing.js"
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { Database, eq, sql } from "@opencode/console-core/drizzle/index.js"
|
||||
import { and, Database, eq, sql } from "@opencode/console-core/drizzle/index.js"
|
||||
import { BillingTable, PaymentTable } from "@opencode/console-core/schema/billing.sql.js"
|
||||
import { Identifier } from "@opencode/console-core/identifier.js"
|
||||
import { centsToMicroCents } from "@opencode/console-core/util/price.js"
|
||||
@@ -91,6 +91,39 @@ export async function POST(input: APIEvent) {
|
||||
})
|
||||
})
|
||||
}
|
||||
if (body.type === "charge.refunded") {
|
||||
const customerID = body.data.object.customer as string
|
||||
const paymentIntentID = body.data.object.payment_intent as string
|
||||
if (!customerID) throw new Error("Customer ID not found")
|
||||
if (!paymentIntentID) throw new Error("Payment ID not found")
|
||||
|
||||
const workspaceID = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
workspaceID: BillingTable.workspaceID,
|
||||
})
|
||||
.from(BillingTable)
|
||||
.where(eq(BillingTable.customerID, customerID))
|
||||
.then((rows) => rows[0]?.workspaceID),
|
||||
)
|
||||
if (!workspaceID) throw new Error("Workspace ID not found")
|
||||
|
||||
await Database.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(PaymentTable)
|
||||
.set({
|
||||
timeRefunded: new Date(body.created * 1000),
|
||||
})
|
||||
.where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID)))
|
||||
|
||||
await tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
balance: sql`${BillingTable.balance} - ${centsToMicroCents(Billing.CHARGE_AMOUNT)}`,
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
})
|
||||
}
|
||||
|
||||
console.log("finished handling")
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import "./[id].css"
|
||||
import { Billing } from "@opencode/console-core/billing.js"
|
||||
import { query, useParams, createAsync } from "@solidjs/router"
|
||||
import { Show } from "solid-js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { MonthlyLimitSection } from "~/component/workspace/monthly-limit-section"
|
||||
import { NewUserSection } from "~/component/workspace/new-user-section"
|
||||
import { BillingSection } from "~/component/workspace/billing-section"
|
||||
@@ -10,17 +6,7 @@ import { PaymentSection } from "~/component/workspace/payment-section"
|
||||
import { UsageSection } from "~/component/workspace/usage-section"
|
||||
import { KeySection } from "~/component/workspace/key-section"
|
||||
|
||||
const getBillingInfo = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(async () => {
|
||||
return await Billing.get()
|
||||
}, workspaceID)
|
||||
}, "billing.get")
|
||||
|
||||
export default function () {
|
||||
const params = useParams()
|
||||
const balanceInfo = createAsync(() => getBillingInfo(params.id))
|
||||
|
||||
return (
|
||||
<div data-page="workspace-[id]">
|
||||
<section data-component="title-section">
|
||||
@@ -38,13 +24,18 @@ export default function () {
|
||||
<NewUserSection />
|
||||
<KeySection />
|
||||
<BillingSection />
|
||||
<Show when={true}>
|
||||
{/*<Show when={balanceInfo()?.reload}>*/}
|
||||
<MonthlyLimitSection />
|
||||
</Show>
|
||||
<MonthlyLimitSection />
|
||||
<UsageSection />
|
||||
<PaymentSection />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function isBeta(workspaceID: string) {
|
||||
return [
|
||||
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // production
|
||||
"wrk_01K4NFRR5P7FSYWH88307B4DDS", // dev
|
||||
"wrk_01K4PJRKJ2WPQZN3FFYRV4673F", // frank
|
||||
].includes(workspaceID)
|
||||
}
|
||||
|
||||
@@ -1,37 +1,15 @@
|
||||
import { z } from "zod"
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import path from "node:path"
|
||||
import { and, Database, eq, isNull, lt, or, sql } from "@opencode/console-core/drizzle/index.js"
|
||||
import { KeyTable } from "@opencode/console-core/schema/key.sql.js"
|
||||
import { BillingTable, PaymentTable, UsageTable } from "@opencode/console-core/schema/billing.sql.js"
|
||||
import { BillingTable, UsageTable } from "@opencode/console-core/schema/billing.sql.js"
|
||||
import { centsToMicroCents } from "@opencode/console-core/util/price.js"
|
||||
import { Identifier } from "@opencode/console-core/identifier.js"
|
||||
import { Resource } from "@opencode/console-resource"
|
||||
import { Billing } from "../../../../core/src/billing"
|
||||
import { Actor } from "@opencode/console-core/actor.js"
|
||||
|
||||
type ModelCost = {
|
||||
input: number
|
||||
output: number
|
||||
cacheRead?: number
|
||||
cacheWrite5m?: number
|
||||
cacheWrite1h?: number
|
||||
}
|
||||
|
||||
type Model = {
|
||||
id: string
|
||||
auth: boolean
|
||||
cost: ModelCost | ((usage: any) => ModelCost)
|
||||
headerMappings: Record<string, string>
|
||||
providers: Record<
|
||||
string,
|
||||
{
|
||||
api: string
|
||||
apiKey: string
|
||||
model: string
|
||||
weight?: number
|
||||
}
|
||||
>
|
||||
}
|
||||
import { WorkspaceTable } from "@opencode/console-core/schema/workspace.sql.js"
|
||||
|
||||
export async function handler(
|
||||
input: APIEvent,
|
||||
@@ -56,184 +34,32 @@ export async function handler(
|
||||
class MonthlyLimitError extends Error {}
|
||||
class ModelError extends Error {}
|
||||
|
||||
const MODELS: Record<string, Model> = {
|
||||
"claude-opus-4-1": {
|
||||
id: "claude-opus-4-1" as const,
|
||||
auth: true,
|
||||
cost: {
|
||||
input: 0.000015,
|
||||
output: 0.000075,
|
||||
cacheRead: 0.0000015,
|
||||
cacheWrite5m: 0.00001875,
|
||||
cacheWrite1h: 0.00003,
|
||||
},
|
||||
headerMappings: {},
|
||||
providers: {
|
||||
anthropic: {
|
||||
api: "https://api.anthropic.com",
|
||||
apiKey: Resource.ANTHROPIC_API_KEY.value,
|
||||
model: "claude-opus-4-1-20250805",
|
||||
},
|
||||
},
|
||||
},
|
||||
"claude-sonnet-4": {
|
||||
id: "claude-sonnet-4" as const,
|
||||
auth: true,
|
||||
cost: (usage: any) => {
|
||||
const totalInputTokens =
|
||||
usage.inputTokens + usage.cacheReadTokens + usage.cacheWrite5mTokens + usage.cacheWrite1hTokens
|
||||
return totalInputTokens <= 200_000
|
||||
? {
|
||||
input: 0.000003,
|
||||
output: 0.000015,
|
||||
cacheRead: 0.0000003,
|
||||
cacheWrite5m: 0.00000375,
|
||||
cacheWrite1h: 0.000006,
|
||||
}
|
||||
: {
|
||||
input: 0.000006,
|
||||
output: 0.0000225,
|
||||
cacheRead: 0.0000006,
|
||||
cacheWrite5m: 0.0000075,
|
||||
cacheWrite1h: 0.000012,
|
||||
}
|
||||
},
|
||||
headerMappings: {},
|
||||
providers: {
|
||||
anthropic: {
|
||||
api: "https://api.anthropic.com",
|
||||
apiKey: Resource.ANTHROPIC_API_KEY.value,
|
||||
model: "claude-sonnet-4-20250514",
|
||||
},
|
||||
},
|
||||
},
|
||||
"claude-3-5-haiku": {
|
||||
id: "claude-3-5-haiku" as const,
|
||||
auth: true,
|
||||
cost: {
|
||||
input: 0.0000008,
|
||||
output: 0.000004,
|
||||
cacheRead: 0.00000008,
|
||||
cacheWrite5m: 0.000001,
|
||||
cacheWrite1h: 0.0000016,
|
||||
},
|
||||
headerMappings: {},
|
||||
providers: {
|
||||
anthropic: {
|
||||
api: "https://api.anthropic.com",
|
||||
apiKey: Resource.ANTHROPIC_API_KEY.value,
|
||||
model: "claude-3-5-haiku-20241022",
|
||||
},
|
||||
},
|
||||
},
|
||||
"gpt-5": {
|
||||
id: "gpt-5" as const,
|
||||
auth: true,
|
||||
cost: {
|
||||
input: 0.00000125,
|
||||
output: 0.00001,
|
||||
cacheRead: 0.000000125,
|
||||
},
|
||||
headerMappings: {},
|
||||
providers: {
|
||||
openai: {
|
||||
api: "https://api.openai.com",
|
||||
apiKey: Resource.OPENAI_API_KEY.value,
|
||||
model: "gpt-5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"qwen3-coder": {
|
||||
id: "qwen3-coder" as const,
|
||||
auth: true,
|
||||
cost: {
|
||||
input: 0.00000045,
|
||||
output: 0.0000018,
|
||||
},
|
||||
headerMappings: {},
|
||||
providers: {
|
||||
baseten: {
|
||||
api: "https://inference.baseten.co",
|
||||
apiKey: Resource.BASETEN_API_KEY.value,
|
||||
model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
|
||||
weight: 4,
|
||||
},
|
||||
fireworks: {
|
||||
api: "https://api.fireworks.ai/inference",
|
||||
apiKey: Resource.FIREWORKS_API_KEY.value,
|
||||
model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct",
|
||||
weight: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
"kimi-k2": {
|
||||
id: "kimi-k2" as const,
|
||||
auth: true,
|
||||
cost: {
|
||||
input: 0.0000006,
|
||||
output: 0.0000025,
|
||||
},
|
||||
headerMappings: {},
|
||||
providers: {
|
||||
baseten: {
|
||||
api: "https://inference.baseten.co",
|
||||
apiKey: Resource.BASETEN_API_KEY.value,
|
||||
model: "moonshotai/Kimi-K2-Instruct-0905",
|
||||
//weight: 4,
|
||||
},
|
||||
//fireworks: {
|
||||
// api: "https://api.fireworks.ai/inference",
|
||||
// apiKey: Resource.FIREWORKS_API_KEY.value,
|
||||
// model: "accounts/fireworks/models/kimi-k2-instruct-0905",
|
||||
// weight: 1,
|
||||
//},
|
||||
},
|
||||
},
|
||||
"grok-code": {
|
||||
id: "grok-code" as const,
|
||||
auth: false,
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
},
|
||||
headerMappings: {
|
||||
"x-grok-conv-id": "x-opencode-session",
|
||||
"x-grok-req-id": "x-opencode-request",
|
||||
},
|
||||
providers: {
|
||||
xai: {
|
||||
api: "https://api.x.ai",
|
||||
apiKey: Resource.XAI_API_KEY.value,
|
||||
model: "grok-code",
|
||||
},
|
||||
},
|
||||
},
|
||||
// deprecated
|
||||
"qwen/qwen3-coder": {
|
||||
id: "qwen/qwen3-coder" as const,
|
||||
auth: true,
|
||||
cost: {
|
||||
input: 0.00000038,
|
||||
output: 0.00000153,
|
||||
},
|
||||
headerMappings: {},
|
||||
providers: {
|
||||
baseten: {
|
||||
api: "https://inference.baseten.co",
|
||||
apiKey: Resource.BASETEN_API_KEY.value,
|
||||
model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
|
||||
weight: 5,
|
||||
},
|
||||
fireworks: {
|
||||
api: "https://api.fireworks.ai/inference",
|
||||
apiKey: Resource.FIREWORKS_API_KEY.value,
|
||||
model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct",
|
||||
weight: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
const ModelCostSchema = z.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cacheRead: z.number().optional(),
|
||||
cacheWrite5m: z.number().optional(),
|
||||
cacheWrite1h: z.number().optional(),
|
||||
})
|
||||
|
||||
const ModelSchema = z.object({
|
||||
cost: ModelCostSchema,
|
||||
cost200K: ModelCostSchema.optional(),
|
||||
allowAnonymous: z.boolean().optional(),
|
||||
providers: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
api: z.string(),
|
||||
apiKey: z.string(),
|
||||
model: z.string(),
|
||||
weight: z.number().optional(),
|
||||
headerMappings: z.record(z.string(), z.string()).optional(),
|
||||
disabled: z.boolean().optional(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
type Model = z.infer<typeof ModelSchema>
|
||||
|
||||
const FREE_WORKSPACES = [
|
||||
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
|
||||
@@ -259,31 +85,29 @@ export async function handler(
|
||||
session: input.request.headers.get("x-opencode-session"),
|
||||
request: input.request.headers.get("x-opencode-request"),
|
||||
})
|
||||
const MODEL = validateModel()
|
||||
const apiKey = await authenticate()
|
||||
const isFree = FREE_WORKSPACES.includes(apiKey?.workspaceID ?? "")
|
||||
await checkCreditsAndLimit()
|
||||
const providerName = selectProvider()
|
||||
const providerData = MODEL.providers[providerName]
|
||||
logger.metric({ provider: providerName })
|
||||
const modelInfo = validateModel(body.model)
|
||||
const providerInfo = selectProvider(modelInfo)
|
||||
const authInfo = await authenticate(modelInfo)
|
||||
validateBilling(modelInfo, authInfo)
|
||||
logger.metric({ provider: providerInfo.id })
|
||||
|
||||
// Request to model provider
|
||||
const startTimestamp = Date.now()
|
||||
const res = await fetch(path.posix.join(providerData.api, url.pathname.replace(/^\/zen/, "") + url.search), {
|
||||
const res = await fetch(path.posix.join(providerInfo.api, url.pathname.replace(/^\/zen/, "") + url.search), {
|
||||
method: "POST",
|
||||
headers: (() => {
|
||||
const headers = input.request.headers
|
||||
headers.delete("host")
|
||||
headers.delete("content-length")
|
||||
opts.setAuthHeader(headers, providerData.apiKey)
|
||||
Object.entries(MODEL.headerMappings ?? {}).forEach(([k, v]) => {
|
||||
opts.setAuthHeader(headers, providerInfo.apiKey)
|
||||
Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => {
|
||||
headers.set(k, headers.get(v)!)
|
||||
})
|
||||
return headers
|
||||
})(),
|
||||
body: JSON.stringify({
|
||||
...(opts.modifyBody?.(body) ?? body),
|
||||
model: providerData.model,
|
||||
model: providerInfo.model,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -302,8 +126,8 @@ export async function handler(
|
||||
const body = JSON.stringify(json)
|
||||
logger.metric({ response_length: body.length })
|
||||
logger.debug(body)
|
||||
await trackUsage(json.usage)
|
||||
await reload()
|
||||
await trackUsage(authInfo, modelInfo, providerInfo.id, json.usage)
|
||||
await reload(authInfo)
|
||||
return new Response(body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
@@ -326,8 +150,8 @@ export async function handler(
|
||||
logger.metric({ response_length: responseLength })
|
||||
const usage = opts.getStreamUsage()
|
||||
if (usage) {
|
||||
await trackUsage(usage)
|
||||
await reload()
|
||||
await trackUsage(authInfo, modelInfo, providerInfo.id, usage)
|
||||
await reload(authInfo)
|
||||
}
|
||||
c.close()
|
||||
return
|
||||
@@ -337,6 +161,7 @@ export async function handler(
|
||||
logger.metric({ time_to_first_byte: Date.now() - startTimestamp })
|
||||
}
|
||||
responseLength += value.length
|
||||
console.log(decoder.decode(value, { stream: true }))
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
|
||||
const parts = buffer.split("\n\n")
|
||||
@@ -363,202 +188,6 @@ export async function handler(
|
||||
statusText: res.statusText,
|
||||
headers: resHeaders,
|
||||
})
|
||||
|
||||
function validateModel() {
|
||||
if (!(body.model in MODELS)) {
|
||||
throw new ModelError(`Model ${body.model} not supported`)
|
||||
}
|
||||
const model = MODELS[body.model as keyof typeof MODELS]
|
||||
logger.metric({ model: model.id })
|
||||
return model
|
||||
}
|
||||
|
||||
async function authenticate() {
|
||||
try {
|
||||
const apiKey = opts.parseApiKey(input.request.headers)
|
||||
if (!apiKey) throw new AuthError("Missing API key.")
|
||||
|
||||
const key = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
id: KeyTable.id,
|
||||
workspaceID: KeyTable.workspaceID,
|
||||
})
|
||||
.from(KeyTable)
|
||||
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
|
||||
if (!key) throw new AuthError("Invalid API key.")
|
||||
logger.metric({
|
||||
api_key: key.id,
|
||||
workspace: key.workspaceID,
|
||||
})
|
||||
return key
|
||||
} catch (e) {
|
||||
// ignore error if model does not require authentication
|
||||
if (!MODEL.auth) return
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function checkCreditsAndLimit() {
|
||||
if (!apiKey || !MODEL.auth || isFree) return
|
||||
|
||||
const billing = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
balance: BillingTable.balance,
|
||||
paymentMethodID: BillingTable.paymentMethodID,
|
||||
monthlyLimit: BillingTable.monthlyLimit,
|
||||
monthlyUsage: BillingTable.monthlyUsage,
|
||||
timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
|
||||
})
|
||||
.from(BillingTable)
|
||||
.where(eq(BillingTable.workspaceID, apiKey.workspaceID))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
|
||||
if (!billing.paymentMethodID) throw new CreditsError("No payment method")
|
||||
if (billing.balance <= 0) throw new CreditsError("Insufficient balance")
|
||||
if (
|
||||
billing.monthlyLimit &&
|
||||
billing.monthlyUsage &&
|
||||
billing.timeMonthlyUsageUpdated &&
|
||||
billing.monthlyUsage >= centsToMicroCents(billing.monthlyLimit * 100)
|
||||
) {
|
||||
const now = new Date()
|
||||
const currentYear = now.getUTCFullYear()
|
||||
const currentMonth = now.getUTCMonth()
|
||||
const dateYear = billing.timeMonthlyUsageUpdated.getUTCFullYear()
|
||||
const dateMonth = billing.timeMonthlyUsageUpdated.getUTCMonth()
|
||||
if (currentYear === dateYear && currentMonth === dateMonth)
|
||||
throw new MonthlyLimitError(`You have reached your monthly spending limit of $${billing.monthlyLimit}.`)
|
||||
}
|
||||
}
|
||||
|
||||
function selectProvider() {
|
||||
const picks = Object.entries(MODEL.providers).flatMap(([name, provider]) =>
|
||||
Array<string>(provider.weight ?? 1).fill(name),
|
||||
)
|
||||
return picks[Math.floor(Math.random() * picks.length)]
|
||||
}
|
||||
|
||||
async function trackUsage(usage: any) {
|
||||
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
|
||||
opts.normalizeUsage(usage)
|
||||
|
||||
const modelCost = typeof MODEL.cost === "function" ? MODEL.cost(usage) : MODEL.cost
|
||||
|
||||
const inputCost = modelCost.input * inputTokens * 100
|
||||
const outputCost = modelCost.output * outputTokens * 100
|
||||
const reasoningCost = (() => {
|
||||
if (!reasoningTokens) return undefined
|
||||
return modelCost.output * reasoningTokens * 100
|
||||
})()
|
||||
const cacheReadCost = (() => {
|
||||
if (!cacheReadTokens) return undefined
|
||||
if (!modelCost.cacheRead) return undefined
|
||||
return modelCost.cacheRead * cacheReadTokens * 100
|
||||
})()
|
||||
const cacheWrite5mCost = (() => {
|
||||
if (!cacheWrite5mTokens) return undefined
|
||||
if (!modelCost.cacheWrite5m) return undefined
|
||||
return modelCost.cacheWrite5m * cacheWrite5mTokens * 100
|
||||
})()
|
||||
const cacheWrite1hCost = (() => {
|
||||
if (!cacheWrite1hTokens) return undefined
|
||||
if (!modelCost.cacheWrite1h) return undefined
|
||||
return modelCost.cacheWrite1h * cacheWrite1hTokens * 100
|
||||
})()
|
||||
const totalCostInCent =
|
||||
inputCost +
|
||||
outputCost +
|
||||
(reasoningCost ?? 0) +
|
||||
(cacheReadCost ?? 0) +
|
||||
(cacheWrite5mCost ?? 0) +
|
||||
(cacheWrite1hCost ?? 0)
|
||||
|
||||
logger.metric({
|
||||
"tokens.input": inputTokens,
|
||||
"tokens.output": outputTokens,
|
||||
"tokens.reasoning": reasoningTokens,
|
||||
"tokens.cache_read": cacheReadTokens,
|
||||
"tokens.cache_write_5m": cacheWrite5mTokens,
|
||||
"tokens.cache_write_1h": cacheWrite1hTokens,
|
||||
"cost.input": Math.round(inputCost),
|
||||
"cost.output": Math.round(outputCost),
|
||||
"cost.reasoning": reasoningCost ? Math.round(reasoningCost) : undefined,
|
||||
"cost.cache_read": cacheReadCost ? Math.round(cacheReadCost) : undefined,
|
||||
"cost.cache_write_5m": cacheWrite5mCost ? Math.round(cacheWrite5mCost) : undefined,
|
||||
"cost.cache_write_1h": cacheWrite1hCost ? Math.round(cacheWrite1hCost) : undefined,
|
||||
"cost.total": Math.round(totalCostInCent),
|
||||
})
|
||||
|
||||
if (!apiKey) return
|
||||
|
||||
const cost = isFree ? 0 : centsToMicroCents(totalCostInCent)
|
||||
await Database.transaction(async (tx) => {
|
||||
await tx.insert(UsageTable).values({
|
||||
workspaceID: apiKey.workspaceID,
|
||||
id: Identifier.create("usage"),
|
||||
model: MODEL.id,
|
||||
provider: providerName,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
reasoningTokens,
|
||||
cacheReadTokens,
|
||||
cacheWrite5mTokens,
|
||||
cacheWrite1hTokens,
|
||||
cost,
|
||||
})
|
||||
await tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
balance: sql`${BillingTable.balance} - ${cost}`,
|
||||
monthlyUsage: sql`
|
||||
CASE
|
||||
WHEN MONTH(${BillingTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${BillingTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${BillingTable.monthlyUsage} + ${cost}
|
||||
ELSE ${cost}
|
||||
END
|
||||
`,
|
||||
timeMonthlyUsageUpdated: sql`now()`,
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, apiKey.workspaceID))
|
||||
})
|
||||
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.update(KeyTable)
|
||||
.set({ timeUsed: sql`now()` })
|
||||
.where(eq(KeyTable.id, apiKey.id)),
|
||||
)
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
if (!apiKey) return
|
||||
|
||||
const lock = await Database.use((tx) =>
|
||||
tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
timeReloadLockedTill: sql`now() + interval 1 minute`,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(BillingTable.workspaceID, apiKey.workspaceID),
|
||||
eq(BillingTable.reload, true),
|
||||
lt(BillingTable.balance, centsToMicroCents(Billing.CHARGE_THRESHOLD)),
|
||||
or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)),
|
||||
),
|
||||
),
|
||||
)
|
||||
if (lock.rowsAffected === 0) return
|
||||
|
||||
await Actor.provide("system", { workspaceID: apiKey.workspaceID }, async () => {
|
||||
await Billing.reload()
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.metric({
|
||||
"error.type": error.constructor.name,
|
||||
@@ -591,4 +220,222 @@ export async function handler(
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
|
||||
function validateModel(reqModel: string) {
|
||||
const json = JSON.parse(Resource.ZEN_MODELS.value)
|
||||
|
||||
const allModels = z.record(z.string(), ModelSchema).parse(json)
|
||||
|
||||
if (!(reqModel in allModels)) {
|
||||
throw new ModelError(`Model ${reqModel} not supported`)
|
||||
}
|
||||
const modelId = reqModel as keyof typeof allModels
|
||||
const modelData = allModels[modelId]
|
||||
|
||||
logger.metric({ model: modelId })
|
||||
|
||||
return { id: modelId, ...modelData }
|
||||
}
|
||||
|
||||
function selectProvider(model: Model) {
|
||||
const providers = model.providers
|
||||
.filter((provider) => !provider.disabled)
|
||||
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
|
||||
return providers[Math.floor(Math.random() * providers.length)]
|
||||
}
|
||||
|
||||
async function authenticate(model: Model) {
|
||||
const apiKey = opts.parseApiKey(input.request.headers)
|
||||
if (!apiKey) {
|
||||
if (model.allowAnonymous) return
|
||||
throw new AuthError("Missing API key.")
|
||||
}
|
||||
|
||||
const data = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
apiKey: KeyTable.id,
|
||||
workspaceID: KeyTable.workspaceID,
|
||||
balance: BillingTable.balance,
|
||||
paymentMethodID: BillingTable.paymentMethodID,
|
||||
monthlyLimit: BillingTable.monthlyLimit,
|
||||
monthlyUsage: BillingTable.monthlyUsage,
|
||||
timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
|
||||
})
|
||||
.from(KeyTable)
|
||||
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
|
||||
.innerJoin(BillingTable, eq(BillingTable.workspaceID, KeyTable.workspaceID))
|
||||
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
|
||||
if (!data) throw new AuthError("Invalid API key.")
|
||||
logger.metric({
|
||||
api_key: data.apiKey,
|
||||
workspace: data.workspaceID,
|
||||
})
|
||||
|
||||
const isFree = FREE_WORKSPACES.includes(data.workspaceID)
|
||||
|
||||
return {
|
||||
apiKeyId: data.apiKey,
|
||||
workspaceID: data.workspaceID,
|
||||
billing: {
|
||||
paymentMethodID: data.paymentMethodID,
|
||||
balance: data.balance,
|
||||
monthlyLimit: data.monthlyLimit,
|
||||
monthlyUsage: data.monthlyUsage,
|
||||
timeMonthlyUsageUpdated: data.timeMonthlyUsageUpdated,
|
||||
},
|
||||
isFree,
|
||||
}
|
||||
}
|
||||
|
||||
function validateBilling(model: Model, authInfo: Awaited<ReturnType<typeof authenticate>>) {
|
||||
if (!authInfo || authInfo.isFree) return
|
||||
if (model.allowAnonymous) return
|
||||
|
||||
const billing = authInfo.billing
|
||||
if (!billing.paymentMethodID) throw new CreditsError("No payment method")
|
||||
if (billing.balance <= 0) throw new CreditsError("Insufficient balance")
|
||||
if (
|
||||
billing.monthlyLimit &&
|
||||
billing.monthlyUsage &&
|
||||
billing.timeMonthlyUsageUpdated &&
|
||||
billing.monthlyUsage >= centsToMicroCents(billing.monthlyLimit * 100)
|
||||
) {
|
||||
const now = new Date()
|
||||
const currentYear = now.getUTCFullYear()
|
||||
const currentMonth = now.getUTCMonth()
|
||||
const dateYear = billing.timeMonthlyUsageUpdated.getUTCFullYear()
|
||||
const dateMonth = billing.timeMonthlyUsageUpdated.getUTCMonth()
|
||||
if (currentYear === dateYear && currentMonth === dateMonth)
|
||||
throw new MonthlyLimitError(`You have reached your monthly spending limit of $${billing.monthlyLimit}.`)
|
||||
}
|
||||
}
|
||||
|
||||
async function trackUsage(
|
||||
authInfo: Awaited<ReturnType<typeof authenticate>>,
|
||||
modelInfo: ReturnType<typeof validateModel>,
|
||||
providerId: string,
|
||||
usage: any,
|
||||
) {
|
||||
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
|
||||
opts.normalizeUsage(usage)
|
||||
|
||||
const modelCost =
|
||||
modelInfo.cost200K &&
|
||||
usage.inputTokens + usage.cacheReadTokens + usage.cacheWrite5mTokens + usage.cacheWrite1hTokens > 200_000
|
||||
? modelInfo.cost200K
|
||||
: modelInfo.cost
|
||||
|
||||
const inputCost = modelCost.input * inputTokens * 100
|
||||
const outputCost = modelCost.output * outputTokens * 100
|
||||
const reasoningCost = (() => {
|
||||
if (!reasoningTokens) return undefined
|
||||
return modelCost.output * reasoningTokens * 100
|
||||
})()
|
||||
const cacheReadCost = (() => {
|
||||
if (!cacheReadTokens) return undefined
|
||||
if (!modelCost.cacheRead) return undefined
|
||||
return modelCost.cacheRead * cacheReadTokens * 100
|
||||
})()
|
||||
const cacheWrite5mCost = (() => {
|
||||
if (!cacheWrite5mTokens) return undefined
|
||||
if (!modelCost.cacheWrite5m) return undefined
|
||||
return modelCost.cacheWrite5m * cacheWrite5mTokens * 100
|
||||
})()
|
||||
const cacheWrite1hCost = (() => {
|
||||
if (!cacheWrite1hTokens) return undefined
|
||||
if (!modelCost.cacheWrite1h) return undefined
|
||||
return modelCost.cacheWrite1h * cacheWrite1hTokens * 100
|
||||
})()
|
||||
const totalCostInCent =
|
||||
inputCost +
|
||||
outputCost +
|
||||
(reasoningCost ?? 0) +
|
||||
(cacheReadCost ?? 0) +
|
||||
(cacheWrite5mCost ?? 0) +
|
||||
(cacheWrite1hCost ?? 0)
|
||||
|
||||
logger.metric({
|
||||
"tokens.input": inputTokens,
|
||||
"tokens.output": outputTokens,
|
||||
"tokens.reasoning": reasoningTokens,
|
||||
"tokens.cache_read": cacheReadTokens,
|
||||
"tokens.cache_write_5m": cacheWrite5mTokens,
|
||||
"tokens.cache_write_1h": cacheWrite1hTokens,
|
||||
"cost.input": Math.round(inputCost),
|
||||
"cost.output": Math.round(outputCost),
|
||||
"cost.reasoning": reasoningCost ? Math.round(reasoningCost) : undefined,
|
||||
"cost.cache_read": cacheReadCost ? Math.round(cacheReadCost) : undefined,
|
||||
"cost.cache_write_5m": cacheWrite5mCost ? Math.round(cacheWrite5mCost) : undefined,
|
||||
"cost.cache_write_1h": cacheWrite1hCost ? Math.round(cacheWrite1hCost) : undefined,
|
||||
"cost.total": Math.round(totalCostInCent),
|
||||
})
|
||||
|
||||
if (!authInfo) return
|
||||
|
||||
const cost = authInfo.isFree ? 0 : centsToMicroCents(totalCostInCent)
|
||||
await Database.transaction(async (tx) => {
|
||||
await tx.insert(UsageTable).values({
|
||||
workspaceID: authInfo.workspaceID,
|
||||
id: Identifier.create("usage"),
|
||||
model: modelInfo.id,
|
||||
provider: providerId,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
reasoningTokens,
|
||||
cacheReadTokens,
|
||||
cacheWrite5mTokens,
|
||||
cacheWrite1hTokens,
|
||||
cost,
|
||||
})
|
||||
await tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
balance: sql`${BillingTable.balance} - ${cost}`,
|
||||
monthlyUsage: sql`
|
||||
CASE
|
||||
WHEN MONTH(${BillingTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${BillingTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${BillingTable.monthlyUsage} + ${cost}
|
||||
ELSE ${cost}
|
||||
END
|
||||
`,
|
||||
timeMonthlyUsageUpdated: sql`now()`,
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, authInfo.workspaceID))
|
||||
})
|
||||
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.update(KeyTable)
|
||||
.set({ timeUsed: sql`now()` })
|
||||
.where(eq(KeyTable.id, authInfo.apiKeyId)),
|
||||
)
|
||||
}
|
||||
|
||||
async function reload(authInfo: Awaited<ReturnType<typeof authenticate>>) {
|
||||
if (!authInfo) return
|
||||
|
||||
const lock = await Database.use((tx) =>
|
||||
tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
timeReloadLockedTill: sql`now() + interval 1 minute`,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(BillingTable.workspaceID, authInfo.workspaceID),
|
||||
eq(BillingTable.reload, true),
|
||||
lt(BillingTable.balance, centsToMicroCents(Billing.CHARGE_THRESHOLD)),
|
||||
or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)),
|
||||
),
|
||||
),
|
||||
)
|
||||
if (lock.rowsAffected === 0) return
|
||||
|
||||
await Actor.provide("system", { workspaceID: authInfo.workspaceID }, async () => {
|
||||
await Billing.reload()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ type Usage = {
|
||||
prompt_tokens?: number
|
||||
completion_tokens?: number
|
||||
total_tokens?: number
|
||||
// used by moonshot
|
||||
cached_tokens?: number
|
||||
// used by xai
|
||||
prompt_tokens_details?: {
|
||||
text_tokens?: number
|
||||
audio_tokens?: number
|
||||
@@ -48,7 +51,7 @@ export function POST(input: APIEvent) {
|
||||
inputTokens: usage.prompt_tokens ?? 0,
|
||||
outputTokens: usage.completion_tokens ?? 0,
|
||||
reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? undefined,
|
||||
cacheReadTokens: usage.prompt_tokens_details?.cached_tokens ?? undefined,
|
||||
cacheReadTokens: usage.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? undefined,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `workspace` ADD `data_share` boolean;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `workspace` DROP COLUMN `data_share`;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `payment` ADD `time_refunded` timestamp(3);
|
||||
@@ -0,0 +1,650 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "12189a4e-5083-4b17-b8e3-8279c9a3e61a",
|
||||
"prevId": "28336c91-553c-4d1d-9875-1ee761e47582",
|
||||
"tables": {
|
||||
"account": {
|
||||
"name": "account",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"email": {
|
||||
"name": "email",
|
||||
"columns": ["email"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"billing": {
|
||||
"name": "billing",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_method_id": {
|
||||
"name": "payment_method_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_method_last4": {
|
||||
"name": "payment_method_last4",
|
||||
"type": "varchar(4)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"balance": {
|
||||
"name": "balance",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"monthly_limit": {
|
||||
"name": "monthly_limit",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"monthly_usage": {
|
||||
"name": "monthly_usage",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_monthly_usage_updated": {
|
||||
"name": "time_monthly_usage_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reload": {
|
||||
"name": "reload",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reload_error": {
|
||||
"name": "reload_error",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_reload_error": {
|
||||
"name": "time_reload_error",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_reload_locked_till": {
|
||||
"name": "time_reload_locked_till",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"global_customer_id": {
|
||||
"name": "global_customer_id",
|
||||
"columns": ["customer_id"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"billing_workspace_id_id_pk": {
|
||||
"name": "billing_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"payment": {
|
||||
"name": "payment",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_id": {
|
||||
"name": "payment_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"amount": {
|
||||
"name": "amount",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"payment_workspace_id_id_pk": {
|
||||
"name": "payment_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"usage": {
|
||||
"name": "usage",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"input_tokens": {
|
||||
"name": "input_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"output_tokens": {
|
||||
"name": "output_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reasoning_tokens": {
|
||||
"name": "reasoning_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_read_tokens": {
|
||||
"name": "cache_read_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_write_5m_tokens": {
|
||||
"name": "cache_write_5m_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_write_1h_tokens": {
|
||||
"name": "cache_write_1h_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cost": {
|
||||
"name": "cost",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"usage_workspace_id_id_pk": {
|
||||
"name": "usage_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"actor": {
|
||||
"name": "actor",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"old_name": {
|
||||
"name": "old_name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_used": {
|
||||
"name": "time_used",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"global_key": {
|
||||
"name": "global_key",
|
||||
"columns": ["key"],
|
||||
"isUnique": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"columns": ["workspace_id", "name"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"key_workspace_id_id_pk": {
|
||||
"name": "key_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"user": {
|
||||
"name": "user",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_seen": {
|
||||
"name": "time_seen",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email": {
|
||||
"name": "user_email",
|
||||
"columns": ["workspace_id", "email"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"user_workspace_id_id_pk": {
|
||||
"name": "user_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"workspace": {
|
||||
"name": "workspace",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"data_share": {
|
||||
"name": "data_share",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"columns": ["slug"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"columns": ["id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,643 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "7f3989cb-3e8b-430e-a0f5-f87051d1d824",
|
||||
"prevId": "12189a4e-5083-4b17-b8e3-8279c9a3e61a",
|
||||
"tables": {
|
||||
"account": {
|
||||
"name": "account",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"email": {
|
||||
"name": "email",
|
||||
"columns": ["email"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"billing": {
|
||||
"name": "billing",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_method_id": {
|
||||
"name": "payment_method_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_method_last4": {
|
||||
"name": "payment_method_last4",
|
||||
"type": "varchar(4)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"balance": {
|
||||
"name": "balance",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"monthly_limit": {
|
||||
"name": "monthly_limit",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"monthly_usage": {
|
||||
"name": "monthly_usage",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_monthly_usage_updated": {
|
||||
"name": "time_monthly_usage_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reload": {
|
||||
"name": "reload",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reload_error": {
|
||||
"name": "reload_error",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_reload_error": {
|
||||
"name": "time_reload_error",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_reload_locked_till": {
|
||||
"name": "time_reload_locked_till",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"global_customer_id": {
|
||||
"name": "global_customer_id",
|
||||
"columns": ["customer_id"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"billing_workspace_id_id_pk": {
|
||||
"name": "billing_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"payment": {
|
||||
"name": "payment",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_id": {
|
||||
"name": "payment_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"amount": {
|
||||
"name": "amount",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"payment_workspace_id_id_pk": {
|
||||
"name": "payment_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"usage": {
|
||||
"name": "usage",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"input_tokens": {
|
||||
"name": "input_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"output_tokens": {
|
||||
"name": "output_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reasoning_tokens": {
|
||||
"name": "reasoning_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_read_tokens": {
|
||||
"name": "cache_read_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_write_5m_tokens": {
|
||||
"name": "cache_write_5m_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_write_1h_tokens": {
|
||||
"name": "cache_write_1h_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cost": {
|
||||
"name": "cost",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"usage_workspace_id_id_pk": {
|
||||
"name": "usage_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"actor": {
|
||||
"name": "actor",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"old_name": {
|
||||
"name": "old_name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_used": {
|
||||
"name": "time_used",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"global_key": {
|
||||
"name": "global_key",
|
||||
"columns": ["key"],
|
||||
"isUnique": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"columns": ["workspace_id", "name"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"key_workspace_id_id_pk": {
|
||||
"name": "key_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"user": {
|
||||
"name": "user",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_seen": {
|
||||
"name": "time_seen",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email": {
|
||||
"name": "user_email",
|
||||
"columns": ["workspace_id", "email"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"user_workspace_id_id_pk": {
|
||||
"name": "user_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"workspace": {
|
||||
"name": "workspace",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"columns": ["slug"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"columns": ["id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,650 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "45b67fb4-77ce-4aa2-b883-1971429c69f5",
|
||||
"prevId": "7f3989cb-3e8b-430e-a0f5-f87051d1d824",
|
||||
"tables": {
|
||||
"account": {
|
||||
"name": "account",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"email": {
|
||||
"name": "email",
|
||||
"columns": ["email"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"billing": {
|
||||
"name": "billing",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_method_id": {
|
||||
"name": "payment_method_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_method_last4": {
|
||||
"name": "payment_method_last4",
|
||||
"type": "varchar(4)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"balance": {
|
||||
"name": "balance",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"monthly_limit": {
|
||||
"name": "monthly_limit",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"monthly_usage": {
|
||||
"name": "monthly_usage",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_monthly_usage_updated": {
|
||||
"name": "time_monthly_usage_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reload": {
|
||||
"name": "reload",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reload_error": {
|
||||
"name": "reload_error",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_reload_error": {
|
||||
"name": "time_reload_error",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_reload_locked_till": {
|
||||
"name": "time_reload_locked_till",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"global_customer_id": {
|
||||
"name": "global_customer_id",
|
||||
"columns": ["customer_id"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"billing_workspace_id_id_pk": {
|
||||
"name": "billing_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"payment": {
|
||||
"name": "payment",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_id": {
|
||||
"name": "payment_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"amount": {
|
||||
"name": "amount",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_refunded": {
|
||||
"name": "time_refunded",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"payment_workspace_id_id_pk": {
|
||||
"name": "payment_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"usage": {
|
||||
"name": "usage",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"input_tokens": {
|
||||
"name": "input_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"output_tokens": {
|
||||
"name": "output_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reasoning_tokens": {
|
||||
"name": "reasoning_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_read_tokens": {
|
||||
"name": "cache_read_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_write_5m_tokens": {
|
||||
"name": "cache_write_5m_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_write_1h_tokens": {
|
||||
"name": "cache_write_1h_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cost": {
|
||||
"name": "cost",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"usage_workspace_id_id_pk": {
|
||||
"name": "usage_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"actor": {
|
||||
"name": "actor",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"old_name": {
|
||||
"name": "old_name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_used": {
|
||||
"name": "time_used",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"global_key": {
|
||||
"name": "global_key",
|
||||
"columns": ["key"],
|
||||
"isUnique": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"columns": ["workspace_id", "name"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"key_workspace_id_id_pk": {
|
||||
"name": "key_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"user": {
|
||||
"name": "user",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_seen": {
|
||||
"name": "time_seen",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email": {
|
||||
"name": "user_email",
|
||||
"columns": ["workspace_id", "email"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"user_workspace_id_id_pk": {
|
||||
"name": "user_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"workspace": {
|
||||
"name": "workspace",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"columns": ["slug"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"columns": ["id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,27 @@
|
||||
"when": 1757956978089,
|
||||
"tag": "0013_absurd_hobgoblin",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "5",
|
||||
"when": 1758289919722,
|
||||
"tag": "0014_demonic_princess_powerful",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 15,
|
||||
"version": "5",
|
||||
"when": 1758428484500,
|
||||
"tag": "0015_cloudy_revanche",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 16,
|
||||
"version": "5",
|
||||
"when": 1758663086739,
|
||||
"tag": "0016_cold_la_nuit",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/console-core",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -30,6 +30,7 @@ export const PaymentTable = mysqlTable(
|
||||
customerID: varchar("customer_id", { length: 255 }),
|
||||
paymentID: varchar("payment_id", { length: 255 }),
|
||||
amount: bigint("amount", { mode: "number" }).notNull(),
|
||||
timeRefunded: utc("time_refunded"),
|
||||
},
|
||||
(table) => [...workspaceIndexes(table)],
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { primaryKey, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||
import { primaryKey, mysqlTable, uniqueIndex, varchar, boolean } from "drizzle-orm/mysql-core"
|
||||
import { timestamps, ulid } from "../drizzle/types"
|
||||
|
||||
export const WorkspaceTable = mysqlTable(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-function",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -9,7 +9,12 @@ export default {
|
||||
if (event.event.request.method !== "POST") continue
|
||||
|
||||
const url = new URL(event.event.request.url)
|
||||
if (url.pathname !== "/zen/v1/chat/completions") return
|
||||
if (
|
||||
url.pathname !== "/zen/v1/chat/completions" &&
|
||||
url.pathname !== "/zen/v1/messages" &&
|
||||
url.pathname !== "/zen/v1/responses"
|
||||
)
|
||||
return
|
||||
|
||||
let metrics = {
|
||||
event_type: "completions",
|
||||
|
||||
+1
-17
@@ -6,18 +6,10 @@
|
||||
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
|
||||
}
|
||||
BASETEN_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
Console: {
|
||||
type: "sst.cloudflare.SolidStart"
|
||||
url: string
|
||||
@@ -30,10 +22,6 @@ declare module "sst" {
|
||||
type: "sst.sst.Linkable"
|
||||
username: string
|
||||
}
|
||||
FIREWORKS_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
GITHUB_APP_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
@@ -58,10 +46,6 @@ declare module "sst" {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
OPENAI_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
STRIPE_SECRET_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
@@ -74,7 +58,7 @@ declare module "sst" {
|
||||
type: "sst.cloudflare.Astro"
|
||||
url: string
|
||||
}
|
||||
XAI_API_KEY: {
|
||||
ZEN_MODELS: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
|
||||
+1
-17
@@ -6,18 +6,10 @@
|
||||
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
|
||||
}
|
||||
BASETEN_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
Console: {
|
||||
type: "sst.cloudflare.SolidStart"
|
||||
url: string
|
||||
@@ -30,10 +22,6 @@ declare module "sst" {
|
||||
type: "sst.sst.Linkable"
|
||||
username: string
|
||||
}
|
||||
FIREWORKS_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
GITHUB_APP_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
@@ -58,10 +46,6 @@ declare module "sst" {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
OPENAI_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
STRIPE_SECRET_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
@@ -74,7 +58,7 @@ declare module "sst" {
|
||||
type: "sst.cloudflare.Astro"
|
||||
url: string
|
||||
}
|
||||
XAI_API_KEY: {
|
||||
ZEN_MODELS: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-scripts",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/function",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
Vendored
+1
-17
@@ -6,18 +6,10 @@
|
||||
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
|
||||
}
|
||||
BASETEN_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
Console: {
|
||||
type: "sst.cloudflare.SolidStart"
|
||||
url: string
|
||||
@@ -30,10 +22,6 @@ declare module "sst" {
|
||||
type: "sst.sst.Linkable"
|
||||
username: string
|
||||
}
|
||||
FIREWORKS_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
GITHUB_APP_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
@@ -58,10 +46,6 @@ declare module "sst" {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
OPENAI_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
STRIPE_SECRET_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
@@ -74,7 +58,7 @@ declare module "sst" {
|
||||
type: "sst.cloudflare.Astro"
|
||||
url: string
|
||||
}
|
||||
XAI_API_KEY: {
|
||||
ZEN_MODELS: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "./script/build.ts",
|
||||
"dev": "bun run --conditions=development ./src/index.ts"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bun
|
||||
const dir = new URL("..", import.meta.url).pathname
|
||||
process.chdir(dir)
|
||||
import { $ } from "bun"
|
||||
|
||||
import pkg from "../package.json"
|
||||
|
||||
const GOARCH: Record<string, string> = {
|
||||
arm64: "arm64",
|
||||
x64: "amd64",
|
||||
"x64-baseline": "amd64",
|
||||
}
|
||||
|
||||
const targets = [
|
||||
["windows", "x64"],
|
||||
["linux", "arm64"],
|
||||
["linux", "x64"],
|
||||
["linux", "x64-baseline"],
|
||||
["darwin", "x64"],
|
||||
["darwin", "x64-baseline"],
|
||||
["darwin", "arm64"],
|
||||
]
|
||||
|
||||
await $`rm -rf dist`
|
||||
|
||||
const binaries: Record<string, string> = {}
|
||||
const version = process.env["OPENCODE_VERSION"] ?? "dev"
|
||||
for (const [os, arch] of targets) {
|
||||
console.log(`building ${os}-${arch}`)
|
||||
const name = `${pkg.name}-${os}-${arch}`
|
||||
await $`mkdir -p dist/${name}/bin`
|
||||
await $`CGO_ENABLED=0 GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X main.Version=${version}" -o ../opencode/dist/${name}/bin/tui ../tui/cmd/opencode/main.go`
|
||||
.cwd("../tui")
|
||||
.quiet()
|
||||
await Bun.build({
|
||||
compile: {
|
||||
target: `bun-${os}-${arch}` as any,
|
||||
outfile: `dist/${name}/bin/opencode`,
|
||||
execArgv: [`--user-agent=opencode/${version}`, `--env-file=""`, `--`],
|
||||
windows: {},
|
||||
},
|
||||
entrypoints: ["./src/index.ts"],
|
||||
define: {
|
||||
OPENCODE_VERSION: `'${version}'`,
|
||||
OPENCODE_TUI_PATH: `'../../../dist/${name}/bin/tui'`,
|
||||
},
|
||||
})
|
||||
await $`rm -rf ./dist/${name}/bin/tui`
|
||||
await Bun.file(`dist/${name}/package.json`).write(
|
||||
JSON.stringify(
|
||||
{
|
||||
name,
|
||||
version,
|
||||
os: [os === "windows" ? "win32" : os],
|
||||
cpu: [arch],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
binaries[name] = version
|
||||
}
|
||||
|
||||
export { binaries }
|
||||
@@ -5,76 +5,22 @@ import { $ } from "bun"
|
||||
|
||||
import pkg from "../package.json"
|
||||
|
||||
const dry = process.env["OPENCODE_DRY"] === "true"
|
||||
const version = process.env["OPENCODE_VERSION"]!
|
||||
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
|
||||
let version = process.env["OPENCODE_VERSION"]
|
||||
const tag = process.env["OPENCODE_TAG"] ?? (snapshot ? "snapshot" : "latest")
|
||||
if (!version && snapshot) {
|
||||
version = `0.0.0-${tag}-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
|
||||
process.env["OPENCODE_VERSION"] = version
|
||||
}
|
||||
if (!version) throw new Error("OPENCODE_VERSION is required")
|
||||
|
||||
console.log(`publishing ${version}`)
|
||||
|
||||
const GOARCH: Record<string, string> = {
|
||||
arm64: "arm64",
|
||||
x64: "amd64",
|
||||
"x64-baseline": "amd64",
|
||||
}
|
||||
|
||||
const targets = [
|
||||
["windows", "x64"],
|
||||
["linux", "arm64"],
|
||||
["linux", "x64"],
|
||||
["linux", "x64-baseline"],
|
||||
["darwin", "x64"],
|
||||
["darwin", "x64-baseline"],
|
||||
["darwin", "arm64"],
|
||||
]
|
||||
|
||||
await $`rm -rf dist`
|
||||
|
||||
const optionalDependencies: Record<string, string> = {}
|
||||
const npmTag = snapshot ? "snapshot" : "latest"
|
||||
for (const [os, arch] of targets) {
|
||||
console.log(`building ${os}-${arch}`)
|
||||
const name = `${pkg.name}-${os}-${arch}`
|
||||
await $`mkdir -p dist/${name}/bin`
|
||||
await $`CGO_ENABLED=0 GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X main.Version=${version}" -o ../opencode/dist/${name}/bin/tui ../tui/cmd/opencode/main.go`.cwd(
|
||||
"../tui",
|
||||
)
|
||||
await Bun.build({
|
||||
compile: {
|
||||
target: `bun-${os}-${arch}` as any,
|
||||
outfile: `dist/${name}/bin/opencode`,
|
||||
execArgv: [`--user-agent=opencode/${version}`, `--env-file=""`, `--`],
|
||||
windows: {},
|
||||
},
|
||||
entrypoints: ["./src/index.ts"],
|
||||
define: {
|
||||
OPENCODE_VERSION: `'${version}'`,
|
||||
OPENCODE_TUI_PATH: `'../../../dist/${name}/bin/tui'`,
|
||||
},
|
||||
})
|
||||
// await $`bun build --define OPENCODE_TUI_PATH="'../../../dist/${name}/bin/tui'" --define OPENCODE_VERSION="'${version}'" --compile --target=bun-${os}-${arch} --outfile=dist/${name}/bin/opencode ./src/index.ts`
|
||||
// Run the binary only if it matches current OS/arch
|
||||
if (
|
||||
process.platform === (os === "windows" ? "win32" : os) &&
|
||||
(process.arch === arch || (process.arch === "x64" && arch === "x64-baseline"))
|
||||
) {
|
||||
console.log(`smoke test: running dist/${name}/bin/opencode --version`)
|
||||
await $`./dist/${name}/bin/opencode --version`
|
||||
}
|
||||
await $`rm -rf ./dist/${name}/bin/tui`
|
||||
await Bun.file(`dist/${name}/package.json`).write(
|
||||
JSON.stringify(
|
||||
{
|
||||
name,
|
||||
version,
|
||||
os: [os === "windows" ? "win32" : os],
|
||||
cpu: [arch],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
if (!dry) await $`cd dist/${name} && chmod 777 -R . && bun publish --access public --tag ${npmTag}`
|
||||
optionalDependencies[name] = version
|
||||
const { binaries } = await import("./build.ts")
|
||||
{
|
||||
const name = `${pkg.name}-${process.platform}-${process.arch}`
|
||||
console.log(`smoke test: running dist/${name}/bin/opencode --version`)
|
||||
await $`./dist/${name}/bin/opencode --version`
|
||||
}
|
||||
|
||||
await $`mkdir -p ./dist/${pkg.name}`
|
||||
@@ -93,16 +39,19 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
||||
postinstall: "node ./postinstall.mjs",
|
||||
},
|
||||
version,
|
||||
optionalDependencies,
|
||||
optionalDependencies: binaries,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
if (!dry) await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${npmTag}`
|
||||
for (const [name] of Object.entries(binaries)) {
|
||||
await $`cd dist/${name} && chmod 777 -R . && bun publish --access public --tag ${tag}`
|
||||
}
|
||||
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${tag}`
|
||||
|
||||
if (!snapshot) {
|
||||
for (const key of Object.keys(optionalDependencies)) {
|
||||
for (const key of Object.keys(binaries)) {
|
||||
await $`cd dist/${key}/bin && zip -r ../../${key}.zip *`
|
||||
}
|
||||
|
||||
@@ -195,7 +144,7 @@ if (!snapshot) {
|
||||
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
|
||||
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
|
||||
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
|
||||
if (!dry) await $`cd ./dist/aur-${pkg} && git push`
|
||||
await $`cd ./dist/aur-${pkg} && git push`
|
||||
}
|
||||
|
||||
// Homebrew formula
|
||||
@@ -254,5 +203,5 @@ if (!snapshot) {
|
||||
await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula)
|
||||
await $`cd ./dist/homebrew-tap && git add opencode.rb`
|
||||
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${version}"`
|
||||
if (!dry) await $`cd ./dist/homebrew-tap && git push`
|
||||
await $`cd ./dist/homebrew-tap && git push`
|
||||
}
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { Format } from "../format"
|
||||
import { LSP } from "../lsp"
|
||||
import { Plugin } from "../plugin"
|
||||
import { InstanceBootstrap } from "../project/bootstrap"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Share } from "../share/share"
|
||||
import { Snapshot } from "../snapshot"
|
||||
|
||||
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
|
||||
return Instance.provide(directory, async () => {
|
||||
await Plugin.init()
|
||||
Share.init()
|
||||
Format.init()
|
||||
LSP.init()
|
||||
Snapshot.init()
|
||||
const result = await cb()
|
||||
await Instance.dispose()
|
||||
return result
|
||||
return Instance.provide({
|
||||
directory,
|
||||
init: InstanceBootstrap,
|
||||
fn: async () => {
|
||||
const result = await cb()
|
||||
await Instance.dispose()
|
||||
return result
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,121 +11,124 @@ const AgentCreateCommand = cmd({
|
||||
command: "create",
|
||||
describe: "create a new agent",
|
||||
async handler() {
|
||||
await Instance.provide(process.cwd(), async () => {
|
||||
UI.empty()
|
||||
prompts.intro("Create agent")
|
||||
const project = Instance.project
|
||||
await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
UI.empty()
|
||||
prompts.intro("Create agent")
|
||||
const project = Instance.project
|
||||
|
||||
let scope: "global" | "project" = "global"
|
||||
if (project.vcs === "git") {
|
||||
const scopeResult = await prompts.select({
|
||||
message: "Location",
|
||||
let scope: "global" | "project" = "global"
|
||||
if (project.vcs === "git") {
|
||||
const scopeResult = await prompts.select({
|
||||
message: "Location",
|
||||
options: [
|
||||
{
|
||||
label: "Current project",
|
||||
value: "project" as const,
|
||||
hint: Instance.worktree,
|
||||
},
|
||||
{
|
||||
label: "Global",
|
||||
value: "global" as const,
|
||||
hint: Global.Path.config,
|
||||
},
|
||||
],
|
||||
})
|
||||
if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
|
||||
scope = scopeResult
|
||||
}
|
||||
|
||||
const query = await prompts.text({
|
||||
message: "Description",
|
||||
placeholder: "What should this agent do?",
|
||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(query)) throw new UI.CancelledError()
|
||||
|
||||
const spinner = prompts.spinner()
|
||||
|
||||
spinner.start("Generating agent configuration...")
|
||||
const generated = await Agent.generate({ description: query }).catch((error) => {
|
||||
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
|
||||
throw new UI.CancelledError()
|
||||
})
|
||||
spinner.stop(`Agent ${generated.identifier} generated`)
|
||||
|
||||
const availableTools = [
|
||||
"bash",
|
||||
"read",
|
||||
"write",
|
||||
"edit",
|
||||
"list",
|
||||
"glob",
|
||||
"grep",
|
||||
"webfetch",
|
||||
"task",
|
||||
"todowrite",
|
||||
"todoread",
|
||||
]
|
||||
|
||||
const selectedTools = await prompts.multiselect({
|
||||
message: "Select tools to enable",
|
||||
options: availableTools.map((tool) => ({
|
||||
label: tool,
|
||||
value: tool,
|
||||
})),
|
||||
initialValues: availableTools,
|
||||
})
|
||||
if (prompts.isCancel(selectedTools)) throw new UI.CancelledError()
|
||||
|
||||
const modeResult = await prompts.select({
|
||||
message: "Agent mode",
|
||||
options: [
|
||||
{
|
||||
label: "Current project",
|
||||
value: "project" as const,
|
||||
hint: Instance.worktree,
|
||||
label: "All",
|
||||
value: "all" as const,
|
||||
hint: "Can function in both primary and subagent roles",
|
||||
},
|
||||
{
|
||||
label: "Global",
|
||||
value: "global" as const,
|
||||
hint: Global.Path.config,
|
||||
label: "Primary",
|
||||
value: "primary" as const,
|
||||
hint: "Acts as a primary/main agent",
|
||||
},
|
||||
{
|
||||
label: "Subagent",
|
||||
value: "subagent" as const,
|
||||
hint: "Can be used as a subagent by other agents",
|
||||
},
|
||||
],
|
||||
initialValue: "all",
|
||||
})
|
||||
if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
|
||||
scope = scopeResult
|
||||
}
|
||||
if (prompts.isCancel(modeResult)) throw new UI.CancelledError()
|
||||
|
||||
const query = await prompts.text({
|
||||
message: "Description",
|
||||
placeholder: "What should this agent do?",
|
||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(query)) throw new UI.CancelledError()
|
||||
|
||||
const spinner = prompts.spinner()
|
||||
|
||||
spinner.start("Generating agent configuration...")
|
||||
const generated = await Agent.generate({ description: query }).catch((error) => {
|
||||
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
|
||||
throw new UI.CancelledError()
|
||||
})
|
||||
spinner.stop(`Agent ${generated.identifier} generated`)
|
||||
|
||||
const availableTools = [
|
||||
"bash",
|
||||
"read",
|
||||
"write",
|
||||
"edit",
|
||||
"list",
|
||||
"glob",
|
||||
"grep",
|
||||
"webfetch",
|
||||
"task",
|
||||
"todowrite",
|
||||
"todoread",
|
||||
]
|
||||
|
||||
const selectedTools = await prompts.multiselect({
|
||||
message: "Select tools to enable",
|
||||
options: availableTools.map((tool) => ({
|
||||
label: tool,
|
||||
value: tool,
|
||||
})),
|
||||
initialValues: availableTools,
|
||||
})
|
||||
if (prompts.isCancel(selectedTools)) throw new UI.CancelledError()
|
||||
|
||||
const modeResult = await prompts.select({
|
||||
message: "Agent mode",
|
||||
options: [
|
||||
{
|
||||
label: "All",
|
||||
value: "all" as const,
|
||||
hint: "Can function in both primary and subagent roles",
|
||||
},
|
||||
{
|
||||
label: "Primary",
|
||||
value: "primary" as const,
|
||||
hint: "Acts as a primary/main agent",
|
||||
},
|
||||
{
|
||||
label: "Subagent",
|
||||
value: "subagent" as const,
|
||||
hint: "Can be used as a subagent by other agents",
|
||||
},
|
||||
],
|
||||
initialValue: "all",
|
||||
})
|
||||
if (prompts.isCancel(modeResult)) throw new UI.CancelledError()
|
||||
|
||||
const tools: Record<string, boolean> = {}
|
||||
for (const tool of availableTools) {
|
||||
if (!selectedTools.includes(tool)) {
|
||||
tools[tool] = false
|
||||
const tools: Record<string, boolean> = {}
|
||||
for (const tool of availableTools) {
|
||||
if (!selectedTools.includes(tool)) {
|
||||
tools[tool] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const frontmatter: any = {
|
||||
description: generated.whenToUse,
|
||||
mode: modeResult,
|
||||
}
|
||||
if (Object.keys(tools).length > 0) {
|
||||
frontmatter.tools = tools
|
||||
}
|
||||
const frontmatter: any = {
|
||||
description: generated.whenToUse,
|
||||
mode: modeResult,
|
||||
}
|
||||
if (Object.keys(tools).length > 0) {
|
||||
frontmatter.tools = tools
|
||||
}
|
||||
|
||||
const content = matter.stringify(generated.systemPrompt, frontmatter)
|
||||
const filePath = path.join(
|
||||
scope === "global" ? Global.Path.config : path.join(Instance.worktree, ".opencode"),
|
||||
`agent`,
|
||||
`${generated.identifier}.md`,
|
||||
)
|
||||
const content = matter.stringify(generated.systemPrompt, frontmatter)
|
||||
const filePath = path.join(
|
||||
scope === "global" ? Global.Path.config : path.join(Instance.worktree, ".opencode"),
|
||||
`agent`,
|
||||
`${generated.identifier}.md`,
|
||||
)
|
||||
|
||||
await Bun.write(filePath, content)
|
||||
await Bun.write(filePath, content)
|
||||
|
||||
prompts.log.success(`Agent created: ${filePath}`)
|
||||
prompts.outro("Done")
|
||||
prompts.log.success(`Agent created: ${filePath}`)
|
||||
prompts.outro("Done")
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -74,196 +74,199 @@ export const AuthLoginCommand = cmd({
|
||||
type: "string",
|
||||
}),
|
||||
async handler(args) {
|
||||
await Instance.provide(process.cwd(), async () => {
|
||||
UI.empty()
|
||||
prompts.intro("Add credential")
|
||||
if (args.url) {
|
||||
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json())
|
||||
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
|
||||
const proc = Bun.spawn({
|
||||
cmd: wellknown.auth.command,
|
||||
stdout: "pipe",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0) {
|
||||
prompts.log.error("Failed")
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
const token = await new Response(proc.stdout).text()
|
||||
await Auth.set(args.url, {
|
||||
type: "wellknown",
|
||||
key: wellknown.auth.env,
|
||||
token: token.trim(),
|
||||
})
|
||||
prompts.log.success("Logged into " + args.url)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
await ModelsDev.refresh().catch(() => {})
|
||||
const providers = await ModelsDev.get()
|
||||
const priority: Record<string, number> = {
|
||||
opencode: 0,
|
||||
anthropic: 1,
|
||||
"github-copilot": 2,
|
||||
openai: 3,
|
||||
google: 4,
|
||||
openrouter: 5,
|
||||
vercel: 6,
|
||||
}
|
||||
let provider = await prompts.autocomplete({
|
||||
message: "Select provider",
|
||||
maxItems: 8,
|
||||
options: [
|
||||
...pipe(
|
||||
providers,
|
||||
values(),
|
||||
sortBy(
|
||||
(x) => priority[x.id] ?? 99,
|
||||
(x) => x.name ?? x.id,
|
||||
),
|
||||
map((x) => ({
|
||||
label: x.name,
|
||||
value: x.id,
|
||||
hint: priority[x.id] <= 1 ? "recommended" : undefined,
|
||||
})),
|
||||
),
|
||||
{
|
||||
value: "other",
|
||||
label: "Other",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
|
||||
const plugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
|
||||
if (plugin && plugin.auth) {
|
||||
let index = 0
|
||||
if (plugin.auth.methods.length > 1) {
|
||||
const method = await prompts.select({
|
||||
message: "Login method",
|
||||
options: [
|
||||
...plugin.auth.methods.map((x, index) => ({
|
||||
label: x.label,
|
||||
value: index.toString(),
|
||||
})),
|
||||
],
|
||||
await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
UI.empty()
|
||||
prompts.intro("Add credential")
|
||||
if (args.url) {
|
||||
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json())
|
||||
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
|
||||
const proc = Bun.spawn({
|
||||
cmd: wellknown.auth.command,
|
||||
stdout: "pipe",
|
||||
})
|
||||
if (prompts.isCancel(method)) throw new UI.CancelledError()
|
||||
index = parseInt(method)
|
||||
}
|
||||
const method = plugin.auth.methods[index]
|
||||
if (method.type === "oauth") {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
const authorize = await method.authorize()
|
||||
|
||||
if (authorize.url) {
|
||||
prompts.log.info("Go to: " + authorize.url)
|
||||
}
|
||||
|
||||
if (authorize.method === "auto") {
|
||||
if (authorize.instructions) {
|
||||
prompts.log.info(authorize.instructions)
|
||||
}
|
||||
const spinner = prompts.spinner()
|
||||
spinner.start("Waiting for authorization...")
|
||||
const result = await authorize.callback()
|
||||
if (result.type === "failed") {
|
||||
spinner.stop("Failed to authorize", 1)
|
||||
}
|
||||
if (result.type === "success") {
|
||||
if ("refresh" in result) {
|
||||
await Auth.set(provider, {
|
||||
type: "oauth",
|
||||
refresh: result.refresh,
|
||||
access: result.access,
|
||||
expires: result.expires,
|
||||
})
|
||||
}
|
||||
if ("key" in result) {
|
||||
await Auth.set(provider, {
|
||||
type: "api",
|
||||
key: result.key,
|
||||
})
|
||||
}
|
||||
spinner.stop("Login successful")
|
||||
}
|
||||
}
|
||||
|
||||
if (authorize.method === "code") {
|
||||
const code = await prompts.text({
|
||||
message: "Paste the authorization code here: ",
|
||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(code)) throw new UI.CancelledError()
|
||||
const result = await authorize.callback(code)
|
||||
if (result.type === "failed") {
|
||||
prompts.log.error("Failed to authorize")
|
||||
}
|
||||
if (result.type === "success") {
|
||||
if ("refresh" in result) {
|
||||
await Auth.set(provider, {
|
||||
type: "oauth",
|
||||
refresh: result.refresh,
|
||||
access: result.access,
|
||||
expires: result.expires,
|
||||
})
|
||||
}
|
||||
if ("key" in result) {
|
||||
await Auth.set(provider, {
|
||||
type: "api",
|
||||
key: result.key,
|
||||
})
|
||||
}
|
||||
prompts.log.success("Login successful")
|
||||
}
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0) {
|
||||
prompts.log.error("Failed")
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
const token = await new Response(proc.stdout).text()
|
||||
await Auth.set(args.url, {
|
||||
type: "wellknown",
|
||||
key: wellknown.auth.env,
|
||||
token: token.trim(),
|
||||
})
|
||||
prompts.log.success("Logged into " + args.url)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === "other") {
|
||||
provider = await prompts.text({
|
||||
message: "Enter provider id",
|
||||
validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
|
||||
await ModelsDev.refresh().catch(() => {})
|
||||
const providers = await ModelsDev.get()
|
||||
const priority: Record<string, number> = {
|
||||
opencode: 0,
|
||||
anthropic: 1,
|
||||
"github-copilot": 2,
|
||||
openai: 3,
|
||||
google: 4,
|
||||
openrouter: 5,
|
||||
vercel: 6,
|
||||
}
|
||||
let provider = await prompts.autocomplete({
|
||||
message: "Select provider",
|
||||
maxItems: 8,
|
||||
options: [
|
||||
...pipe(
|
||||
providers,
|
||||
values(),
|
||||
sortBy(
|
||||
(x) => priority[x.id] ?? 99,
|
||||
(x) => x.name ?? x.id,
|
||||
),
|
||||
map((x) => ({
|
||||
label: x.name,
|
||||
value: x.id,
|
||||
hint: priority[x.id] <= 1 ? "recommended" : undefined,
|
||||
})),
|
||||
),
|
||||
{
|
||||
value: "other",
|
||||
label: "Other",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
|
||||
const plugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
|
||||
if (plugin && plugin.auth) {
|
||||
let index = 0
|
||||
if (plugin.auth.methods.length > 1) {
|
||||
const method = await prompts.select({
|
||||
message: "Login method",
|
||||
options: [
|
||||
...plugin.auth.methods.map((x, index) => ({
|
||||
label: x.label,
|
||||
value: index.toString(),
|
||||
})),
|
||||
],
|
||||
})
|
||||
if (prompts.isCancel(method)) throw new UI.CancelledError()
|
||||
index = parseInt(method)
|
||||
}
|
||||
const method = plugin.auth.methods[index]
|
||||
if (method.type === "oauth") {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
const authorize = await method.authorize()
|
||||
|
||||
if (authorize.url) {
|
||||
prompts.log.info("Go to: " + authorize.url)
|
||||
}
|
||||
|
||||
if (authorize.method === "auto") {
|
||||
if (authorize.instructions) {
|
||||
prompts.log.info(authorize.instructions)
|
||||
}
|
||||
const spinner = prompts.spinner()
|
||||
spinner.start("Waiting for authorization...")
|
||||
const result = await authorize.callback()
|
||||
if (result.type === "failed") {
|
||||
spinner.stop("Failed to authorize", 1)
|
||||
}
|
||||
if (result.type === "success") {
|
||||
if ("refresh" in result) {
|
||||
await Auth.set(provider, {
|
||||
type: "oauth",
|
||||
refresh: result.refresh,
|
||||
access: result.access,
|
||||
expires: result.expires,
|
||||
})
|
||||
}
|
||||
if ("key" in result) {
|
||||
await Auth.set(provider, {
|
||||
type: "api",
|
||||
key: result.key,
|
||||
})
|
||||
}
|
||||
spinner.stop("Login successful")
|
||||
}
|
||||
}
|
||||
|
||||
if (authorize.method === "code") {
|
||||
const code = await prompts.text({
|
||||
message: "Paste the authorization code here: ",
|
||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(code)) throw new UI.CancelledError()
|
||||
const result = await authorize.callback(code)
|
||||
if (result.type === "failed") {
|
||||
prompts.log.error("Failed to authorize")
|
||||
}
|
||||
if (result.type === "success") {
|
||||
if ("refresh" in result) {
|
||||
await Auth.set(provider, {
|
||||
type: "oauth",
|
||||
refresh: result.refresh,
|
||||
access: result.access,
|
||||
expires: result.expires,
|
||||
})
|
||||
}
|
||||
if ("key" in result) {
|
||||
await Auth.set(provider, {
|
||||
type: "api",
|
||||
key: result.key,
|
||||
})
|
||||
}
|
||||
prompts.log.success("Login successful")
|
||||
}
|
||||
}
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === "other") {
|
||||
provider = await prompts.text({
|
||||
message: "Enter provider id",
|
||||
validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
|
||||
})
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
provider = provider.replace(/^@ai-sdk\//, "")
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
prompts.log.warn(
|
||||
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (provider === "amazon-bedrock") {
|
||||
prompts.log.info(
|
||||
"Amazon bedrock can be configured with standard AWS environment variables like AWS_BEARER_TOKEN_BEDROCK, AWS_PROFILE or AWS_ACCESS_KEY_ID",
|
||||
)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
||||
if (provider === "opencode") {
|
||||
prompts.log.info("Create an api key at https://opencode.ai/auth")
|
||||
}
|
||||
|
||||
if (provider === "vercel") {
|
||||
prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token")
|
||||
}
|
||||
|
||||
const key = await prompts.password({
|
||||
message: "Enter your API key",
|
||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
||||
await Auth.set(provider, {
|
||||
type: "api",
|
||||
key,
|
||||
})
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
provider = provider.replace(/^@ai-sdk\//, "")
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
prompts.log.warn(
|
||||
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (provider === "amazon-bedrock") {
|
||||
prompts.log.info(
|
||||
"Amazon bedrock can be configured with standard AWS environment variables like AWS_BEARER_TOKEN_BEDROCK, AWS_PROFILE or AWS_ACCESS_KEY_ID",
|
||||
)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
||||
if (provider === "opencode") {
|
||||
prompts.log.info("Create an api key at https://opencode.ai/auth")
|
||||
}
|
||||
|
||||
if (provider === "vercel") {
|
||||
prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token")
|
||||
}
|
||||
|
||||
const key = await prompts.password({
|
||||
message: "Enter your API key",
|
||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
||||
await Auth.set(provider, {
|
||||
type: "api",
|
||||
key,
|
||||
})
|
||||
|
||||
prompts.outro("Done")
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -21,190 +21,194 @@ export const GithubInstallCommand = cmd({
|
||||
command: "install",
|
||||
describe: "install the GitHub agent",
|
||||
async handler() {
|
||||
await Instance.provide(process.cwd(), async () => {
|
||||
UI.empty()
|
||||
prompts.intro("Install GitHub agent")
|
||||
const app = await getAppInfo()
|
||||
await installGitHubApp()
|
||||
await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
UI.empty()
|
||||
prompts.intro("Install GitHub agent")
|
||||
const app = await getAppInfo()
|
||||
await installGitHubApp()
|
||||
|
||||
const providers = await ModelsDev.get()
|
||||
const provider = await promptProvider()
|
||||
const model = await promptModel()
|
||||
//const key = await promptKey()
|
||||
const providers = await ModelsDev.get()
|
||||
const provider = await promptProvider()
|
||||
const model = await promptModel()
|
||||
//const key = await promptKey()
|
||||
|
||||
await addWorkflowFiles()
|
||||
printNextSteps()
|
||||
await addWorkflowFiles()
|
||||
printNextSteps()
|
||||
|
||||
function printNextSteps() {
|
||||
let step2
|
||||
if (provider === "amazon-bedrock") {
|
||||
step2 =
|
||||
"Configure OIDC in AWS - https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services"
|
||||
} else {
|
||||
step2 = [
|
||||
` 2. Add the following secrets in org or repo (${app.owner}/${app.repo}) settings`,
|
||||
"",
|
||||
...providers[provider].env.map((e) => ` - ${e}`),
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
prompts.outro(
|
||||
[
|
||||
"Next steps:",
|
||||
"",
|
||||
` 1. Commit the \`${WORKFLOW_FILE}\` file and push`,
|
||||
step2,
|
||||
"",
|
||||
" 3. Go to a GitHub issue and comment `/oc summarize` to see the agent in action",
|
||||
"",
|
||||
" Learn more about the GitHub agent - https://opencode.ai/docs/github/#usage-examples",
|
||||
].join("\n"),
|
||||
)
|
||||
}
|
||||
|
||||
async function getAppInfo() {
|
||||
const project = Instance.project
|
||||
if (project.vcs !== "git") {
|
||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||
throw new UI.CancelledError()
|
||||
}
|
||||
|
||||
// Get repo info
|
||||
const info = await $`git remote get-url origin`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
.then((text) => text.trim())
|
||||
// match https or git pattern
|
||||
// ie. https://github.com/sst/opencode.git
|
||||
// ie. https://github.com/sst/opencode
|
||||
// ie. git@github.com:sst/opencode.git
|
||||
// ie. git@github.com:sst/opencode
|
||||
// ie. ssh://git@github.com/sst/opencode.git
|
||||
// ie. ssh://git@github.com/sst/opencode
|
||||
const parsed = info.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
|
||||
if (!parsed) {
|
||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||
throw new UI.CancelledError()
|
||||
}
|
||||
const [, owner, repo] = parsed
|
||||
return { owner, repo, root: Instance.worktree }
|
||||
}
|
||||
|
||||
async function promptProvider() {
|
||||
const priority: Record<string, number> = {
|
||||
opencode: 0,
|
||||
anthropic: 1,
|
||||
"github-copilot": 2,
|
||||
openai: 3,
|
||||
google: 4,
|
||||
openrouter: 5,
|
||||
vercel: 6,
|
||||
}
|
||||
let provider = await prompts.select({
|
||||
message: "Select provider",
|
||||
maxItems: 8,
|
||||
options: pipe(
|
||||
providers,
|
||||
values(),
|
||||
sortBy(
|
||||
(x) => priority[x.id] ?? 99,
|
||||
(x) => x.name ?? x.id,
|
||||
),
|
||||
map((x) => ({
|
||||
label: x.name,
|
||||
value: x.id,
|
||||
hint: priority[x.id] <= 1 ? "recommended" : undefined,
|
||||
})),
|
||||
),
|
||||
})
|
||||
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
async function promptModel() {
|
||||
const providerData = providers[provider]!
|
||||
|
||||
const model = await prompts.select({
|
||||
message: "Select model",
|
||||
maxItems: 8,
|
||||
options: pipe(
|
||||
providerData.models,
|
||||
values(),
|
||||
sortBy((x) => x.name ?? x.id),
|
||||
map((x) => ({
|
||||
label: x.name ?? x.id,
|
||||
value: x.id,
|
||||
})),
|
||||
),
|
||||
})
|
||||
|
||||
if (prompts.isCancel(model)) throw new UI.CancelledError()
|
||||
return model
|
||||
}
|
||||
|
||||
async function installGitHubApp() {
|
||||
const s = prompts.spinner()
|
||||
s.start("Installing GitHub app")
|
||||
|
||||
// Get installation
|
||||
const installation = await getInstallation()
|
||||
if (installation) return s.stop("GitHub app already installed")
|
||||
|
||||
// Open browser
|
||||
const url = "https://github.com/apps/opencode-agent"
|
||||
const command =
|
||||
process.platform === "darwin"
|
||||
? `open "${url}"`
|
||||
: process.platform === "win32"
|
||||
? `start "${url}"`
|
||||
: `xdg-open "${url}"`
|
||||
|
||||
exec(command, (error) => {
|
||||
if (error) {
|
||||
prompts.log.warn(`Could not open browser. Please visit: ${url}`)
|
||||
function printNextSteps() {
|
||||
let step2
|
||||
if (provider === "amazon-bedrock") {
|
||||
step2 =
|
||||
"Configure OIDC in AWS - https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services"
|
||||
} else {
|
||||
step2 = [
|
||||
` 2. Add the following secrets in org or repo (${app.owner}/${app.repo}) settings`,
|
||||
"",
|
||||
...providers[provider].env.map((e) => ` - ${e}`),
|
||||
].join("\n")
|
||||
}
|
||||
})
|
||||
|
||||
// Wait for installation
|
||||
s.message("Waiting for GitHub app to be installed")
|
||||
const MAX_RETRIES = 120
|
||||
let retries = 0
|
||||
do {
|
||||
const installation = await getInstallation()
|
||||
if (installation) break
|
||||
prompts.outro(
|
||||
[
|
||||
"Next steps:",
|
||||
"",
|
||||
` 1. Commit the \`${WORKFLOW_FILE}\` file and push`,
|
||||
step2,
|
||||
"",
|
||||
" 3. Go to a GitHub issue and comment `/oc summarize` to see the agent in action",
|
||||
"",
|
||||
" Learn more about the GitHub agent - https://opencode.ai/docs/github/#usage-examples",
|
||||
].join("\n"),
|
||||
)
|
||||
}
|
||||
|
||||
if (retries > MAX_RETRIES) {
|
||||
s.stop(
|
||||
`Failed to detect GitHub app installation. Make sure to install the app for the \`${app.owner}/${app.repo}\` repository.`,
|
||||
)
|
||||
async function getAppInfo() {
|
||||
const project = Instance.project
|
||||
if (project.vcs !== "git") {
|
||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||
throw new UI.CancelledError()
|
||||
}
|
||||
|
||||
retries++
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
} while (true)
|
||||
|
||||
s.stop("Installed GitHub app")
|
||||
|
||||
async function getInstallation() {
|
||||
return await fetch(`https://api.opencode.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => data.installation)
|
||||
// Get repo info
|
||||
const info = await $`git remote get-url origin`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
.then((text) => text.trim())
|
||||
// match https or git pattern
|
||||
// ie. https://github.com/sst/opencode.git
|
||||
// ie. https://github.com/sst/opencode
|
||||
// ie. git@github.com:sst/opencode.git
|
||||
// ie. git@github.com:sst/opencode
|
||||
// ie. ssh://git@github.com/sst/opencode.git
|
||||
// ie. ssh://git@github.com/sst/opencode
|
||||
const parsed = info.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
|
||||
if (!parsed) {
|
||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||
throw new UI.CancelledError()
|
||||
}
|
||||
const [, owner, repo] = parsed
|
||||
return { owner, repo, root: Instance.worktree }
|
||||
}
|
||||
}
|
||||
|
||||
async function addWorkflowFiles() {
|
||||
const envStr =
|
||||
provider === "amazon-bedrock"
|
||||
? ""
|
||||
: `\n env:${providers[provider].env.map((e) => `\n ${e}: \${{ secrets.${e} }}`).join("")}`
|
||||
async function promptProvider() {
|
||||
const priority: Record<string, number> = {
|
||||
opencode: 0,
|
||||
anthropic: 1,
|
||||
"github-copilot": 2,
|
||||
openai: 3,
|
||||
google: 4,
|
||||
openrouter: 5,
|
||||
vercel: 6,
|
||||
}
|
||||
let provider = await prompts.select({
|
||||
message: "Select provider",
|
||||
maxItems: 8,
|
||||
options: pipe(
|
||||
providers,
|
||||
values(),
|
||||
sortBy(
|
||||
(x) => priority[x.id] ?? 99,
|
||||
(x) => x.name ?? x.id,
|
||||
),
|
||||
map((x) => ({
|
||||
label: x.name,
|
||||
value: x.id,
|
||||
hint: priority[x.id] <= 1 ? "recommended" : undefined,
|
||||
})),
|
||||
),
|
||||
})
|
||||
|
||||
await Bun.write(
|
||||
path.join(app.root, WORKFLOW_FILE),
|
||||
`
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
async function promptModel() {
|
||||
const providerData = providers[provider]!
|
||||
|
||||
const model = await prompts.select({
|
||||
message: "Select model",
|
||||
maxItems: 8,
|
||||
options: pipe(
|
||||
providerData.models,
|
||||
values(),
|
||||
sortBy((x) => x.name ?? x.id),
|
||||
map((x) => ({
|
||||
label: x.name ?? x.id,
|
||||
value: x.id,
|
||||
})),
|
||||
),
|
||||
})
|
||||
|
||||
if (prompts.isCancel(model)) throw new UI.CancelledError()
|
||||
return model
|
||||
}
|
||||
|
||||
async function installGitHubApp() {
|
||||
const s = prompts.spinner()
|
||||
s.start("Installing GitHub app")
|
||||
|
||||
// Get installation
|
||||
const installation = await getInstallation()
|
||||
if (installation) return s.stop("GitHub app already installed")
|
||||
|
||||
// Open browser
|
||||
const url = "https://github.com/apps/opencode-agent"
|
||||
const command =
|
||||
process.platform === "darwin"
|
||||
? `open "${url}"`
|
||||
: process.platform === "win32"
|
||||
? `start "${url}"`
|
||||
: `xdg-open "${url}"`
|
||||
|
||||
exec(command, (error) => {
|
||||
if (error) {
|
||||
prompts.log.warn(`Could not open browser. Please visit: ${url}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Wait for installation
|
||||
s.message("Waiting for GitHub app to be installed")
|
||||
const MAX_RETRIES = 120
|
||||
let retries = 0
|
||||
do {
|
||||
const installation = await getInstallation()
|
||||
if (installation) break
|
||||
|
||||
if (retries > MAX_RETRIES) {
|
||||
s.stop(
|
||||
`Failed to detect GitHub app installation. Make sure to install the app for the \`${app.owner}/${app.repo}\` repository.`,
|
||||
)
|
||||
throw new UI.CancelledError()
|
||||
}
|
||||
|
||||
retries++
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
} while (true)
|
||||
|
||||
s.stop("Installed GitHub app")
|
||||
|
||||
async function getInstallation() {
|
||||
return await fetch(
|
||||
`https://api.opencode.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`,
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => data.installation)
|
||||
}
|
||||
}
|
||||
|
||||
async function addWorkflowFiles() {
|
||||
const envStr =
|
||||
provider === "amazon-bedrock"
|
||||
? ""
|
||||
: `\n env:${providers[provider].env.map((e) => `\n ${e}: \${{ secrets.${e} }}`).join("")}`
|
||||
|
||||
await Bun.write(
|
||||
path.join(app.root, WORKFLOW_FILE),
|
||||
`
|
||||
name: opencode
|
||||
|
||||
on:
|
||||
@@ -231,10 +235,11 @@ jobs:
|
||||
with:
|
||||
model: ${provider}/${model}
|
||||
`.trim(),
|
||||
)
|
||||
)
|
||||
|
||||
prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
|
||||
}
|
||||
prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -6,14 +6,17 @@ export const ModelsCommand = cmd({
|
||||
command: "models",
|
||||
describe: "list all available models",
|
||||
handler: async () => {
|
||||
await Instance.provide(process.cwd(), async () => {
|
||||
const providers = await Provider.list()
|
||||
await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
const providers = await Provider.list()
|
||||
|
||||
for (const [providerID, provider] of Object.entries(providers)) {
|
||||
for (const modelID of Object.keys(provider.info.models)) {
|
||||
console.log(`${providerID}/${modelID}`)
|
||||
for (const [providerID, provider] of Object.entries(providers)) {
|
||||
for (const modelID of Object.keys(provider.info.models)) {
|
||||
console.log(`${providerID}/${modelID}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -64,6 +64,12 @@ export const RunCommand = cmd({
|
||||
type: "string",
|
||||
describe: "agent to use",
|
||||
})
|
||||
.option("format", {
|
||||
type: "string",
|
||||
choices: ["default", "json"],
|
||||
default: "default",
|
||||
describe: "format: default (formatted) or json (raw JSON events)",
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
let message = args.message.join(" ")
|
||||
@@ -144,6 +150,20 @@ export const RunCommand = cmd({
|
||||
)
|
||||
}
|
||||
|
||||
function outputJsonEvent(type: string, data: any) {
|
||||
if (args.format === "json") {
|
||||
const jsonEvent = {
|
||||
type,
|
||||
timestamp: Date.now(),
|
||||
sessionID: session?.id,
|
||||
...data,
|
||||
}
|
||||
process.stdout.write(JSON.stringify(jsonEvent) + "\n")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
let text = ""
|
||||
|
||||
Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
|
||||
@@ -152,6 +172,7 @@ export const RunCommand = cmd({
|
||||
const part = evt.properties.part
|
||||
|
||||
if (part.type === "tool" && part.state.status === "completed") {
|
||||
if (outputJsonEvent("tool_use", { part })) return
|
||||
const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD]
|
||||
const title =
|
||||
part.state.title ||
|
||||
@@ -169,6 +190,7 @@ export const RunCommand = cmd({
|
||||
text = part.text
|
||||
|
||||
if (part.time?.end) {
|
||||
if (outputJsonEvent("text", { part })) return
|
||||
UI.empty()
|
||||
UI.println(UI.markdown(text))
|
||||
UI.empty()
|
||||
@@ -189,6 +211,7 @@ export const RunCommand = cmd({
|
||||
}
|
||||
errorMsg = errorMsg ? errorMsg + "\n" + err : err
|
||||
|
||||
if (outputJsonEvent("error", { error })) return
|
||||
UI.error(err)
|
||||
})
|
||||
|
||||
@@ -225,6 +248,7 @@ export const RunCommand = cmd({
|
||||
const isPiped = !process.stdout.isTTY
|
||||
if (isPiped) {
|
||||
const match = result.parts.findLast((x: any) => x.type === "text") as any
|
||||
if (outputJsonEvent("text", { text: match })) return
|
||||
if (match) process.stdout.write(UI.markdown(match.text))
|
||||
if (errorMsg) process.stdout.write(errorMsg)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Global } from "../../global"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { Server } from "../../server/server"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import path from "path"
|
||||
@@ -16,6 +15,7 @@ import { Ide } from "../../ide"
|
||||
import { Flag } from "../../flag/flag"
|
||||
import { Session } from "../../session"
|
||||
import { $ } from "bun"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
|
||||
declare global {
|
||||
const OPENCODE_TUI_PATH: string
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Flag } from "../flag/flag"
|
||||
import { Auth } from "../auth"
|
||||
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
|
||||
import { Instance } from "../project/instance"
|
||||
import { LSPServer } from "../lsp/server"
|
||||
|
||||
export namespace Config {
|
||||
const log = Log.create({ service: "config" })
|
||||
@@ -482,7 +483,22 @@ export namespace Config {
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.optional(),
|
||||
.optional()
|
||||
.refine(
|
||||
(data) => {
|
||||
if (!data) return true
|
||||
const serverIds = new Set(Object.values(LSPServer).map((s) => s.id))
|
||||
|
||||
return Object.entries(data).every(([id, config]) => {
|
||||
if (config.disabled) return true
|
||||
if (serverIds.has(id)) return true
|
||||
return Boolean(config.extensions)
|
||||
})
|
||||
},
|
||||
{
|
||||
error: "For custom LSP servers, 'extensions' array is required.",
|
||||
},
|
||||
),
|
||||
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
|
||||
layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
|
||||
permission: z
|
||||
|
||||
@@ -28,7 +28,6 @@ export namespace FileWatcher {
|
||||
const ignore = (cfg.watcher?.ignore ?? []).map((v) => new Bun.Glob(v))
|
||||
const watcher = chokidar.watch(Instance.directory, {
|
||||
ignoreInitial: true,
|
||||
awaitWriteFinish: true,
|
||||
ignored: (filepath) => {
|
||||
return FileIgnore.match(filepath, {
|
||||
extra: ignore,
|
||||
|
||||
@@ -72,7 +72,7 @@ export namespace LSP {
|
||||
...existing,
|
||||
id: name,
|
||||
root: existing?.root ?? (async () => Instance.directory),
|
||||
extensions: item.extensions ?? existing.extensions,
|
||||
extensions: item.extensions ?? existing?.extensions ?? [],
|
||||
spawn: async (root) => {
|
||||
return {
|
||||
process: spawn(item.command[0], item.command.slice(1), {
|
||||
@@ -126,12 +126,22 @@ export namespace LSP {
|
||||
result.push(match)
|
||||
continue
|
||||
}
|
||||
const handle = await server.spawn(root).catch((err) => {
|
||||
s.broken.add(root + server.id)
|
||||
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
|
||||
return undefined
|
||||
})
|
||||
const handle = await server
|
||||
.spawn(root)
|
||||
.then((h) => {
|
||||
if (h === undefined) {
|
||||
s.broken.add(root + server.id)
|
||||
}
|
||||
return h
|
||||
})
|
||||
.catch((err) => {
|
||||
s.broken.add(root + server.id)
|
||||
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
|
||||
return undefined
|
||||
})
|
||||
if (!handle) continue
|
||||
log.info("spawned lsp server", { serverID: server.id })
|
||||
|
||||
const client = await LSPClient.create({
|
||||
serverID: server.id,
|
||||
server: handle,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from "child_process"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { Global } from "../global"
|
||||
import { Log } from "../util/log"
|
||||
import { BunProc } from "../bun"
|
||||
@@ -706,4 +707,91 @@ export namespace LSPServer {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const JDTLS: Info = {
|
||||
id: "jdtls",
|
||||
root: NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"]),
|
||||
extensions: [".java"],
|
||||
async spawn(root) {
|
||||
const java = Bun.which("java")
|
||||
if (!java) {
|
||||
log.error("Java 21 or newer is required to run the JDTLS. Please install it first.")
|
||||
return
|
||||
}
|
||||
const javaMajorVersion = await $`java -version`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.then(({ stderr }) => {
|
||||
const m = /"(\d+)\.\d+\.\d+"/.exec(stderr.toString())
|
||||
return !m ? undefined : parseInt(m[1])
|
||||
})
|
||||
if (javaMajorVersion == null || javaMajorVersion < 21) {
|
||||
log.error("JDTLS requires at least Java 21.")
|
||||
return
|
||||
}
|
||||
const distPath = path.join(Global.Path.bin, "jdtls")
|
||||
const launcherDir = path.join(distPath, "plugins")
|
||||
const installed = await fs.exists(launcherDir)
|
||||
if (!installed) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("Downloading JDTLS LSP server.")
|
||||
await fs.mkdir(distPath, { recursive: true })
|
||||
const releaseURL =
|
||||
"https://www.eclipse.org/downloads/download.php?file=/jdtls/snapshots/jdt-language-server-latest.tar.gz"
|
||||
const archivePath = path.join(distPath, "release.tar.gz")
|
||||
await $`curl -L -o '${archivePath}' '${releaseURL}'`.quiet().nothrow()
|
||||
await $`tar -xzf ${archivePath}`.cwd(distPath).quiet().nothrow()
|
||||
await fs.rm(archivePath, { force: true })
|
||||
}
|
||||
const jarFileName = await $`ls org.eclipse.equinox.launcher_*.jar`
|
||||
.cwd(launcherDir)
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.then(({ stdout }) => stdout.toString().trim())
|
||||
const launcherJar = path.join(launcherDir, jarFileName)
|
||||
if (!(await fs.exists(launcherJar))) {
|
||||
log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`)
|
||||
return
|
||||
}
|
||||
const configFile = path.join(
|
||||
distPath,
|
||||
(() => {
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
return "config_mac"
|
||||
case "linux":
|
||||
return "config_linux"
|
||||
case "win32":
|
||||
return "config_windows"
|
||||
default:
|
||||
return "config_linux"
|
||||
}
|
||||
})(),
|
||||
)
|
||||
const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-jdtls-data"))
|
||||
return {
|
||||
process: spawn(
|
||||
java,
|
||||
[
|
||||
"-jar",
|
||||
launcherJar,
|
||||
"-configuration",
|
||||
configFile,
|
||||
"-data",
|
||||
dataDir,
|
||||
"-Declipse.application=org.eclipse.jdt.ls.core.id1",
|
||||
"-Dosgi.bundles.defaultStartLevel=4",
|
||||
"-Declipse.product=org.eclipse.jdt.ls.core.product",
|
||||
"-Dlog.level=ALL",
|
||||
"--add-modules=ALL-SYSTEM",
|
||||
"--add-opens java.base/java.util=ALL-UNNAMED",
|
||||
"--add-opens java.base/java.lang=ALL-UNNAMED",
|
||||
],
|
||||
{
|
||||
cwd: root,
|
||||
},
|
||||
),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export namespace Plugin {
|
||||
}
|
||||
const plugins = [...(config.plugin ?? [])]
|
||||
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
|
||||
plugins.push("opencode-copilot-auth@0.0.2")
|
||||
plugins.push("opencode-copilot-auth@0.0.3")
|
||||
plugins.push("opencode-anthropic-auth@0.0.2")
|
||||
}
|
||||
for (let plugin of plugins) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Plugin } from "../plugin"
|
||||
import { Share } from "../share/share"
|
||||
import { Format } from "../format"
|
||||
import { LSP } from "../lsp"
|
||||
import { Snapshot } from "../snapshot"
|
||||
import { FileWatcher } from "../file/watcher"
|
||||
|
||||
export async function InstanceBootstrap() {
|
||||
await Plugin.init()
|
||||
Share.init()
|
||||
Format.init()
|
||||
LSP.init()
|
||||
Snapshot.init()
|
||||
FileWatcher.init()
|
||||
}
|
||||
@@ -2,12 +2,32 @@ import { Context } from "../util/context"
|
||||
import { Project } from "./project"
|
||||
import { State } from "./state"
|
||||
|
||||
const context = Context.create<{ directory: string; worktree: string; project: Project.Info }>("path")
|
||||
interface Context {
|
||||
directory: string
|
||||
worktree: string
|
||||
project: Project.Info
|
||||
}
|
||||
const context = Context.create<Context>("instance")
|
||||
const cache = new Map<string, Context>()
|
||||
|
||||
export const Instance = {
|
||||
async provide<R>(directory: string, cb: () => R): Promise<R> {
|
||||
const project = await Project.fromDirectory(directory)
|
||||
return context.provide({ directory, worktree: project.worktree, project }, cb)
|
||||
async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
|
||||
let existing = cache.get(input.directory)
|
||||
if (!existing) {
|
||||
const project = await Project.fromDirectory(input.directory)
|
||||
existing = {
|
||||
directory: input.directory,
|
||||
worktree: project.worktree,
|
||||
project,
|
||||
}
|
||||
}
|
||||
return context.provide(existing, async () => {
|
||||
if (!cache.has(input.directory)) {
|
||||
await input.init?.()
|
||||
cache.set(input.directory, existing)
|
||||
}
|
||||
return input.fn()
|
||||
})
|
||||
},
|
||||
get directory() {
|
||||
return context.use().directory
|
||||
|
||||
@@ -22,73 +22,64 @@ export namespace Project {
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
const cache = new Map<string, Info>()
|
||||
export async function fromDirectory(directory: string) {
|
||||
log.info("fromDirectory", { directory })
|
||||
const fn = async () => {
|
||||
const matches = Filesystem.up({ targets: [".git"], start: directory })
|
||||
const git = await matches.next().then((x) => x.value)
|
||||
await matches.return()
|
||||
if (!git) {
|
||||
const project: Info = {
|
||||
id: "global",
|
||||
worktree: "/",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
}
|
||||
await Storage.write<Info>(["project", "global"], project)
|
||||
return project
|
||||
}
|
||||
let worktree = path.dirname(git)
|
||||
const [id] = await $`git rev-list --max-parents=0 --all`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(worktree)
|
||||
.text()
|
||||
.then((x) =>
|
||||
x
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((x) => x.trim())
|
||||
.toSorted(),
|
||||
)
|
||||
if (!id) {
|
||||
const project: Info = {
|
||||
id: "global",
|
||||
worktree: "/",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
}
|
||||
await Storage.write<Info>(["project", "global"], project)
|
||||
return project
|
||||
}
|
||||
worktree = path.dirname(
|
||||
await $`git rev-parse --path-format=absolute --git-common-dir`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(worktree)
|
||||
.text()
|
||||
.then((x) => x.trim()),
|
||||
)
|
||||
const matches = Filesystem.up({ targets: [".git"], start: directory })
|
||||
const git = await matches.next().then((x) => x.value)
|
||||
await matches.return()
|
||||
if (!git) {
|
||||
const project: Info = {
|
||||
id,
|
||||
worktree,
|
||||
vcs: "git",
|
||||
id: "global",
|
||||
worktree: "/",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
}
|
||||
await Storage.write<Info>(["project", id], project)
|
||||
await Storage.write<Info>(["project", "global"], project)
|
||||
return project
|
||||
}
|
||||
if (cache.has(directory)) {
|
||||
return cache.get(directory)!
|
||||
let worktree = path.dirname(git)
|
||||
const [id] = await $`git rev-list --max-parents=0 --all`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(worktree)
|
||||
.text()
|
||||
.then((x) =>
|
||||
x
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((x) => x.trim())
|
||||
.toSorted(),
|
||||
)
|
||||
if (!id) {
|
||||
const project: Info = {
|
||||
id: "global",
|
||||
worktree: "/",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
}
|
||||
await Storage.write<Info>(["project", "global"], project)
|
||||
return project
|
||||
}
|
||||
const result = await fn()
|
||||
cache.set(directory, result)
|
||||
return result
|
||||
worktree = path.dirname(
|
||||
await $`git rev-parse --path-format=absolute --git-common-dir`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(worktree)
|
||||
.text()
|
||||
.then((x) => x.trim()),
|
||||
)
|
||||
const project: Info = {
|
||||
id,
|
||||
worktree,
|
||||
vcs: "git",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
}
|
||||
await Storage.write<Info>(["project", id], project)
|
||||
return project
|
||||
}
|
||||
|
||||
export async function setInitialized(projectID: string) {
|
||||
|
||||
@@ -16,10 +16,7 @@ import { Flag } from "../flag/flag"
|
||||
export namespace Provider {
|
||||
const log = Log.create({ service: "provider" })
|
||||
|
||||
type CustomLoader = (
|
||||
provider: ModelsDev.Provider,
|
||||
api?: string,
|
||||
) => Promise<{
|
||||
type CustomLoader = (provider: ModelsDev.Provider) => Promise<{
|
||||
autoload: boolean
|
||||
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||
options?: Record<string, any>
|
||||
@@ -40,6 +37,19 @@ export namespace Provider {
|
||||
}
|
||||
},
|
||||
async opencode(input) {
|
||||
const hasKey = await (async () => {
|
||||
if (input.env.some((item) => process.env[item])) return true
|
||||
if (await Auth.get(input.id)) return true
|
||||
return false
|
||||
})()
|
||||
|
||||
if (!hasKey) {
|
||||
for (const [key, value] of Object.entries(input.models)) {
|
||||
if (value.cost.input === 0) continue
|
||||
delete input.models[key]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
autoload: Object.keys(input.models).length > 0,
|
||||
options: {},
|
||||
|
||||
@@ -92,14 +92,14 @@ export namespace ProviderTransform {
|
||||
}
|
||||
|
||||
if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) {
|
||||
result["reasoningEffort"] = "high"
|
||||
result["reasoningEffort"] = "medium"
|
||||
if (providerID !== "azure") {
|
||||
result["textVerbosity"] = "low"
|
||||
result["textVerbosity"] = modelID.includes("codex") ? "medium" : "low"
|
||||
}
|
||||
if (providerID === "opencode") {
|
||||
result["promptCacheKey"] = sessionID
|
||||
// result["include"] = ["reasoning.encrypted_content"]
|
||||
// result["reasoningSummary"] = "detailed"
|
||||
// result["reasoningSummary"] = "auto"
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -29,6 +29,7 @@ import { SessionPrompt } from "../session/prompt"
|
||||
import { SessionCompaction } from "../session/compaction"
|
||||
import { SessionRevert } from "../session/revert"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { InstanceBootstrap } from "../project/bootstrap"
|
||||
|
||||
const ERRORS = {
|
||||
400: {
|
||||
@@ -90,8 +91,12 @@ export namespace Server {
|
||||
})
|
||||
.use(async (c, next) => {
|
||||
const directory = c.req.query("directory") ?? process.cwd()
|
||||
return Instance.provide(directory, async () => {
|
||||
return next()
|
||||
return Instance.provide({
|
||||
directory,
|
||||
init: InstanceBootstrap,
|
||||
async fn() {
|
||||
return next()
|
||||
},
|
||||
})
|
||||
})
|
||||
.use(cors())
|
||||
|
||||
@@ -1445,7 +1445,7 @@ export namespace SessionPrompt {
|
||||
})()
|
||||
|
||||
const agent = await Agent.get(agentName)
|
||||
if (agent.mode === "subagent" || command.subtask) {
|
||||
if ((agent.mode === "subagent" && command.subtask !== false) || command.subtask === true) {
|
||||
using abort = lock(input.sessionID)
|
||||
|
||||
const userMsg: MessageV2.User = {
|
||||
|
||||
@@ -14,7 +14,10 @@ export namespace Storage {
|
||||
const MIGRATIONS: Migration[] = [
|
||||
async (dir) => {
|
||||
const project = path.resolve(dir, "../project")
|
||||
for await (const projectDir of new Bun.Glob("*").scan({ cwd: project, onlyFiles: false })) {
|
||||
for await (const projectDir of new Bun.Glob("*").scan({
|
||||
cwd: project,
|
||||
onlyFiles: false,
|
||||
})) {
|
||||
log.info(`migrating project ${projectDir}`)
|
||||
let projectID = projectDir
|
||||
const fullProjectDir = path.join(project, projectDir)
|
||||
|
||||
@@ -75,10 +75,14 @@ export const TaskTool = Tool.define("task", async () => {
|
||||
],
|
||||
})
|
||||
unsub()
|
||||
let all
|
||||
all = await Session.messages(session.id)
|
||||
all = all.filter((x) => x.info.role === "assistant")
|
||||
all = all.flatMap((msg) => msg.parts.filter((x: any) => x.type === "tool") as MessageV2.ToolPart[])
|
||||
return {
|
||||
title: params.description,
|
||||
metadata: {
|
||||
summary: result.parts.filter((x: any) => x.type === "tool"),
|
||||
summary: all,
|
||||
},
|
||||
output: (result.parts.findLast((x: any) => x.type === "text") as any)?.text ?? "",
|
||||
}
|
||||
|
||||
@@ -27,355 +27,418 @@ async function bootstrap() {
|
||||
|
||||
test("tracks deleted files correctly", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`rm ${tmp.dir}/a.txt`.quiet()
|
||||
await $`rm ${tmp.dir}/a.txt`.quiet()
|
||||
|
||||
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.dir}/a.txt`)
|
||||
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.dir}/a.txt`)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("revert should remove new files", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Bun.write(`${tmp.dir}/new.txt`, "NEW")
|
||||
await Bun.write(`${tmp.dir}/new.txt`, "NEW")
|
||||
|
||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||
|
||||
expect(await Bun.file(`${tmp.dir}/new.txt`).exists()).toBe(false)
|
||||
expect(await Bun.file(`${tmp.dir}/new.txt`).exists()).toBe(false)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("revert in subdirectory", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`mkdir -p ${tmp.dir}/sub`.quiet()
|
||||
await Bun.write(`${tmp.dir}/sub/file.txt`, "SUB")
|
||||
await $`mkdir -p ${tmp.dir}/sub`.quiet()
|
||||
await Bun.write(`${tmp.dir}/sub/file.txt`, "SUB")
|
||||
|
||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||
|
||||
expect(await Bun.file(`${tmp.dir}/sub/file.txt`).exists()).toBe(false)
|
||||
// Note: revert currently only removes files, not directories
|
||||
// The empty subdirectory will remain
|
||||
expect(await Bun.file(`${tmp.dir}/sub/file.txt`).exists()).toBe(false)
|
||||
// Note: revert currently only removes files, not directories
|
||||
// The empty subdirectory will remain
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("multiple file operations", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`rm ${tmp.dir}/a.txt`.quiet()
|
||||
await Bun.write(`${tmp.dir}/c.txt`, "C")
|
||||
await $`mkdir -p ${tmp.dir}/dir`.quiet()
|
||||
await Bun.write(`${tmp.dir}/dir/d.txt`, "D")
|
||||
await Bun.write(`${tmp.dir}/b.txt`, "MODIFIED")
|
||||
await $`rm ${tmp.dir}/a.txt`.quiet()
|
||||
await Bun.write(`${tmp.dir}/c.txt`, "C")
|
||||
await $`mkdir -p ${tmp.dir}/dir`.quiet()
|
||||
await Bun.write(`${tmp.dir}/dir/d.txt`, "D")
|
||||
await Bun.write(`${tmp.dir}/b.txt`, "MODIFIED")
|
||||
|
||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||
|
||||
expect(await Bun.file(`${tmp.dir}/a.txt`).text()).toBe(tmp.aContent)
|
||||
expect(await Bun.file(`${tmp.dir}/c.txt`).exists()).toBe(false)
|
||||
// Note: revert currently only removes files, not directories
|
||||
// The empty directory will remain
|
||||
expect(await Bun.file(`${tmp.dir}/b.txt`).text()).toBe(tmp.bContent)
|
||||
expect(await Bun.file(`${tmp.dir}/a.txt`).text()).toBe(tmp.aContent)
|
||||
expect(await Bun.file(`${tmp.dir}/c.txt`).exists()).toBe(false)
|
||||
// Note: revert currently only removes files, not directories
|
||||
// The empty directory will remain
|
||||
expect(await Bun.file(`${tmp.dir}/b.txt`).text()).toBe(tmp.bContent)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("empty directory handling", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`mkdir ${tmp.dir}/empty`.quiet()
|
||||
await $`mkdir ${tmp.dir}/empty`.quiet()
|
||||
|
||||
expect((await Snapshot.patch(before!)).files.length).toBe(0)
|
||||
expect((await Snapshot.patch(before!)).files.length).toBe(0)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("binary file handling", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Bun.write(`${tmp.dir}/image.png`, Buffer.from([0x89, 0x50, 0x4e, 0x47]))
|
||||
await Bun.write(`${tmp.dir}/image.png`, Buffer.from([0x89, 0x50, 0x4e, 0x47]))
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(`${tmp.dir}/image.png`)
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(`${tmp.dir}/image.png`)
|
||||
|
||||
await Snapshot.revert([patch])
|
||||
expect(await Bun.file(`${tmp.dir}/image.png`).exists()).toBe(false)
|
||||
await Snapshot.revert([patch])
|
||||
expect(await Bun.file(`${tmp.dir}/image.png`).exists()).toBe(false)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("symlink handling", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`ln -s ${tmp.dir}/a.txt ${tmp.dir}/link.txt`.quiet()
|
||||
await $`ln -s ${tmp.dir}/a.txt ${tmp.dir}/link.txt`.quiet()
|
||||
|
||||
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.dir}/link.txt`)
|
||||
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.dir}/link.txt`)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("large file handling", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Bun.write(`${tmp.dir}/large.txt`, "x".repeat(1024 * 1024))
|
||||
await Bun.write(`${tmp.dir}/large.txt`, "x".repeat(1024 * 1024))
|
||||
|
||||
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.dir}/large.txt`)
|
||||
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.dir}/large.txt`)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("nested directory revert", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`mkdir -p ${tmp.dir}/level1/level2/level3`.quiet()
|
||||
await Bun.write(`${tmp.dir}/level1/level2/level3/deep.txt`, "DEEP")
|
||||
await $`mkdir -p ${tmp.dir}/level1/level2/level3`.quiet()
|
||||
await Bun.write(`${tmp.dir}/level1/level2/level3/deep.txt`, "DEEP")
|
||||
|
||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||
|
||||
expect(await Bun.file(`${tmp.dir}/level1/level2/level3/deep.txt`).exists()).toBe(false)
|
||||
expect(await Bun.file(`${tmp.dir}/level1/level2/level3/deep.txt`).exists()).toBe(false)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("special characters in filenames", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Bun.write(`${tmp.dir}/file with spaces.txt`, "SPACES")
|
||||
await Bun.write(`${tmp.dir}/file-with-dashes.txt`, "DASHES")
|
||||
await Bun.write(`${tmp.dir}/file_with_underscores.txt`, "UNDERSCORES")
|
||||
await Bun.write(`${tmp.dir}/file with spaces.txt`, "SPACES")
|
||||
await Bun.write(`${tmp.dir}/file-with-dashes.txt`, "DASHES")
|
||||
await Bun.write(`${tmp.dir}/file_with_underscores.txt`, "UNDERSCORES")
|
||||
|
||||
const files = (await Snapshot.patch(before!)).files
|
||||
expect(files).toContain(`${tmp.dir}/file with spaces.txt`)
|
||||
expect(files).toContain(`${tmp.dir}/file-with-dashes.txt`)
|
||||
expect(files).toContain(`${tmp.dir}/file_with_underscores.txt`)
|
||||
const files = (await Snapshot.patch(before!)).files
|
||||
expect(files).toContain(`${tmp.dir}/file with spaces.txt`)
|
||||
expect(files).toContain(`${tmp.dir}/file-with-dashes.txt`)
|
||||
expect(files).toContain(`${tmp.dir}/file_with_underscores.txt`)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("revert with empty patches", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
// Should not crash with empty patches
|
||||
expect(Snapshot.revert([])).resolves.toBeUndefined()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
// Should not crash with empty patches
|
||||
expect(Snapshot.revert([])).resolves.toBeUndefined()
|
||||
|
||||
// Should not crash with patches that have empty file lists
|
||||
expect(Snapshot.revert([{ hash: "dummy", files: [] }])).resolves.toBeUndefined()
|
||||
// Should not crash with patches that have empty file lists
|
||||
expect(Snapshot.revert([{ hash: "dummy", files: [] }])).resolves.toBeUndefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("patch with invalid hash", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
// Create a change
|
||||
await Bun.write(`${tmp.dir}/test.txt`, "TEST")
|
||||
// Create a change
|
||||
await Bun.write(`${tmp.dir}/test.txt`, "TEST")
|
||||
|
||||
// Try to patch with invalid hash - should handle gracefully
|
||||
const patch = await Snapshot.patch("invalid-hash-12345")
|
||||
expect(patch.files).toEqual([])
|
||||
expect(patch.hash).toBe("invalid-hash-12345")
|
||||
// Try to patch with invalid hash - should handle gracefully
|
||||
const patch = await Snapshot.patch("invalid-hash-12345")
|
||||
expect(patch.files).toEqual([])
|
||||
expect(patch.hash).toBe("invalid-hash-12345")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("revert non-existent file", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
// Try to revert a file that doesn't exist in the snapshot
|
||||
// This should not crash
|
||||
expect(
|
||||
Snapshot.revert([
|
||||
{
|
||||
hash: before!,
|
||||
files: [`${tmp.dir}/nonexistent.txt`],
|
||||
},
|
||||
]),
|
||||
).resolves.toBeUndefined()
|
||||
// Try to revert a file that doesn't exist in the snapshot
|
||||
// This should not crash
|
||||
expect(
|
||||
Snapshot.revert([
|
||||
{
|
||||
hash: before!,
|
||||
files: [`${tmp.dir}/nonexistent.txt`],
|
||||
},
|
||||
]),
|
||||
).resolves.toBeUndefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("unicode filenames", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
const unicodeFiles = [
|
||||
`${tmp.dir}/文件.txt`,
|
||||
`${tmp.dir}/🚀rocket.txt`,
|
||||
`${tmp.dir}/café.txt`,
|
||||
`${tmp.dir}/файл.txt`,
|
||||
]
|
||||
const unicodeFiles = [
|
||||
`${tmp.dir}/文件.txt`,
|
||||
`${tmp.dir}/🚀rocket.txt`,
|
||||
`${tmp.dir}/café.txt`,
|
||||
`${tmp.dir}/файл.txt`,
|
||||
]
|
||||
|
||||
for (const file of unicodeFiles) {
|
||||
await Bun.write(file, "unicode content")
|
||||
}
|
||||
for (const file of unicodeFiles) {
|
||||
await Bun.write(file, "unicode content")
|
||||
}
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
// Note: git escapes unicode characters by default, so we just check that files are detected
|
||||
// The actual filenames will be escaped like "caf\303\251.txt" but functionality works
|
||||
expect(patch.files.length).toBe(4)
|
||||
const patch = await Snapshot.patch(before!)
|
||||
// Note: git escapes unicode characters by default, so we just check that files are detected
|
||||
// The actual filenames will be escaped like "caf\303\251.txt" but functionality works
|
||||
expect(patch.files.length).toBe(4)
|
||||
|
||||
// Skip revert test due to git filename escaping issues
|
||||
// The functionality works but git uses escaped filenames internally
|
||||
// Skip revert test due to git filename escaping issues
|
||||
// The functionality works but git uses escaped filenames internally
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("very long filenames", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
const longName = "a".repeat(200) + ".txt"
|
||||
const longFile = `${tmp.dir}/${longName}`
|
||||
const longName = "a".repeat(200) + ".txt"
|
||||
const longFile = `${tmp.dir}/${longName}`
|
||||
|
||||
await Bun.write(longFile, "long filename content")
|
||||
await Bun.write(longFile, "long filename content")
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(longFile)
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(longFile)
|
||||
|
||||
await Snapshot.revert([patch])
|
||||
expect(await Bun.file(longFile).exists()).toBe(false)
|
||||
await Snapshot.revert([patch])
|
||||
expect(await Bun.file(longFile).exists()).toBe(false)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("hidden files", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Bun.write(`${tmp.dir}/.hidden`, "hidden content")
|
||||
await Bun.write(`${tmp.dir}/.gitignore`, "*.log")
|
||||
await Bun.write(`${tmp.dir}/.config`, "config content")
|
||||
await Bun.write(`${tmp.dir}/.hidden`, "hidden content")
|
||||
await Bun.write(`${tmp.dir}/.gitignore`, "*.log")
|
||||
await Bun.write(`${tmp.dir}/.config`, "config content")
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(`${tmp.dir}/.hidden`)
|
||||
expect(patch.files).toContain(`${tmp.dir}/.gitignore`)
|
||||
expect(patch.files).toContain(`${tmp.dir}/.config`)
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(`${tmp.dir}/.hidden`)
|
||||
expect(patch.files).toContain(`${tmp.dir}/.gitignore`)
|
||||
expect(patch.files).toContain(`${tmp.dir}/.config`)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("nested symlinks", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`mkdir -p ${tmp.dir}/sub/dir`.quiet()
|
||||
await Bun.write(`${tmp.dir}/sub/dir/target.txt`, "target content")
|
||||
await $`ln -s ${tmp.dir}/sub/dir/target.txt ${tmp.dir}/sub/dir/link.txt`.quiet()
|
||||
await $`ln -s ${tmp.dir}/sub ${tmp.dir}/sub-link`.quiet()
|
||||
await $`mkdir -p ${tmp.dir}/sub/dir`.quiet()
|
||||
await Bun.write(`${tmp.dir}/sub/dir/target.txt`, "target content")
|
||||
await $`ln -s ${tmp.dir}/sub/dir/target.txt ${tmp.dir}/sub/dir/link.txt`.quiet()
|
||||
await $`ln -s ${tmp.dir}/sub ${tmp.dir}/sub-link`.quiet()
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(`${tmp.dir}/sub/dir/link.txt`)
|
||||
expect(patch.files).toContain(`${tmp.dir}/sub-link`)
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(`${tmp.dir}/sub/dir/link.txt`)
|
||||
expect(patch.files).toContain(`${tmp.dir}/sub-link`)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("file permissions and ownership changes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
// Change permissions multiple times
|
||||
await $`chmod 600 ${tmp.dir}/a.txt`.quiet()
|
||||
await $`chmod 755 ${tmp.dir}/a.txt`.quiet()
|
||||
await $`chmod 644 ${tmp.dir}/a.txt`.quiet()
|
||||
// Change permissions multiple times
|
||||
await $`chmod 600 ${tmp.dir}/a.txt`.quiet()
|
||||
await $`chmod 755 ${tmp.dir}/a.txt`.quiet()
|
||||
await $`chmod 644 ${tmp.dir}/a.txt`.quiet()
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
// Note: git doesn't track permission changes on existing files by default
|
||||
// Only tracks executable bit when files are first added
|
||||
expect(patch.files.length).toBe(0)
|
||||
const patch = await Snapshot.patch(before!)
|
||||
// Note: git doesn't track permission changes on existing files by default
|
||||
// Only tracks executable bit when files are first added
|
||||
expect(patch.files.length).toBe(0)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("circular symlinks", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
// Create circular symlink
|
||||
await $`ln -s ${tmp.dir}/circular ${tmp.dir}/circular`.quiet().nothrow()
|
||||
// Create circular symlink
|
||||
await $`ln -s ${tmp.dir}/circular ${tmp.dir}/circular`.quiet().nothrow()
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files.length).toBeGreaterThanOrEqual(0) // Should not crash
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files.length).toBeGreaterThanOrEqual(0) // Should not crash
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("gitignore changes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Bun.write(`${tmp.dir}/.gitignore`, "*.ignored")
|
||||
await Bun.write(`${tmp.dir}/test.ignored`, "ignored content")
|
||||
await Bun.write(`${tmp.dir}/normal.txt`, "normal content")
|
||||
await Bun.write(`${tmp.dir}/.gitignore`, "*.ignored")
|
||||
await Bun.write(`${tmp.dir}/test.ignored`, "ignored content")
|
||||
await Bun.write(`${tmp.dir}/normal.txt`, "normal content")
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
const patch = await Snapshot.patch(before!)
|
||||
|
||||
// Should track gitignore itself
|
||||
expect(patch.files).toContain(`${tmp.dir}/.gitignore`)
|
||||
// Should track normal files
|
||||
expect(patch.files).toContain(`${tmp.dir}/normal.txt`)
|
||||
// Should not track ignored files (git won't see them)
|
||||
expect(patch.files).not.toContain(`${tmp.dir}/test.ignored`)
|
||||
// Should track gitignore itself
|
||||
expect(patch.files).toContain(`${tmp.dir}/.gitignore`)
|
||||
// Should track normal files
|
||||
expect(patch.files).toContain(`${tmp.dir}/normal.txt`)
|
||||
// Should not track ignored files (git won't see them)
|
||||
expect(patch.files).not.toContain(`${tmp.dir}/test.ignored`)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("concurrent file operations during patch", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
// Start creating files
|
||||
const createPromise = (async () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await Bun.write(`${tmp.dir}/concurrent${i}.txt`, `concurrent${i}`)
|
||||
// Small delay to simulate concurrent operations
|
||||
await new Promise((resolve) => setTimeout(resolve, 1))
|
||||
}
|
||||
})()
|
||||
// Start creating files
|
||||
const createPromise = (async () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await Bun.write(`${tmp.dir}/concurrent${i}.txt`, `concurrent${i}`)
|
||||
// Small delay to simulate concurrent operations
|
||||
await new Promise((resolve) => setTimeout(resolve, 1))
|
||||
}
|
||||
})()
|
||||
|
||||
// Get patch while files are being created
|
||||
const patchPromise = Snapshot.patch(before!)
|
||||
// Get patch while files are being created
|
||||
const patchPromise = Snapshot.patch(before!)
|
||||
|
||||
await createPromise
|
||||
const patch = await patchPromise
|
||||
await createPromise
|
||||
const patch = await patchPromise
|
||||
|
||||
// Should capture some or all of the concurrent files
|
||||
expect(patch.files.length).toBeGreaterThanOrEqual(0)
|
||||
// Should capture some or all of the concurrent files
|
||||
expect(patch.files.length).toBeGreaterThanOrEqual(0)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -384,75 +447,90 @@ test("snapshot state isolation between projects", async () => {
|
||||
await using tmp1 = await bootstrap()
|
||||
await using tmp2 = await bootstrap()
|
||||
|
||||
await Instance.provide(tmp1.dir, async () => {
|
||||
const before1 = await Snapshot.track()
|
||||
await Bun.write(`${tmp1.dir}/project1.txt`, "project1 content")
|
||||
const patch1 = await Snapshot.patch(before1!)
|
||||
expect(patch1.files).toContain(`${tmp1.dir}/project1.txt`)
|
||||
await Instance.provide({
|
||||
directory: tmp1.dir,
|
||||
fn: async () => {
|
||||
const before1 = await Snapshot.track()
|
||||
await Bun.write(`${tmp1.dir}/project1.txt`, "project1 content")
|
||||
const patch1 = await Snapshot.patch(before1!)
|
||||
expect(patch1.files).toContain(`${tmp1.dir}/project1.txt`)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide(tmp2.dir, async () => {
|
||||
const before2 = await Snapshot.track()
|
||||
await Bun.write(`${tmp2.dir}/project2.txt`, "project2 content")
|
||||
const patch2 = await Snapshot.patch(before2!)
|
||||
expect(patch2.files).toContain(`${tmp2.dir}/project2.txt`)
|
||||
await Instance.provide({
|
||||
directory: tmp2.dir,
|
||||
fn: async () => {
|
||||
const before2 = await Snapshot.track()
|
||||
await Bun.write(`${tmp2.dir}/project2.txt`, "project2 content")
|
||||
const patch2 = await Snapshot.patch(before2!)
|
||||
expect(patch2.files).toContain(`${tmp2.dir}/project2.txt`)
|
||||
|
||||
// Ensure project1 files don't appear in project2
|
||||
expect(patch2.files).not.toContain(`${tmp1?.dir}/project1.txt`)
|
||||
// Ensure project1 files don't appear in project2
|
||||
expect(patch2.files).not.toContain(`${tmp1?.dir}/project1.txt`)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("track with no changes returns same hash", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const hash1 = await Snapshot.track()
|
||||
expect(hash1).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const hash1 = await Snapshot.track()
|
||||
expect(hash1).toBeTruthy()
|
||||
|
||||
// Track again with no changes
|
||||
const hash2 = await Snapshot.track()
|
||||
expect(hash2).toBe(hash1!)
|
||||
// Track again with no changes
|
||||
const hash2 = await Snapshot.track()
|
||||
expect(hash2).toBe(hash1!)
|
||||
|
||||
// Track again
|
||||
const hash3 = await Snapshot.track()
|
||||
expect(hash3).toBe(hash1!)
|
||||
// Track again
|
||||
const hash3 = await Snapshot.track()
|
||||
expect(hash3).toBe(hash1!)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("diff function with various changes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
// Make various changes
|
||||
await $`rm ${tmp.dir}/a.txt`.quiet()
|
||||
await Bun.write(`${tmp.dir}/new.txt`, "new content")
|
||||
await Bun.write(`${tmp.dir}/b.txt`, "modified content")
|
||||
// Make various changes
|
||||
await $`rm ${tmp.dir}/a.txt`.quiet()
|
||||
await Bun.write(`${tmp.dir}/new.txt`, "new content")
|
||||
await Bun.write(`${tmp.dir}/b.txt`, "modified content")
|
||||
|
||||
const diff = await Snapshot.diff(before!)
|
||||
expect(diff).toContain("deleted")
|
||||
expect(diff).toContain("modified")
|
||||
// Note: git diff only shows changes to tracked files, not untracked files like new.txt
|
||||
const diff = await Snapshot.diff(before!)
|
||||
expect(diff).toContain("deleted")
|
||||
expect(diff).toContain("modified")
|
||||
// Note: git diff only shows changes to tracked files, not untracked files like new.txt
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("restore function", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
await Instance.provide({
|
||||
directory: tmp.dir,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
// Make changes
|
||||
await $`rm ${tmp.dir}/a.txt`.quiet()
|
||||
await Bun.write(`${tmp.dir}/new.txt`, "new content")
|
||||
await Bun.write(`${tmp.dir}/b.txt`, "modified")
|
||||
// Make changes
|
||||
await $`rm ${tmp.dir}/a.txt`.quiet()
|
||||
await Bun.write(`${tmp.dir}/new.txt`, "new content")
|
||||
await Bun.write(`${tmp.dir}/b.txt`, "modified")
|
||||
|
||||
// Restore to original state
|
||||
await Snapshot.restore(before!)
|
||||
// Restore to original state
|
||||
await Snapshot.restore(before!)
|
||||
|
||||
expect(await Bun.file(`${tmp.dir}/a.txt`).exists()).toBe(true)
|
||||
expect(await Bun.file(`${tmp.dir}/a.txt`).text()).toBe(tmp.aContent)
|
||||
expect(await Bun.file(`${tmp.dir}/new.txt`).exists()).toBe(true) // New files should remain
|
||||
expect(await Bun.file(`${tmp.dir}/b.txt`).text()).toBe(tmp.bContent)
|
||||
expect(await Bun.file(`${tmp.dir}/a.txt`).exists()).toBe(true)
|
||||
expect(await Bun.file(`${tmp.dir}/a.txt`).text()).toBe(tmp.aContent)
|
||||
expect(await Bun.file(`${tmp.dir}/new.txt`).exists()).toBe(true) // New files should remain
|
||||
expect(await Bun.file(`${tmp.dir}/b.txt`).text()).toBe(tmp.bContent)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,30 +19,36 @@ Log.init({ print: false })
|
||||
|
||||
describe("tool.bash", () => {
|
||||
test("basic", async () => {
|
||||
await Instance.provide(projectRoot, async () => {
|
||||
const result = await bash.execute(
|
||||
{
|
||||
command: "echo 'test'",
|
||||
description: "Echo test message",
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
expect(result.metadata.exit).toBe(0)
|
||||
expect(result.metadata.output).toContain("test")
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const result = await bash.execute(
|
||||
{
|
||||
command: "echo 'test'",
|
||||
description: "Echo test message",
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
expect(result.metadata.exit).toBe(0)
|
||||
expect(result.metadata.output).toContain("test")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("cd ../ should fail outside of project root", async () => {
|
||||
await Instance.provide(projectRoot, async () => {
|
||||
expect(
|
||||
bash.execute(
|
||||
{
|
||||
command: "cd ../",
|
||||
description: "Try to cd to parent directory",
|
||||
},
|
||||
ctx,
|
||||
),
|
||||
).rejects.toThrow("This command references paths outside of")
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
expect(
|
||||
bash.execute(
|
||||
{
|
||||
command: "cd ../",
|
||||
description: "Try to cd to parent directory",
|
||||
},
|
||||
ctx,
|
||||
),
|
||||
).rejects.toThrow("This command references paths outside of")
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,38 +20,47 @@ const fixturePath = path.join(__dirname, "../fixtures/example")
|
||||
|
||||
describe("tool.glob", () => {
|
||||
test("truncate", async () => {
|
||||
await Instance.provide(projectRoot, async () => {
|
||||
let result = await glob.execute(
|
||||
{
|
||||
pattern: "**/*",
|
||||
path: "../../node_modules",
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
expect(result.metadata.truncated).toBe(true)
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
let result = await glob.execute(
|
||||
{
|
||||
pattern: "**/*",
|
||||
path: "../../node_modules",
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
expect(result.metadata.truncated).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
test("basic", async () => {
|
||||
await Instance.provide(projectRoot, async () => {
|
||||
let result = await glob.execute(
|
||||
{
|
||||
pattern: "*.json",
|
||||
path: undefined,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
expect(result.metadata).toMatchObject({
|
||||
truncated: false,
|
||||
count: 2,
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
let result = await glob.execute(
|
||||
{
|
||||
pattern: "*.json",
|
||||
path: undefined,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
expect(result.metadata).toMatchObject({
|
||||
truncated: false,
|
||||
count: 2,
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("tool.ls", () => {
|
||||
test("basic", async () => {
|
||||
const result = await Instance.provide(projectRoot, async () => {
|
||||
return await list.execute({ path: fixturePath, ignore: [".git"] }, ctx)
|
||||
const result = await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
return await list.execute({ path: fixturePath, ignore: [".git"] }, ctx)
|
||||
},
|
||||
})
|
||||
|
||||
// Normalize absolute path to relative for consistent snapshots
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
|
||||
@@ -1105,13 +1105,6 @@ export type EventSessionError = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventServerConnected = {
|
||||
type: "server.connected"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type EventFileWatcherUpdated = {
|
||||
type: "file.watcher.updated"
|
||||
properties: {
|
||||
@@ -1120,6 +1113,13 @@ export type EventFileWatcherUpdated = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventServerConnected = {
|
||||
type: "server.connected"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type EventIdeInstalled = {
|
||||
type: "ide.installed"
|
||||
properties: {
|
||||
@@ -1142,8 +1142,8 @@ export type Event =
|
||||
| EventSessionUpdated
|
||||
| EventSessionDeleted
|
||||
| EventSessionError
|
||||
| EventServerConnected
|
||||
| EventFileWatcherUpdated
|
||||
| EventServerConnected
|
||||
| EventIdeInstalled
|
||||
|
||||
export type ProjectListData = {
|
||||
|
||||
@@ -478,9 +478,6 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||
}
|
||||
|
||||
case opencode.AssistantMessage:
|
||||
if casted.Summary {
|
||||
continue
|
||||
}
|
||||
if casted.ID == m.app.Session.Revert.MessageID {
|
||||
reverted = true
|
||||
revertedMessageCount = 1
|
||||
|
||||
@@ -954,7 +954,6 @@ func (a Model) home() (string, int, int) {
|
||||
baseStyle := styles.NewStyle().Foreground(t.Text()).Background(t.Background())
|
||||
base := baseStyle.Render
|
||||
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
|
||||
highlight := styles.NewStyle().Foreground(t.Accent()).Background(t.Background()).Render
|
||||
|
||||
open := `
|
||||
█▀▀█ █▀▀█ █▀▀ █▀▀▄
|
||||
@@ -989,9 +988,9 @@ func (a Model) home() (string, int, int) {
|
||||
)
|
||||
|
||||
// Use limit of 4 for vscode, 6 for others
|
||||
limit := 4
|
||||
limit := 6
|
||||
if util.IsVSCode() {
|
||||
limit = 2
|
||||
limit = 4
|
||||
}
|
||||
|
||||
showVscode := util.IsVSCode()
|
||||
@@ -1008,14 +1007,6 @@ func (a Model) home() (string, int, int) {
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
|
||||
grok := highlight("Grok Code is free for a limited time")
|
||||
grok = lipgloss.PlaceHorizontal(
|
||||
effectiveWidth,
|
||||
lipgloss.Center,
|
||||
grok,
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
|
||||
lines := []string{}
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, logoAndVersion)
|
||||
@@ -1023,8 +1014,6 @@ func (a Model) home() (string, int, int) {
|
||||
lines = append(lines, cmds)
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, grok)
|
||||
lines = append(lines, "")
|
||||
|
||||
mainHeight := lipgloss.Height(strings.Join(lines, "\n"))
|
||||
|
||||
@@ -1219,7 +1208,11 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
||||
if a.app.Session.ID == "" {
|
||||
return a, nil
|
||||
}
|
||||
response, err := a.app.Client.Session.Share(context.Background(), a.app.Session.ID, opencode.SessionShareParams{})
|
||||
response, err := a.app.Client.Session.Share(
|
||||
context.Background(),
|
||||
a.app.Session.ID,
|
||||
opencode.SessionShareParams{},
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to share session", "error", err)
|
||||
return a, toast.NewErrorToast("Failed to share session")
|
||||
@@ -1231,7 +1224,11 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
||||
if a.app.Session.ID == "" {
|
||||
return a, nil
|
||||
}
|
||||
_, err := a.app.Client.Session.Unshare(context.Background(), a.app.Session.ID, opencode.SessionUnshareParams{})
|
||||
_, err := a.app.Client.Session.Unshare(
|
||||
context.Background(),
|
||||
a.app.Session.ID,
|
||||
opencode.SessionUnshareParams{},
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to unshare session", "error", err)
|
||||
return a, toast.NewErrorToast("Failed to unshare session")
|
||||
@@ -1259,7 +1256,11 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
||||
var parentSession *opencode.Session
|
||||
if a.app.Session.ParentID != "" {
|
||||
parentSessionID = a.app.Session.ParentID
|
||||
session, err := a.app.Client.Session.Get(context.Background(), parentSessionID, opencode.SessionGetParams{})
|
||||
session, err := a.app.Client.Session.Get(
|
||||
context.Background(),
|
||||
parentSessionID,
|
||||
opencode.SessionGetParams{},
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get parent session", "error", err)
|
||||
return toast.NewErrorToast("Failed to get parent session")
|
||||
@@ -1269,7 +1270,11 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
||||
parentSession = a.app.Session
|
||||
}
|
||||
|
||||
children, err := a.app.Client.Session.Children(context.Background(), parentSessionID, opencode.SessionChildrenParams{})
|
||||
children, err := a.app.Client.Session.Children(
|
||||
context.Background(),
|
||||
parentSessionID,
|
||||
opencode.SessionChildrenParams{},
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get session children", "error", err)
|
||||
return toast.NewErrorToast("Failed to get session children")
|
||||
@@ -1317,7 +1322,11 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
||||
var parentSession *opencode.Session
|
||||
if a.app.Session.ParentID != "" {
|
||||
parentSessionID = a.app.Session.ParentID
|
||||
session, err := a.app.Client.Session.Get(context.Background(), parentSessionID, opencode.SessionGetParams{})
|
||||
session, err := a.app.Client.Session.Get(
|
||||
context.Background(),
|
||||
parentSessionID,
|
||||
opencode.SessionGetParams{},
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get parent session", "error", err)
|
||||
return toast.NewErrorToast("Failed to get parent session")
|
||||
@@ -1327,7 +1336,11 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
||||
parentSession = a.app.Session
|
||||
}
|
||||
|
||||
children, err := a.app.Client.Session.Children(context.Background(), parentSessionID, opencode.SessionChildrenParams{})
|
||||
children, err := a.app.Client.Session.Children(
|
||||
context.Background(),
|
||||
parentSessionID,
|
||||
opencode.SessionChildrenParams{},
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get session children", "error", err)
|
||||
return toast.NewErrorToast("Failed to get session children")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode/web",
|
||||
"type": "module",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
@@ -23,11 +23,11 @@
|
||||
"diff": "8.0.2",
|
||||
"js-base64": "3.7.7",
|
||||
"lang-map": "0.4.0",
|
||||
"luxon": "3.6.1",
|
||||
"luxon": "catalog:",
|
||||
"marked": "15.0.12",
|
||||
"marked-shiki": "1.2.1",
|
||||
"rehype-autolink-headings": "7.1.0",
|
||||
"remeda": "2.26.0",
|
||||
"remeda": "catalog:",
|
||||
"sharp": "0.32.5",
|
||||
"shiki": "3.4.2",
|
||||
"solid-js": "catalog:",
|
||||
|
||||
@@ -495,9 +495,7 @@ For quick reference, here are common setups.
|
||||
}
|
||||
```
|
||||
|
||||
See the full permissions guide for more patterns.
|
||||
|
||||
- /docs/permissions
|
||||
See the full [permissions guide](/docs/permissions) for more patterns.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -144,6 +144,8 @@ opencode models
|
||||
|
||||
This command displays all models available across your configured providers in the format `provider/model`.
|
||||
|
||||
This is useful for figuring out the exact model name to use in [your config](/docs/config/).
|
||||
|
||||
---
|
||||
|
||||
### run
|
||||
|
||||
@@ -227,6 +227,8 @@ This is shown as the description in the TUI when you type in the command.
|
||||
### Agent
|
||||
|
||||
Use the `agent` config to optionally specify which [agent](/docs/agents) should execute this command.
|
||||
If this is a [subagent](/docs/agents/#subagents) the command will trigger a subagent invocation by default.
|
||||
To disable this behavior, set `subtask` to `false`.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
@@ -238,7 +240,26 @@ Use the `agent` config to optionally specify which [agent](/docs/agents) should
|
||||
}
|
||||
```
|
||||
|
||||
This is an **optional** config option. If not specified, defaults to "build".
|
||||
This is an **optional** config option. If not specified, defaults to your current agent.
|
||||
|
||||
---
|
||||
|
||||
### Subtask
|
||||
|
||||
Use the `subtask` boolean to force the command to trigger a [subagent](/docs/agents/#subagents) invocation.
|
||||
This useful if you want the command to not pollute your primary context.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"command": {
|
||||
"analyze": {
|
||||
"subtask": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is an **optional** config option.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
---
|
||||
title: Custom tools
|
||||
description: Create custom tools to extend opencode capabilities.
|
||||
title: Custom Tools
|
||||
description: Create tools the LLM can call in opencode.
|
||||
---
|
||||
|
||||
Custom tools are functions you create that the LLM can call during conversations. They work alongside opencode's built-in tools like `read`, `write`, and `bash`.
|
||||
|
||||
---
|
||||
|
||||
## Tool structure
|
||||
## Creating a tool
|
||||
|
||||
Tools are defined as `.ts/.js` files in the `.opencode/tool/` directory. They
|
||||
can also be defined globally in `~/.config/opencode/tool/`.
|
||||
Tools are defined as **TypeScript** or **JavaScript** files.
|
||||
|
||||
The easiest way to create tools is using the `tool()` helper which provides type safety and validation. Use `tool.schema` (which is just [Zod](https://zod.dev)) to define argument types:
|
||||
---
|
||||
|
||||
```ts title=".opencode/tool/database.ts"
|
||||
### Location
|
||||
|
||||
They can defined:
|
||||
|
||||
- Locally by placing them in the `.opencode/tool/` directory of your project.
|
||||
- Or globally, by placing them in `~/.config/opencode/tool/`.
|
||||
|
||||
---
|
||||
|
||||
### Structure
|
||||
|
||||
The easiest way to create tools is using the `tool()` helper which provides type-safety and validation.
|
||||
|
||||
```ts title=".opencode/tool/database.ts" {1}
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
|
||||
export default tool({
|
||||
@@ -29,9 +41,23 @@ export default tool({
|
||||
})
|
||||
```
|
||||
|
||||
The **filename** becomes the **tool name**. The above creates a `database` tool.
|
||||
|
||||
---
|
||||
|
||||
### Arguments
|
||||
|
||||
You can use `tool.schema`, which is just [Zod](https://zod.dev), to define argument types.
|
||||
|
||||
```ts "tool.schema"
|
||||
args: {
|
||||
query: tool.schema.string().describe("SQL query to execute")
|
||||
}
|
||||
```
|
||||
|
||||
You can also import [Zod](https://zod.dev) directly and return a plain object:
|
||||
|
||||
```ts
|
||||
```ts {6}
|
||||
import { z } from "zod"
|
||||
|
||||
export default {
|
||||
@@ -46,13 +72,31 @@ export default {
|
||||
}
|
||||
```
|
||||
|
||||
The filename becomes the tool name. This creates a `database` tool.
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tools receive context about the current session:
|
||||
|
||||
```ts title=".opencode/tool/project.ts" {8}
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
|
||||
export default tool({
|
||||
description: "Get project information",
|
||||
args: {},
|
||||
async execute(args, context) {
|
||||
// Access context information
|
||||
const { agent, sessionID, messageID } = context
|
||||
return `Agent: ${agent}, Session: ${sessionID}, Message: ${messageID}`
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multiple tools per file
|
||||
|
||||
You can export multiple tools from a single file. Each export becomes a separate tool with the name `<filename>_<exportname>`:
|
||||
You can also export multiple tools from a single file. Each export becomes **a separate tool** with the name **`<filename>_<exportname>`**:
|
||||
|
||||
```ts title=".opencode/tool/math.ts"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
@@ -81,45 +125,3 @@ export const multiply = tool({
|
||||
```
|
||||
|
||||
This creates two tools: `math_add` and `math_multiply`.
|
||||
|
||||
---
|
||||
|
||||
## Arguments
|
||||
|
||||
Use `tool.schema` (which is just [Zod](https://zod.dev)) to define tool arguments with validation and descriptions:
|
||||
|
||||
```ts title=".opencode/tool/calculator.ts"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
|
||||
export default tool({
|
||||
description: "Perform mathematical calculations",
|
||||
args: {
|
||||
expression: tool.schema.string().describe("Mathematical expression to evaluate"),
|
||||
precision: tool.schema.number().optional().describe("Decimal precision"),
|
||||
},
|
||||
async execute(args) {
|
||||
// Your calculation logic here
|
||||
return `Result: ${eval(args.expression).toFixed(args.precision || 2)}`
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tools receive context about the current session:
|
||||
|
||||
```ts title=".opencode/tool/project.ts"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
|
||||
export default tool({
|
||||
description: "Get project information",
|
||||
args: {},
|
||||
async execute(args, context) {
|
||||
// Access context information
|
||||
const { project, directory, worktree } = context
|
||||
return `Project: ${project.name}, Directory: ${directory}`
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -24,6 +24,8 @@ To install opencode on VS Code and popular forks like Cursor, Windsurf, VSCodium
|
||||
2. Open the integrated terminal
|
||||
3. Run `opencode` - the extension installs automatically
|
||||
|
||||
If on the other hand you want to use your own IDE when you run `/editor` or `/export` from the TUI, you'll need to set `export EDITOR="code --wait"`. [Learn more](/docs/tui/#editor-setup).
|
||||
|
||||
---
|
||||
|
||||
### Manual Install
|
||||
|
||||
@@ -37,7 +37,7 @@ The easiest way to install opencode is through the install script.
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
```
|
||||
|
||||
You can also install it with the following:
|
||||
You can also install it with the following commands:
|
||||
|
||||
- **Using Node.js**
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: LSP Servers
|
||||
description: opencode integrates with your LSP servers.
|
||||
---
|
||||
|
||||
opencode integrates with your Language Server Protocol (LSP) to help the LLM interacts with your codebase. It uses diagnostics to provide feedback to the LLM. And _go-to-definition_ and _find-references_ to help navigate your codebase.
|
||||
opencode integrates with your Language Server Protocol (LSP) to help the LLM interact with your codebase. It uses diagnostics to provide feedback to the LLM.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -356,6 +356,8 @@ To use your GitHub Copilot subscription with opencode:
|
||||
:::note
|
||||
Some models might need a [Pro+
|
||||
subscription](https://github.com/features/copilot/plans) to use.
|
||||
|
||||
Some models need to be manually enabled in your [GitHub Copilot settings](https://docs.github.com/en/copilot/how-tos/use-ai-models/configure-access-to-ai-models#setup-for-individual-use).
|
||||
:::
|
||||
|
||||
1. Run `opencode auth login` and select GitHub Copilot.
|
||||
|
||||
@@ -25,18 +25,18 @@ Without truecolor support, themes may appear with reduced color accuracy or fall
|
||||
|
||||
opencode comes with several built-in themes.
|
||||
|
||||
| Name | Description |
|
||||
| ------------ | ------------------------------------------ |
|
||||
| `system` | Adapts to your terminal's background color |
|
||||
| `tokyonight` | Based on the Tokyonight theme |
|
||||
| `everforest` | Based on the Everforest theme |
|
||||
| `ayu` | Based on the Ayu dark theme |
|
||||
| `catppuccin` | Based on the Catppuccin theme |
|
||||
| `gruvbox` | Based on the Gruvbox theme |
|
||||
| `kanagawa` | Based on the Kanagawa theme |
|
||||
| `nord` | Based on the Nord theme |
|
||||
| `matrix` | Hacker-style green on black theme |
|
||||
| `one-dark` | Based on the Atom One Dark theme |
|
||||
| Name | Description |
|
||||
| ------------ | ---------------------------------------------------------------------------- |
|
||||
| `system` | Adapts to your terminal's background color |
|
||||
| `tokyonight` | Based on the [Tokyonight](https://github.com/folke/tokyonight.nvim) theme |
|
||||
| `everforest` | Based on the [Everforest](https://github.com/sainnhe/everforest) theme |
|
||||
| `ayu` | Based on the [Ayu](https://github.com/ayu-theme) dark theme |
|
||||
| `catppuccin` | Based on the [Catppuccin](https://github.com/catppuccin) theme |
|
||||
| `gruvbox` | Based on the [Gruvbox](https://github.com/morhetz/gruvbox) theme |
|
||||
| `kanagawa` | Based on the [Kanagawa](https://github.com/rebelot/kanagawa.nvim) theme |
|
||||
| `nord` | Based on the [Nord](https://github.com/nordtheme/nord) theme |
|
||||
| `matrix` | Hacker-style green on black theme |
|
||||
| `one-dark` | Based on the [Atom One](https://github.com/Th3Whit3Wolf/one-nvim) Dark theme |
|
||||
|
||||
And more, we are constantly adding new themes.
|
||||
|
||||
|
||||
@@ -185,6 +185,9 @@ Redo a previously undone message. Only available after using `/undo`.
|
||||
Any file changes will also be restored.
|
||||
:::
|
||||
|
||||
Internally, this uses Git to manage the file changes. So your project **needs to
|
||||
be a Git repository**.
|
||||
|
||||
```bash frame="none"
|
||||
/redo
|
||||
```
|
||||
@@ -237,6 +240,9 @@ Undo last message in the conversation. Removes the most recent user message, all
|
||||
Any file changes made will also be reverted.
|
||||
:::
|
||||
|
||||
Internally, this uses Git to manage the file changes. So your project **needs to
|
||||
be a Git repository**.
|
||||
|
||||
```bash frame="none"
|
||||
/undo
|
||||
```
|
||||
@@ -266,7 +272,8 @@ Both the `/editor` and `/export` commands use the editor specified in your `EDIT
|
||||
export EDITOR=nano
|
||||
export EDITOR=vim
|
||||
|
||||
# For GUI editors (VS Code, Cursor, VSCodium, Windsurf, Zed, etc.) include --wait
|
||||
# For GUI editors, VS Code, Cursor, VSCodium, Windsurf, Zed, etc.
|
||||
# include --wait
|
||||
export EDITOR="code --wait"
|
||||
```
|
||||
|
||||
@@ -279,7 +286,8 @@ Both the `/editor` and `/export` commands use the editor specified in your `EDIT
|
||||
```bash
|
||||
set EDITOR=notepad
|
||||
|
||||
# For GUI editors (VS Code, Cursor, VSCodium, Windsurf, Zed, etc.) include --wait
|
||||
# For GUI editors, VS Code, Cursor, VSCodium, Windsurf, Zed, etc.
|
||||
# include --wait
|
||||
set EDITOR=code --wait
|
||||
```
|
||||
|
||||
@@ -292,7 +300,8 @@ Both the `/editor` and `/export` commands use the editor specified in your `EDIT
|
||||
```powershell
|
||||
$env:EDITOR = "notepad"
|
||||
|
||||
# For GUI editors (VS Code, Cursor, VSCodium, Windsurf, Zed, etc.) include --wait
|
||||
# For GUI editors, VS Code, Cursor, VSCodium, Windsurf, Zed, etc.
|
||||
# include --wait
|
||||
$env:EDITOR = "code --wait"
|
||||
```
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@ You can also access our models through the following API endpoints.
|
||||
|
||||
| Model | Mode ID | Endpoint | AI SDK Package |
|
||||
| ---------------- | ---------------- | --------------------------------------------- | --------------------------- |
|
||||
| GPT-5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -81,19 +82,24 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
|
||||
| Model | Input | Output | Cached Read | Cached Write |
|
||||
| ------------------------------- | ------ | ------ | ----------- | ------------ |
|
||||
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
|
||||
| Grok Code Fast 1 | Free | Free | Free | - |
|
||||
| Grok Code Fast 1 | Free | Free | - | - |
|
||||
| Code Supernova | Free | Free | - | - |
|
||||
| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
|
||||
| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
|
||||
| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 |
|
||||
| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 |
|
||||
| GPT 5 | $1.25 | $10.00 | $0.125 | - |
|
||||
| GPT 5 Codex | $1.25 | $10.00 | $0.125 | - |
|
||||
| Kimi K2 | $0.60 | $2.50 | - | - |
|
||||
|
||||
:::note
|
||||
Credit card fees are passed along at cost; we don’t charge anything beyond that.
|
||||
:::
|
||||
|
||||
Grok Code Fast 1 is currently free on opencode for a limited time. The xAI team is using this time to collect feedback and improve Grok Code.
|
||||
The free models:
|
||||
|
||||
- Grok Code Fast 1 is currently free on opencode for a limited time. The xAI team is using this time to collect feedback and improve Grok Code.
|
||||
- Code Supernova is a stealth model that's free on opencode for a limited time. The team is using this time to collect feedback and improve the model.
|
||||
|
||||
:::tip
|
||||
Subscription plans and a free tier are coming soon.
|
||||
@@ -108,6 +114,8 @@ Subscription plans and a free tier are coming soon.
|
||||
All our models are hosted in the US. Our providers follow a zero-retention policy and do not use your data for model training, with the following exceptions:
|
||||
|
||||
- Grok Code Fast 1: During its free period, collected data may be used to improve Grok Code.
|
||||
- Code Supernova: During its free period, collected data may be used to improve
|
||||
the model.
|
||||
- OpenAI APIs: Requests are retained for 30 days in accordance with [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
|
||||
- Anthropic APIs: Requests are retained for 30 days in accordance with [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.3",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Vendored
+1
-17
@@ -5,10 +5,6 @@
|
||||
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
ANTHROPIC_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
AUTH_API_URL: {
|
||||
type: "sst.sst.Linkable"
|
||||
value: string
|
||||
@@ -24,10 +20,6 @@ declare module "sst" {
|
||||
AuthStorage: {
|
||||
type: "sst.cloudflare.Kv"
|
||||
}
|
||||
BASETEN_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
Bucket: {
|
||||
name: string
|
||||
type: "sst.cloudflare.Bucket"
|
||||
@@ -44,10 +36,6 @@ declare module "sst" {
|
||||
type: "sst.sst.Linkable"
|
||||
username: string
|
||||
}
|
||||
FIREWORKS_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
GITHUB_APP_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
@@ -75,10 +63,6 @@ declare module "sst" {
|
||||
LogProcessor: {
|
||||
type: "sst.cloudflare.Worker"
|
||||
}
|
||||
OPENAI_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
STRIPE_SECRET_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
@@ -91,7 +75,7 @@ declare module "sst" {
|
||||
type: "sst.cloudflare.Astro"
|
||||
url: string
|
||||
}
|
||||
XAI_API_KEY: {
|
||||
ZEN_MODELS: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user