summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-05-18 14:13:04 -0400
committerDax Raad <[email protected]>2025-05-26 12:40:17 -0400
commit0e303e6508edb4374213d1f98ec383b266339774 (patch)
treef7dc146eb58126f55f470ef135b66c678bf16898
parentbcd2fd68b7fa00af055f558049994c2975d9515d (diff)
downloadopencode-0e303e6508edb4374213d1f98ec383b266339774.tar.gz
opencode-0e303e6508edb4374213d1f98ec383b266339774.zip
sync
-rw-r--r--js/.gitignore32
-rw-r--r--js/bun.lock103
-rw-r--r--js/example/client.tsx116
-rw-r--r--js/package.json9
-rw-r--r--js/src/bus/index.ts79
-rw-r--r--js/src/id/id.ts11
-rw-r--r--js/src/index.ts22
-rw-r--r--js/src/server/server.ts86
-rw-r--r--js/src/session/session.ts138
-rw-r--r--js/src/storage/storage.ts20
-rw-r--r--js/src/util/event.ts0
-rw-r--r--js/src/util/log.ts21
12 files changed, 493 insertions, 144 deletions
diff --git a/js/.gitignore b/js/.gitignore
index a14702c40..f06235c46 100644
--- a/js/.gitignore
+++ b/js/.gitignore
@@ -1,34 +1,2 @@
-# dependencies (bun install)
node_modules
-
-# output
-out
dist
-*.tgz
-
-# code coverage
-coverage
-*.lcov
-
-# logs
-logs
-_.log
-report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
-
-# dotenv environment variable files
-.env
-.env.development.local
-.env.test.local
-.env.production.local
-.env.local
-
-# caches
-.eslintcache
-.cache
-*.tsbuildinfo
-
-# IntelliJ based IDEs
-.idea
-
-# Finder (MacOS) folder config
-.DS_Store
diff --git a/js/bun.lock b/js/bun.lock
index 71a2c74a6..ba45595a4 100644
--- a/js/bun.lock
+++ b/js/bun.lock
@@ -7,13 +7,18 @@
"@ai-sdk/anthropic": "^2.0.0-alpha.2",
"@flystorage/file-storage": "^1.1.0",
"@flystorage/local-fs": "^1.1.0",
+ "@hono/zod-validator": "^0.5.0",
"ai": "^5.0.0-alpha.2",
- "ulid": "3.0.0",
+ "hono": "^4.7.10",
"zod": "^3.25.0-beta.20250518T002810",
},
"devDependencies": {
"@tsconfig/bun": "^1.0.7",
"@types/bun": "latest",
+ "@types/react": "18",
+ "ink": "^5.2.1",
+ "ink-text-input": "^6.0.0",
+ "react": "^18.0.0",
},
"peerDependencies": {
"typescript": "5",
@@ -27,12 +32,16 @@
"@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.2", "@standard-schema/spec": "^1.0.0", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-oTlF6UlVitSdVPQv0e+kAkZmbuunJAUYdVEh7ZRvoti+kY/T4vOT6p22X0xTaWgl0+MI1igAT+c83j7tCMuo2w=="],
+ "@alcalzone/ansi-tokenize": ["@alcalzone/[email protected]", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw=="],
+
"@flystorage/dynamic-import": ["@flystorage/[email protected]", "", {}, "sha512-CIbIUrBdaPFyKnkVBaqzksvzNtsMSXITR/G/6zlil3MBnPFq2LX+X4Mv5p2XOmv/3OulFs/ff2SNb+5dc2Twtg=="],
"@flystorage/file-storage": ["@flystorage/[email protected]", "", {}, "sha512-25Gd5EsXDmhHrK5orpRuVqebQms1Cm9m5ACMZ0sVDX+Sbl1V0G88CbcWt7mEoWRYLvQ1U072htqg6Sav76ZlVA=="],
"@flystorage/local-fs": ["@flystorage/[email protected]", "", { "dependencies": { "@flystorage/dynamic-import": "^1.0.0", "@flystorage/file-storage": "^1.1.0", "file-type": "^20.5.0", "mime-types": "^3.0.1" } }, "sha512-dbErRhqmCv2UF0zPdeH7iVWuVeTWAJHuJD/mXDe2V370/SL7XIvdE3ditBHWC+1SzBKXJ0lkykOenwlum+oqIA=="],
+ "@hono/zod-validator": ["@hono/[email protected]", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-ds5bW6DCgAnNHP33E3ieSbaZFd5dkV52ZjyaXtGoR06APFrCtzAsKZxTHwOrJNBdXsi0e5wNwo5L4nVEVnJUdg=="],
+
"@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@standard-schema/spec": ["@standard-schema/[email protected]", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
@@ -47,42 +56,130 @@
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="],
+ "@types/prop-types": ["@types/[email protected]", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="],
+
+ "@types/react": ["@types/[email protected]", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-gXLBtmlcRJeT09/sI4PxVwyrku6SaNUj/6cMubjE6T6XdY1fDmBL7r0nX0jbSZPU/Xr0KuwLLZh6aOYY5d91Xw=="],
+
"ai": ["[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.2", "@ai-sdk/provider-utils": "3.0.0-alpha.2", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-42asUyoFcqjV5AoZezJPawODCPT5Rb1y/UipVlcXn1tpqlypCchSEukjNw/l09YPVucqCbW19IVqojLttkTTVA=="],
+ "ansi-escapes": ["[email protected]", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="],
+
+ "ansi-regex": ["[email protected]", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
+
+ "ansi-styles": ["[email protected]", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
+
+ "auto-bind": ["[email protected]", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
+
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="],
+ "chalk": ["[email protected]", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
+
+ "cli-boxes": ["[email protected]", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
+
+ "cli-cursor": ["[email protected]", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
+
+ "cli-truncate": ["[email protected]", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
+
+ "code-excerpt": ["[email protected]", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
+
+ "convert-to-spaces": ["[email protected]", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
+
+ "csstype": ["[email protected]", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+
"debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
+ "emoji-regex": ["[email protected]", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
+
+ "environment": ["[email protected]", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
+
+ "es-toolkit": ["[email protected]", "", {}, "sha512-OT3AxczYYd3W50bCj4V0hKoOAfqIy9tof0leNQYekEDxVKir3RTVTJOLij7VAe6fsCNsGhC0JqIkURpMXTCSEA=="],
+
+ "escape-string-regexp": ["[email protected]", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
+
"fflate": ["[email protected]", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"file-type": ["[email protected]", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="],
+ "get-east-asian-width": ["[email protected]", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
+
+ "hono": ["[email protected]", "", {}, "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ=="],
+
"ieee754": ["[email protected]", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
+ "indent-string": ["[email protected]", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
+
+ "ink": ["[email protected]", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.22.0", "indent-string": "^5.0.0", "is-in-ci": "^1.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.29.0", "scheduler": "^0.23.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=18.0.0", "react": ">=18.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg=="],
+
+ "ink-text-input": ["[email protected]", "", { "dependencies": { "chalk": "^5.3.0", "type-fest": "^4.18.2" }, "peerDependencies": { "ink": ">=5", "react": ">=18" } }, "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw=="],
+
+ "is-fullwidth-code-point": ["[email protected]", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
+
+ "is-in-ci": ["[email protected]", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg=="],
+
+ "js-tokens": ["[email protected]", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
"json-schema": ["[email protected]", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
+ "loose-envify": ["[email protected]", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
+
"mime-db": ["[email protected]", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["[email protected]", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
+ "mimic-fn": ["[email protected]", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
+
"ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+ "onetime": ["[email protected]", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
+
+ "patch-console": ["[email protected]", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
+
"peek-readable": ["[email protected]", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="],
+ "react": ["[email protected]", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
+
+ "react-reconciler": ["[email protected]", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg=="],
+
+ "restore-cursor": ["[email protected]", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
+
+ "scheduler": ["[email protected]", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
+
+ "signal-exit": ["[email protected]", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
+
+ "slice-ansi": ["[email protected]", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="],
+
+ "stack-utils": ["[email protected]", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
+
+ "string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
+
+ "strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
+
"strtok3": ["[email protected]", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="],
"token-types": ["[email protected]", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
+ "type-fest": ["[email protected]", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
+
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"uint8array-extras": ["[email protected]", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
- "ulid": ["[email protected]", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-yvZYdXInnJve6LdlPIuYmURdS2NP41ZoF4QW7SXwbUKYt53+0eDAySO+rGSvM2O/ciuB/G+8N7GQrZ1mCJpuqw=="],
-
"undici-types": ["[email protected]", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+ "widest-line": ["[email protected]", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
+
+ "wrap-ansi": ["[email protected]", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="],
+
+ "ws": ["[email protected]", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="],
+
+ "yoga-layout": ["[email protected]", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
+
"zod": ["[email protected]", "", {}, "sha512-3/aIqMbUXG9EjTelJkDcWd+izJP5MxFgQEMSYI8n41pwYhRDYYxy2dnbkgfNcnLbFZ9uByZn9XXqHTh05QHqSQ=="],
"zod-to-json-schema": ["[email protected]", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
+
+ "cli-truncate/slice-ansi": ["[email protected]", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
+
+ "slice-ansi/is-fullwidth-code-point": ["[email protected]", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="],
}
}
diff --git a/js/example/client.tsx b/js/example/client.tsx
new file mode 100644
index 000000000..65f0ebe15
--- /dev/null
+++ b/js/example/client.tsx
@@ -0,0 +1,116 @@
+import React, { useEffect, useState } from "react";
+import type { Server } from "../src/server/server";
+import type { Session } from "../src/session/session";
+import { hc } from "hono/client";
+import { createInterface, Interface } from "readline";
+
+const client = hc<Server.App>(`http://localhost:16713`);
+
+
+const session = await client.session_create.$post().then((res) => res.json());
+
+const initial: {
+ session: {
+ info: {
+ [sessionID: string]: Session.Info;
+ };
+ message: {
+ [sessionID: string]: {
+ [messageID: string]: Session.Message;
+ };
+ };
+ };
+} = {
+ session: {
+ info: {
+ [session.id]: session
+ },
+ message: {
+ [session.id]: {}
+ },
+ },
+};
+
+import { render, Text, Newline, useStdout, Box } from "ink";
+import TextInput from "ink-text-input"
+
+function App() {
+ const [state, setState] = useState(initial)
+ const [input, setInput] = useState("")
+
+ useEffect(() => {
+ fetch("http://localhost:16713/event")
+ .then(stream => {
+ const decoder = new TextDecoder();
+ stream.body!.pipeTo(
+ new WritableStream({
+ write(chunk) {
+ const data = decoder.decode(chunk);
+ if (data.startsWith("data: ")) {
+ try {
+ const event = JSON.parse(data.substring(6));
+ switch (event.type) {
+ case "storage.write":
+ const splits: string[] = event.properties.key.split("/");
+ let item = state as any;
+ for (let i = 0; i < splits.length; i++) {
+ const part = splits[i];
+ if (i === splits.length - 1) {
+ item[part] = event.properties.body;
+ continue;
+ }
+ if (!item[part]) item[part] = {};
+ item = item[part];
+ }
+ }
+ setState({ ...state })
+ } catch {
+ }
+ }
+ },
+ }),
+ )
+ });
+ }, [])
+
+
+ return (
+ <>
+ <Text>{session.title}</Text>
+ {
+ Object.values(state.session.message[session.id]).map(message => {
+ return Object.values(message.parts).map((part, index) => {
+ if (part.type === "text") {
+ return <Text key={`${message.id}-${index}`}>{message.role}: {part.text}</Text>
+ }
+ })
+ })
+ }
+ <Box gap={1} >
+ <Text>Input:</Text>
+ <TextInput
+ value={input}
+ onChange={setInput}
+ onSubmit={() => {
+ setInput("")
+ client.session_chat.$post({
+ json: {
+ sessionID: session.id,
+ parts: [
+ {
+ type: "text",
+ text: input,
+ },
+ ],
+ }
+ })
+ }}
+ />
+ </Box>
+ </>
+ );
+};
+
+console.clear();
+render(<App />);
+
diff --git a/js/package.json b/js/package.json
index 741042e04..03b9ad035 100644
--- a/js/package.json
+++ b/js/package.json
@@ -4,7 +4,11 @@
"private": true,
"devDependencies": {
"@tsconfig/bun": "^1.0.7",
- "@types/bun": "latest"
+ "@types/bun": "latest",
+ "@types/react": "18",
+ "ink": "^5.2.1",
+ "ink-text-input": "^6.0.0",
+ "react": "^18.0.0"
},
"peerDependencies": {
"typescript": "5"
@@ -13,8 +17,9 @@
"@ai-sdk/anthropic": "^2.0.0-alpha.2",
"@flystorage/file-storage": "^1.1.0",
"@flystorage/local-fs": "^1.1.0",
+ "@hono/zod-validator": "^0.5.0",
"ai": "^5.0.0-alpha.2",
- "ulid": "3.0.0",
+ "hono": "^4.7.10",
"zod": "^3.25.0-beta.20250518T002810"
}
}
diff --git a/js/src/bus/index.ts b/js/src/bus/index.ts
new file mode 100644
index 000000000..5359debd9
--- /dev/null
+++ b/js/src/bus/index.ts
@@ -0,0 +1,79 @@
+import type { z, ZodSchema } from "zod/v4";
+import { App } from "../app";
+import { Log } from "../util/log";
+
+export namespace Bus {
+ const log = Log.create({ service: "bus" });
+ type Subscription = (event: any) => void;
+
+ const state = App.state("bus", () => {
+ const subscriptions = new Map<any, Subscription[]>();
+
+ return {
+ subscriptions,
+ };
+ });
+
+ export type EventDefinition = ReturnType<typeof event>;
+
+ export function event<Type extends string, Properties extends ZodSchema>(
+ type: Type,
+ properties: Properties,
+ ) {
+ return {
+ type,
+ properties,
+ };
+ }
+
+ export function publish<Definition extends EventDefinition>(
+ def: Definition,
+ properties: z.output<Definition["properties"]>,
+ ) {
+ const payload = {
+ type: def.type,
+ properties,
+ };
+ log.info("publishing", {
+ type: def.type,
+ ...properties,
+ });
+ for (const key of [def.type, "*"]) {
+ const match = state().subscriptions.get(key);
+ for (const sub of match ?? []) {
+ sub(payload);
+ }
+ }
+ }
+
+ export function subscribe<Definition extends EventDefinition>(
+ def: Definition,
+ callback: (event: {
+ type: Definition["type"];
+ properties: z.infer<Definition["properties"]>;
+ }) => void,
+ ) {
+ return raw(def.type, callback);
+ }
+
+ export function subscribeAll(callback: (event: any) => void) {
+ return raw("*", callback);
+ }
+
+ function raw(type: string, callback: (event: any) => void) {
+ log.info("subscribing", { type });
+ const subscriptions = state().subscriptions;
+ let match = subscriptions.get(type) ?? [];
+ match.push(callback);
+ subscriptions.set(type, match);
+
+ return () => {
+ log.info("unsubscribing", { type });
+ const match = subscriptions.get(type);
+ if (!match) return;
+ const index = match.indexOf(callback);
+ if (index === -1) return;
+ match.splice(index, 1);
+ };
+ }
+}
diff --git a/js/src/id/id.ts b/js/src/id/id.ts
index e4744f172..39ee5326f 100644
--- a/js/src/id/id.ts
+++ b/js/src/id/id.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { randomBytes } from "crypto";
export namespace Identifier {
@@ -11,7 +11,7 @@ export namespace Identifier {
return z.string().startsWith(prefixes[prefix]);
}
- const LENGTH = 24;
+ const LENGTH = 26;
export function ascending(prefix: keyof typeof prefixes, given?: string) {
return generateID(prefix, false, given);
@@ -45,6 +45,11 @@ export namespace Identifier {
const randLength = (LENGTH - 12) / 2;
const random = randomBytes(randLength);
- return prefix + "_" + timeBytes.toString("hex") + random.toString("hex");
+ return (
+ prefixes[prefix] +
+ "_" +
+ timeBytes.toString("hex") +
+ random.toString("hex")
+ );
}
}
diff --git a/js/src/index.ts b/js/src/index.ts
index 72c32c8e9..8f98310be 100644
--- a/js/src/index.ts
+++ b/js/src/index.ts
@@ -1,29 +1,11 @@
import { App } from "./app";
import process from "node:process";
-import { RPC } from "./server/server";
-import { Session } from "./session/session";
-import { Identifier } from "./id/id";
+import { Server } from "./server/server";
const app = await App.create({
directory: process.cwd(),
});
App.provide(app, async () => {
- const sessionID = await Session.list()
- [Symbol.asyncIterator]()
- .next()
- .then((v) => v.value ?? Session.create().then((s) => s.id));
-
- await Session.chat(sessionID, {
- role: "user",
- id: Identifier.ascending("message"),
- parts: [
- {
- type: "text",
- text: "Hey how are you? try to use tools",
- },
- ],
- });
-
- const rpc = RPC.listen();
+ const server = Server.listen();
});
diff --git a/js/src/server/server.ts b/js/src/server/server.ts
index 6266e9421..dd066c400 100644
--- a/js/src/server/server.ts
+++ b/js/src/server/server.ts
@@ -1,34 +1,64 @@
import { Log } from "../util/log";
+import { Bus } from "../bus";
-export namespace RPC {
- const log = Log.create({ service: "rpc" });
+import { Hono } from "hono";
+import { streamSSE } from "hono/streaming";
+import { Session } from "../session/session";
+import { zValidator } from "@hono/zod-validator";
+import { z } from "zod";
+
+export namespace Server {
+ const log = Log.create({ service: "server" });
const PORT = 16713;
- export function listen(input?: { port?: number }) {
- const port = input?.port ?? PORT;
- log.info("trying", { port });
- try {
- const server = Bun.serve({
- port,
- websocket: {
- open() {},
- message() {},
- },
- routes: {
- "/ws": (req, server) => {
- if (server.upgrade(req)) return;
- return new Response("Not a websocket request", { status: 400 });
- },
+
+ export type App = ReturnType<typeof listen>;
+
+ export function listen() {
+ const app = new Hono()
+ .get("/event", async (c) => {
+ log.info("event connected");
+ return streamSSE(c, async (stream) => {
+ const unsub = Bus.subscribeAll(async (event) => {
+ await stream.writeSSE({
+ data: JSON.stringify(event),
+ });
+ });
+ await new Promise<void>((resolve) => {
+ stream.onAbort(() => {
+ unsub();
+ resolve();
+ log.info("event disconnected");
+ });
+ });
+ });
+ })
+ .post("/session_create", async (c) => {
+ const session = await Session.create();
+ return c.json(session);
+ })
+ .post(
+ "/session_chat",
+ zValidator(
+ "json",
+ z.object({
+ sessionID: z.string(),
+ parts: z.custom<Session.Message["parts"]>(),
+ }),
+ ),
+ async (c) => {
+ const body = c.req.valid("json");
+ const msg = await Session.chat(body.sessionID, ...body.parts);
+ return c.json(msg);
},
- });
- log.info("listening", { port });
- return {
- server,
- };
- } catch (e: any) {
- if (e?.code === "EADDRINUSE") {
- return listen({ port: port + 1 });
- }
- throw e;
- }
+ );
+
+ Bun.serve({
+ port: PORT,
+ hostname: "0.0.0.0",
+ idleTimeout: 0,
+ fetch: app.fetch,
+ });
+
+ return app;
}
}
diff --git a/js/src/session/session.ts b/js/src/session/session.ts
index d07c799b9..fb45f0e59 100644
--- a/js/src/session/session.ts
+++ b/js/src/session/session.ts
@@ -1,5 +1,5 @@
import path from "path";
-import { z } from "zod";
+import { z } from "zod/v3";
import { App } from "../app/";
import { Identifier } from "../id/id";
import { LLM } from "../llm/llm";
@@ -11,7 +11,9 @@ import {
tool,
type TextUIPart,
type ToolInvocationUIPart,
+ type UIDataTypes,
type UIMessage,
+ type UIMessagePart,
} from "ai";
export namespace Session {
@@ -20,11 +22,18 @@ export namespace Session {
export interface Info {
id: string;
title: string;
+ tokens: {
+ input: number;
+ output: number;
+ reasoning: number;
+ };
}
+ export type Message = UIMessage<{ sessionID: string }>;
+
const state = App.state("session", () => {
const sessions = new Map<string, Info>();
- const messages = new Map<string, UIMessage[]>();
+ const messages = new Map<string, Message[]>();
return {
sessions,
@@ -36,12 +45,14 @@ export namespace Session {
const result: Info = {
id: Identifier.descending("session"),
title: "New Session - " + new Date().toISOString(),
+ tokens: {
+ input: 0,
+ output: 0,
+ reasoning: 0,
+ },
};
log.info("created", result);
- await Storage.write(
- "session/info/" + result.id + ".json",
- JSON.stringify(result),
- );
+ await Storage.writeJSON("session/info/" + result.id, result);
state().sessions.set(result.id, result);
return result;
}
@@ -51,23 +62,35 @@ export namespace Session {
if (result) {
return result;
}
- const read = JSON.parse(await Storage.readToString("session/info/" + id));
+ const read = await Storage.readJSON<Info>("session/info/" + id);
state().sessions.set(id, read);
- return read;
+ return read as Info;
+ }
+
+ export async function update(session: Info) {
+ state().sessions.set(session.id, session);
+ await Storage.writeJSON("session/info/" + session.id, session);
}
export async function messages(sessionID: string) {
- const result = state().messages.get(sessionID);
- if (result) {
- return result;
+ const match = state().messages.get(sessionID);
+ if (match) {
+ return match;
+ }
+ const result = [] as Message[];
+ const list = await Storage.list("session/message/" + sessionID)
+ .then((x) => x.toArray())
+ .catch(() => {});
+ if (!list) return result;
+ for (const item of list) {
+ const messageID = path.basename(item.path, ".json");
+ const read = await Storage.readJSON<Message>(
+ "session/message/" + sessionID + "/" + messageID,
+ );
+ result.push(read);
}
- const read = JSON.parse(
- await Storage.readToString(
- "session/message/" + sessionID + ".json",
- ).catch(() => "[]"),
- );
- state().messages.set(sessionID, read);
- return read;
+ state().messages.set(sessionID, result);
+ return result;
}
export async function* list() {
@@ -81,11 +104,23 @@ export namespace Session {
}
}
- export async function chat(sessionID: string, msg: UIMessage) {
+ export async function chat(
+ sessionID: string,
+ ...parts: UIMessagePart<UIDataTypes>[]
+ ) {
+ const session = await get(sessionID);
const l = log.clone().tag("session", sessionID);
l.info("chatting");
- const msgs = (await messages(sessionID)) ?? [
- {
+
+ const msgs = await messages(sessionID);
+ async function write(msg: Message) {
+ return Storage.writeJSON(
+ "session/message/" + sessionID + "/" + msg.id,
+ msg,
+ );
+ }
+ if (msgs.length === 0) {
+ const system: UIMessage<{ sessionID: string }> = {
id: Identifier.ascending("message"),
role: "system",
parts: [
@@ -94,40 +129,38 @@ export namespace Session {
text: "You are a helpful assistant called opencode",
},
],
- } as UIMessage,
- ];
- msgs.push(msg);
- state().messages.set(sessionID, msgs);
- async function write() {
- return Storage.write(
- "session/message/" + sessionID + ".json",
- JSON.stringify(msgs),
- );
+ metadata: {
+ sessionID,
+ },
+ };
+ msgs.push(system);
+ state().messages.set(sessionID, msgs);
+ await write(system);
}
- await write();
+ const msg: Message = {
+ role: "user",
+ id: Identifier.ascending("message"),
+ parts,
+ metadata: {
+ sessionID,
+ },
+ };
+ msgs.push(msg);
+ await write(msg);
const model = await LLM.findModel("claude-3-7-sonnet-20250219");
const result = streamText({
messages: convertToModelMessages(msgs),
temperature: 0,
- tools: {
- test: tool({
- id: "opencode.test" as const,
- parameters: z.object({
- feeling: z.string(),
- }),
- execute: async () => {
- return `Hello`;
- },
- description: "call this tool to get a greeting",
- }),
- },
model,
});
- const next: UIMessage = {
+ const next: Message = {
id: Identifier.ascending("message"),
role: "assistant",
parts: [],
+ metadata: {
+ sessionID,
+ },
};
msgs.push(next);
let text: TextUIPart | undefined;
@@ -135,7 +168,9 @@ export namespace Session {
while (true) {
const { done, value } = await reader.read();
if (done) break;
- l.info("part", value);
+ l.info("part", {
+ type: value.type,
+ });
switch (value.type) {
case "start":
break;
@@ -175,15 +210,15 @@ export namespace Session {
state: "result",
result: value.result,
};
- await write();
}
break;
case "finish":
- await write();
break;
case "finish-step":
- await write();
+ break;
+ case "error":
+ log.error("error", value);
break;
default:
@@ -191,6 +226,13 @@ export namespace Session {
type: value.type,
});
}
+ await write(next);
}
+ const usage = await result.totalUsage;
+ session.tokens.input += usage.inputTokens || 0;
+ session.tokens.output += usage.outputTokens || 0;
+ session.tokens.reasoning += usage.reasoningTokens || 0;
+ await update(session);
+ return next;
}
}
diff --git a/js/src/storage/storage.ts b/js/src/storage/storage.ts
index 19e8dc06f..89c8b4d17 100644
--- a/js/src/storage/storage.ts
+++ b/js/src/storage/storage.ts
@@ -4,10 +4,19 @@ import fs from "fs/promises";
import { Log } from "../util/log";
import { App } from "../app";
import { AppPath } from "../app/path";
+import { Bus } from "../bus";
+import z from "zod/v4";
export namespace Storage {
const log = Log.create({ service: "storage" });
+ export const Event = {
+ Write: Bus.event(
+ "storage.write",
+ z.object({ key: z.string(), body: z.any() }),
+ ),
+ };
+
const state = App.state("storage", async () => {
const app = await App.use();
const storageDir = AppPath.storage(app.root);
@@ -36,4 +45,15 @@ export namespace Storage {
export const read = expose("read");
export const list = expose("list");
export const readToString = expose("readToString");
+
+ export async function readJSON<T>(key: string) {
+ const data = await readToString(key + ".json");
+ return JSON.parse(data) as T;
+ }
+
+ export async function writeJSON<T>(key: string, data: T) {
+ Bus.publish(Event.Write, { key, body: data });
+ const json = JSON.stringify(data);
+ await write(key + ".json", json);
+ }
}
diff --git a/js/src/util/event.ts b/js/src/util/event.ts
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/src/util/event.ts
diff --git a/js/src/util/log.ts b/js/src/util/log.ts
index 9de4eb495..8f7157140 100644
--- a/js/src/util/log.ts
+++ b/js/src/util/log.ts
@@ -2,16 +2,21 @@ export namespace Log {
export function create(tags?: Record<string, any>) {
tags = tags || {};
+ function build(message: any, extra?: Record<string, any>) {
+ const prefix = Object.entries({
+ ...tags,
+ ...extra,
+ })
+ .map(([key, value]) => `${key}=${value}`)
+ .join(" ");
+ return [prefix, message];
+ }
const result = {
info(message?: any, extra?: Record<string, any>) {
- const prefix = Object.entries({
- ...tags,
- ...extra,
- })
- .map(([key, value]) => `${key}=${value}`)
- .join(" ");
- console.log(prefix, message);
- return result;
+ console.log(...build(message, extra));
+ },
+ error(message?: any, extra?: Record<string, any>) {
+ console.error(...build(message, extra));
},
tag(key: string, value: string) {
if (tags) tags[key] = value;