Dax Raad 9 달 전
부모
커밋
0e303e6508
12개의 변경된 파일493개의 추가작업 그리고 144개의 파일을 삭제
  1. 0 32
      js/.gitignore
  2. 100 3
      js/bun.lock
  3. 116 0
      js/example/client.tsx
  4. 7 2
      js/package.json
  5. 79 0
      js/src/bus/index.ts
  6. 8 3
      js/src/id/id.ts
  7. 2 20
      js/src/index.ts
  8. 58 28
      js/src/server/server.ts
  9. 90 48
      js/src/session/session.ts
  10. 20 0
      js/src/storage/storage.ts
  11. 0 0
      js/src/util/event.ts
  12. 13 8
      js/src/util/log.ts

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

+ 100 - 3
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=="],
   }
 }

+ 116 - 0
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 />);
+

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

+ 79 - 0
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);
+    };
+  }
+}

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

+ 2 - 20
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();
 });

+ 58 - 28
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;
   }
 }

+ 90 - 48
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;
   }
 }

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

+ 0 - 0
js/src/util/event.ts


+ 13 - 8
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;