From 3c282c3c37a269e2e4b27e7fe92c57c0ce295ceb Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 8 Oct 2025 06:56:18 -0500 Subject: [PATCH 01/10] fix(tui): suggestions gap on home page --- packages/tui/internal/tui/tui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 3310d517c..50b503c66 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -1069,7 +1069,7 @@ func (a Model) home() (string, int, int) { mainLayout = layout.PlaceOverlay( editorX, - editorY-overlayHeight+1, + editorY-overlayHeight+2, overlay, mainLayout, ) From a63fa64dece5a75082f4a5335d1f4bd20af8cc50 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 8 Oct 2025 12:04:31 +0000 Subject: [PATCH 02/10] ignore: update download stats 2025-10-08 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index 6d1d68a43..b1c90d402 100644 --- a/STATS.md +++ b/STATS.md @@ -102,3 +102,4 @@ | 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) | | 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) | | 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) | +| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) | From 1d621260ff83751e70859c3f4c6a834bb481ed81 Mon Sep 17 00:00:00 2001 From: Jay V Date: Wed, 8 Oct 2025 12:13:38 -0400 Subject: [PATCH 03/10] docs: fix permission docs --- packages/web/src/content/docs/agents.mdx | 104 +++----- packages/web/src/content/docs/config.mdx | 9 +- packages/web/src/content/docs/permissions.mdx | 241 +++++++++++------- packages/web/src/styles/custom.css | 2 +- 4 files changed, 189 insertions(+), 167 deletions(-) diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index 82d3c8ccb..7016d6576 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -362,42 +362,33 @@ Here are all the tools can be controlled through the agent config. ### Permissions -Permissions control what actions an agent can take. +You can configure permissions to manage what actions an agent can take. Currently, the permissions for the `edit`, `bash`, and `webfetch` tools can be configured to: -- edit, bash, webfetch - -Each permission can be set to allow, ask, or deny. - -- allow, ask, deny - -Configure permissions globally in opencode.json. +- `"ask"` — Prompt for approval before running the tool +- `"allow"` — Allow all operations without approval +- `"deny"` — Disable the tool ```json title="opencode.json" { "$schema": "https://opencode.ai/config.json", "permission": { - "edit": "ask", - "bash": "allow", - "webfetch": "deny" + "edit": "deny" } } ``` -You can override permissions per agent in JSON. +You can override these permissions per agent. -```json title="opencode.json" {7-18} +```json title="opencode.json" {3-5,8-10} { "$schema": "https://opencode.ai/config.json", + "permission": { + "edit": "deny" + }, "agent": { "build": { "permission": { - "edit": "allow", - "bash": { - "*": "allow", - "git push": "ask", - "terraform *": "deny" - }, - "webfetch": "ask" + "edit": "ask" } } } @@ -419,83 +410,60 @@ permission: Only analyze code and suggest changes. ``` -Bash permissions support granular patterns for fine-grained control. +You can set permissions for specific bash commands. -```json title="Allow most, ask for risky, deny terraform" +```json title="opencode.json" {7} { "$schema": "https://opencode.ai/config.json", - "permission": { - "bash": { - "*": "allow", - "git push": "ask", - "terraform *": "deny" - } - } -} -``` - -If you provide a granular bash map, the default becomes ask unless you set \* explicitly. - -```json title="Granular defaults to ask" -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "bash": { - "git status": "allow" - } - } -} -``` - -Agent-level permissions merge over global settings. - -- Global sets defaults; agent overrides when specified - -Specific bash rules can override a global default. - -```json title="Global ask, agent allows safe commands" -{ - "$schema": "https://opencode.ai/config.json", - "permission": { "bash": "ask" }, "agent": { "build": { "permission": { - "bash": { "git status": "allow", "*": "ask" } + "bash": { + "git push": "ask" + } } } } } ``` -Permissions affect tool availability and prompts differently. +This can take a glob pattern. -- deny hides tools (edit also hides write/patch); ask prompts; allow runs - -For quick reference, here are common setups. - -```json title="Read-only reviewer" +```json title="opencode.json" {7} { "$schema": "https://opencode.ai/config.json", "agent": { - "review": { - "permission": { "edit": "deny", "bash": "deny", "webfetch": "allow" } + "build": { + "permission": { + "bash": { + "git *": "ask" + } + } } } } ``` -```json title="Planning agent that can browse but cannot change code" +And you can also use the `*` wildcard to manage permissions for all commands. +Where the specific rule can override the `*` wildcard. + +```json title="opencode.json" {8} { "$schema": "https://opencode.ai/config.json", "agent": { - "plan": { - "permission": { "edit": "deny", "bash": "deny", "webfetch": "ask" } + "build": { + "permission": { + "bash": { + "git status": "allow", + "*": "ask" + } + } } } } ``` -See the full [permissions guide](/docs/permissions) for more patterns. +[Learn more about permissions](/docs/permissions). --- diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index 53b06f9d0..d996cb94b 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -249,7 +249,9 @@ You can configure code formatters through the `formatter` option. ### Permissions -You can configure permissions to control what AI agents can do in your codebase through the `permission` option. +By default, opencode **allows all operations** without requiring explicit approval. You can change this using the `permission` option. + +For example, to ensure that the `edit` and `bash` tools require user approval: ```json title="opencode.json" { @@ -261,11 +263,6 @@ You can configure permissions to control what AI agents can do in your codebase } ``` -This allows you to configure explicit approval requirements for sensitive operations: - -- `edit` - Controls whether file editing operations require user approval (`"ask"` or `"allow"`) -- `bash` - Controls whether bash commands require user approval (can be `"ask"`/`"allow"` or a pattern map) - [Learn more about permissions here](/docs/permissions). --- diff --git a/packages/web/src/content/docs/permissions.mdx b/packages/web/src/content/docs/permissions.mdx index 68ae8b087..4579c2127 100644 --- a/packages/web/src/content/docs/permissions.mdx +++ b/packages/web/src/content/docs/permissions.mdx @@ -1,27 +1,32 @@ --- title: Permissions -description: Control what agents can do in your codebase. +description: Control which actions require approval to run. --- -By default, opencode **allows all operations** without requiring explicit approval. +By default, OpenCode **allows all operations** without requiring explicit approval. You can configure this using the `permission` option. -The permissions system provides granular control to restrict what actions AI agents can perform in your codebase, allowing you to configure explicit approval requirements for sensitive operations like file editing, bash commands, and more. +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "permission": { + "edit": "allow", + "bash": "ask", + "webfetch": "deny" + } +} +``` + +This lets you configure granular controls for the `edit`, `bash`, and `webfetch` tools. + +- `"ask"` — Prompt for approval before running the tool +- `"allow"` — Allow all operations without approval +- `"deny"` — Disable the tool --- -## Configure +## Tools -Permissions are configured in your `opencode.json` file under the `permission` key. Here are the available options. - -### Tool Permission Support - -| Tool | Description | -| ---------- | ------------------------------- | -| `edit` | Control file editing operations | -| `bash` | Control bash command execution | -| `webfetch` | Control web content fetching | - -They can also be configured per agent, see [Agent Configuration](/docs/agents#agent-configuration) for more details. +Currently, the permissions for the `edit`, `bash`, and `webfetch` tools can be configured through the `permission` option. --- @@ -29,10 +34,6 @@ They can also be configured per agent, see [Agent Configuration](/docs/agents#ag Use the `permission.edit` key to control whether file editing operations require user approval. -- `"ask"` - Prompt for approval before editing files -- `"allow"` - Allow all file editing operations without approval -- `"deny"` - Make all file editing tools disabled and unavailable - ```json title="opencode.json" {4} { "$schema": "https://opencode.ai/config.json", @@ -46,88 +47,144 @@ Use the `permission.edit` key to control whether file editing operations require ### bash -Controls whether bash commands require user approval. +You can use the `permission.bash` key to control whether bash commands as a +whole need user approval. -:::tip -You can specify which commands you want to have run without approval. -::: - -This can be configured globally or with specific patterns. Setting this to `"ask"`, requiring approval for all bash commands. -Setting this to `"deny"` is the strictest option, blocking LLM from running that command or command pattern. - -For example. - -- **Ask for approval for all commands** - - ```json title="opencode.json" - { - "$schema": "https://opencode.ai/config.json", - "permission": { - "bash": "ask" - } - } - ``` - -- **Disable all Terraform commands** - - ```json title="opencode.json" - { - "$schema": "https://opencode.ai/config.json", - "permission": { - "bash": { - "terraform *": "deny" - } - } - } - ``` - -- **Approve specific commands** - - ```json title="opencode.json" - { - "$schema": "https://opencode.ai/config.json", - "permission": { - "bash": { - "git status": "allow", - "git diff": "allow", - "npm run build": "allow", - "ls": "allow", - "pwd": "allow" - } - } - } - ``` - -- **Use wildcard patterns to restrict specific commands** - - ```json title="opencode.json" - { - "$schema": "https://opencode.ai/config.json", - "permission": { - "bash": { - "git push": "ask", - "*": "allow" - } - } - } - ``` - This configuration allows all commands by default (`"*": "allow"`) but requires approval for `git push` commands. - -### Agents - -Configure agent specific permissions - -```json +```json title="opencode.json" {4} { "$schema": "https://opencode.ai/config.json", + "permission": { + "bash": "ask" + } +} +``` + +Or, you can target specific commands and set it to `allow`, `ask`, or `deny`. + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "permission": { + "bash": { + "git push": "ask", + "git status": "allow", + "git diff": "allow", + "npm run build": "allow", + "ls": "allow", + "pwd": "allow" + } + } +} +``` + +--- + +#### Wildcards + +You can also use wildcards to manage permissions for specific bash commands. + +:::tip +You can use wildcards to manage permissions for specific bash commands. +::: + +For example, **disable all** Terraform commands. + +```json title="opencode.json" {5} +{ + "$schema": "https://opencode.ai/config.json", + "permission": { + "bash": { + "terraform *": "deny" + } + } +} +``` + +You can also use the `*` wildcard to manage permissions for all commands. For +example, **deny all commands** except a couple of specific ones. + +```json title="opencode.json" {5} +{ + "$schema": "https://opencode.ai/config.json", + "permission": { + "bash": { + "*": "deny", + "pwd": "allow", + "git status": "ask" + } + } +} +``` + +Here a specific rule can override the `*` wildcard. + +--- + +##### Glob patterns + +The wildcard uses simple regex globbing patterns. + +- `*` matches zero or more of any character +- `?` matches exactly one character +- All other characters match literally + +--- + +### webfetch + +Use the `permission.webfetch` key to control whether the LLM can fetch web pages. + +```json title="opencode.json" {4} +{ + "$schema": "https://opencode.ai/config.json", + "permission": { + "webfetch": "ask" + } +} +``` + +--- + +## Agents + +You can also configure permissions per agent. Where the agent specific config +overrides the global config. [Learn more](/docs/agents#permissions) about agent permissions. + +```json title="opencode.json" {3-7,10-14} +{ + "$schema": "https://opencode.ai/config.json", + "permission": { + "bash": { + "git push": "ask" + } + }, "agent": { - "plan": { + "build": { "permission": { "bash": { - "echo *": "allow" + "git push": "allow" } } } } } ``` + +For example, here the `build` agent overrides the global `bash` permission to +allow `git push` commands. + +You can also configure permissions for agents in Markdown. + +```markdown title="~/.config/opencode/agent/review.md" +--- +description: Code review without edits +mode: subagent +permission: + edit: deny + bash: ask + webfetch: deny +--- + +Only analyze code and suggest changes. +``` + diff --git a/packages/web/src/styles/custom.css b/packages/web/src/styles/custom.css index 1dcc08904..8a0999032 100644 --- a/packages/web/src/styles/custom.css +++ b/packages/web/src/styles/custom.css @@ -367,7 +367,7 @@ nav.sidebar ul.top-level > li > details > summary .group-label > span { } .expressive-code { - margin: 12px 0 56px 0 !important; + margin: 0.75rem 0 3rem 0 !important; border-radius: 6px; } From b168bfe40df1ac9c3185766cdcaed688572c1a8a Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 8 Oct 2025 13:31:12 -0400 Subject: [PATCH 04/10] wip: zen --- .../console/app/src/routes/workspace/[id].tsx | 2 + .../workspace/provider-section.module.css | 107 +++ .../src/routes/workspace/provider-section.tsx | 163 ++++ .../migrations/0031_outgoing_outlaw_kid.sql | 11 + .../core/migrations/meta/0031_snapshot.json | 879 ++++++++++++++++++ .../core/migrations/meta/_journal.json | 7 + packages/console/core/src/identifier.ts | 1 + packages/console/core/src/provider.ts | 49 + .../console/core/src/schema/provider.sql.ts | 14 + 9 files changed, 1233 insertions(+) create mode 100644 packages/console/app/src/routes/workspace/provider-section.module.css create mode 100644 packages/console/app/src/routes/workspace/provider-section.tsx create mode 100644 packages/console/core/migrations/0031_outgoing_outlaw_kid.sql create mode 100644 packages/console/core/migrations/meta/0031_snapshot.json create mode 100644 packages/console/core/src/provider.ts create mode 100644 packages/console/core/src/schema/provider.sql.ts diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx index 952c1417d..a44ddd927 100644 --- a/packages/console/app/src/routes/workspace/[id].tsx +++ b/packages/console/app/src/routes/workspace/[id].tsx @@ -8,6 +8,7 @@ import { KeySection } from "./key-section" import { MemberSection } from "./member-section" import { SettingsSection } from "./settings-section" import { ModelSection } from "./model-section" +import { ProviderSection } from "./provider-section" import { Show } from "solid-js" import { createAsync, query, useParams } from "@solidjs/router" import { Actor } from "@opencode-ai/console-core/actor.js" @@ -52,6 +53,7 @@ export default function () { + diff --git a/packages/console/app/src/routes/workspace/provider-section.module.css b/packages/console/app/src/routes/workspace/provider-section.module.css new file mode 100644 index 000000000..5f18862f5 --- /dev/null +++ b/packages/console/app/src/routes/workspace/provider-section.module.css @@ -0,0 +1,107 @@ +.root { + [data-slot="providers-table"] { + overflow-x: auto; + } + + [data-slot="providers-table-element"] { + width: 100%; + border-collapse: collapse; + font-size: var(--font-size-sm); + + thead { + border-bottom: 1px solid var(--color-border); + } + + th { + padding: var(--space-3) var(--space-4); + text-align: left; + font-weight: normal; + color: var(--color-text-muted); + text-transform: uppercase; + } + + td { + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--color-border-muted); + color: var(--color-text-muted); + font-family: var(--font-mono); + + &[data-slot="provider-name"] { + color: var(--color-text); + font-family: var(--font-mono); + font-weight: 500; + } + + &[data-slot="provider-status"] { + text-align: left; + color: var(--color-text); + } + + &[data-slot="provider-toggle"] { + text-align: left; + font-family: var(--font-sans); + + [data-slot="edit-form"] { + display: flex; + flex-direction: column; + gap: var(--space-3); + + [data-slot="input-wrapper"] { + display: flex; + flex-direction: column; + gap: var(--space-1); + + input { + padding: var(--space-2) var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-bg); + color: var(--color-text); + font-size: var(--font-size-sm); + font-family: var(--font-mono); + + &:focus { + outline: none; + border-color: var(--color-accent); + } + + &::placeholder { + color: var(--color-text-disabled); + } + } + + [data-slot="form-error"] { + color: var(--color-danger); + font-size: var(--font-size-sm); + line-height: 1.4; + } + } + + [data-slot="form-actions"] { + display: flex; + gap: var(--space-2); + } + } + } + } + + tbody tr { + &[data-enabled="false"] { + opacity: 0.6; + } + + &:last-child td { + border-bottom: none; + } + } + + @media (max-width: 40rem) { + + th, + td { + padding: var(--space-2) var(--space-3); + font-size: var(--font-size-xs); + } + } + } +} \ No newline at end of file diff --git a/packages/console/app/src/routes/workspace/provider-section.tsx b/packages/console/app/src/routes/workspace/provider-section.tsx new file mode 100644 index 000000000..856b3a6a2 --- /dev/null +++ b/packages/console/app/src/routes/workspace/provider-section.tsx @@ -0,0 +1,163 @@ +import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router" +import { createEffect, For, Show } from "solid-js" +import { Provider } from "@opencode-ai/console-core/provider.js" +import { withActor } from "~/context/auth.withActor" +import { createStore } from "solid-js/store" +import styles from "./provider-section.module.css" + +const PROVIDERS = [ + { name: "OpenAI", key: "openai", prefix: "sk-" }, + { name: "Anthropic", key: "anthropic", prefix: "sk-ant-" }, +] as const + +type Provider = (typeof PROVIDERS)[number] + +const removeProvider = action(async (form: FormData) => { + "use server" + const provider = form.get("provider")?.toString() + if (!provider) return { error: "Provider is required" } + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required" } + return json(await withActor(() => Provider.remove({ provider }), workspaceID), { revalidate: listProviders.key }) +}, "provider.remove") + +const saveProvider = action(async (form: FormData) => { + "use server" + const provider = form.get("provider")?.toString() + const credentials = form.get("credentials")?.toString() + if (!provider) return { error: "Provider is required" } + if (!credentials) return { error: "API key is required" } + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required" } + return json( + await withActor( + () => + Provider.create({ provider, credentials }) + .then(() => ({ error: undefined })) + .catch((e) => ({ error: e.message as string })), + workspaceID, + ), + { revalidate: listProviders.key }, + ) +}, "provider.save") + +const listProviders = query(async (workspaceID: string) => { + "use server" + return withActor(() => Provider.list(), workspaceID) +}, "provider.list") + +function ProviderRow(props: { provider: Provider }) { + const params = useParams() + const providers = createAsync(() => listProviders(params.id)) + const saveSubmission = useSubmission(saveProvider, ([fd]) => fd.get("provider")?.toString() === props.provider.key) + const removeSubmission = useSubmission( + removeProvider, + ([fd]) => fd.get("provider")?.toString() === props.provider.key, + ) + const [store, setStore] = createStore({ editing: false }) + + let input: HTMLInputElement + + const isEnabled = () => providers()?.some((p) => p.provider === props.provider.key) + + createEffect(() => { + if (!saveSubmission.pending && saveSubmission.result && !saveSubmission.result.error) { + hide() + } + }) + + function show() { + while (true) { + saveSubmission.clear() + if (!saveSubmission.result) break + } + setStore("editing", true) + setTimeout(() => input?.focus(), 0) + } + + function hide() { + setStore("editing", false) + } + + return ( + + {props.provider.name} + {isEnabled() ? "Configured" : "Not Configured"} + + show()}> + Configure + + } + > +
+ + + +
+
+ } + > +
+
+ (input = r)} + name="credentials" + type="text" + placeholder={`Enter ${props.provider.name} API key (${props.provider.prefix}...)`} + autocomplete="off" + data-form-type="other" + data-lpignore="true" + /> + + {(err) =>
{err()}
} +
+
+ + +
+ + +
+
+ + + + ) +} + +export function ProviderSection() { + return ( +
+
+

Bring Your Own Key

+

Configure your own API keys from AI providers.

+
+
+ + + + + + + + + + {(provider) => } + +
ProviderStatusAction
+
+
+ ) +} diff --git a/packages/console/core/migrations/0031_outgoing_outlaw_kid.sql b/packages/console/core/migrations/0031_outgoing_outlaw_kid.sql new file mode 100644 index 000000000..c9f385e08 --- /dev/null +++ b/packages/console/core/migrations/0031_outgoing_outlaw_kid.sql @@ -0,0 +1,11 @@ +CREATE TABLE `provider` ( + `id` varchar(30) NOT NULL, + `workspace_id` varchar(30) NOT NULL, + `time_created` timestamp(3) NOT NULL DEFAULT (now()), + `time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + `time_deleted` timestamp(3), + `provider` varchar(64) NOT NULL, + `credentials` text NOT NULL, + CONSTRAINT `provider_workspace_id_id_pk` PRIMARY KEY(`workspace_id`,`id`), + CONSTRAINT `workspace_provider` UNIQUE(`workspace_id`,`provider`) +); diff --git a/packages/console/core/migrations/meta/0031_snapshot.json b/packages/console/core/migrations/meta/0031_snapshot.json new file mode 100644 index 000000000..ba964881d --- /dev/null +++ b/packages/console/core/migrations/meta/0031_snapshot.json @@ -0,0 +1,879 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "9dceb591-8e08-4991-a49c-1f1741ec1e57", + "prevId": "eae45fcf-dc0f-4756-bc5d-30791f2965a2", + "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 + }, + "invoice_id": { + "name": "invoice_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 + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(30)", + "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 + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "key_workspace_id_id_pk": { + "name": "key_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "model": { + "name": "model", + "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(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "model_workspace_model": { + "name": "model_workspace_model", + "columns": [ + "workspace_id", + "model" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_workspace_id_id_pk": { + "name": "model_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "provider": { + "name": "provider", + "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 + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentials": { + "name": "credentials", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "workspace_provider": { + "name": "workspace_provider", + "columns": [ + "workspace_id", + "provider" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "provider_workspace_id_id_pk": { + "name": "provider_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 + }, + "account_id": { + "name": "account_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "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 + }, + "role": { + "name": "role", + "type": "enum('admin','member')", + "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 + } + }, + "indexes": { + "user_account_id": { + "name": "user_account_id", + "columns": [ + "workspace_id", + "account_id" + ], + "isUnique": true + }, + "user_email": { + "name": "user_email", + "columns": [ + "workspace_id", + "email" + ], + "isUnique": true + }, + "global_account_id": { + "name": "global_account_id", + "columns": [ + "account_id" + ], + "isUnique": false + }, + "global_email": { + "name": "global_email", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "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": 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 + } + }, + "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": {} + } +} \ No newline at end of file diff --git a/packages/console/core/migrations/meta/_journal.json b/packages/console/core/migrations/meta/_journal.json index b178c11cc..b9fe352fe 100644 --- a/packages/console/core/migrations/meta/_journal.json +++ b/packages/console/core/migrations/meta/_journal.json @@ -218,6 +218,13 @@ "when": 1759878278492, "tag": "0030_ordinary_ultragirl", "breakpoints": true + }, + { + "idx": 31, + "version": "5", + "when": 1759940238478, + "tag": "0031_outgoing_outlaw_kid", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/console/core/src/identifier.ts b/packages/console/core/src/identifier.ts index 98c12a6c6..502ad6ac2 100644 --- a/packages/console/core/src/identifier.ts +++ b/packages/console/core/src/identifier.ts @@ -8,6 +8,7 @@ export namespace Identifier { key: "key", model: "mod", payment: "pay", + provider: "prv", usage: "usg", user: "usr", workspace: "wrk", diff --git a/packages/console/core/src/provider.ts b/packages/console/core/src/provider.ts new file mode 100644 index 000000000..1f8c07b9f --- /dev/null +++ b/packages/console/core/src/provider.ts @@ -0,0 +1,49 @@ +import { z } from "zod" +import { fn } from "./util/fn" +import { Actor } from "./actor" +import { and, Database, eq, isNull } from "./drizzle" +import { Identifier } from "./identifier" +import { ProviderTable } from "./schema/provider.sql" + +export namespace Provider { + export const list = fn(z.void(), () => + Database.use((tx) => + tx + .select() + .from(ProviderTable) + .where(and(eq(ProviderTable.workspaceID, Actor.workspace()), isNull(ProviderTable.timeDeleted))), + ), + ) + + export const create = fn( + z.object({ + provider: z.string().min(1).max(64), + credentials: z.string(), + }), + ({ provider, credentials }) => + Database.use((tx) => + tx + .insert(ProviderTable) + .values({ + id: Identifier.create("provider"), + workspaceID: Actor.workspace(), + provider, + credentials, + }) + .onDuplicateKeyUpdate({ + set: { + credentials, + timeDeleted: null, + }, + }), + ), + ) + + export const remove = fn(z.object({ provider: z.string() }), ({ provider }) => + Database.transaction((tx) => + tx + .delete(ProviderTable) + .where(and(eq(ProviderTable.provider, provider), eq(ProviderTable.workspaceID, Actor.workspace()))), + ), + ) +} diff --git a/packages/console/core/src/schema/provider.sql.ts b/packages/console/core/src/schema/provider.sql.ts new file mode 100644 index 000000000..11be5b4d7 --- /dev/null +++ b/packages/console/core/src/schema/provider.sql.ts @@ -0,0 +1,14 @@ +import { mysqlTable, text, uniqueIndex, varchar } from "drizzle-orm/mysql-core" +import { timestamps, workspaceColumns } from "../drizzle/types" +import { workspaceIndexes } from "./workspace.sql" + +export const ProviderTable = mysqlTable( + "provider", + { + ...workspaceColumns, + ...timestamps, + provider: varchar("provider", { length: 64 }).notNull(), + credentials: text("credentials").notNull(), + }, + (table) => [...workspaceIndexes(table), uniqueIndex("workspace_provider").on(table.workspaceID, table.provider)], +) From c93c0d402d3130c6e69129395993ac1776132234 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 8 Oct 2025 15:20:50 -0400 Subject: [PATCH 05/10] wip: zen --- .../console/app/src/routes/zen/handler.ts | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/console/app/src/routes/zen/handler.ts b/packages/console/app/src/routes/zen/handler.ts index a24a6240d..5e826c12b 100644 --- a/packages/console/app/src/routes/zen/handler.ts +++ b/packages/console/app/src/routes/zen/handler.ts @@ -13,6 +13,7 @@ import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.j import { ZenModel } from "@opencode-ai/console-core/model.js" import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js" +import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js" export async function handler( input: APIEvent, @@ -67,9 +68,10 @@ export async function handler( }) const modelInfo = validateModel(body.model) const providerInfo = selectProvider(modelInfo) - const authInfo = await authenticate(modelInfo) + const authInfo = await authenticate(modelInfo, providerInfo) validateBilling(modelInfo, authInfo) validateModelSettings(authInfo) + updateProviderKey(authInfo, providerInfo) logger.metric({ provider: providerInfo.id }) // Request to model provider @@ -232,7 +234,10 @@ export async function handler( return providers[Math.floor(Math.random() * providers.length)] } - async function authenticate(model: Awaited>) { + async function authenticate( + model: Awaited>, + providerInfo: Awaited>, + ) { const apiKey = opts.parseApiKey(input.request.headers) if (!apiKey) { if (model.allowAnonymous) return @@ -257,6 +262,9 @@ export async function handler( monthlyUsage: UserTable.monthlyUsage, timeMonthlyUsageUpdated: UserTable.timeMonthlyUsageUpdated, }, + provider: { + credentials: ProviderTable.credentials, + }, timeDisabled: ModelTable.timeCreated, }) .from(KeyTable) @@ -264,6 +272,10 @@ export async function handler( .innerJoin(BillingTable, eq(BillingTable.workspaceID, KeyTable.workspaceID)) .innerJoin(UserTable, and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID))) .leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, model.id))) + .leftJoin( + ProviderTable, + and(eq(ProviderTable.workspaceID, KeyTable.workspaceID), eq(ProviderTable.provider, providerInfo.id)), + ) .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted))) .then((rows) => rows[0]), ) @@ -279,6 +291,7 @@ export async function handler( workspaceID: data.workspaceID, billing: data.billing, user: data.user, + provider: data.provider, isFree: FREE_WORKSPACES.includes(data.workspaceID), isDisabled: !!data.timeDisabled, } @@ -327,6 +340,15 @@ export async function handler( if (authInfo.isDisabled) throw new ModelError("Model is disabled") } + function updateProviderKey( + authInfo: Awaited>, + providerInfo: Awaited>, + ) { + if (!authInfo) return + if (!authInfo.provider?.credentials) return + providerInfo.apiKey = authInfo.provider.credentials + } + async function trackUsage( authInfo: Awaited>, modelInfo: ReturnType, @@ -389,7 +411,7 @@ export async function handler( if (!authInfo) return - const cost = authInfo.isFree ? 0 : centsToMicroCents(totalCostInCent) + const cost = authInfo.isFree || authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent) await Database.transaction(async (tx) => { await tx.insert(UsageTable).values({ workspaceID: authInfo.workspaceID, @@ -441,6 +463,8 @@ export async function handler( async function reload(authInfo: Awaited>) { if (!authInfo) return + if (authInfo.isFree) return + if (authInfo.provider?.credentials) return const lock = await Database.use((tx) => tx From d18b6673e6f81472bf4486d911f20562c3c7ef91 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 8 Oct 2025 17:03:42 -0400 Subject: [PATCH 06/10] wip: zen --- .../app/src/routes/workspace/key-section.tsx | 50 ++++++++++--------- packages/console/core/src/key.ts | 40 ++++++++++++--- 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/packages/console/app/src/routes/workspace/key-section.tsx b/packages/console/app/src/routes/workspace/key-section.tsx index e94d2bf86..1c2316db7 100644 --- a/packages/console/app/src/routes/workspace/key-section.tsx +++ b/packages/console/app/src/routes/workspace/key-section.tsx @@ -7,6 +7,11 @@ import { createStore } from "solid-js/store" import { formatDateUTC, formatDateForTable } from "./common" import styles from "./key-section.module.css" import { Actor } from "@opencode-ai/console-core/actor.js" +import { and, Database, eq, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js" +import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js" +import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" +import { AccountTable } from "@opencode-ai/console-core/schema/account.sql.js" +import { User } from "@opencode-ai/console-core/user.js" const removeKey = action(async (form: FormData) => { "use server" @@ -108,11 +113,6 @@ export function KeySection() { const params = useParams() const keys = createAsync(() => listKeys(params.id)) - function formatKey(key: string) { - if (key.length <= 11) return key - return `${key.slice(0, 7)}...${key.slice(-4)}` - } - return (
@@ -134,7 +134,8 @@ export function KeySection() { Name Key - Created + Created By + Last Used @@ -147,24 +148,27 @@ export function KeySection() { {key.name} - + {key.keyDisplay}}> + + - - {formatDateForTable(key.timeCreated)} + {key.email} + + {key.timeUsed ? formatDateForTable(key.timeUsed) : "-"}
diff --git a/packages/console/core/src/key.ts b/packages/console/core/src/key.ts index 938c1ae8f..3a4426d28 100644 --- a/packages/console/core/src/key.ts +++ b/packages/console/core/src/key.ts @@ -4,19 +4,45 @@ import { Actor } from "./actor" import { and, Database, eq, isNull, sql } from "./drizzle" import { Identifier } from "./identifier" import { KeyTable } from "./schema/key.sql" +import { AccountTable } from "./schema/account.sql" +import { UserTable } from "./schema/user.sql" +import { User } from "./user" export namespace Key { - export const list = async () => { - const workspace = Actor.workspace() + export const list = fn(z.void(), async () => { + const userID = Actor.assert("user").properties.userID + const user = await User.fromID(userID) const keys = await Database.use((tx) => tx - .select() + .select({ + id: KeyTable.id, + name: KeyTable.name, + key: KeyTable.key, + timeUsed: KeyTable.timeUsed, + userID: KeyTable.userID, + email: AccountTable.email, + }) .from(KeyTable) - .where(and(eq(KeyTable.workspaceID, workspace), isNull(KeyTable.timeDeleted))) - .orderBy(sql`${KeyTable.timeCreated} DESC`), + .innerJoin(UserTable, and(eq(KeyTable.userID, UserTable.id), eq(KeyTable.workspaceID, UserTable.workspaceID))) + .innerJoin(AccountTable, eq(UserTable.accountID, AccountTable.id)) + .where( + and( + ...[ + eq(KeyTable.workspaceID, Actor.workspace()), + isNull(KeyTable.timeDeleted), + ...(user.role === "admin" ? [] : [eq(KeyTable.userID, userID)]), + ], + ), + ) + .orderBy(sql`${KeyTable.name} DESC`), ) - return keys - } + // only return value for user's keys + return keys.map((key) => ({ + ...key, + key: key.userID === userID ? key.key : undefined, + keyDisplay: `${key.key.slice(0, 7)}...${key.key.slice(-4)}`, + })) + }) export const create = fn( z.object({ From 5b1fd7e5397eb72a6f710a3d9c183f2c5dbf3562 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 8 Oct 2025 18:59:41 -0400 Subject: [PATCH 07/10] wip: zen --- packages/console/app/src/context/auth.ts | 1 + .../console/app/src/routes/workspace/[id].tsx | 4 +- .../app/src/routes/workspace/key-section.tsx | 5 -- .../src/routes/workspace/member-section.tsx | 88 +++++++++---------- packages/console/core/src/actor.ts | 10 +++ packages/console/core/src/key.ts | 20 +++-- packages/console/core/src/user.ts | 23 ++--- 7 files changed, 79 insertions(+), 72 deletions(-) diff --git a/packages/console/app/src/context/auth.ts b/packages/console/app/src/context/auth.ts index 14f275565..c177049c3 100644 --- a/packages/console/app/src/context/auth.ts +++ b/packages/console/app/src/context/auth.ts @@ -74,6 +74,7 @@ export const getActor = async (workspace?: string): Promise => { userID: user.id, workspaceID: user.workspaceID, accountID: user.accountID, + role: user.role, }, } } diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx index a44ddd927..15aeb57a0 100644 --- a/packages/console/app/src/routes/workspace/[id].tsx +++ b/packages/console/app/src/routes/workspace/[id].tsx @@ -48,10 +48,12 @@ export default function () {
+ + + - diff --git a/packages/console/app/src/routes/workspace/key-section.tsx b/packages/console/app/src/routes/workspace/key-section.tsx index 1c2316db7..3b7e399aa 100644 --- a/packages/console/app/src/routes/workspace/key-section.tsx +++ b/packages/console/app/src/routes/workspace/key-section.tsx @@ -7,11 +7,6 @@ import { createStore } from "solid-js/store" import { formatDateUTC, formatDateForTable } from "./common" import styles from "./key-section.module.css" import { Actor } from "@opencode-ai/console-core/actor.js" -import { and, Database, eq, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js" -import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js" -import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" -import { AccountTable } from "@opencode-ai/console-core/schema/account.sql.js" -import { User } from "@opencode-ai/console-core/user.js" const removeKey = action(async (form: FormData) => { "use server" diff --git a/packages/console/app/src/routes/workspace/member-section.tsx b/packages/console/app/src/routes/workspace/member-section.tsx index 6774bb48e..b13e8e5ed 100644 --- a/packages/console/app/src/routes/workspace/member-section.tsx +++ b/packages/console/app/src/routes/workspace/member-section.tsx @@ -10,10 +10,10 @@ import { User } from "@opencode-ai/console-core/user.js" const listMembers = query(async (workspaceID: string) => { "use server" return withActor(async () => { - const actor = Actor.assert("user") return { members: await User.list(), - currentUserID: actor.properties.userID, + actorID: Actor.userID(), + actorRole: Actor.userRole(), } }, workspaceID) }, "member.list") @@ -158,10 +158,11 @@ export function MemberCreateForm() { ) } -function MemberRow(props: { member: any; workspaceID: string; currentUserID: string | null }) { +function MemberRow(props: { member: any; workspaceID: string; actorID: string; actorRole: string }) { const [editing, setEditing] = createSignal(false) const submission = useSubmission(updateMember) - const isCurrentUser = () => props.currentUserID === props.member.id + const isCurrentUser = () => props.actorID === props.member.id + const isAdmin = () => props.actorRole === "admin" createEffect(() => { if (!submission.pending && submission.result && !submission.result.error) { @@ -200,19 +201,19 @@ function MemberRow(props: { member: any; workspaceID: string; currentUserID: str {props.member.accountEmail ?? props.member.email} {props.member.role} {getUsageDisplay()} - }> - invited - + {props.member.timeSeen ? "" : "invited"} - - - - - - - + + + +
+ + + +
+
@@ -293,37 +294,34 @@ export function MemberSection() {

Members

-

Manage your members for accessing opencode services.

- + + +
- -

Invite a member to your workspace

-
- } - > - - - - - - - - - - - - - {(member) => ( - - )} - - -
EmailRoleUsage
- + + + + + + + + + + + + + {(member) => ( + + )} + + +
EmailRoleUsage
) diff --git a/packages/console/core/src/actor.ts b/packages/console/core/src/actor.ts index ae11335f8..88c5e4b51 100644 --- a/packages/console/core/src/actor.ts +++ b/packages/console/core/src/actor.ts @@ -1,4 +1,5 @@ import { Context } from "./context" +import { UserRole } from "./schema/user.sql" import { Log } from "./util/log" export namespace Actor { @@ -21,6 +22,7 @@ export namespace Actor { userID: string workspaceID: string accountID: string + role: (typeof UserRole)[number] } } @@ -80,4 +82,12 @@ export namespace Actor { } throw new Error(`actor of type "${actor.type}" is not associated with an account`) } + + export function userID() { + return Actor.assert("user").properties.userID + } + + export function userRole() { + return Actor.assert("user").properties.role + } } diff --git a/packages/console/core/src/key.ts b/packages/console/core/src/key.ts index 3a4426d28..e2d5c5eff 100644 --- a/packages/console/core/src/key.ts +++ b/packages/console/core/src/key.ts @@ -10,8 +10,6 @@ import { User } from "./user" export namespace Key { export const list = fn(z.void(), async () => { - const userID = Actor.assert("user").properties.userID - const user = await User.fromID(userID) const keys = await Database.use((tx) => tx .select({ @@ -30,7 +28,7 @@ export namespace Key { ...[ eq(KeyTable.workspaceID, Actor.workspace()), isNull(KeyTable.timeDeleted), - ...(user.role === "admin" ? [] : [eq(KeyTable.userID, userID)]), + ...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]), ], ), ) @@ -39,7 +37,7 @@ export namespace Key { // only return value for user's keys return keys.map((key) => ({ ...key, - key: key.userID === userID ? key.key : undefined, + key: key.userID === Actor.userID() ? key.key : undefined, keyDisplay: `${key.key.slice(0, 7)}...${key.key.slice(-4)}`, })) }) @@ -78,14 +76,22 @@ export namespace Key { ) export const remove = fn(z.object({ id: z.string() }), async (input) => { - const workspace = Actor.workspace() - await Database.transaction((tx) => + // only admin can remove other user's keys + await Database.use((tx) => tx .update(KeyTable) .set({ timeDeleted: sql`now()`, }) - .where(and(eq(KeyTable.id, input.id), eq(KeyTable.workspaceID, workspace))), + .where( + and( + ...[ + eq(KeyTable.id, input.id), + eq(KeyTable.workspaceID, Actor.workspace()), + ...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]), + ], + ), + ), ) }) } diff --git a/packages/console/core/src/user.ts b/packages/console/core/src/user.ts index 5e7605e94..38c8e5e3a 100644 --- a/packages/console/core/src/user.ts +++ b/packages/console/core/src/user.ts @@ -1,5 +1,5 @@ import { z } from "zod" -import { and, eq, getTableColumns, inArray, isNull, or, sql } from "drizzle-orm" +import { and, eq, getTableColumns, isNull, sql } from "drizzle-orm" import { fn } from "./util/fn" import { Database } from "./drizzle" import { UserRole, UserTable } from "./schema/user.sql" @@ -13,19 +13,14 @@ import { Key } from "./key" import { KeyTable } from "./schema/key.sql" export namespace User { - const assertAdmin = async () => { - const actor = Actor.assert("user") - const user = await User.fromID(actor.properties.userID) - if (user?.role !== "admin") { - throw new Error(`Expected admin user, got ${user?.role}`) - } + const assertAdmin = () => { + if (Actor.userRole() === "admin") return + throw new Error(`Expected admin user, got ${Actor.userRole()}`) } const assertNotSelf = (id: string) => { - const actor = Actor.assert("user") - if (actor.properties.userID === id) { - throw new Error(`Expected not self actor, got self actor`) - } + if (Actor.userID() !== id) return + throw new Error(`Expected not self actor, got self actor`) } export const list = fn(z.void(), () => @@ -70,7 +65,7 @@ export namespace User { role: z.enum(UserRole), }), async ({ email, role }) => { - await assertAdmin() + assertAdmin() const workspaceID = Actor.workspace() // create user @@ -181,7 +176,7 @@ export namespace User { monthlyLimit: z.number().nullable(), }), async ({ id, role, monthlyLimit }) => { - await assertAdmin() + assertAdmin() if (role === "member") assertNotSelf(id) return await Database.use((tx) => tx @@ -193,7 +188,7 @@ export namespace User { ) export const remove = fn(z.string(), async (id) => { - await assertAdmin() + assertAdmin() assertNotSelf(id) return await Database.use((tx) => From 3ed4f1078fd708847b3e36b73d2ac7615be478df Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 8 Oct 2025 22:33:20 -0400 Subject: [PATCH 08/10] wip: zen --- .../console/app/src/routes/zen/handler.ts | 1 + .../migrations/0032_white_doctor_doom.sql | 1 + .../core/migrations/meta/0032_snapshot.json | 886 ++++++++++++++++++ .../core/migrations/meta/_journal.json | 7 + .../console/core/src/schema/billing.sql.ts | 3 +- 5 files changed, 897 insertions(+), 1 deletion(-) create mode 100644 packages/console/core/migrations/0032_white_doctor_doom.sql create mode 100644 packages/console/core/migrations/meta/0032_snapshot.json diff --git a/packages/console/app/src/routes/zen/handler.ts b/packages/console/app/src/routes/zen/handler.ts index 5e826c12b..1f93d971c 100644 --- a/packages/console/app/src/routes/zen/handler.ts +++ b/packages/console/app/src/routes/zen/handler.ts @@ -425,6 +425,7 @@ export async function handler( cacheWrite5mTokens, cacheWrite1hTokens, cost, + keyID: authInfo.apiKeyId, }) await tx .update(BillingTable) diff --git a/packages/console/core/migrations/0032_white_doctor_doom.sql b/packages/console/core/migrations/0032_white_doctor_doom.sql new file mode 100644 index 000000000..cc8416352 --- /dev/null +++ b/packages/console/core/migrations/0032_white_doctor_doom.sql @@ -0,0 +1 @@ +ALTER TABLE `usage` ADD `key_id` varchar(30); \ No newline at end of file diff --git a/packages/console/core/migrations/meta/0032_snapshot.json b/packages/console/core/migrations/meta/0032_snapshot.json new file mode 100644 index 000000000..344fde6fd --- /dev/null +++ b/packages/console/core/migrations/meta/0032_snapshot.json @@ -0,0 +1,886 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "b2406421-f22d-4153-a2a4-6deafe70ee54", + "prevId": "9dceb591-8e08-4991-a49c-1f1741ec1e57", + "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 + }, + "invoice_id": { + "name": "invoice_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 + }, + "key_id": { + "name": "key_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "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 + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(30)", + "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 + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "key_workspace_id_id_pk": { + "name": "key_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "model": { + "name": "model", + "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(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "model_workspace_model": { + "name": "model_workspace_model", + "columns": [ + "workspace_id", + "model" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_workspace_id_id_pk": { + "name": "model_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "provider": { + "name": "provider", + "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 + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentials": { + "name": "credentials", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "workspace_provider": { + "name": "workspace_provider", + "columns": [ + "workspace_id", + "provider" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "provider_workspace_id_id_pk": { + "name": "provider_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 + }, + "account_id": { + "name": "account_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "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 + }, + "role": { + "name": "role", + "type": "enum('admin','member')", + "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 + } + }, + "indexes": { + "user_account_id": { + "name": "user_account_id", + "columns": [ + "workspace_id", + "account_id" + ], + "isUnique": true + }, + "user_email": { + "name": "user_email", + "columns": [ + "workspace_id", + "email" + ], + "isUnique": true + }, + "global_account_id": { + "name": "global_account_id", + "columns": [ + "account_id" + ], + "isUnique": false + }, + "global_email": { + "name": "global_email", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "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": 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 + } + }, + "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": {} + } +} \ No newline at end of file diff --git a/packages/console/core/migrations/meta/_journal.json b/packages/console/core/migrations/meta/_journal.json index b9fe352fe..ab781877f 100644 --- a/packages/console/core/migrations/meta/_journal.json +++ b/packages/console/core/migrations/meta/_journal.json @@ -225,6 +225,13 @@ "when": 1759940238478, "tag": "0031_outgoing_outlaw_kid", "breakpoints": true + }, + { + "idx": 32, + "version": "5", + "when": 1759976329502, + "tag": "0032_white_doctor_doom", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/console/core/src/schema/billing.sql.ts b/packages/console/core/src/schema/billing.sql.ts index 302e01133..215f0a472 100644 --- a/packages/console/core/src/schema/billing.sql.ts +++ b/packages/console/core/src/schema/billing.sql.ts @@ -1,5 +1,5 @@ import { bigint, boolean, int, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core" -import { timestamps, utc, workspaceColumns } from "../drizzle/types" +import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types" import { workspaceIndexes } from "./workspace.sql" export const BillingTable = mysqlTable( @@ -50,6 +50,7 @@ export const UsageTable = mysqlTable( cacheWrite5mTokens: int("cache_write_5m_tokens"), cacheWrite1hTokens: int("cache_write_1h_tokens"), cost: bigint("cost", { mode: "number" }).notNull(), + keyID: ulid("key_id"), }, (table) => [...workspaceIndexes(table)], ) From c5b5795636c16574c1b9d7bc8529c77a2e41db08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Haris=20Gu=C5=A1i=C4=87?= Date: Thu, 9 Oct 2025 07:46:19 +0200 Subject: [PATCH 09/10] fix: process.stdout.write instead of console.log for export cmd (#3049) --- packages/opencode/src/cli/cmd/export.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/export.ts b/packages/opencode/src/cli/cmd/export.ts index 996522b5a..c8825c83d 100644 --- a/packages/opencode/src/cli/cmd/export.ts +++ b/packages/opencode/src/cli/cmd/export.ts @@ -66,7 +66,8 @@ export const ExportCommand = cmd({ })), } - console.log(JSON.stringify(exportData, null, 2)) + process.stdout.write(JSON.stringify(exportData, null, 2)) + process.stdout.write("\n") } catch (error) { UI.error(`Session not found: ${sessionID!}`) process.exit(1) From c0bd29155d6397af60a7a23bd357a1b6b3417734 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 9 Oct 2025 04:21:51 -0400 Subject: [PATCH 10/10] lsp: simplify language server root detection to use lock files Improves project boundary detection by focusing on package manager lock files instead of config files, providing more reliable workspace identification across different project types. --- packages/opencode/src/lsp/server.ts | 46 +++-------------------------- 1 file changed, 4 insertions(+), 42 deletions(-) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index c680c2896..591910236 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -44,7 +44,7 @@ export namespace LSPServer { export const Typescript: Info = { id: "typescript", - root: NearestRoot(["tsconfig.json", "package.json", "jsconfig.json"]), + root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"], async spawn(root) { const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {}) @@ -70,20 +70,7 @@ export namespace LSPServer { export const Vue: Info = { id: "vue", extensions: [".vue"], - root: NearestRoot([ - "tsconfig.json", - "jsconfig.json", - "package.json", - "pnpm-lock.yaml", - "yarn.lock", - "bun.lockb", - "bun.lock", - "vite.config.ts", - "vite.config.js", - "nuxt.config.ts", - "nuxt.config.js", - "vue.config.js", - ]), + root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), async spawn(root) { let binary = Bun.which("vue-language-server") const args: string[] = [] @@ -131,20 +118,7 @@ export namespace LSPServer { export const ESLint: Info = { id: "eslint", - root: NearestRoot([ - "eslint.config.js", - "eslint.config.mjs", - "eslint.config.cjs", - "eslint.config.ts", - "eslint.config.mts", - "eslint.config.cts", - ".eslintrc.js", - ".eslintrc.cjs", - ".eslintrc.yaml", - ".eslintrc.yml", - ".eslintrc.json", - "package.json", - ]), + root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"], async spawn(root) { const eslint = await Bun.resolve("eslint", Instance.directory).catch(() => {}) @@ -659,19 +633,7 @@ export namespace LSPServer { export const Svelte: Info = { id: "svelte", extensions: [".svelte"], - root: NearestRoot([ - "tsconfig.json", - "jsconfig.json", - "package.json", - "pnpm-lock.yaml", - "yarn.lock", - "bun.lockb", - "bun.lock", - "vite.config.ts", - "vite.config.js", - "svelte.config.ts", - "svelte.config.js", - ]), + root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), async spawn(root) { let binary = Bun.which("svelteserver") const args: string[] = []