summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-05-19 15:43:14 -0400
committerDax Raad <[email protected]>2025-05-26 12:40:17 -0400
commit652429377b99085d686d6b907c2f550c304e6b98 (patch)
treee4fa0de6b0503885c76bb77e1e0d0e7d56e5117e
parent99af6146d5def31c59993636d60eb75a483a283b (diff)
downloadopencode-652429377b99085d686d6b907c2f550c304e6b98.tar.gz
opencode-652429377b99085d686d6b907c2f550c304e6b98.zip
sync
-rw-r--r--cmd/root.go12
-rw-r--r--go.mod12
-rw-r--r--go.sum17
-rw-r--r--internal/app/app.go9
-rw-r--r--js/.gitignore1
-rw-r--r--js/bun.lock4
-rw-r--r--js/openapi.json90
-rw-r--r--js/opencode.jsonc3
-rw-r--r--js/package.json2
-rw-r--r--js/schema.json20
-rw-r--r--js/src/bus/index.ts22
-rw-r--r--js/src/index.ts19
-rw-r--r--js/src/storage/storage.ts2
-rw-r--r--pkg/client/.gitignore2
-rw-r--r--pkg/client/client.go4
-rw-r--r--pkg/client/config.yml5
-rw-r--r--pkg/client/event.go74
-rw-r--r--pkg/client/generated.go388
18 files changed, 188 insertions, 498 deletions
diff --git a/cmd/root.go b/cmd/root.go
index ab102afe6..81083ef72 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -182,6 +182,18 @@ to assist developers in writing, debugging, and understanding code directly from
}
}()
+ evts, err := app.Client.Event(ctx)
+ if err != nil {
+ slog.Error("Failed to subscribe to events", "error", err)
+ return err
+ }
+
+ go func() {
+ for item := range evts {
+ program.Send(item)
+ }
+ }()
+
// Cleanup function for when the program exits
cleanup := func() {
// Cancel subscriptions first
diff --git a/go.mod b/go.mod
index 4f444c54a..79d3221a0 100644
--- a/go.mod
+++ b/go.mod
@@ -46,10 +46,13 @@ require (
require (
cloud.google.com/go v0.120.0 // indirect
cloud.google.com/go/auth v0.15.0 // indirect
+ dario.cat/mergo v1.0.2 // indirect
+ github.com/atombender/go-jsonschema v0.20.0 // indirect
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
+ github.com/goccy/go-yaml v1.17.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/invopop/yaml v0.3.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
@@ -57,11 +60,15 @@ require (
github.com/labstack/gommon v0.4.2 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/sanity-io/litter v1.5.8 // indirect
+ github.com/sosodev/duration v1.3.1 // indirect
github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
@@ -166,4 +173,7 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
)
-tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
+tool (
+ github.com/atombender/go-jsonschema
+ github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
+)
diff --git a/go.sum b/go.sum
index c08a2195d..028f1edb6 100644
--- a/go.sum
+++ b/go.sum
@@ -15,6 +15,8 @@ cloud.google.com/go/monitoring v1.24.1 h1:vKiypZVFD/5a3BbQMvI4gZdl8445ITzXFh257X
cloud.google.com/go/monitoring v1.24.1/go.mod h1:Z05d1/vn9NaujqY2voG6pVQXoJGbp+r3laV+LySt9K0=
cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw=
cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc=
+dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
+dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys=
@@ -50,6 +52,8 @@ github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsVi
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/anthropics/anthropic-sdk-go v0.2.0-beta.2 h1:h7qxtumNjKPWFv1QM/HJy60MteeW23iKeEtBoY7bYZk=
github.com/anthropics/anthropic-sdk-go v0.2.0-beta.2/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c=
+github.com/atombender/go-jsonschema v0.20.0 h1:AHg0LeI0HcjQ686ALwUNqVJjNRcSXpIR6U+wC2J0aFY=
+github.com/atombender/go-jsonschema v0.20.0/go.mod h1:ZmbuR11v2+cMM0PdP6ySxtyZEGFBmhgF4xa4J6Hdls8=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
@@ -133,6 +137,7 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -186,6 +191,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
+github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -288,6 +295,8 @@ github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6B
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
+github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
+github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
@@ -335,8 +344,11 @@ github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0V
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
+github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pressly/goose/v3 v3.24.2 h1:c/ie0Gm8rnIVKvnDQ/scHErv46jrDv9b4I0WRcFJzYU=
@@ -355,6 +367,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
+github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
+github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y=
github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
@@ -364,6 +378,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
+github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
+github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/speakeasy-api/openapi-overlay v0.9.0 h1:Wrz6NO02cNlLzx1fB093lBlYxSI54VRhy1aSutx0PQg=
@@ -381,6 +397,7 @@ github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqj
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
diff --git a/internal/app/app.go b/internal/app/app.go
index 41070684e..0ef89d1b9 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -20,6 +20,7 @@ import (
"github.com/sst/opencode/internal/session"
"github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/pkg/client"
)
type App struct {
@@ -30,6 +31,7 @@ type App struct {
History history.Service
Permissions permission.Service
Status status.Service
+ Client *client.Client
PrimaryAgent agent.Service
@@ -79,7 +81,14 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
}
fileutil.Init()
+ client, err := client.NewClient("http://localhost:16713")
+ if err != nil {
+ slog.Error("Failed to create client", "error", err)
+ return nil, err
+ }
+
app := &App{
+ Client: client,
CurrentSession: &session.Session{},
Logs: logging.GetService(),
Sessions: session.GetService(),
diff --git a/js/.gitignore b/js/.gitignore
index f06235c46..5f9a3e7af 100644
--- a/js/.gitignore
+++ b/js/.gitignore
@@ -1,2 +1,3 @@
node_modules
dist
+gen
diff --git a/js/bun.lock b/js/bun.lock
index e6d8f9074..df1b2a17f 100644
--- a/js/bun.lock
+++ b/js/bun.lock
@@ -12,7 +12,7 @@
"clipanion": "^4.0.0-rc.4",
"hono": "^4.7.10",
"hono-openapi": "^0.4.8",
- "zod": "^3.24.4",
+ "zod": "^3.25.3",
"zod-openapi": "^4.2.4",
},
"devDependencies": {
@@ -199,7 +199,7 @@
"yoga-layout": ["[email protected]", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
- "zod": ["[email protected]", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="],
+ "zod": ["[email protected]", "", {}, "sha512-VGZqnyYNrl8JpEJRZaFPqeVNIuqgXNu4cXZ5cOb6zEUO1OxKbRnWB4UdDIXMmiERWncs0yDQukssHov8JUxykQ=="],
"zod-openapi": ["[email protected]", "", { "peerDependencies": { "zod": "^3.21.4" } }, "sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g=="],
diff --git a/js/openapi.json b/js/openapi.json
deleted file mode 100644
index 8dbe78550..000000000
--- a/js/openapi.json
+++ /dev/null
@@ -1,90 +0,0 @@
-{
- "openapi": "3.1.0",
- "info": {
- "title": "opencode",
- "description": "opencode api",
- "version": "1.0.0"
- },
- "paths": {
- "/session_create": {
- "post": {
- "responses": {
- "200": {
- "description": "Successfully created session",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^ses"
- },
- "title": {
- "type": "string"
- },
- "tokens": {
- "type": "object",
- "properties": {
- "input": {
- "type": "number"
- },
- "output": {
- "type": "number"
- },
- "reasoning": {
- "type": "number"
- }
- },
- "required": [
- "input",
- "output",
- "reasoning"
- ]
- }
- },
- "required": [
- "id",
- "title",
- "tokens"
- ]
- }
- }
- }
- }
- },
- "operationId": "postSession_create",
- "parameters": [],
- "description": "Create a new session"
- }
- },
- "/session_chat": {
- "post": {
- "responses": {},
- "operationId": "postSession_chat",
- "parameters": [],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "sessionID": {
- "type": "string"
- },
- "parts": {}
- },
- "required": [
- "sessionID"
- ]
- }
- }
- }
- }
- }
- }
- },
- "components": {
- "schemas": {}
- }
-} \ No newline at end of file
diff --git a/js/opencode.jsonc b/js/opencode.jsonc
deleted file mode 100644
index 87b07bc4e..000000000
--- a/js/opencode.jsonc
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "providers": {}
-}
diff --git a/js/package.json b/js/package.json
index 63429b846..3703c10b0 100644
--- a/js/package.json
+++ b/js/package.json
@@ -22,7 +22,7 @@
"clipanion": "^4.0.0-rc.4",
"hono": "^4.7.10",
"hono-openapi": "^0.4.8",
- "zod": "^3.24.4",
+ "zod": "^3.25.3",
"zod-openapi": "^4.2.4"
}
}
diff --git a/js/schema.json b/js/schema.json
new file mode 100644
index 000000000..bc08652e0
--- /dev/null
+++ b/js/schema.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "storage.write"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "body": {}
+ },
+ "required": ["key", "body"]
+ }
+ },
+ "required": ["type", "properties"],
+ "$schema": "https://json-schema.org/draft-2020-12/schema"
+}
diff --git a/js/src/bus/index.ts b/js/src/bus/index.ts
index 4f3b40041..f320aeef3 100644
--- a/js/src/bus/index.ts
+++ b/js/src/bus/index.ts
@@ -1,4 +1,4 @@
-import type { z, ZodSchema } from "zod";
+import { z, type ZodType } from "zod/v4";
import { App } from "../app";
import { Log } from "../util/log";
@@ -16,14 +16,30 @@ export namespace Bus {
export type EventDefinition = ReturnType<typeof event>;
- export function event<Type extends string, Properties extends ZodSchema>(
+ const registry = new Map<string, EventDefinition>();
+
+ export function event<Type extends string, Properties extends ZodType>(
type: Type,
properties: Properties,
) {
- return {
+ const result = {
type,
properties,
};
+ registry.set(type, result);
+ return result;
+ }
+
+ export function specs() {
+ const children = {} as any;
+ for (const [type, def] of registry.entries()) {
+ children[def.type] = def.properties;
+ }
+ const result = z.toJSONSchema(z.object(children)) as any;
+ result.definitions = result.properties;
+ delete result.properties;
+ delete result.required;
+ return result;
}
export function publish<Definition extends EventDefinition>(
diff --git a/js/src/index.ts b/js/src/index.ts
index 10b3fef32..b2a7c5f21 100644
--- a/js/src/index.ts
+++ b/js/src/index.ts
@@ -1,6 +1,9 @@
import { App } from "./app";
import { Server } from "./server/server";
import { Cli, Command, runExit } from "clipanion";
+import fs from "fs/promises";
+import path from "path";
+import { Bus } from "./bus";
const cli = new Cli({
binaryLabel: `opencode`,
@@ -22,11 +25,21 @@ cli.register(
},
);
cli.register(
- class OpenApi extends Command {
- static paths = [["openapi"]];
+ class Generate extends Command {
+ static paths = [["generate"]];
async execute() {
const specs = await Server.openapi();
- this.context.stdout.write(JSON.stringify(specs, null, 2));
+ const dir = "gen";
+ await fs.rmdir(dir, { recursive: true }).catch(() => {});
+ await fs.mkdir(dir, { recursive: true });
+ await Bun.write(
+ path.join(dir, "openapi.json"),
+ JSON.stringify(specs, null, 2),
+ );
+ await Bun.write(
+ path.join(dir, "event.json"),
+ JSON.stringify(Bus.specs(), null, 2),
+ );
}
},
);
diff --git a/js/src/storage/storage.ts b/js/src/storage/storage.ts
index c24666c9f..50364beeb 100644
--- a/js/src/storage/storage.ts
+++ b/js/src/storage/storage.ts
@@ -5,7 +5,7 @@ import { Log } from "../util/log";
import { App } from "../app";
import { AppPath } from "../app/path";
import { Bus } from "../bus";
-import z from "zod";
+import z from "zod/v4";
export namespace Storage {
const log = Log.create({ service: "storage" });
diff --git a/pkg/client/.gitignore b/pkg/client/.gitignore
new file mode 100644
index 000000000..c56971e89
--- /dev/null
+++ b/pkg/client/.gitignore
@@ -0,0 +1,2 @@
+gen
+generated-*.go
diff --git a/pkg/client/client.go b/pkg/client/client.go
index 0afda3ec4..98a49a0d9 100644
--- a/pkg/client/client.go
+++ b/pkg/client/client.go
@@ -1,3 +1,5 @@
package client
-//go:generate go tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=./config.yml ../../js/openapi.json
+//go:generate bun run ../../js/src/index.ts generate
+//go:generate go tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --package=client --generate=types,client -o generated-client.go ./gen/openapi.json
+//go:generate go tool github.com/atombender/go-jsonschema -p client -o generated-event.go ./gen/event.json
diff --git a/pkg/client/config.yml b/pkg/client/config.yml
deleted file mode 100644
index 245969174..000000000
--- a/pkg/client/config.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-package: client
-output: generated.go
-generate:
- - client
- - types
diff --git a/pkg/client/event.go b/pkg/client/event.go
new file mode 100644
index 000000000..a6d7798fa
--- /dev/null
+++ b/pkg/client/event.go
@@ -0,0 +1,74 @@
+package client
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "net/http"
+ "reflect"
+ "strings"
+)
+
+var EventMap = map[string]any{
+ "storage.write": StorageWrite{},
+}
+
+type EventMessage struct {
+ Type string `json:"type"`
+ Properties json.RawMessage `json:"properties"`
+}
+
+func (c *Client) Event(ctx context.Context) (<-chan any, error) {
+ events := make(chan any)
+ req, err := http.NewRequestWithContext(ctx, "GET", c.Server+"/event", nil)
+ if err != nil {
+ return nil, err
+ }
+
+ go func() {
+ defer close(events)
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return
+ }
+ defer resp.Body.Close()
+
+ scanner := bufio.NewScanner(resp.Body)
+ for scanner.Scan() {
+ line := scanner.Text()
+ if strings.HasPrefix(line, "data: ") {
+ data := strings.TrimPrefix(line, "data: ")
+
+ var eventMsg EventMessage
+ if err := json.Unmarshal([]byte(data), &eventMsg); err != nil {
+ continue
+ }
+
+ eventTemplate, exists := EventMap[eventMsg.Type]
+ if !exists {
+ select {
+ case events <- eventMsg:
+ case <-ctx.Done():
+ return
+ }
+ continue
+ }
+
+ eventValue := reflect.New(reflect.TypeOf(eventTemplate)).Interface()
+
+ if err := json.Unmarshal(eventMsg.Properties, eventValue); err != nil {
+ continue
+ }
+
+ select {
+ case events <- eventValue:
+ case <-ctx.Done():
+ return
+ }
+ }
+ }
+ }()
+
+ return events, nil
+}
diff --git a/pkg/client/generated.go b/pkg/client/generated.go
deleted file mode 100644
index 26903ab99..000000000
--- a/pkg/client/generated.go
+++ /dev/null
@@ -1,388 +0,0 @@
-// Package client provides primitives to interact with the openapi HTTP API.
-//
-// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT.
-package client
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "strings"
-)
-
-// PostSessionChatJSONBody defines parameters for PostSessionChat.
-type PostSessionChatJSONBody struct {
- Parts *interface{} `json:"parts,omitempty"`
- SessionID string `json:"sessionID"`
-}
-
-// PostSessionChatJSONRequestBody defines body for PostSessionChat for application/json ContentType.
-type PostSessionChatJSONRequestBody PostSessionChatJSONBody
-
-// RequestEditorFn is the function signature for the RequestEditor callback function
-type RequestEditorFn func(ctx context.Context, req *http.Request) error
-
-// Doer performs HTTP requests.
-//
-// The standard http.Client implements this interface.
-type HttpRequestDoer interface {
- Do(req *http.Request) (*http.Response, error)
-}
-
-// Client which conforms to the OpenAPI3 specification for this service.
-type Client struct {
- // The endpoint of the server conforming to this interface, with scheme,
- // https://api.deepmap.com for example. This can contain a path relative
- // to the server, such as https://api.deepmap.com/dev-test, and all the
- // paths in the swagger spec will be appended to the server.
- Server string
-
- // Doer for performing requests, typically a *http.Client with any
- // customized settings, such as certificate chains.
- Client HttpRequestDoer
-
- // A list of callbacks for modifying requests which are generated before sending over
- // the network.
- RequestEditors []RequestEditorFn
-}
-
-// ClientOption allows setting custom parameters during construction
-type ClientOption func(*Client) error
-
-// Creates a new Client, with reasonable defaults
-func NewClient(server string, opts ...ClientOption) (*Client, error) {
- // create a client with sane default values
- client := Client{
- Server: server,
- }
- // mutate client and add all optional params
- for _, o := range opts {
- if err := o(&client); err != nil {
- return nil, err
- }
- }
- // ensure the server URL always has a trailing slash
- if !strings.HasSuffix(client.Server, "/") {
- client.Server += "/"
- }
- // create httpClient, if not already present
- if client.Client == nil {
- client.Client = &http.Client{}
- }
- return &client, nil
-}
-
-// WithHTTPClient allows overriding the default Doer, which is
-// automatically created using http.Client. This is useful for tests.
-func WithHTTPClient(doer HttpRequestDoer) ClientOption {
- return func(c *Client) error {
- c.Client = doer
- return nil
- }
-}
-
-// WithRequestEditorFn allows setting up a callback function, which will be
-// called right before sending the request. This can be used to mutate the request.
-func WithRequestEditorFn(fn RequestEditorFn) ClientOption {
- return func(c *Client) error {
- c.RequestEditors = append(c.RequestEditors, fn)
- return nil
- }
-}
-
-// The interface specification for the client above.
-type ClientInterface interface {
- // PostSessionChatWithBody request with any body
- PostSessionChatWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
-
- PostSessionChat(ctx context.Context, body PostSessionChatJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
-
- // PostSessionCreate request
- PostSessionCreate(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
-}
-
-func (c *Client) PostSessionChatWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
- req, err := NewPostSessionChatRequestWithBody(c.Server, contentType, body)
- if err != nil {
- return nil, err
- }
- req = req.WithContext(ctx)
- if err := c.applyEditors(ctx, req, reqEditors); err != nil {
- return nil, err
- }
- return c.Client.Do(req)
-}
-
-func (c *Client) PostSessionChat(ctx context.Context, body PostSessionChatJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
- req, err := NewPostSessionChatRequest(c.Server, body)
- if err != nil {
- return nil, err
- }
- req = req.WithContext(ctx)
- if err := c.applyEditors(ctx, req, reqEditors); err != nil {
- return nil, err
- }
- return c.Client.Do(req)
-}
-
-func (c *Client) PostSessionCreate(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
- req, err := NewPostSessionCreateRequest(c.Server)
- if err != nil {
- return nil, err
- }
- req = req.WithContext(ctx)
- if err := c.applyEditors(ctx, req, reqEditors); err != nil {
- return nil, err
- }
- return c.Client.Do(req)
-}
-
-// NewPostSessionChatRequest calls the generic PostSessionChat builder with application/json body
-func NewPostSessionChatRequest(server string, body PostSessionChatJSONRequestBody) (*http.Request, error) {
- var bodyReader io.Reader
- buf, err := json.Marshal(body)
- if err != nil {
- return nil, err
- }
- bodyReader = bytes.NewReader(buf)
- return NewPostSessionChatRequestWithBody(server, "application/json", bodyReader)
-}
-
-// NewPostSessionChatRequestWithBody generates requests for PostSessionChat with any type of body
-func NewPostSessionChatRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) {
- var err error
-
- serverURL, err := url.Parse(server)
- if err != nil {
- return nil, err
- }
-
- operationPath := fmt.Sprintf("/session_chat")
- if operationPath[0] == '/' {
- operationPath = "." + operationPath
- }
-
- queryURL, err := serverURL.Parse(operationPath)
- if err != nil {
- return nil, err
- }
-
- req, err := http.NewRequest("POST", queryURL.String(), body)
- if err != nil {
- return nil, err
- }
-
- req.Header.Add("Content-Type", contentType)
-
- return req, nil
-}
-
-// NewPostSessionCreateRequest generates requests for PostSessionCreate
-func NewPostSessionCreateRequest(server string) (*http.Request, error) {
- var err error
-
- serverURL, err := url.Parse(server)
- if err != nil {
- return nil, err
- }
-
- operationPath := fmt.Sprintf("/session_create")
- if operationPath[0] == '/' {
- operationPath = "." + operationPath
- }
-
- queryURL, err := serverURL.Parse(operationPath)
- if err != nil {
- return nil, err
- }
-
- req, err := http.NewRequest("POST", queryURL.String(), nil)
- if err != nil {
- return nil, err
- }
-
- return req, nil
-}
-
-func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error {
- for _, r := range c.RequestEditors {
- if err := r(ctx, req); err != nil {
- return err
- }
- }
- for _, r := range additionalEditors {
- if err := r(ctx, req); err != nil {
- return err
- }
- }
- return nil
-}
-
-// ClientWithResponses builds on ClientInterface to offer response payloads
-type ClientWithResponses struct {
- ClientInterface
-}
-
-// NewClientWithResponses creates a new ClientWithResponses, which wraps
-// Client with return type handling
-func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) {
- client, err := NewClient(server, opts...)
- if err != nil {
- return nil, err
- }
- return &ClientWithResponses{client}, nil
-}
-
-// WithBaseURL overrides the baseURL.
-func WithBaseURL(baseURL string) ClientOption {
- return func(c *Client) error {
- newBaseURL, err := url.Parse(baseURL)
- if err != nil {
- return err
- }
- c.Server = newBaseURL.String()
- return nil
- }
-}
-
-// ClientWithResponsesInterface is the interface specification for the client with responses above.
-type ClientWithResponsesInterface interface {
- // PostSessionChatWithBodyWithResponse request with any body
- PostSessionChatWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionChatResponse, error)
-
- PostSessionChatWithResponse(ctx context.Context, body PostSessionChatJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionChatResponse, error)
-
- // PostSessionCreateWithResponse request
- PostSessionCreateWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostSessionCreateResponse, error)
-}
-
-type PostSessionChatResponse struct {
- Body []byte
- HTTPResponse *http.Response
-}
-
-// Status returns HTTPResponse.Status
-func (r PostSessionChatResponse) Status() string {
- if r.HTTPResponse != nil {
- return r.HTTPResponse.Status
- }
- return http.StatusText(0)
-}
-
-// StatusCode returns HTTPResponse.StatusCode
-func (r PostSessionChatResponse) StatusCode() int {
- if r.HTTPResponse != nil {
- return r.HTTPResponse.StatusCode
- }
- return 0
-}
-
-type PostSessionCreateResponse struct {
- Body []byte
- HTTPResponse *http.Response
- JSON200 *struct {
- Id string `json:"id"`
- Title string `json:"title"`
- Tokens struct {
- Input float32 `json:"input"`
- Output float32 `json:"output"`
- Reasoning float32 `json:"reasoning"`
- } `json:"tokens"`
- }
-}
-
-// Status returns HTTPResponse.Status
-func (r PostSessionCreateResponse) Status() string {
- if r.HTTPResponse != nil {
- return r.HTTPResponse.Status
- }
- return http.StatusText(0)
-}
-
-// StatusCode returns HTTPResponse.StatusCode
-func (r PostSessionCreateResponse) StatusCode() int {
- if r.HTTPResponse != nil {
- return r.HTTPResponse.StatusCode
- }
- return 0
-}
-
-// PostSessionChatWithBodyWithResponse request with arbitrary body returning *PostSessionChatResponse
-func (c *ClientWithResponses) PostSessionChatWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionChatResponse, error) {
- rsp, err := c.PostSessionChatWithBody(ctx, contentType, body, reqEditors...)
- if err != nil {
- return nil, err
- }
- return ParsePostSessionChatResponse(rsp)
-}
-
-func (c *ClientWithResponses) PostSessionChatWithResponse(ctx context.Context, body PostSessionChatJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionChatResponse, error) {
- rsp, err := c.PostSessionChat(ctx, body, reqEditors...)
- if err != nil {
- return nil, err
- }
- return ParsePostSessionChatResponse(rsp)
-}
-
-// PostSessionCreateWithResponse request returning *PostSessionCreateResponse
-func (c *ClientWithResponses) PostSessionCreateWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostSessionCreateResponse, error) {
- rsp, err := c.PostSessionCreate(ctx, reqEditors...)
- if err != nil {
- return nil, err
- }
- return ParsePostSessionCreateResponse(rsp)
-}
-
-// ParsePostSessionChatResponse parses an HTTP response from a PostSessionChatWithResponse call
-func ParsePostSessionChatResponse(rsp *http.Response) (*PostSessionChatResponse, error) {
- bodyBytes, err := io.ReadAll(rsp.Body)
- defer func() { _ = rsp.Body.Close() }()
- if err != nil {
- return nil, err
- }
-
- response := &PostSessionChatResponse{
- Body: bodyBytes,
- HTTPResponse: rsp,
- }
-
- return response, nil
-}
-
-// ParsePostSessionCreateResponse parses an HTTP response from a PostSessionCreateWithResponse call
-func ParsePostSessionCreateResponse(rsp *http.Response) (*PostSessionCreateResponse, error) {
- bodyBytes, err := io.ReadAll(rsp.Body)
- defer func() { _ = rsp.Body.Close() }()
- if err != nil {
- return nil, err
- }
-
- response := &PostSessionCreateResponse{
- Body: bodyBytes,
- HTTPResponse: rsp,
- }
-
- switch {
- case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
- var dest struct {
- Id string `json:"id"`
- Title string `json:"title"`
- Tokens struct {
- Input float32 `json:"input"`
- Output float32 `json:"output"`
- Reasoning float32 `json:"reasoning"`
- } `json:"tokens"`
- }
- if err := json.Unmarshal(bodyBytes, &dest); err != nil {
- return nil, err
- }
- response.JSON200 = &dest
-
- }
-
- return response, nil
-}