Dax Raad 9 месяцев назад
Родитель
Сommit
652429377b
18 измененных файлов с 188 добавлено и 498 удалено
  1. 12 0
      cmd/root.go
  2. 11 1
      go.mod
  3. 17 0
      go.sum
  4. 9 0
      internal/app/app.go
  5. 1 0
      js/.gitignore
  6. 2 2
      js/bun.lock
  7. 0 90
      js/openapi.json
  8. 0 3
      js/opencode.jsonc
  9. 1 1
      js/package.json
  10. 20 0
      js/schema.json
  11. 19 3
      js/src/bus/index.ts
  12. 16 3
      js/src/index.ts
  13. 1 1
      js/src/storage/storage.ts
  14. 2 0
      pkg/client/.gitignore
  15. 3 1
      pkg/client/client.go
  16. 0 5
      pkg/client/config.yml
  17. 74 0
      pkg/client/event.go
  18. 0 388
      pkg/client/generated.go

+ 12 - 0
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

+ 11 - 1
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
+)

+ 17 - 0
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=

+ 9 - 0
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(),

+ 1 - 0
js/.gitignore

@@ -1,2 +1,3 @@
 node_modules
 dist
+gen

+ 2 - 2
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]4.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="],
+    "zod": ["[email protected]5.3", "", {}, "sha512-VGZqnyYNrl8JpEJRZaFPqeVNIuqgXNu4cXZ5cOb6zEUO1OxKbRnWB4UdDIXMmiERWncs0yDQukssHov8JUxykQ=="],
 
     "zod-openapi": ["[email protected]", "", { "peerDependencies": { "zod": "^3.21.4" } }, "sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g=="],
 

+ 0 - 90
js/openapi.json

@@ -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": {}
-  }
-}

+ 0 - 3
js/opencode.jsonc

@@ -1,3 +0,0 @@
-{
-  "providers": {}
-}

+ 1 - 1
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"
   }
 }

+ 20 - 0
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"
+}

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

+ 16 - 3
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),
+      );
     }
   },
 );

+ 1 - 1
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" });

+ 2 - 0
pkg/client/.gitignore

@@ -0,0 +1,2 @@
+gen
+generated-*.go

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

+ 0 - 5
pkg/client/config.yml

@@ -1,5 +0,0 @@
-package: client
-output: generated.go
-generate:
-  - client
-  - types

+ 74 - 0
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
+}

+ 0 - 388
pkg/client/generated.go

@@ -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
-}