Compare commits

..
Author SHA1 Message Date
opencode 7b62c10553 release: v0.6.1 2025-09-01 22:07:53 +00:00
Dax Raad 61c7196bd9 catch migration failures 2025-09-01 18:00:40 -04:00
opencode 365fdd9ff8 release: v0.6.0 2025-09-01 21:43:13 +00:00
Dax Raad f6bc9238df docs: sdk 2025-09-01 17:35:52 -04:00
Aiden ClineandGitHub 26f75d4e68 fix: tui attachment bound (#2361) 2025-09-01 16:33:36 -05:00
Jay V 8ba8d3c7e3 docs: update email 2025-09-01 17:30:32 -04:00
DaxandGitHub f993541e0b Refactor to support multiple instances inside single opencode process (#2360)
This release has a bunch of minor breaking changes if you are using opencode plugins or sdk

1. storage events have been removed (we might bring this back but had some issues)
2. concept of `app` is gone - there is a new concept called `project` and endpoints to list projects and get the current project
3. plugin receives `directory` which is cwd and `worktree` which is where the root of the project is if it's a git repo
4. the session.chat function has been renamed to session.prompt in sdk. it no longer requires model to be passed in (model is now an object)
5. every endpoint takes an optional `directory` parameter to operate as though opencode is running in that directory
2025-09-01 17:15:49 -04:00
Aiden ClineandGitHub e2df3eb44d add --command to opencode run (#2348) 2025-09-01 14:19:18 -05:00
Dax Raad 38f9ce05f6 wip: cloud 2025-09-01 11:53:43 -04:00
Dax Raad a6e09363b8 wip: cloud 2025-09-01 11:47:58 -04:00
GitHub Action 49629bb58e ignore: update download stats 2025-09-01 2025-09-01 12:04:32 +00:00
Dax Raad 2bb5b9b13a wip: cloud 2025-09-01 04:03:07 -04:00
Dax Raad 41338d1bf9 wip: cloud 2025-09-01 03:55:48 -04:00
Dax Raad 41ee9c94c7 wip: cloud 2025-09-01 03:53:49 -04:00
Dax Raad 9c16db0f36 wip: cloud 2025-09-01 03:51:45 -04:00
Dax Raad 721869353b wip: sync 2025-09-01 03:15:38 -04:00
Dax Raad 6d22ade771 wip: cloud 2025-09-01 03:13:05 -04:00
Dax Raad fbcceeb781 wip: cloud 2025-09-01 03:10:40 -04:00
Dax Raad 95775d68b7 wip: cloud 2025-09-01 03:04:49 -04:00
Dax Raad cf11669618 wip: cloud 2025-09-01 03:04:07 -04:00
Dax Raad 65dc19e85a wip: cloud 2025-09-01 02:57:47 -04:00
Dax Raad cfcfceca6d wip: cloud 2025-09-01 02:55:10 -04:00
Dax Raad 9f8899a9f9 wip: cloud 2025-09-01 02:28:21 -04:00
Dax Raad 449a063fe2 wip: cloud 2025-09-01 02:21:36 -04:00
Régis BlancandGitHub 37530359ee fix: ensure gopls lsp id matches docs (#2344) 2025-08-31 21:52:08 -05:00
Aiden ClineandGitHub 65f0bea146 ignore: better error logging (#2346) 2025-08-31 17:11:04 -05:00
Beshoy GirgisandGitHub e4cc05a975 feat: Allow provider timeout override (#1982) 2025-08-31 14:06:02 -04:00
Aiden ClineandGitHub 029612d8d5 fix: ensure shell cmds can be properly aborted (#2339) 2025-08-31 12:48:30 -05:00
Aiden ClineandGitHub e9826e8a22 fix: adjust title generation prompt to prevent direct response instead of title gen (#2338) 2025-08-31 11:01:19 -05:00
GitHub Action ad5f209dc8 ignore: update download stats 2025-08-31 2025-08-31 12:04:06 +00:00
fcfeac57c5 fix: resolve virtual envs for python LSP (#2155)
Co-authored-by: rekram1-node <aidenpcline@gmail.com>
2025-08-30 23:53:03 -05:00
Aiden ClineandGitHub 2946898934 fix: ensure command uses currently selected model (#2336) 2025-08-30 15:41:06 -05:00
Aiden ClineandGitHub b4d95545e0 add support for lsp workspace/didChangeConfiguration (#2334) 2025-08-30 14:49:13 -05:00
Jay V d3bbaa141c ignore: cloud 2025-08-30 15:28:35 -04:00
Jay V 8714f23509 ignore: cloud styles 2025-08-30 15:27:46 -04:00
52 changed files with 1144 additions and 884 deletions
+2
View File
@@ -64,3 +64,5 @@
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
+1 -1
View File
@@ -6,7 +6,7 @@
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "vinxi build",
"start": "vinxi start",
"version": "0.5.29"
"version": "0.6.1"
},
"dependencies": {
"@ibm/plex": "6.4.1",
+1 -1
View File
@@ -52,7 +52,7 @@ export const getActor = query(async (): Promise<Actor.Info> => {
}
}
const workspaceHint = splits[1]
const accounts = Object.keys(auth.data.account)
const accounts = Object.keys(auth.data.account ?? {})
const result = await Database.transaction(async (tx) => {
return await tx
.select({
+3 -1
View File
@@ -20,4 +20,6 @@ export default createHandler(() => (
</html>
)}
/>
))
), {
mode: "sync",
})
+6 -2
View File
@@ -1,9 +1,13 @@
import type { APIEvent } from "@solidjs/start/server"
import { Resource } from "@opencode/cloud-resource"
import { json } from "@solidjs/router"
import { Database } from "@opencode/cloud-core/drizzle/index.js"
import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
export async function GET(evt: APIEvent) {
return json({
data: Resource.Database.host,
data: await Database.use(async (tx) => {
const result = await tx.$count(UserTable)
return result
}),
})
}
+3
View File
@@ -54,7 +54,10 @@
a,
button {
appearance: none;
background: none;
border: none;
cursor: pointer;
padding: 0;
color: var(--color-text);
text-decoration: underline;
+18 -2
View File
@@ -1,7 +1,20 @@
import "./workspace.css"
import { useAuthSession } from "~/context/auth.session"
import { IconLogo } from "../component/icon"
import { action, redirect, RouteSectionProps } from "@solidjs/router"
import { withActor } from "~/context/auth.withActor"
import "./workspace.css"
import { query, action, redirect, createAsync, RouteSectionProps } from "@solidjs/router"
import { User } from "@opencode/cloud-core/user.js"
import { Actor } from "@opencode/cloud-core/actor.js"
const getUserInfo = query(async () => {
"use server"
return withActor(async () => {
const actor = Actor.assert("user")
const user = await User.fromID(actor.properties.userID)
return { user }
})
}, "userInfo")
const logout = action(async () => {
"use server"
@@ -17,6 +30,9 @@ const logout = action(async () => {
})
export default function WorkspaceLayout(props: RouteSectionProps) {
const userInfo = createAsync(() => getUserInfo(), {
deferStream: true,
})
return (
<main data-page="workspace">
<header data-component="workspace-header">
@@ -26,7 +42,7 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
</a>
</div>
<div data-slot="header-actions">
<span>name@example.com</span>
<span>{userInfo()?.user.email}</span>
<form action={logout} method="post">
<button type="submit" formaction={logout}>Logout</button>
</form>
+386 -389
View File
@@ -23,453 +23,450 @@
padding-bottom: var(--space-16);
}
}
}
/* Common elements */
button {
padding: var(--space-3) var(--space-4);
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-sans);
font-weight: 500;
text-transform: uppercase;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background-color: var(--color-surface-hover);
border-color: var(--color-accent);
}
&:active {
transform: translateY(1px);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
&:hover {
background-color: var(--color-bg);
border-color: var(--color-border);
transform: none;
}
}
&[color="primary"] {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-primary-text);
&:hover {
background-color: var(--color-primary-hover);
border-color: var(--color-primary-hover);
}
}
&[color="ghost"] {
background-color: transparent;
border-color: transparent;
color: var(--color-text-muted);
/* Common elements */
button {
padding: var(--space-3) var(--space-4);
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-sans);
font-weight: 500;
text-transform: uppercase;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background-color: var(--color-surface-hover);
border-color: var(--color-border);
color: var(--color-text);
border-color: var(--color-accent);
}
}
}
a {
color: var(--color-text);
text-decoration: underline;
text-underline-offset: var(--space-0-75);
text-decoration-thickness: 1px;
}
&:active {
transform: translateY(1px);
}
[data-slot="empty-state"] {
padding: var(--space-20) var(--space-6);
text-align: center;
border: 1px dashed var(--color-border);
border-radius: var(--border-radius-sm);
display: flex;
flex-direction: column;
gap: var(--space-2);
&:disabled {
opacity: 0.5;
cursor: not-allowed;
p {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
margin: 0;
}
}
&:hover {
background-color: var(--color-bg);
border-color: var(--color-border);
transform: none;
}
}
/* Title section */
[data-slot="title-section"] {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding-bottom: var(--space-8);
border-bottom: 1px solid var(--color-border);
&[data-color="primary"] {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-primary-text);
h1 {
font-size: var(--font-size-2xl);
font-weight: 500;
line-height: 1.2;
letter-spacing: -0.03125rem;
margin: 0;
text-transform: uppercase;
&:hover {
background-color: var(--color-primary-hover);
border-color: var(--color-primary-hover);
}
}
@media (max-width: 30rem) {
font-size: var(--font-size-xl);
line-height: 1.25;
&[data-color="ghost"] {
background-color: transparent;
border-color: transparent;
color: var(--color-text-muted);
&:hover {
background-color: var(--color-surface-hover);
border-color: var(--color-border);
color: var(--color-text);
}
}
}
p {
font-size: var(--font-size-md);
color: var(--color-text-muted);
a {
color: var(--color-text);
text-decoration: underline;
text-underline-offset: var(--space-0-75);
text-decoration-thickness: 1px;
}
a {
[data-slot="empty-state"] {
padding: var(--space-20) var(--space-6);
text-align: center;
border: 1px dashed var(--color-border);
border-radius: var(--border-radius-sm);
display: flex;
flex-direction: column;
gap: var(--space-2);
p {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
margin: 0;
}
}
/* Title section */
[data-slot="title-section"] {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding-bottom: var(--space-8);
border-bottom: 1px solid var(--color-border);
h1 {
font-size: var(--font-size-2xl);
font-weight: 500;
line-height: 1.2;
letter-spacing: -0.03125rem;
margin: 0;
text-transform: uppercase;
@media (max-width: 30rem) {
font-size: var(--font-size-xl);
line-height: 1.25;
}
}
p {
font-size: var(--font-size-md);
color: var(--color-text-muted);
a {
color: var(--color-text-muted);
}
}
}
/* Section titles */
[data-slot="section-title"] {
display: flex;
flex-direction: column;
gap: var(--space-1);
h2 {
font-size: var(--font-size-md);
font-weight: 600;
line-height: 1.2;
letter-spacing: -0.03125rem;
margin: 0;
color: var(--color-text-secondary);
text-transform: uppercase;
@media (max-width: 30rem) {
font-size: var(--font-size-lg);
line-height: 1.25;
}
}
p {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
}
}
/* Section titles */
[data-slot="section-title"] {
display: flex;
flex-direction: column;
gap: var(--space-1);
h2 {
font-size: var(--font-size-md);
font-weight: 600;
line-height: 1.2;
letter-spacing: -0.03125rem;
margin: 0;
color: var(--color-text-secondary);
text-transform: uppercase;
@media (max-width: 30rem) {
font-size: var(--font-size-lg);
line-height: 1.25;
}
}
p {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
}
/* API Keys Section */
[data-slot="api-keys-section"] {
[data-slot="create-form"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-4);
background-color: var(--color-bg-surface);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
max-width: 32rem;
input {
padding: var(--space-2) var(--space-3);
/* API Keys Section */
[data-slot="api-keys-section"] {
[data-slot="create-form"] {
display: flex;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
color: var(--color-text);
input {
flex: 1;
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-actions"] {
display: flex;
gap: var(--space-2);
}
}
[data-slot="api-keys-table"] {
overflow-x: auto;
}
[data-slot="api-keys-table-element"] {
width: 100%;
border-collapse: collapse;
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-actions"] {
display: flex;
gap: var(--space-2);
justify-content: flex-end;
}
}
[data-slot="api-keys-table"] {
overflow-x: auto;
}
[data-slot="api-keys-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="key-name"] {
color: var(--color-text);
font-family: var(--font-sans);
font-weight: 500;
}
&[data-slot="key-value"] {
font-family: var(--font-mono);
div {
cursor: pointer;
display: flex;
align-items: center;
gap: var(--space-2);
}
}
&[data-slot="key-date"] {
color: var(--color-text);
}
&[data-slot="key-actions"] {
font-family: var(--font-sans);
}
}
tbody tr {
&: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);
thead {
border-bottom: 1px solid var(--color-border);
}
th {
&:nth-child(3) /* Date */ {
display: none;
}
padding: var(--space-3) var(--space-4);
text-align: left;
font-weight: normal;
color: var(--color-text-muted);
text-transform: uppercase;
}
td {
&:nth-child(3) /* Date */ {
display: none;
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="key-name"] {
color: var(--color-text);
font-family: var(--font-sans);
font-weight: 500;
}
&[data-slot="key-value"] {
font-family: var(--font-mono);
div {
cursor: pointer;
display: flex;
align-items: center;
gap: var(--space-2);
}
}
&[data-slot="key-date"] {
color: var(--color-text);
}
&[data-slot="key-actions"] {
font-family: var(--font-sans);
}
}
tbody tr {
&: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);
}
th {
&:nth-child(3) /* Date */ {
display: none;
}
}
td {
&:nth-child(3) /* Date */ {
display: none;
}
}
}
}
}
}
/* Balance Section */
[data-slot="balance-section"] {
[data-slot="balance"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
min-width: 14.5rem;
width: fit-content;
[data-slot="amount"] {
padding: var(--space-3-5) var(--space-4);
background-color: var(--color-bg-surface);
/* Balance Section */
[data-slot="balance-section"] {
[data-slot="balance"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
display: flex;
align-items: baseline;
gap: var(--space-1);
justify-content: flex-end;
min-width: 14.5rem;
width: fit-content;
[data-slot="amount"] {
padding: var(--space-3-5) var(--space-4);
background-color: var(--color-bg-surface);
border-radius: var(--border-radius-sm);
display: flex;
align-items: baseline;
gap: var(--space-1);
justify-content: flex-end;
&.danger {
[data-slot="value"] {
color: var(--color-danger);
}
}
[data-slot="currency"] {
position: relative;
bottom: 2px;
font-size: var(--font-size-lg);
color: var(--color-text-muted);
font-weight: 400;
}
&.danger {
[data-slot="value"] {
color: var(--color-danger);
font-size: var(--font-size-3xl);
font-weight: 500;
color: var(--color-text);
}
}
}
}
[data-slot="currency"] {
position: relative;
bottom: 2px;
font-size: var(--font-size-lg);
/* Payments Section */
[data-slot="payments-section"] {
[data-slot="payments-table"] {
overflow-x: auto;
}
[data-slot="payments-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);
font-weight: 400;
text-transform: uppercase;
}
[data-slot="value"] {
font-size: var(--font-size-3xl);
font-weight: 500;
color: var(--color-text);
}
}
}
}
/* Payments Section */
[data-slot="payments-section"] {
[data-slot="payments-table"] {
overflow-x: auto;
}
[data-slot="payments-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="payment-date"] {
color: var(--color-text);
}
&[data-slot="payment-id"] {
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);
font-weight: 400;
&[data-slot="payment-date"] {
color: var(--color-text);
}
&[data-slot="payment-id"] {
font-family: var(--font-mono);
font-weight: 400;
color: var(--color-text-muted);
max-width: 200px;
word-break: break-word;
}
&[data-slot="payment-amount"] {
color: var(--color-text);
}
}
tbody tr {
&: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);
}
th {
&:nth-child(2) /* Payment ID */ {
display: none;
}
}
td {
&:nth-child(2) /* Payment ID */ {
display: none;
}
}
}
}
}
/* Usage Section */
[data-slot="usage-section"] {
[data-slot="usage-table"] {
overflow-x: auto;
}
[data-slot="usage-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);
max-width: 200px;
word-break: break-word;
text-transform: uppercase;
}
&[data-slot="payment-amount"] {
color: var(--color-text);
}
}
tbody tr {
&: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);
}
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);
th {
&:nth-child(2) /* Payment ID */ {
display: none;
&[data-slot="usage-date"] {
color: var(--color-text);
}
&[data-slot="usage-model"] {
font-family: var(--font-sans);
font-weight: 400;
color: var(--color-text-secondary);
max-width: 200px;
word-break: break-word;
}
&[data-slot="usage-cost"] {
color: var(--color-text);
}
}
td {
&:nth-child(2) /* Payment ID */ {
display: none;
tbody tr {
&:last-child td {
border-bottom: none;
}
}
}
}
}
/* Usage Section */
[data-slot="usage-section"] {
[data-slot="usage-table"] {
overflow-x: auto;
}
[data-slot="usage-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="usage-date"] {
color: var(--color-text);
}
&[data-slot="usage-model"] {
font-family: var(--font-sans);
font-weight: 400;
color: var(--color-text-secondary);
max-width: 200px;
word-break: break-word;
}
&[data-slot="usage-cost"] {
color: var(--color-text);
}
}
tbody tr {
&: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);
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
th {
&:nth-child(2) /* Model */ {
display: none;
th {
&:nth-child(2) /* Model */ {
display: none;
}
}
}
td {
&:nth-child(2) /* Model */ {
display: none;
td {
&:nth-child(2) /* Model */ {
display: none;
}
}
}
}
+57 -173
View File
@@ -63,121 +63,14 @@ const createPortalUrl = action(async (returnUrl: string) => {
return withActor(() => Billing.generatePortalUrl({ returnUrl }))
}, "portalUrl")
const dummyUsageData = [
{
model: "claude-3-5-sonnet-20241022",
inputTokens: 1250,
outputTokens: 890,
reasoningTokens: 150,
cacheReadTokens: 0,
cacheWriteTokens: 45,
cost: 12340000,
timeCreated: new Date("2025-01-28T10:30:00Z"),
},
{
model: "claude-3-haiku-20240307",
inputTokens: 2100,
outputTokens: 450,
reasoningTokens: null,
cacheReadTokens: 120,
cacheWriteTokens: 0,
cost: 5670000,
timeCreated: new Date("2025-01-27T15:22:00Z"),
},
{
model: "claude-3-5-sonnet-20241022",
inputTokens: 850,
outputTokens: 1200,
reasoningTokens: 220,
cacheReadTokens: 30,
cacheWriteTokens: 15,
cost: 18990000,
timeCreated: new Date("2025-01-27T09:15:00Z"),
},
{
model: "claude-3-opus-20240229",
inputTokens: 3200,
outputTokens: 1800,
reasoningTokens: 400,
cacheReadTokens: 0,
cacheWriteTokens: 100,
cost: 45670000,
timeCreated: new Date("2025-01-26T14:45:00Z"),
},
{
model: "claude-3-haiku-20240307",
inputTokens: 650,
outputTokens: 280,
reasoningTokens: null,
cacheReadTokens: 200,
cacheWriteTokens: 0,
cost: 2340000,
timeCreated: new Date("2025-01-25T16:18:00Z"),
},
]
const dummyPaymentData = [
{
id: "pay_1Ab2Cd3Ef4Gh5678",
amount: 2000000000,
timeCreated: new Date("2025-01-28T14:32:00Z"),
},
{
id: "pay_9Ij8Kl7Mn6Op5432",
amount: 1000000000,
timeCreated: new Date("2025-01-25T09:18:00Z"),
},
{
id: "pay_5Qr4St3Uv2Wx1098",
amount: 5000000000,
timeCreated: new Date("2025-01-20T16:45:00Z"),
},
{
id: "pay_7Yz6Ab5Cd4Ef3210",
amount: 1500000000,
timeCreated: new Date("2025-01-15T11:22:00Z"),
},
{
id: "pay_3Gh2Ij1Kl0Mn9876",
amount: 3000000000,
timeCreated: new Date("2025-01-10T13:55:00Z"),
},
]
const dummyApiKeyData = [
{
id: "key_1Ab2Cd3Ef4Gh5678",
name: "Production API",
key: "oc_live_sk_1Ab2Cd3Ef4Gh567890123456789012345678901234567890",
timeCreated: new Date("2025-01-28T14:32:00Z"),
timeUsed: new Date("2025-01-29T09:15:00Z"),
},
{
id: "key_9Ij8Kl7Mn6Op5432",
name: "Development Key",
key: "oc_test_sk_9Ij8Kl7Mn6Op543210987654321098765432109876543210",
timeCreated: new Date("2025-01-25T09:18:00Z"),
timeUsed: null,
},
{
id: "key_5Qr4St3Uv2Wx1098",
name: "CI/CD Pipeline",
key: "oc_live_sk_5Qr4St3Uv2Wx109876543210987654321098765432109876",
timeCreated: new Date("2025-01-20T16:45:00Z"),
timeUsed: new Date("2025-01-28T12:30:00Z"),
},
]
export default function () {
const actor = createAsync(() => getActor())
onMount(() => {
console.log("MOUNTED", actor())
})
/////////////////
// Keys section
/////////////////
const keys = createAsync(() => listKeys())
const keys = createAsync(() => listKeys(), {
deferStream: true,
})
const createKeyAction = useAction(createKey)
const removeKeyAction = useAction(removeKey)
const createKeySubmission = useSubmission(createKey)
@@ -265,7 +158,9 @@ export default function () {
/////////////////
// Billing section
/////////////////
const billingInfo = createAsync(() => getBillingInfo())
const billingInfo = createAsync(() => getBillingInfo(), {
deferStream: true,
})
const createCheckoutUrlAction = useAction(createCheckoutUrl)
const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
@@ -292,15 +187,6 @@ export default function () {
</section>
<div data-slot="sections">
{/* Actor Section */}
<section data-slot="actor-section">
<div data-slot="section-title">
<h2>Actor</h2>
<p>Current authenticated user information and session details.</p>
</div>
<div>{JSON.stringify(actor())}</div>
</section>
{/* API Keys Section */}
<section data-slot="api-keys-section">
<div data-slot="section-title">
@@ -321,14 +207,7 @@ export default function () {
/>
<div data-slot="form-actions">
<button
color="primary"
disabled={createKeySubmission.pending || !keyName().trim()}
onClick={handleCreateKey}
>
{createKeySubmission.pending ? "Creating..." : "Create"}
</button>
<button
color="ghost"
data-color="ghost"
onClick={() => {
setShowCreateForm(false)
setKeyName("")
@@ -336,12 +215,19 @@ export default function () {
>
Cancel
</button>
<button
data-color="primary"
disabled={createKeySubmission.pending || !keyName().trim()}
onClick={handleCreateKey}
>
{createKeySubmission.pending ? "Creating..." : "Create"}
</button>
</div>
</div>
}
>
<button
color="primary"
data-color="primary"
onClick={() => {
console.log("clicked")
setShowCreateForm(true)
@@ -370,7 +256,6 @@ export default function () {
</thead>
<tbody>
<For each={keys()!}>
{/* Real data: keys() */}
{(key) => (
<tr>
<td data-slot="key-name">{key.name}</td>
@@ -389,7 +274,7 @@ export default function () {
{formatDateForTable(key.timeCreated)}
</td>
<td data-slot="key-actions">
<button color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
<button data-color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
Delete
</button>
</td>
@@ -426,51 +311,12 @@ export default function () {
})()}
</span>
</div>
<button color="primary" disabled={createCheckoutUrlSubmission.pending} onClick={handleBuyCredits}>
<button data-color="primary" disabled={createCheckoutUrlSubmission.pending} onClick={handleBuyCredits}>
{createCheckoutUrlSubmission.pending ? "Loading..." : "Buy Credits"}
</button>
</div>
</section>
{/* Payments Section */}
<Show when={dummyPaymentData.length > 0}>
{/* Real data condition: billingInfo() && billingInfo()!.payments.length > 0 */}
<section data-slot="payments-section">
<div data-slot="section-title">
<h2>Payments History</h2>
<p>Recent payment transactions.</p>
</div>
<div data-slot="payments-table">
<table data-slot="payments-table-element">
<thead>
<tr>
<th>Date</th>
<th>Payment ID</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<For each={dummyPaymentData}>
{/* Real data: billingInfo()?.payments */}
{(payment) => {
const date = new Date(payment.timeCreated)
return (
<tr>
<td data-slot="payment-date" title={formatDateUTC(date)}>
{formatDateForTable(date)}
</td>
<td data-slot="payment-id">{payment.id}</td>
<td data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</td>
</tr>
)
}}
</For>
</tbody>
</table>
</div>
</section>
</Show>
{/* Usage Section */}
<section data-slot="usage-section">
<div data-slot="section-title">
@@ -479,7 +325,7 @@ export default function () {
</div>
<div data-slot="usage-table">
<Show
when={dummyUsageData.length > 0}
when={billingInfo() && billingInfo()!.usage.length > 0}
fallback={
<div data-slot="empty-state">
<p>Make your first API call to get started.</p>
@@ -496,7 +342,7 @@ export default function () {
</tr>
</thead>
<tbody>
<For each={dummyUsageData}>
<For each={billingInfo()!.usage}>
{(usage) => {
const totalTokens = usage.inputTokens + usage.outputTokens + (usage.reasoningTokens || 0)
const date = new Date(usage.timeCreated)
@@ -517,6 +363,44 @@ export default function () {
</Show>
</div>
</section>
{/* Payments Section */}
<Show when={billingInfo() && billingInfo()!.payments.length > 0}>
<section data-slot="payments-section">
<div data-slot="section-title">
<h2>Payments History</h2>
<p>Recent payment transactions.</p>
</div>
<div data-slot="payments-table">
<table data-slot="payments-table-element">
<thead>
<tr>
<th>Date</th>
<th>Payment ID</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<For each={billingInfo()!.payments}>
{(payment) => {
const date = new Date(payment.timeCreated)
return (
<tr>
<td data-slot="payment-date" title={formatDateUTC(date)}>
{formatDateForTable(date)}
</td>
<td data-slot="payment-id">{payment.id}</td>
<td data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</td>
</tr>
)
}}
</For>
</tbody>
</table>
</div>
</section>
</Show>
</div>
</div>
)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cloud-core",
"version": "0.5.29",
"version": "0.6.1",
"private": true,
"type": "module",
"dependencies": {
+2 -2
View File
@@ -15,12 +15,12 @@ const init = () => {
ssl: {
rejectUnauthorized: false,
},
max: 1,
max: 6,
})
return drizzle(client, {})
}
const createClient = "NODE_ENV" in process.env ? memo(init) : init
const createClient = init
import { PgTransaction, type PgTransactionConfig } from "drizzle-orm/pg-core"
import type { ExtractTablesWithRelations } from "drizzle-orm"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/cloud-function",
"version": "0.5.29",
"version": "0.6.1",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -13,7 +13,7 @@ export const database = new sst.Linkable("Database", {
database: "postgres",
username: DATABASE_USERNAME.value,
password: DATABASE_PASSWORD.value,
port: 5432,
port: 6432,
},
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/function",
"version": "0.5.29",
"version": "0.6.1",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.5.29",
"version": "0.6.1",
"name": "opencode",
"type": "module",
"private": true,
+32 -6
View File
@@ -10,6 +10,7 @@ import { bootstrap } from "../bootstrap"
import { MessageV2 } from "../../session/message-v2"
import { Identifier } from "../../id/id"
import { Agent } from "../../agent/agent"
import { Command } from "../../command"
const TOOL: Record<string, [string, string]> = {
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
@@ -35,6 +36,10 @@ export const RunCommand = cmd({
array: true,
default: [],
})
.option("command", {
describe: "the command to run, use message for args",
type: "string",
})
.option("continue", {
alias: ["c"],
describe: "continue the last session",
@@ -64,12 +69,19 @@ export const RunCommand = cmd({
if (!process.stdin.isTTY) message += "\n" + (await Bun.stdin.text())
if (message.trim().length === 0) {
UI.error("Message cannot be empty")
if (message.trim().length === 0 && !args.command) {
UI.error("You must provide a message or a command")
return
}
await bootstrap(process.cwd(), async () => {
if (args.command) {
const exists = await Command.get(args.command)
if (!exists) {
UI.error(`Command "${args.command}" not found`)
return
}
}
const session = await (async () => {
if (args.continue) {
const it = Session.list()
@@ -172,12 +184,26 @@ export const RunCommand = cmd({
UI.error(err)
})
if (args.command) {
await Session.command({
messageID: Identifier.ascending("message"),
sessionID: session.id,
agent: agent.name,
model: providerID + "/" + modelID,
command: args.command,
arguments: message,
})
return
}
const messageID = Identifier.ascending("message")
const result = await Session.chat({
const result = await Session.prompt({
sessionID: session.id,
messageID,
providerID,
modelID,
model: {
providerID,
modelID,
},
agent: agent.name,
parts: [
{
@@ -190,7 +216,7 @@ export const RunCommand = cmd({
const isPiped = !process.stdout.isTTY
if (isPiped) {
const match = result.parts.findLast((x) => x.type === "text")
const match = result.parts.findLast((x: any) => x.type === "text") as any
if (match) process.stdout.write(UI.markdown(match.text))
if (errorMsg) process.stdout.write(errorMsg)
}
+15
View File
@@ -397,6 +397,21 @@ export namespace Config {
.object({
apiKey: z.string().optional(),
baseURL: z.string().optional(),
timeout: z
.union([
z
.number()
.int()
.positive()
.describe(
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
),
z.literal(false).describe("Disable timeout for this provider entirely."),
])
.optional()
.describe(
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
),
})
.catchall(z.any())
.optional(),
+8 -1
View File
@@ -59,7 +59,8 @@ export namespace LSPClient {
return null
})
connection.onRequest("workspace/configuration", async () => {
return [{}]
// Return server initialization options
return [input.server.initialization ?? {}]
})
connection.listen()
@@ -108,6 +109,12 @@ export namespace LSPClient {
await connection.sendNotification("initialized", {})
if (input.server.initialization) {
await connection.sendNotification("workspace/didChangeConfiguration", {
settings: input.server.initialization,
})
}
const files: {
[path: string]: number
} = {}
+19 -1
View File
@@ -193,7 +193,7 @@ export namespace LSPServer {
}
export const Gopls: Info = {
id: "golang",
id: "gopls",
root: async (file) => {
const work = await NearestRoot(["go.work"])(file)
if (work) return work
@@ -298,6 +298,23 @@ export namespace LSPServer {
args.push(...["run", js])
}
args.push("--stdio")
const initialization: Record<string, string> = {}
const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
(p): p is string => p !== undefined,
)
for (const venvPath of potentialVenvPaths) {
const isWindows = process.platform === "win32"
const potentialPythonPath = isWindows
? path.join(venvPath, "Scripts", "python.exe")
: path.join(venvPath, "bin", "python")
if (await Bun.file(potentialPythonPath).exists()) {
initialization["pythonPath"] = potentialPythonPath
break
}
}
const proc = spawn(binary, args, {
cwd: root,
env: {
@@ -307,6 +324,7 @@ export namespace LSPServer {
})
return {
process: proc,
initialization,
}
},
}
+16 -2
View File
@@ -153,7 +153,10 @@ export namespace Provider {
options: Record<string, any>
}
} = {}
const models = new Map<string, { info: ModelsDev.Model; language: LanguageModel }>()
const models = new Map<
string,
{ providerID: string; modelID: string; info: ModelsDev.Model; language: LanguageModel }
>()
const sdk = new Map<string, SDK>()
log.info("init")
@@ -320,9 +323,16 @@ export namespace Provider {
const pkg = provider.npm ?? provider.id
const mod = await import(await BunProc.install(pkg, "latest"))
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
let options = { ...s.providers[provider.id]?.options }
if (options["timeout"] !== undefined) {
// Only override fetch if user explicitly sets timeout
options["fetch"] = async (input: any, init?: any) => {
return await fetch(input, { ...init, timeout: options["timeout"] })
}
}
const loaded = fn({
name: provider.id,
...s.providers[provider.id]?.options,
...options,
})
s.sdk.set(provider.id, loaded)
return loaded as SDK
@@ -355,10 +365,14 @@ export namespace Provider {
const language = provider.getModel ? await provider.getModel(sdk, modelID) : sdk.languageModel(modelID)
log.info("found", { providerID, modelID })
s.models.set(key, {
providerID,
modelID,
info,
language,
})
return {
modelID,
providerID,
info,
language,
}
+1 -1
View File
@@ -30,7 +30,7 @@ export const ProjectRoute = new Hono()
"/current",
describeRoute({
description: "Get the current project",
operationId: "butt.current",
operationId: "project.current",
responses: {
200: {
description: "Current project",
+6 -3
View File
@@ -53,6 +53,9 @@ export namespace Server {
const app = new Hono()
export const App = app
.onError((err, c) => {
log.error("failed", {
error: err,
})
if (err instanceof NamedError) {
return c.json(err.toObject(), {
status: 400,
@@ -606,7 +609,7 @@ export namespace Server {
"/session/:id/message",
describeRoute({
description: "Create and send a new message to a session",
operationId: "session.chat",
operationId: "session.prompt",
responses: {
200: {
description: "Created message",
@@ -629,11 +632,11 @@ export namespace Server {
id: z.string().openapi({ description: "Session ID" }),
}),
),
zValidator("json", Session.ChatInput.omit({ sessionID: true })),
zValidator("json", Session.PromptInput.omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
const msg = await Session.chat({ ...body, sessionID })
const msg = await Session.prompt({ ...body, sessionID })
return c.json(msg)
},
)
+67 -46
View File
@@ -364,11 +364,15 @@ export namespace Session {
return part
}
export const ChatInput = z.object({
export const PromptInput = z.object({
sessionID: Identifier.schema("session"),
messageID: Identifier.schema("message").optional(),
providerID: z.string(),
modelID: z.string(),
model: z
.object({
providerID: z.string(),
modelID: z.string(),
})
.optional(),
agent: z.string().optional(),
system: z.string().optional(),
tools: z.record(z.boolean()).optional(),
@@ -407,10 +411,10 @@ export namespace Session {
]),
),
})
export type ChatInput = z.infer<typeof ChatInput>
export type ChatInput = z.infer<typeof PromptInput>
export async function chat(
input: z.infer<typeof ChatInput>,
export async function prompt(
input: z.infer<typeof PromptInput>,
): Promise<{ info: MessageV2.Assistant; parts: MessageV2.Part[] }> {
const l = log.clone().tag("session", input.sessionID)
l.info("chatting")
@@ -652,7 +656,16 @@ export namespace Session {
})
}
const model = await Provider.getModel(input.providerID, input.modelID)
const agent = await Agent.get(inputAgent)
const model = await (async () => {
if (input.model) {
return input.model
}
if (agent.model) {
return agent.model
}
return Provider.defaultModel()
})().then((x) => Provider.getModel(x.providerID, x.modelID))
let msgs = await messages(input.sessionID)
const previous = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
@@ -667,10 +680,10 @@ export namespace Session {
await summarize({
sessionID: input.sessionID,
providerID: input.providerID,
modelID: input.modelID,
providerID: model.providerID,
modelID: model.info.id,
})
return chat(input)
return prompt(input)
}
}
using abort = lock(input.sessionID)
@@ -679,17 +692,17 @@ export namespace Session {
if (lastSummary) msgs = msgs.filter((msg) => msg.info.id >= lastSummary.info.id)
if (msgs.filter((m) => m.info.role === "user").length === 1 && !session.parentID && isDefaultTitle(session.title)) {
const small = (await Provider.getSmallModel(input.providerID)) ?? model
const small = (await Provider.getSmallModel(model.providerID)) ?? model
generateText({
maxOutputTokens: small.info.reasoning ? 1024 : 20,
providerOptions: {
[input.providerID]: {
[model.providerID]: {
...small.info.options,
...ProviderTransform.options(input.providerID, small.info.id, input.sessionID),
...ProviderTransform.options(small.providerID, small.modelID, input.sessionID),
},
},
messages: [
...SystemPrompt.title(input.providerID).map(
...SystemPrompt.title(model.providerID).map(
(x): ModelMessage => ({
role: "system",
content: x,
@@ -724,7 +737,6 @@ export namespace Session {
})
}
const agent = await Agent.get(inputAgent)
if (agent.name === "plan") {
msgs.at(-1)?.parts.push({
id: Identifier.ascending("part"),
@@ -747,12 +759,12 @@ export namespace Session {
synthetic: true,
})
}
let system = SystemPrompt.header(input.providerID)
let system = SystemPrompt.header(model.providerID)
system.push(
...(() => {
if (input.system) return [input.system]
if (agent.prompt) return [agent.prompt]
return SystemPrompt.provider(input.modelID)
return SystemPrompt.provider(model.modelID)
})(),
)
system.push(...(await SystemPrompt.environment()))
@@ -777,8 +789,8 @@ export namespace Session {
reasoning: 0,
cache: { read: 0, write: 0 },
},
modelID: input.modelID,
providerID: input.providerID,
modelID: model.modelID,
providerID: model.providerID,
time: {
created: Date.now(),
},
@@ -796,10 +808,10 @@ export namespace Session {
const enabledTools = pipe(
agent.tools,
mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID, agent)),
mergeDeep(await ToolRegistry.enabled(model.providerID, model.modelID, agent)),
mergeDeep(input.tools ?? {}),
)
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
for (const item of await ToolRegistry.tools(model.providerID, model.modelID)) {
if (Wildcard.all(item.id, enabledTools) === false) continue
tools[item.id] = tool({
id: item.id as any,
@@ -909,16 +921,16 @@ export namespace Session {
"chat.params",
{
model: model.info,
provider: await Provider.getProvider(input.providerID),
provider: await Provider.getProvider(model.providerID),
message: userMsg,
},
{
temperature: model.info.temperature
? (agent.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID))
? (agent.temperature ?? ProviderTransform.temperature(model.providerID, model.modelID))
: undefined,
topP: agent.topP ?? ProviderTransform.topP(input.providerID, input.modelID),
topP: agent.topP ?? ProviderTransform.topP(model.providerID, model.modelID),
options: {
...ProviderTransform.options(input.providerID, input.modelID, input.sessionID),
...ProviderTransform.options(model.providerID, model.modelID, input.sessionID),
...model.info.options,
...agent.options,
},
@@ -962,8 +974,8 @@ export namespace Session {
reasoning: 0,
cache: { read: 0, write: 0 },
},
modelID: input.modelID,
providerID: input.providerID,
modelID: model.modelID,
providerID: model.providerID,
mode: inputAgent,
time: {
created: Date.now(),
@@ -987,7 +999,7 @@ export namespace Session {
}
},
headers:
input.providerID === "opencode"
model.providerID === "opencode"
? {
"x-opencode-session": input.sessionID,
"x-opencode-request": userMsg.id,
@@ -1010,7 +1022,7 @@ export namespace Session {
return false
},
providerOptions: {
[input.providerID]: params.options,
[model.providerID]: params.options,
},
temperature: params.temperature,
topP: params.topP,
@@ -1031,7 +1043,7 @@ export namespace Session {
async transformParams(args) {
if (args.type === "stream") {
// @ts-expect-error
args.params.prompt = ProviderTransform.message(args.params.prompt, input.providerID, input.modelID)
args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
}
return args.params
},
@@ -1044,7 +1056,7 @@ export namespace Session {
const unprocessed = queued.find((x) => !x.processed)
if (unprocessed) {
unprocessed.processed = true
return chat(unprocessed.input)
return prompt(unprocessed.input)
}
for (const item of queued) {
item.callback(result)
@@ -1143,6 +1155,7 @@ export namespace Session {
const proc = spawn(shell, args, {
cwd: Instance.directory,
signal: abort.signal,
detached: true,
stdio: ["ignore", "pipe", "pipe"],
env: {
...process.env,
@@ -1150,6 +1163,11 @@ export namespace Session {
},
})
abort.signal.addEventListener("abort", () => {
if (!proc.pid) return
process.kill(-proc.pid)
})
let output = ""
proc.stdout?.on("data", (chunk) => {
@@ -1214,16 +1232,9 @@ export namespace Session {
const fileRegex = /@([^\s]+)/g
export async function command(input: CommandInput) {
log.info("command", input)
const command = await Command.get(input.command)
const agent = command.agent ?? input.agent ?? "build"
const fmtModel = (model: { providerID: string; modelID: string }) => `${model.providerID}/${model.modelID}`
const model =
command.model ??
(command.agent && (await Agent.get(command.agent).then((x) => (x.model ? fmtModel(x.model) : undefined)))) ??
input.model ??
(input.agent && (await Agent.get(input.agent).then((x) => (x.model ? fmtModel(x.model) : undefined)))) ??
fmtModel(await Provider.defaultModel())
let template = command.template.replace("$ARGUMENTS", input.arguments)
@@ -1267,10 +1278,18 @@ export namespace Session {
})
}
return chat({
return prompt({
sessionID: input.sessionID,
messageID: input.messageID,
...Provider.parseModel(model!),
model: (() => {
if (input.model) {
return Provider.parseModel(input.model)
}
if (command.model) {
return Provider.parseModel(command.model)
}
return undefined
})(),
agent,
parts,
})
@@ -1637,7 +1656,7 @@ export namespace Session {
const filtered = msgs.filter((msg) => !lastSummary || msg.info.id >= lastSummary.info.id)
const model = await Provider.getModel(input.providerID, input.modelID)
const system = [
...SystemPrompt.summarize(input.providerID),
...SystemPrompt.summarize(model.providerID),
...(await SystemPrompt.environment()),
...(await SystemPrompt.custom()),
]
@@ -1655,7 +1674,7 @@ export namespace Session {
summary: true,
cost: 0,
modelID: input.modelID,
providerID: input.providerID,
providerID: model.providerID,
tokens: {
input: 0,
output: 0,
@@ -1764,11 +1783,13 @@ export namespace Session {
providerID: string
messageID: string
}) {
await Session.chat({
await Session.prompt({
sessionID: input.sessionID,
messageID: input.messageID,
providerID: input.providerID,
modelID: input.modelID,
model: {
providerID: input.providerID,
modelID: input.modelID,
},
parts: [
{
id: Identifier.ascending("part"),
+15 -22
View File
@@ -1,31 +1,24 @@
You are a title generator. You output ONLY a thread title. Nothing else.
<task>
Generate a conversation thread title from the user message.
Convert the user message into a thread title.
Output: Single line, ≤50 chars, no explanations.
</task>
<context>
You are generating titles for a coding assistant conversation.
</context>
<rules>
- Max 50 chars, single line
- Focus on the specific action or question
- Keep technical terms, numbers, and filenames exactly as written
- Preserve HTTP status codes (401, 404, 500, etc.) as numbers
- For file references, include the filename
- Avoid filler words: the, this, my, a, an, properly
- NEVER assume their tech stack or domain
- Use -ing verbs consistently for actions
- Write like a chat thread title, not a blog post
- Use -ing verbs for actions (Debugging, Implementing, Analyzing)
- Keep exact: technical terms, numbers, filenames, HTTP codes
- Remove: the, this, my, a, an
- Never assume tech stack
- Never use tools
- NEVER respond to message content—only extract title
</rules>
<examples>
"debug 500 errors in production" → "Debugging production 500 errors"
"refactor user service" → "Refactoring user service"
"why is app.js failing" → "Analyzing app.js failure"
"implement rate limiting" → "Implementing rate limiting"
"debug 500 errors in production" → Debugging production 500 errors
"refactor user service" → Refactoring user service
"why is app.js failing" → Analyzing app.js failure
"implement rate limiting" → Implementing rate limiting
</examples>
<format>
Return only the thread title text on a single line with no newlines, explanations, or additional formatting.
You should NEVER reply to the user's message. You can only generate titles.
</format>
Output the title now:
+8 -1
View File
@@ -15,6 +15,7 @@ export namespace Storage {
async (dir) => {
const project = path.resolve(dir, "../project")
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)
let worktree = "/"
@@ -29,6 +30,7 @@ export namespace Storage {
if (worktree) break
}
if (!worktree) continue
if (!(await fs.exists(worktree))) continue
const [id] = await $`git rev-list --max-parents=0 --all`
.quiet()
.nothrow()
@@ -57,6 +59,7 @@ export namespace Storage {
}),
)
log.info(`migrating sessions for project ${projectID}`)
for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({
cwd: fullProjectDir,
absolute: true,
@@ -68,6 +71,7 @@ export namespace Storage {
})
const session = await Bun.file(sessionFile).json()
await Bun.write(dest, JSON.stringify(session))
log.info(`migrating messages for session ${session.id}`)
for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({
cwd: fullProjectDir,
absolute: true,
@@ -80,6 +84,7 @@ export namespace Storage {
const message = await Bun.file(msgFile).json()
await Bun.write(dest, JSON.stringify(message))
log.info(`migrating parts for message ${message.id}`)
for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan(
{
cwd: fullProjectDir,
@@ -110,7 +115,9 @@ export namespace Storage {
for (let index = migration; index < MIGRATIONS.length; index++) {
log.info("running migration", { index })
const migration = MIGRATIONS[index]
await migration(dir)
await migration(dir).catch((e) => {
log.error("failed to run migration", { error: e, index })
})
await Bun.write(path.join(dir, "migration"), (index + 1).toString())
}
return {
+7 -5
View File
@@ -51,11 +51,13 @@ export const TaskTool = Tool.define("task", async () => {
ctx.abort.addEventListener("abort", () => {
Session.abort(session.id)
})
const result = await Session.chat({
const result = await Session.prompt({
messageID,
sessionID: session.id,
modelID: model.modelID,
providerID: model.providerID,
model: {
modelID: model.modelID,
providerID: model.providerID,
},
agent: agent.name,
tools: {
todowrite: false,
@@ -75,9 +77,9 @@ export const TaskTool = Tool.define("task", async () => {
return {
title: params.description,
metadata: {
summary: result.parts.filter((x) => x.type === "tool"),
summary: result.parts.filter((x: any) => x.type === "tool"),
},
output: result.parts.findLast((x) => x.type === "text")?.text ?? "",
output: (result.parts.findLast((x: any) => x.type === "text") as any)?.text ?? "",
}
},
}
+8 -1
View File
@@ -83,6 +83,13 @@ export namespace Log {
await Promise.all(filesToDelete.map((file) => fs.unlink(file).catch(() => {})))
}
function formatError(error: Error, depth = 0): string {
const result = error.message
return error.cause instanceof Error && depth < 10
? result + " Caused by: " + formatError(error.cause, depth + 1)
: result
}
let last = Date.now()
export function create(tags?: Record<string, any>) {
tags = tags || {}
@@ -103,7 +110,7 @@ export namespace Log {
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => {
const prefix = `${key}=`
if (value instanceof Error) return prefix + value.message
if (value instanceof Error) return prefix + formatError(value)
if (typeof value === "object") return prefix + JSON.stringify(value)
return prefix + value
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "0.5.29",
"version": "0.6.1",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
+8 -2
View File
@@ -1,9 +1,15 @@
import { Plugin } from "./index"
export const ExamplePlugin: Plugin = async ({ client, $ }) => {
export const ExamplePlugin: Plugin = async ({
client: _client,
$: _shell,
project: _project,
directory: _directory,
worktree: _worktree,
}) => {
return {
permission: {},
async "chat.params"(input, output) {
async "chat.params"(_input, output) {
output.topP = 1
},
}
@@ -1,3 +1,3 @@
{
".": "0.5.0"
".": "0.8.0"
}
+3 -3
View File
@@ -1,4 +1,4 @@
configured_endpoints: 43
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-24d6bfcc66bba2e3a826fdad7e48a474c5aa9193a6bb89dc8ab1feb1f3d9bf72.yml
openapi_spec_hash: db8b553192d9027e1f9254096406cfc2
config_hash: 1b0d220e033fe9f683abf7048e7ad076
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-97b61518d8666ea7cb310af04248e00bcf8dc9753ba3c7e84471df72b3232004.yml
openapi_spec_hash: a3500531973ad999c350b87c21aa3ab8
config_hash: 026ef000d34bf2f930e7b41e77d2d3ff
+24
View File
@@ -1,5 +1,29 @@
# Changelog
## 0.8.0 (2025-09-01)
Full Changelog: [v0.7.0...v0.8.0](https://github.com/sst/opencode-sdk-go/compare/v0.7.0...v0.8.0)
### Features
* **api:** api update ([ae87a71](https://github.com/sst/opencode-sdk-go/commit/ae87a71949994590ace8285a39f0991ef34b664d))
## 0.7.0 (2025-09-01)
Full Changelog: [v0.6.0...v0.7.0](https://github.com/sst/opencode-sdk-go/compare/v0.6.0...v0.7.0)
### Features
* **api:** api update ([64bb1b1](https://github.com/sst/opencode-sdk-go/commit/64bb1b1ee0cbe153abc6fb7bd9703b47911724d4))
## 0.6.0 (2025-09-01)
Full Changelog: [v0.5.0...v0.6.0](https://github.com/sst/opencode-sdk-go/compare/v0.5.0...v0.6.0)
### Features
* **api:** api update ([928e384](https://github.com/sst/opencode-sdk-go/commit/928e3843355f96899f046f002b84372281dad0c8))
## 0.5.0 (2025-08-31)
Full Changelog: [v0.4.0...v0.5.0](https://github.com/sst/opencode-sdk-go/compare/v0.4.0...v0.5.0)
+1 -1
View File
@@ -24,7 +24,7 @@ Or to pin the version:
<!-- x-release-please-start-version -->
```sh
go get -u 'github.com/sst/opencode-sdk-go@v0.5.0'
go get -u 'github.com/sst/opencode-sdk-go@v0.8.0'
```
<!-- x-release-please-end -->
+2 -2
View File
@@ -143,10 +143,10 @@ Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStatePending">ToolStatePending</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateRunning">ToolStateRunning</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessage">UserMessage</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatResponse">SessionChatResponse</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandResponse">SessionCommandResponse</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionPromptResponse">SessionPromptResponse</a>
Methods:
@@ -155,13 +155,13 @@ Methods:
- <code title="get /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionListParams">SessionListParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="delete /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionDeleteParams">SessionDeleteParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionAbortParams">SessionAbortParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatResponse">SessionChatResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session/{id}/children">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Children">Children</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChildrenParams">SessionChildrenParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/command">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Command">Command</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandParams">SessionCommandParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandResponse">SessionCommandResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionGetParams">SessionGetParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session/{id}/message/{messageID}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Message">Message</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, messageID <a href="https://pkg.go.dev/builtin#string">string</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageParams">SessionMessageParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesParams">SessionMessagesParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Prompt">Prompt</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionPromptParams">SessionPromptParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionPromptResponse">SessionPromptResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/revert">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Revert">Revert</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionRevertParams">SessionRevertParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionShareParams">SessionShareParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/shell">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Shell">Shell</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionShellParams">SessionShellParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+36 -4
View File
@@ -13,6 +13,7 @@ import (
"github.com/sst/opencode-sdk-go/internal/param"
"github.com/sst/opencode-sdk-go/internal/requestconfig"
"github.com/sst/opencode-sdk-go/option"
"github.com/sst/opencode-sdk-go/shared"
"github.com/tidwall/gjson"
)
@@ -1650,10 +1651,13 @@ func (r configProviderModelsLimitJSON) RawJSON() string {
}
type ConfigProviderOptions struct {
APIKey string `json:"apiKey"`
BaseURL string `json:"baseURL"`
ExtraFields map[string]interface{} `json:"-,extras"`
JSON configProviderOptionsJSON `json:"-"`
APIKey string `json:"apiKey"`
BaseURL string `json:"baseURL"`
// Timeout in milliseconds for requests to this provider. Default is 300000 (5
// minutes). Set to false to disable timeout.
Timeout ConfigProviderOptionsTimeoutUnion `json:"timeout"`
ExtraFields map[string]interface{} `json:"-,extras"`
JSON configProviderOptionsJSON `json:"-"`
}
// configProviderOptionsJSON contains the JSON metadata for the struct
@@ -1661,6 +1665,7 @@ type ConfigProviderOptions struct {
type configProviderOptionsJSON struct {
APIKey apijson.Field
BaseURL apijson.Field
Timeout apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
@@ -1673,6 +1678,33 @@ func (r configProviderOptionsJSON) RawJSON() string {
return r.raw
}
// Timeout in milliseconds for requests to this provider. Default is 300000 (5
// minutes). Set to false to disable timeout.
//
// Union satisfied by [shared.UnionInt] or [shared.UnionBool].
type ConfigProviderOptionsTimeoutUnion interface {
ImplementsConfigProviderOptionsTimeoutUnion()
}
func init() {
apijson.RegisterUnion(
reflect.TypeOf((*ConfigProviderOptionsTimeoutUnion)(nil)).Elem(),
"",
apijson.UnionVariant{
TypeFilter: gjson.Number,
Type: reflect.TypeOf(shared.UnionInt(0)),
},
apijson.UnionVariant{
TypeFilter: gjson.True,
Type: reflect.TypeOf(shared.UnionBool(false)),
},
apijson.UnionVariant{
TypeFilter: gjson.False,
Type: reflect.TypeOf(shared.UnionBool(false)),
},
)
}
// Control sharing behavior:'manual' allows manual sharing via commands, 'auto'
// enables automatic sharing, 'disabled' disables all sharing
type ConfigShare string
+1 -1
View File
@@ -2,4 +2,4 @@
package internal
const PackageVersion = "0.5.0" // x-release-please-version
const PackageVersion = "0.8.0" // x-release-please-version
+7 -3
View File
@@ -42,15 +42,19 @@ func (r *PathService) Get(ctx context.Context, query PathGetParams, opts ...opti
}
type Path struct {
Config string `json:"config,required"`
State string `json:"state,required"`
JSON pathJSON `json:"-"`
Config string `json:"config,required"`
Directory string `json:"directory,required"`
State string `json:"state,required"`
Worktree string `json:"worktree,required"`
JSON pathJSON `json:"-"`
}
// pathJSON contains the JSON metadata for the struct [Path]
type pathJSON struct {
Config apijson.Field
Directory apijson.Field
State apijson.Field
Worktree apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
+110 -102
View File
@@ -92,18 +92,6 @@ func (r *SessionService) Abort(ctx context.Context, id string, body SessionAbort
return
}
// Create and send a new message to a session
func (r *SessionService) Chat(ctx context.Context, id string, params SessionChatParams, opts ...option.RequestOption) (res *SessionChatResponse, err error) {
opts = append(r.Options[:], opts...)
if id == "" {
err = errors.New("missing required id parameter")
return
}
path := fmt.Sprintf("session/%s/message", id)
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...)
return
}
// Get a session's children
func (r *SessionService) Children(ctx context.Context, id string, query SessionChildrenParams, opts ...option.RequestOption) (res *[]Session, err error) {
opts = append(r.Options[:], opts...)
@@ -180,6 +168,18 @@ func (r *SessionService) Messages(ctx context.Context, id string, query SessionM
return
}
// Create and send a new message to a session
func (r *SessionService) Prompt(ctx context.Context, id string, params SessionPromptParams, opts ...option.RequestOption) (res *SessionPromptResponse, err error) {
opts = append(r.Options[:], opts...)
if id == "" {
err = errors.New("missing required id parameter")
return
}
path := fmt.Sprintf("session/%s/message", id)
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...)
return
}
// Revert a message
func (r *SessionService) Revert(ctx context.Context, id string, params SessionRevertParams, opts ...option.RequestOption) (res *Session, err error) {
opts = append(r.Options[:], opts...)
@@ -333,7 +333,7 @@ func (r AgentPartInputParam) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
func (r AgentPartInputParam) implementsSessionChatParamsPartUnion() {}
func (r AgentPartInputParam) implementsSessionPromptParamsPartUnion() {}
type AgentPartInputType string
@@ -709,7 +709,7 @@ func (r FilePartInputParam) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
func (r FilePartInputParam) implementsSessionChatParamsPartUnion() {}
func (r FilePartInputParam) implementsSessionPromptParamsPartUnion() {}
type FilePartInputType string
@@ -1820,7 +1820,7 @@ func (r TextPartInputParam) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
func (r TextPartInputParam) implementsSessionChatParamsPartUnion() {}
func (r TextPartInputParam) implementsSessionPromptParamsPartUnion() {}
type TextPartInputType string
@@ -2296,29 +2296,6 @@ func (r userMessageTimeJSON) RawJSON() string {
return r.raw
}
type SessionChatResponse struct {
Info AssistantMessage `json:"info,required"`
Parts []Part `json:"parts,required"`
JSON sessionChatResponseJSON `json:"-"`
}
// sessionChatResponseJSON contains the JSON metadata for the struct
// [SessionChatResponse]
type sessionChatResponseJSON struct {
Info apijson.Field
Parts apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *SessionChatResponse) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r sessionChatResponseJSON) RawJSON() string {
return r.raw
}
type SessionCommandResponse struct {
Info AssistantMessage `json:"info,required"`
Parts []Part `json:"parts,required"`
@@ -2388,6 +2365,29 @@ func (r sessionMessagesResponseJSON) RawJSON() string {
return r.raw
}
type SessionPromptResponse struct {
Info AssistantMessage `json:"info,required"`
Parts []Part `json:"parts,required"`
JSON sessionPromptResponseJSON `json:"-"`
}
// sessionPromptResponseJSON contains the JSON metadata for the struct
// [SessionPromptResponse]
type sessionPromptResponseJSON struct {
Info apijson.Field
Parts apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *SessionPromptResponse) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r sessionPromptResponseJSON) RawJSON() string {
return r.raw
}
type SessionNewParams struct {
Directory param.Field[string] `query:"directory"`
ParentID param.Field[string] `json:"parentID"`
@@ -2459,70 +2459,6 @@ func (r SessionAbortParams) URLQuery() (v url.Values) {
})
}
type SessionChatParams struct {
ModelID param.Field[string] `json:"modelID,required"`
Parts param.Field[[]SessionChatParamsPartUnion] `json:"parts,required"`
ProviderID param.Field[string] `json:"providerID,required"`
Directory param.Field[string] `query:"directory"`
Agent param.Field[string] `json:"agent"`
MessageID param.Field[string] `json:"messageID"`
System param.Field[string] `json:"system"`
Tools param.Field[map[string]bool] `json:"tools"`
}
func (r SessionChatParams) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
// URLQuery serializes [SessionChatParams]'s query parameters as `url.Values`.
func (r SessionChatParams) URLQuery() (v url.Values) {
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
ArrayFormat: apiquery.ArrayQueryFormatComma,
NestedFormat: apiquery.NestedQueryFormatBrackets,
})
}
type SessionChatParamsPart struct {
Type param.Field[SessionChatParamsPartsType] `json:"type,required"`
ID param.Field[string] `json:"id"`
Filename param.Field[string] `json:"filename"`
Mime param.Field[string] `json:"mime"`
Name param.Field[string] `json:"name"`
Source param.Field[interface{}] `json:"source"`
Synthetic param.Field[bool] `json:"synthetic"`
Text param.Field[string] `json:"text"`
Time param.Field[interface{}] `json:"time"`
URL param.Field[string] `json:"url"`
}
func (r SessionChatParamsPart) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
func (r SessionChatParamsPart) implementsSessionChatParamsPartUnion() {}
// Satisfied by [TextPartInputParam], [FilePartInputParam], [AgentPartInputParam],
// [SessionChatParamsPart].
type SessionChatParamsPartUnion interface {
implementsSessionChatParamsPartUnion()
}
type SessionChatParamsPartsType string
const (
SessionChatParamsPartsTypeText SessionChatParamsPartsType = "text"
SessionChatParamsPartsTypeFile SessionChatParamsPartsType = "file"
SessionChatParamsPartsTypeAgent SessionChatParamsPartsType = "agent"
)
func (r SessionChatParamsPartsType) IsKnown() bool {
switch r {
case SessionChatParamsPartsTypeText, SessionChatParamsPartsTypeFile, SessionChatParamsPartsTypeAgent:
return true
}
return false
}
type SessionChildrenParams struct {
Directory param.Field[string] `query:"directory"`
}
@@ -2611,6 +2547,78 @@ func (r SessionMessagesParams) URLQuery() (v url.Values) {
})
}
type SessionPromptParams struct {
Parts param.Field[[]SessionPromptParamsPartUnion] `json:"parts,required"`
Directory param.Field[string] `query:"directory"`
Agent param.Field[string] `json:"agent"`
MessageID param.Field[string] `json:"messageID"`
Model param.Field[SessionPromptParamsModel] `json:"model"`
System param.Field[string] `json:"system"`
Tools param.Field[map[string]bool] `json:"tools"`
}
func (r SessionPromptParams) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
// URLQuery serializes [SessionPromptParams]'s query parameters as `url.Values`.
func (r SessionPromptParams) URLQuery() (v url.Values) {
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
ArrayFormat: apiquery.ArrayQueryFormatComma,
NestedFormat: apiquery.NestedQueryFormatBrackets,
})
}
type SessionPromptParamsPart struct {
Type param.Field[SessionPromptParamsPartsType] `json:"type,required"`
ID param.Field[string] `json:"id"`
Filename param.Field[string] `json:"filename"`
Mime param.Field[string] `json:"mime"`
Name param.Field[string] `json:"name"`
Source param.Field[interface{}] `json:"source"`
Synthetic param.Field[bool] `json:"synthetic"`
Text param.Field[string] `json:"text"`
Time param.Field[interface{}] `json:"time"`
URL param.Field[string] `json:"url"`
}
func (r SessionPromptParamsPart) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
func (r SessionPromptParamsPart) implementsSessionPromptParamsPartUnion() {}
// Satisfied by [TextPartInputParam], [FilePartInputParam], [AgentPartInputParam],
// [SessionPromptParamsPart].
type SessionPromptParamsPartUnion interface {
implementsSessionPromptParamsPartUnion()
}
type SessionPromptParamsPartsType string
const (
SessionPromptParamsPartsTypeText SessionPromptParamsPartsType = "text"
SessionPromptParamsPartsTypeFile SessionPromptParamsPartsType = "file"
SessionPromptParamsPartsTypeAgent SessionPromptParamsPartsType = "agent"
)
func (r SessionPromptParamsPartsType) IsKnown() bool {
switch r {
case SessionPromptParamsPartsTypeText, SessionPromptParamsPartsTypeFile, SessionPromptParamsPartsTypeAgent:
return true
}
return false
}
type SessionPromptParamsModel struct {
ModelID param.Field[string] `json:"modelID,required"`
ProviderID param.Field[string] `json:"providerID,required"`
}
func (r SessionPromptParamsModel) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
type SessionRevertParams struct {
MessageID param.Field[string] `json:"messageID,required"`
Directory param.Field[string] `query:"directory"`
+48 -46
View File
@@ -148,52 +148,6 @@ func TestSessionAbortWithOptionalParams(t *testing.T) {
}
}
func TestSessionChatWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
baseURL = envURL
}
if !testutil.CheckTestServer(t, baseURL) {
return
}
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
_, err := client.Session.Chat(
context.TODO(),
"id",
opencode.SessionChatParams{
ModelID: opencode.F("modelID"),
Parts: opencode.F([]opencode.SessionChatParamsPartUnion{opencode.TextPartInputParam{
Text: opencode.F("text"),
Type: opencode.F(opencode.TextPartInputTypeText),
ID: opencode.F("id"),
Synthetic: opencode.F(true),
Time: opencode.F(opencode.TextPartInputTimeParam{
Start: opencode.F(0.000000),
End: opencode.F(0.000000),
}),
}}),
ProviderID: opencode.F("providerID"),
Directory: opencode.F("directory"),
Agent: opencode.F("agent"),
MessageID: opencode.F("msg"),
System: opencode.F("system"),
Tools: opencode.F(map[string]bool{
"foo": true,
}),
},
)
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
t.Log(string(apierr.DumpRequest(true)))
}
t.Fatalf("err should be nil: %s", err.Error())
}
}
func TestSessionChildrenWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
@@ -371,6 +325,54 @@ func TestSessionMessagesWithOptionalParams(t *testing.T) {
}
}
func TestSessionPromptWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
baseURL = envURL
}
if !testutil.CheckTestServer(t, baseURL) {
return
}
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
_, err := client.Session.Prompt(
context.TODO(),
"id",
opencode.SessionPromptParams{
Parts: opencode.F([]opencode.SessionPromptParamsPartUnion{opencode.TextPartInputParam{
Text: opencode.F("text"),
Type: opencode.F(opencode.TextPartInputTypeText),
ID: opencode.F("id"),
Synthetic: opencode.F(true),
Time: opencode.F(opencode.TextPartInputTimeParam{
Start: opencode.F(0.000000),
End: opencode.F(0.000000),
}),
}}),
Directory: opencode.F("directory"),
Agent: opencode.F("agent"),
MessageID: opencode.F("msg"),
Model: opencode.F(opencode.SessionPromptParamsModel{
ModelID: opencode.F("modelID"),
ProviderID: opencode.F("providerID"),
}),
System: opencode.F("system"),
Tools: opencode.F(map[string]bool{
"foo": true,
}),
},
)
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
t.Log(string(apierr.DumpRequest(true)))
}
t.Fatalf("err should be nil: %s", err.Error())
}
}
func TestSessionRevertWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
+11
View File
@@ -0,0 +1,11 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package shared
type UnionBool bool
func (UnionBool) ImplementsConfigProviderOptionsTimeoutUnion() {}
type UnionInt int64
func (UnionInt) ImplementsConfigProviderOptionsTimeoutUnion() {}
+55
View File
@@ -0,0 +1,55 @@
import { createOpencodeClient, createOpencodeServer } from "@opencode-ai/sdk"
const server = await createOpencodeServer()
const client = createOpencodeClient({ baseUrl: server.url })
const input = await Array.fromAsync(new Bun.Glob("packages/core/*.ts").scan())
const tasks: Promise<void>[] = []
for await (const file of input) {
console.log("processing", file)
const session = await client.session.create()
tasks.push(
client.session.prompt({
path: { id: session.data.id },
body: {
parts: [
{
type: "file",
mime: "text/plain",
url: `file://${file}`,
},
{
type: "text",
text: `Write tests for every public function in this file.`,
},
],
},
}),
)
console.log("done", file)
}
await Promise.all(
input.map(async (file) => {
const session = await client.session.create()
console.log("processing", file)
await client.session.prompt({
path: { id: session.data.id },
body: {
parts: [
{
type: "file",
mime: "text/plain",
url: `file://${file}`,
},
{
type: "text",
text: `Write tests for every public function in this file.`,
},
],
},
})
console.log("done", file)
}),
)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "0.5.29",
"version": "0.6.1",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
+16 -4
View File
@@ -4,6 +4,8 @@ import type { Options as ClientOptions, TDataShape, Client } from "./client/inde
import type {
ProjectListData,
ProjectListResponses,
ProjectCurrentData,
ProjectCurrentResponses,
EventSubscribeData,
EventSubscribeResponses,
ConfigGetData,
@@ -35,8 +37,8 @@ import type {
SessionSummarizeResponses,
SessionMessagesData,
SessionMessagesResponses,
SessionChatData,
SessionChatResponses,
SessionPromptData,
SessionPromptResponses,
SessionMessageData,
SessionMessageResponses,
SessionCommandData,
@@ -130,6 +132,16 @@ class Project extends _HeyApiClient {
...options,
})
}
/**
* Get the current project
*/
public current<ThrowOnError extends boolean = false>(options?: Options<ProjectCurrentData, ThrowOnError>) {
return (options?.client ?? this._client).get<ProjectCurrentResponses, unknown, ThrowOnError>({
url: "/project/current",
...options,
})
}
}
class Event extends _HeyApiClient {
@@ -318,8 +330,8 @@ class Session extends _HeyApiClient {
/**
* Create and send a new message to a session
*/
public chat<ThrowOnError extends boolean = false>(options: Options<SessionChatData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionChatResponses, unknown, ThrowOnError>({
public prompt<ThrowOnError extends boolean = false>(options: Options<SessionPromptData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionPromptResponses, unknown, ThrowOnError>({
url: "/session/{id}/message",
...options,
headers: {
+32 -6
View File
@@ -645,7 +645,11 @@ export type Config = {
options?: {
apiKey?: string
baseURL?: string
[key: string]: unknown | string | undefined
/**
* Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.
*/
timeout?: number | false
[key: string]: unknown | string | (number | false) | undefined
}
}
}
@@ -1052,6 +1056,8 @@ export type LayoutConfig = "auto" | "stretch"
export type Path = {
state: string
config: string
worktree: string
directory: string
}
export type _Error = {
@@ -1196,6 +1202,24 @@ export type ProjectListResponses = {
export type ProjectListResponse = ProjectListResponses[keyof ProjectListResponses]
export type ProjectCurrentData = {
body?: never
path?: never
query?: {
directory?: string
}
url: "/project/current"
}
export type ProjectCurrentResponses = {
/**
* Current project
*/
200: Project
}
export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses]
export type EventSubscribeData = {
body?: never
path?: never
@@ -1519,11 +1543,13 @@ export type SessionMessagesResponses = {
export type SessionMessagesResponse = SessionMessagesResponses[keyof SessionMessagesResponses]
export type SessionChatData = {
export type SessionPromptData = {
body?: {
messageID?: string
providerID: string
modelID: string
model?: {
providerID: string
modelID: string
}
agent?: string
system?: string
tools?: {
@@ -1553,7 +1579,7 @@ export type SessionChatData = {
url: "/session/{id}/message"
}
export type SessionChatResponses = {
export type SessionPromptResponses = {
/**
* Created message
*/
@@ -1563,7 +1589,7 @@ export type SessionChatResponses = {
}
}
export type SessionChatResponse = SessionChatResponses[keyof SessionChatResponses]
export type SessionPromptResponse = SessionPromptResponses[keyof SessionPromptResponses]
export type SessionMessageData = {
body?: never
+1 -1
View File
@@ -147,7 +147,7 @@ resources:
summarize: post /session/{id}/summarize
message: get /session/{id}/message/{messageID}
messages: get /session/{id}/message
chat: post /session/{id}/message
prompt: post /session/{id}/message
command: post /session/{id}/command
shell: post /session/{id}/shell
update: patch /session/{id}
+17 -12
View File
@@ -786,12 +786,14 @@ func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) {
a.Messages = append(a.Messages, message)
cmds = append(cmds, func() tea.Msg {
_, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{
ProviderID: opencode.F(a.Provider.ID),
ModelID: opencode.F(a.Model.ID),
Agent: opencode.F(a.Agent().Name),
MessageID: opencode.F(messageID),
Parts: opencode.F(message.ToSessionChatParams()),
_, err := a.Client.Session.Prompt(ctx, a.Session.ID, opencode.SessionPromptParams{
Model: opencode.F(opencode.SessionPromptParamsModel{
ProviderID: opencode.F(a.Provider.ID),
ModelID: opencode.F(a.Model.ID),
}),
Agent: opencode.F(a.Agent().Name),
MessageID: opencode.F(messageID),
Parts: opencode.F(message.ToSessionChatParams()),
})
if err != nil {
errormsg := fmt.Sprintf("failed to send message: %v", err)
@@ -818,15 +820,18 @@ func (a *App) SendCommand(ctx context.Context, command string, args string) (*Ap
}
cmds = append(cmds, func() tea.Msg {
params := opencode.SessionCommandParams{
Command: opencode.F(command),
Arguments: opencode.F(args),
Agent: opencode.F(a.Agents[a.AgentIndex].Name),
}
if a.Provider != nil && a.Model != nil {
params.Model = opencode.F(a.Provider.ID + "/" + a.Model.ID)
}
_, err := a.Client.Session.Command(
context.Background(),
a.Session.ID,
opencode.SessionCommandParams{
Command: opencode.F(command),
Arguments: opencode.F(args),
Agent: opencode.F(a.Agents[a.AgentIndex].Name),
Model: opencode.F(a.State.Provider + "/" + a.State.Model),
},
params,
)
if err != nil {
slog.Error("Failed to execute command", "error", err)
+5 -2
View File
@@ -43,6 +43,9 @@ func (p Prompt) ToMessage(
}
for _, att := range textAttachments {
if source, ok := att.GetTextSource(); ok {
if att.StartIndex > att.EndIndex || att.EndIndex > len(text) {
continue
}
text = text[:att.StartIndex] + source.Value + text[att.EndIndex:]
}
}
@@ -204,8 +207,8 @@ func (m Message) ToPrompt() (*Prompt, error) {
return nil, errors.New("unknown message type")
}
func (m Message) ToSessionChatParams() []opencode.SessionChatParamsPartUnion {
parts := []opencode.SessionChatParamsPartUnion{}
func (m Message) ToSessionChatParams() []opencode.SessionPromptParamsPartUnion {
parts := []opencode.SessionPromptParamsPartUnion{}
for _, part := range m.Parts {
switch p := part.(type) {
case opencode.TextPart:
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode/web",
"type": "module",
"version": "0.5.29",
"version": "0.6.1",
"scripts": {
"dev": "astro dev",
"dev:remote": "sst shell --stage=dev --target=Web astro dev",
@@ -9,7 +9,7 @@ you to use opencode at your organization.
To get started, we recommend:
1. Do a trial internally with your team.
2. [**Contact us**](mailto:hello@anoma.ly) to discuss pricing and implementation options.
2. [**Contact us**](mailto:contact@anoma.ly) to discuss pricing and implementation options.
---
@@ -55,7 +55,7 @@ We recommend you disable this for your trial.
## Deployment
Once you have completed your trial and you are ready to self-host opencode at
your organization, you can [**contact us**](mailto:hello@anoma.ly) to discuss
your organization, you can [**contact us**](mailto:contact@anoma.ly) to discuss
pricing and implementation options.
---
@@ -26,7 +26,7 @@ Plugins are loaded from:
### Basic structure
```js title=".opencode/plugin/example.js"
export const MyPlugin = async ({ app, client, $ }) => {
export const MyPlugin = async ({ project, client, $, directory, worktree }) => {
console.log("Plugin initialized!")
return {
@@ -37,7 +37,9 @@ export const MyPlugin = async ({ app, client, $ }) => {
The plugin function receives:
- `app`: The opencode application instance.
- `project`: The current project information.
- `directory`: The current working directory.
- `worktree`: The git worktree path.
- `client`: An opencode SDK client for interacting with the AI.
- `$`: Bun's [shell API](https://bun.com/docs/runtime/shell) for executing commands.
@@ -50,7 +52,7 @@ For TypeScript plugins, you can import types from the plugin package:
```ts title="my-plugin.ts" {1}
import type { Plugin } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async ({ app, client, $ }) => {
export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
return {
// Type-safe hook implementations
}
@@ -70,7 +72,7 @@ Here are some examples of plugins you can use to extend opencode.
Send notifications when certain events occur:
```js title=".opencode/plugin/notification.js"
export const NotificationPlugin = async ({ client, $ }) => {
export const NotificationPlugin = async ({ project, client, $, directory, worktree }) => {
return {
event: async ({ event }) => {
// Send notification on session completion
@@ -91,13 +93,13 @@ We are using `osascript` to run AppleScript on macOS. Here we are using it to se
Prevent opencode from reading `.env` files:
```javascript title=".opencode/plugin/env-protection.js"
export const EnvProtection = async ({ client, $ }) => {
export const EnvProtection = async ({ project, client, $, directory, worktree }) => {
return {
"tool.execute.before": async (input, output) => {
if (input.tool === "read" && output.args.filePath.includes(".env")) {
throw new Error("Do not read .env files")
}
}
},
}
}
```
+66 -17
View File
@@ -10,6 +10,8 @@ The opencode [JS/TS SDK](https://www.npmjs.com/package/@opencode-ai/sdk) provide
[Learn more](/docs/server) about how the opencode server works.
> **Note**: Many API endpoints now require a `directory` query parameter to specify the working directory context.
---
## Install
@@ -65,12 +67,12 @@ server.close()
#### Options
| Option | Type | Description | Default |
| ---------- | -------------- | ------------------------------ | ------------- |
| `hostname` | `string` | Server hostname | `127.0.0.1` |
| `port` | `number` | Server port | `4096` |
| `signal` | `AbortSignal` | Abort signal for cancellation | `undefined` |
| `timeout` | `number` | Timeout in ms for server start | `5000` |
| Option | Type | Description | Default |
| ---------- | ------------- | ------------------------------ | ----------- |
| `hostname` | `string` | Server hostname | `127.0.0.1` |
| `port` | `number` | Server port | `4096` |
| `signal` | `AbortSignal` | Abort signal for cancellation | `undefined` |
| `timeout` | `number` | Timeout in ms for server start | `5000` |
---
@@ -80,7 +82,7 @@ The SDK includes TypeScript definitions for all API types. Import them directly:
```typescript
import type { Session, Message, Part } from "@opencode-ai/sdk"
````
```
All types are generated from the server's OpenAPI specification and available in the <a href={typesUrl}>types file</a>.
@@ -108,18 +110,63 @@ The SDK exposes all server APIs through a type-safe client interface.
### App
| Method | Description | Response |
| ------------ | ------------------ | --------------------------------------- |
| `app.get()` | Get app info | <a href={typesUrl}><code>App</code></a> |
| `app.init()` | Initialize the app | `boolean` |
| Method | Description | Response |
| -------------- | ------------------------- | ------------------------------------------- |
| `app.log()` | Write a log entry | `boolean` |
| `app.agents()` | List all available agents | <a href={typesUrl}><code>Agent[]</code></a> |
---
#### Examples
```javascript
const app = await client.app.get()
await client.app.init()
// Log an entry
await client.app.log({
service: "my-app",
level: "info",
message: "Operation completed",
})
// List available agents
const agents = await client.app.agents()
```
---
### Project
| Method | Description | Response |
| ------------------- | ------------------- | --------------------------------------------- |
| `project.list()` | List all projects | <a href={typesUrl}><code>Project[]</code></a> |
| `project.current()` | Get current project | <a href={typesUrl}><code>Project</code></a> |
---
#### Examples
```javascript
// List all projects
const projects = await client.project.list()
// Get current project
const currentProject = await client.project.current()
```
---
### Path
| Method | Description | Response |
| ------------ | ---------------- | ---------------------------------------- |
| `path.get()` | Get current path | <a href={typesUrl}><code>Path</code></a> |
---
#### Examples
```javascript
// Get current path information
const pathInfo = await client.path.get()
```
---
@@ -159,7 +206,7 @@ const { providers, default: defaults } = await client.config.providers()
| `session.summarize({ id, providerID, modelID })` | Summarize session | Returns `boolean` |
| `session.messages({ id })` | List messages in a session | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}[]` |
| `session.message({ id, messageID })` | Get message details | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.chat({ id, ...chatInput })` | Send chat message | Returns <a href={typesUrl}><code>Message</code></a> |
| `session.prompt({ id, ...promptInput })` | Send prompt message | Returns <a href={typesUrl}><code>Message</code></a> |
| `session.shell({ id, agent, command })` | Run a shell command | Returns <a href={typesUrl}><code>Message</code></a> |
| `session.revert({ id, messageID, partID? })` | Revert a message | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unrevert({ id })` | Restore reverted messages | Returns <a href={typesUrl}><code>Session</code></a> |
@@ -175,10 +222,12 @@ const session = await client.session.create({ title: "My session" })
const sessions = await client.session.list()
// Send messages
const message = await client.session.chat({
const message = await client.session.prompt({
id: session.id,
providerID: "anthropic",
modelID: "claude-3-5-sonnet-20241022",
model: {
providerID: "anthropic",
modelID: "claude-3-5-sonnet-20241022",
},
parts: [{ type: "text", text: "Hello!" }],
})
```
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "0.5.29",
"version": "0.6.1",
"publisher": "sst-dev",
"repository": {
"type": "git",