Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b478e5655c | ||
|
|
f884766445 | ||
|
|
76b2e4539c | ||
|
|
d87922c0eb | ||
|
|
2446483df5 | ||
|
|
f4c453155d | ||
|
|
969ad80ed2 | ||
|
|
af064b41d7 | ||
|
|
ea6bfef21a | ||
|
|
107363b1d9 | ||
|
|
85214d7c59 | ||
|
|
997cb2d945 | ||
|
|
45b139390c | ||
|
|
994368de15 | ||
|
|
143fd8e076 | ||
|
|
06dba28bd6 | ||
|
|
b8d276a049 | ||
|
|
ee01f01271 | ||
|
|
32d5db4f0a | ||
|
|
f6108b7be8 | ||
|
|
94ef341c9d | ||
|
|
f9abc7c84f | ||
|
|
891ed6ebc0 | ||
|
|
163e23a68b | ||
|
|
f13b0af491 | ||
|
|
4a0be45d3d | ||
|
|
23788674c8 | ||
|
|
121eb24e73 | ||
|
|
571d60182a | ||
|
|
167a9dcaf3 | ||
|
|
37327259cb | ||
|
|
cdb25656d5 | ||
|
|
25c876caa2 | ||
|
|
cf83e31f23 | ||
|
|
3bc238b58b | ||
|
|
b8de69dced | ||
|
|
e7fcb692a4 | ||
|
|
dae38574ab | ||
|
|
ed4f862b49 | ||
|
|
fce59db94a | ||
|
|
3e2a0c7281 | ||
|
|
5a0910ea79 | ||
|
|
1dffabcfda | ||
|
|
c389e0ed43 | ||
|
|
204801052a | ||
|
|
2528d8cb88 | ||
|
|
6b73ffd1c1 | ||
|
|
0eadc50a33 | ||
|
|
aeea84a877 | ||
|
|
a54c5c6298 | ||
|
|
8825cd3811 | ||
|
|
3d9a5d9970 | ||
|
|
1f9e195fa6 | ||
|
|
73c012c76c | ||
|
|
2ace57404b | ||
|
|
8c4b5e088b | ||
|
|
69920a73d7 | ||
|
|
ae76a3467a | ||
|
|
701107cda4 | ||
|
|
b99565959b | ||
|
|
67aa7ce04d | ||
|
|
c663fbc3ee | ||
|
|
2090bab537 | ||
|
|
64d5fff9a3 | ||
|
|
925f695503 | ||
|
|
f1c925795d | ||
|
|
c82a060eca | ||
|
|
63e783ef79 | ||
|
|
35d6273fb3 | ||
|
|
b89d4a16fd | ||
|
|
2799a96032 | ||
|
|
8f4b79227c | ||
|
|
c810b6d206 | ||
|
|
fa35407572 | ||
|
|
8bbbc07aff | ||
|
|
75a21ba3ce | ||
|
|
0d6fb68a88 | ||
|
|
242b886434 | ||
|
|
caf465a9da | ||
|
|
bbf77c6139 | ||
|
|
53b7e04b86 | ||
|
|
9e75e3ed18 | ||
|
|
6389858d41 | ||
|
|
7e5941e14b | ||
|
|
c68aeed8d9 | ||
|
|
b199a609a8 | ||
|
|
4a5a93b3f8 | ||
|
|
e99bdcefac | ||
|
|
26dcb85de1 | ||
|
|
11d042be25 | ||
|
|
33b5fe236a | ||
|
|
d56991006c | ||
|
|
739a9f71c3 | ||
|
|
aef81fce0b | ||
|
|
8f3d7b4038 | ||
|
|
de15e67834 | ||
|
|
fea56d8de6 | ||
|
|
3d71be2b45 | ||
|
|
58baca2a5b | ||
|
|
ef73926db6 | ||
|
|
9ad1687f04 | ||
|
|
c573270e66 | ||
|
|
9ebad68274 | ||
|
|
03664ba588 | ||
|
|
5a107b275c | ||
|
|
dd5736fe5f | ||
|
|
9f3ba03965 | ||
|
|
d090c08ef0 | ||
|
|
68e82e4d94 | ||
|
|
a4aa0e6f8d | ||
|
|
8c1ae2717c | ||
|
|
72d48759d7 | ||
|
|
986144b377 | ||
|
|
1fdb326aa7 | ||
|
|
463257e7e4 | ||
|
|
0f41e60bd6 | ||
|
|
7df81f7b3e | ||
|
|
dd22cb2bb0 |
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
max_line_length = 80
|
||||
@@ -28,5 +28,5 @@ jobs:
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add STATS.md
|
||||
git diff --staged --quiet || git commit -m "Update download stats $(date -I)"
|
||||
git diff --staged --quiet || git commit -m "ignore: update download stats $(date -I)"
|
||||
git push
|
||||
|
||||
@@ -5,3 +5,4 @@ node_modules
|
||||
.env
|
||||
.idea
|
||||
.vscode
|
||||
openapi.json
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</p>
|
||||
<p align="center">AI coding agent, built for the terminal.</p>
|
||||
<p align="center">
|
||||
<a href="https://opencode.ai/docs"><img alt="View docs" src="https://img.shields.io/badge/view-docs-blue?style=flat-square" /></a>
|
||||
<a href="https://discord.gg/opencode"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
||||
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
||||
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||
</p>
|
||||
@@ -40,6 +40,9 @@ For more info on how to configure opencode [**head over to our docs**](https://o
|
||||
|
||||
For any new features we'd appreciate it if you could open an issue first to discuss what you'd like to implement. We're pretty responsive there and it'll save you from working on something that we don't end up using. No need to do this for simpler fixes.
|
||||
|
||||
> **Note**: Please talk to us via github issues before spending time working on
|
||||
> a new feature
|
||||
|
||||
To run opencode locally you need.
|
||||
|
||||
- Bun
|
||||
@@ -67,14 +70,10 @@ It's very similar to Claude Code in terms of capability. Here are the key differ
|
||||
- A focus on TUI. opencode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
||||
- A client/server architecture. This for example can allow opencode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
|
||||
|
||||
#### What about Windows support?
|
||||
|
||||
There are some minor problems blocking opencode from working on windows. We are working on on them now. You'll need to use WSL for now.
|
||||
|
||||
#### What's the other repo?
|
||||
|
||||
The other confusingly named repo has no relation to this one. You can [read the story behind it here](https://x.com/thdxr/status/1933561254481666466).
|
||||
|
||||
---
|
||||
|
||||
**Join our community** [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)
|
||||
**Join our community** [Discord](https://discord.gg/opencode) | [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Download Stats
|
||||
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | ---------------- | ---------------- | ---------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | ---------------- | --------------- | --------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "opencode",
|
||||
"devDependencies": {
|
||||
"prettier": "3.5.3",
|
||||
"sst": "3.17.6",
|
||||
"sst": "3.17.8",
|
||||
},
|
||||
},
|
||||
"packages/function": {
|
||||
@@ -36,6 +36,7 @@
|
||||
"env-paths": "3.0.0",
|
||||
"hono": "4.7.10",
|
||||
"hono-openapi": "0.4.8",
|
||||
"isomorphic-git": "1.32.1",
|
||||
"open": "10.1.2",
|
||||
"remeda": "2.22.3",
|
||||
"ts-lsp-client": "1.0.3",
|
||||
@@ -77,11 +78,12 @@
|
||||
"lang-map": "0.4.0",
|
||||
"luxon": "3.6.1",
|
||||
"marked": "15.0.12",
|
||||
"marked-shiki": "1.2.0",
|
||||
"rehype-autolink-headings": "7.1.0",
|
||||
"sharp": "0.32.5",
|
||||
"shiki": "3.4.2",
|
||||
"solid-js": "1.9.7",
|
||||
"toolbeam-docs-theme": "0.3.0",
|
||||
"toolbeam-docs-theme": "0.4.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
@@ -94,30 +96,22 @@
|
||||
"sharp",
|
||||
"esbuild",
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"ai@4.3.16": "patches/ai@4.3.16.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"zod": "3.24.2",
|
||||
},
|
||||
"catalog": {
|
||||
"@types/node": "22.13.9",
|
||||
"ai": "4.3.16",
|
||||
"ai": "5.0.0-beta.7",
|
||||
"typescript": "5.8.2",
|
||||
"zod": "3.24.2",
|
||||
"zod": "3.25.49",
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@2.2.10", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icLGO7Q0NinnHIPgT+y1QjHVwH4HwV+brWbvM+FfCG2Afpa89PyKa3Ret91kGjZpBgM/xnj1B7K5eM+rRlsXQA=="],
|
||||
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-beta.3", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.2" }, "peerDependencies": { "zod": "^3.25.49" } }, "sha512-g49gMSkXy94lYvl5LRh438OR/0JCG6ol0jV+iLot7cy5HLltZlGocEuauETBu4b10mDXOd7XIjTEZoQpYFMYLQ=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-beta.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Z8SPncMtS3RsoXITmT7NVwrAq6M44dmw0DoUOYJqNNtCu8iMWuxB8Nxsoqpa0uEEy9R1V1ZThJAXTYgjTUxl3w=="],
|
||||
|
||||
"@ai-sdk/react": ["@ai-sdk/react@1.2.12", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/ui-utils": "1.2.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g=="],
|
||||
|
||||
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="],
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-beta.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.49" } }, "sha512-H4K+4weOVgWqrDDeAbQWoA4U5mN4WrQPHQFdH7ynQYcnhj/pzctU9Q6mGlR5ESMWxaXxazxlOblSITlXo9bahA=="],
|
||||
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
@@ -461,12 +455,10 @@
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="],
|
||||
"@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
|
||||
"@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||
|
||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||
@@ -491,6 +483,8 @@
|
||||
|
||||
"@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||
|
||||
"@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="],
|
||||
|
||||
"@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="],
|
||||
@@ -511,7 +505,7 @@
|
||||
|
||||
"acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
|
||||
|
||||
"ai": ["ai@4.3.16", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g=="],
|
||||
"ai": ["ai@5.0.0-beta.7", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-beta.3", "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.2", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.49" } }, "sha512-oC4KzUJCQPMB7v9rCqL/rVk2ogZvI6lYiXfKjzPYHwa1zIgy329qqRLmAd3mKEDTTG6By1r0zasQu7FKmG+4gw=="],
|
||||
|
||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||
|
||||
@@ -541,6 +535,8 @@
|
||||
|
||||
"astro-expressive-code": ["astro-expressive-code@0.41.2", "", { "dependencies": { "rehype-expressive-code": "^0.41.2" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0" } }, "sha512-HN0jWTnhr7mIV/2e6uu4PPRNNo/k4UEgTLZqbp3MrHU+caCARveG2yZxaZVBmxyiVdYqW5Pd3u3n2zjnshixbw=="],
|
||||
|
||||
"async-lock": ["async-lock@1.4.1", "", {}, "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="],
|
||||
|
||||
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
@@ -599,7 +595,7 @@
|
||||
|
||||
"buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
|
||||
"bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
@@ -633,6 +629,8 @@
|
||||
|
||||
"ci-info": ["ci-info@4.2.0", "", {}, "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg=="],
|
||||
|
||||
"clean-git-ref": ["clean-git-ref@2.0.1", "", {}, "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw=="],
|
||||
|
||||
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
|
||||
|
||||
"cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
|
||||
@@ -669,6 +667,8 @@
|
||||
|
||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||
|
||||
"crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
|
||||
|
||||
"cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="],
|
||||
|
||||
"crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
|
||||
@@ -723,7 +723,7 @@
|
||||
|
||||
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
|
||||
|
||||
"diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="],
|
||||
"diff3": ["diff3@0.0.3", "", {}, "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g=="],
|
||||
|
||||
"direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
|
||||
|
||||
@@ -791,7 +791,7 @@
|
||||
|
||||
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.0.2", "", {}, "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA=="],
|
||||
"eventsource-parser": ["eventsource-parser@3.0.3", "", {}, "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA=="],
|
||||
|
||||
"exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
|
||||
|
||||
@@ -939,6 +939,8 @@
|
||||
|
||||
"ieee754": ["ieee754@1.1.13", "", {}, "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
@@ -987,6 +989,8 @@
|
||||
|
||||
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
||||
|
||||
"isomorphic-git": ["isomorphic-git@1.32.1", "", { "dependencies": { "async-lock": "^1.4.1", "clean-git-ref": "^2.0.1", "crc-32": "^1.2.0", "diff3": "0.0.3", "ignore": "^5.1.4", "minimisted": "^2.0.0", "pako": "^1.0.10", "path-browserify": "^1.0.1", "pify": "^4.0.1", "readable-stream": "^3.4.0", "sha.js": "^2.4.9", "simple-get": "^4.0.1" }, "bin": { "isogit": "cli.cjs" } }, "sha512-NZCS7qpLkCZ1M/IrujYBD31sM6pd/fMVArK4fz4I7h6m0rUW2AsYU7S7zXeABuHL6HIfW6l53b4UQ/K441CQjg=="],
|
||||
|
||||
"jmespath": ["jmespath@0.16.0", "", {}, "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw=="],
|
||||
|
||||
"jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="],
|
||||
@@ -1009,8 +1013,6 @@
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
|
||||
@@ -1037,6 +1039,8 @@
|
||||
|
||||
"marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
|
||||
|
||||
"marked-shiki": ["marked-shiki@1.2.0", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-N924hp8veE6Mc91g5/kCNVoTU7TkeJfB2G2XEWb+k1fVA0Bck2T0rVt93d39BlOYH6ohP4Q9BFlPk+UkblhXbg=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],
|
||||
@@ -1169,6 +1173,8 @@
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"minimisted": ["minimisted@2.0.1", "", { "dependencies": { "minimist": "^1.2.5" } }, "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA=="],
|
||||
|
||||
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
|
||||
|
||||
"mri": ["mri@1.1.4", "", {}, "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w=="],
|
||||
@@ -1247,7 +1253,7 @@
|
||||
|
||||
"pagefind": ["pagefind@1.3.0", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.3.0", "@pagefind/darwin-x64": "1.3.0", "@pagefind/linux-arm64": "1.3.0", "@pagefind/linux-x64": "1.3.0", "@pagefind/windows-x64": "1.3.0" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-8KPLGT5g9s+olKMRTU9LFekLizkVIu9tes90O1/aigJ0T5LmyPqTzGJrETnSw3meSYg58YH7JTzhTTW/3z6VAw=="],
|
||||
|
||||
"pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
|
||||
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||
|
||||
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||
|
||||
@@ -1257,6 +1263,8 @@
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
@@ -1267,6 +1275,8 @@
|
||||
|
||||
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
|
||||
|
||||
"pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="],
|
||||
|
||||
"pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="],
|
||||
@@ -1319,8 +1329,6 @@
|
||||
|
||||
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||
|
||||
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
@@ -1417,6 +1425,8 @@
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"sha.js": ["sha.js@2.4.11", "", { "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" }, "bin": { "sha.js": "./bin.js" } }, "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ=="],
|
||||
|
||||
"sharp": ["sharp@0.32.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.2", "node-addon-api": "^6.1.0", "prebuild-install": "^7.1.1", "semver": "^7.5.4", "simple-get": "^4.0.1", "tar-fs": "^3.0.4", "tunnel-agent": "^0.6.0" } }, "sha512-0dap3iysgDkNaPOaOL4X/0akdu0ma62GcdC2NBQ+93eqpePdDdr2/LM0sFdDSMmN7yS+odyZtPsb7tx/cYBKnQ=="],
|
||||
|
||||
"shiki": ["shiki@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/engine-javascript": "3.4.2", "@shikijs/engine-oniguruma": "3.4.2", "@shikijs/langs": "3.4.2", "@shikijs/themes": "3.4.2", "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-wuxzZzQG8kvZndD7nustrNFIKYJ1jJoWIPaBpVe2+KHSvtzMi4SBjOxrigs8qeqce/l3U0cwiC+VAkLKSunHQQ=="],
|
||||
@@ -1455,23 +1465,23 @@
|
||||
|
||||
"split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="],
|
||||
|
||||
"sst": ["sst@3.17.6", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.6", "sst-darwin-x64": "3.17.6", "sst-linux-arm64": "3.17.6", "sst-linux-x64": "3.17.6", "sst-linux-x86": "3.17.6", "sst-win32-arm64": "3.17.6", "sst-win32-x64": "3.17.6", "sst-win32-x86": "3.17.6" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-p+AcqwfYQUdkxeRjCikQoTMviPCBiGoU7M0vcV6GDVmVis8hzhVw4EFfHTafZC+aWfy1Ke2UQi66vZlEVWuEqA=="],
|
||||
"sst": ["sst@3.17.8", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.8", "sst-darwin-x64": "3.17.8", "sst-linux-arm64": "3.17.8", "sst-linux-x64": "3.17.8", "sst-linux-x86": "3.17.8", "sst-win32-arm64": "3.17.8", "sst-win32-x64": "3.17.8", "sst-win32-x86": "3.17.8" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-P/a9/ZsjtQRrTBerBMO1ODaVa5HVTmNLrQNJiYvu2Bgd0ov+vefQeHv6oima8HLlPwpDIPS2gxJk8BZrTZMfCA=="],
|
||||
|
||||
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6tb7KlcPR7PTi3ofQv8dX/n6Jf7pNP9VfrnYL4HBWnWrcYaZeJ5MWobILfIJ/y2jHgoqmg9e5C3266Eds0JQyw=="],
|
||||
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-50P6YRMnZVItZUfB0+NzqMww2mmm4vB3zhTVtWUtGoXeiw78g1AEnVlmS28gYXPHM1P987jTvR7EON9u9ig/Dg=="],
|
||||
|
||||
"sst-darwin-x64": ["sst-darwin-x64@3.17.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-lFakq6/EgTuBSjbl8Kry4pfgAPEIyn6o7ZkyRz3hz5331wUaX88yfjs3tL9JQ8Ey6jBUYxwhP/Q1n7fzIG046g=="],
|
||||
"sst-darwin-x64": ["sst-darwin-x64@3.17.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-P0pnMHCmpkpcsxkWpilmeoD79LkbkoIcv6H0aeM9ArT/71/JBhvqH+HjMHSJCzni/9uR6er+nH5F+qol0UO6Bw=="],
|
||||
|
||||
"sst-linux-arm64": ["sst-linux-arm64@3.17.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-SdTxXMbTEdiwOqp37w31kXv97vHqSx3oK9h/76lKg7V9k5JxPJ6JMefPLhoKWwK0Zh6AndY2zo2oRoEv4SIaDw=="],
|
||||
"sst-linux-arm64": ["sst-linux-arm64@3.17.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-vun54YA/UzprCu9p8BC4rMwFU5Cj9xrHAHYLYUp/yq4H0pfmBIiQM62nsfIKizRThe/TkBFy60EEi9myf6raYA=="],
|
||||
|
||||
"sst-linux-x64": ["sst-linux-x64@3.17.6", "", { "os": "linux", "cpu": "x64" }, "sha512-qneh7uWDiTUYx8X1Y3h2YVw3SJ0ybBBlRrVybIvCM09JqQ8+qq/XjKXGzA/3/EF0Jr7Ug8cARSn9CwxhdQGN7Q=="],
|
||||
"sst-linux-x64": ["sst-linux-x64@3.17.8", "", { "os": "linux", "cpu": "x64" }, "sha512-HqByCaLE2gEJbM20P1QRd+GqDMAiieuU53FaZA1F+AGxQi+kR82NWjrPqFcMj4dMYg8w/TWXuV+G5+PwoUmpDw=="],
|
||||
|
||||
"sst-linux-x86": ["sst-linux-x86@3.17.6", "", { "os": "linux", "cpu": "none" }, "sha512-pU3D5OeqnmfxGqN31DxuwWnc1OayxhkErnITHhZ39D0MTiwbIgCapH26FuLW8B08/uxJWG8djUlOboCRhSBvWA=="],
|
||||
"sst-linux-x86": ["sst-linux-x86@3.17.8", "", { "os": "linux", "cpu": "none" }, "sha512-bCd6QM3MejfSmdvg8I/k+aUJQIZEQJg023qmN78fv00vwlAtfECvY7tjT9E2m3LDp33pXrcRYbFOQzPu+tWFfA=="],
|
||||
|
||||
"sst-win32-arm64": ["sst-win32-arm64@3.17.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-Rr3RTYWAsH9sM9CbM/sAZCk7dB1OsSAljjJuuHMvdSAYW3RDpXEza0PBJGxnBID2eOrpswEchzMPL2d8LtL7oA=="],
|
||||
"sst-win32-arm64": ["sst-win32-arm64@3.17.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-pilx0n8gm4aHJae/vNiqIwZkWF3tdwWzD/ON7hkytw+CVSZ0FXtyFW/yO/+2u3Yw0Kj0lSWPnUqYgm/eHPLwQA=="],
|
||||
|
||||
"sst-win32-x64": ["sst-win32-x64@3.17.6", "", { "os": "win32", "cpu": "x64" }, "sha512-yZ3roxwI0Wve9PFzdrrF1kfzCmIMFCCoa8qKeXY7LxCJ4QQIqHbCOccLK1Wv/MIU/mcZHWXTQVCLHw77uaa0GQ=="],
|
||||
"sst-win32-x64": ["sst-win32-x64@3.17.8", "", { "os": "win32", "cpu": "x64" }, "sha512-Jb0FVRyiOtESudF1V8ucW65PuHrx/iOHUamIO0JnbujWNHZBTRPB2QHN1dbewgkueYDaCmyS8lvuIImLwYJnzQ=="],
|
||||
|
||||
"sst-win32-x86": ["sst-win32-x86@3.17.6", "", { "os": "win32", "cpu": "none" }, "sha512-zV7TJWPJN9PmIXr15iXFSs0tbGsa52oBR3+xiKrUj2qj9XsZe7HBFwskRnHyiFq0durZY9kk9ZtoVlpuUuzr1g=="],
|
||||
"sst-win32-x86": ["sst-win32-x86@3.17.8", "", { "os": "win32", "cpu": "none" }, "sha512-oVmFa/PoElQmfnGJlB0w6rPXiYuldiagO6AbrLMT/6oAnWerLQ8Uhv9tJWfMh3xtPLImQLTjxDo1v0AIzEv9QA=="],
|
||||
|
||||
"stacktracey": ["stacktracey@2.1.8", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw=="],
|
||||
|
||||
@@ -1503,8 +1513,6 @@
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="],
|
||||
|
||||
"tar-fs": ["tar-fs@3.0.9", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA=="],
|
||||
|
||||
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
||||
@@ -1513,8 +1521,6 @@
|
||||
|
||||
"thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="],
|
||||
|
||||
"throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
|
||||
|
||||
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||
|
||||
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||
@@ -1525,7 +1531,7 @@
|
||||
|
||||
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
|
||||
|
||||
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.3.0", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-qlBkKRp8HVYV7p7jaG9lT2lvQY7c8b9czZ0tnsJUrN2TBTtEyFJymCdkhhpZNC9U4oGZ7lLk0glRJHrndWvVsg=="],
|
||||
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.1", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-lTI4dHZaVNQky29m7sb36Oy4tWPwxsCuFxFjF8hgGW0vpV+S6qPvI9SwsJFvdE/OHO5DoI7VMbryV1pxZHkkHQ=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
@@ -1599,8 +1605,6 @@
|
||||
|
||||
"url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="],
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
|
||||
|
||||
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
@@ -1677,7 +1681,7 @@
|
||||
|
||||
"youch": ["youch@3.3.4", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="],
|
||||
|
||||
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
|
||||
"zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="],
|
||||
|
||||
"zod-openapi": ["zod-openapi@4.2.4", "", { "peerDependencies": { "zod": "^3.21.4" } }, "sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g=="],
|
||||
|
||||
@@ -1689,12 +1693,22 @@
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
|
||||
|
||||
"@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="],
|
||||
|
||||
"@astrojs/sitemap/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
|
||||
|
||||
"@aws-crypto/crc32/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
@@ -1713,6 +1727,8 @@
|
||||
|
||||
"@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@modelcontextprotocol/sdk/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
|
||||
|
||||
"@openauthjs/openauth/@standard-schema/spec": ["@standard-schema/spec@1.0.0-beta.3", "", {}, "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw=="],
|
||||
|
||||
"@openauthjs/openauth/aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
|
||||
@@ -1749,10 +1765,14 @@
|
||||
|
||||
"astro/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
|
||||
|
||||
"astro/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
|
||||
|
||||
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
|
||||
|
||||
"bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"eventsource/eventsource-parser": ["eventsource-parser@3.0.2", "", {}, "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA=="],
|
||||
|
||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"get-source/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
@@ -1765,8 +1785,12 @@
|
||||
|
||||
"miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
|
||||
|
||||
"miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
|
||||
|
||||
"opencontrol/hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
|
||||
|
||||
"opencontrol/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
|
||||
|
||||
"opencontrol/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="],
|
||||
|
||||
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
|
||||
@@ -1793,6 +1817,8 @@
|
||||
|
||||
"token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
|
||||
|
||||
"unstorage/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.1.0", "", {}, "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw=="],
|
||||
|
||||
@@ -9,6 +9,9 @@ const bucket = new sst.cloudflare.Bucket("Bucket")
|
||||
export const api = new sst.cloudflare.Worker("Api", {
|
||||
domain: `api.${domain}`,
|
||||
handler: "packages/function/src/api.ts",
|
||||
environment: {
|
||||
WEB_DOMAIN: domain,
|
||||
},
|
||||
url: true,
|
||||
link: [bucket],
|
||||
transform: {
|
||||
|
||||
+7
-7
@@ -5,7 +5,9 @@
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.2.14",
|
||||
"scripts": {
|
||||
"dev": "bun run packages/opencode/src/index.ts",
|
||||
"typecheck": "bun run --filter='*' typecheck",
|
||||
"stainless": "bun run ./packages/opencode/src/index.ts serve ",
|
||||
"postinstall": "./scripts/hooks"
|
||||
},
|
||||
"workspaces": {
|
||||
@@ -15,13 +17,13 @@
|
||||
"catalog": {
|
||||
"typescript": "5.8.2",
|
||||
"@types/node": "22.13.9",
|
||||
"zod": "3.24.2",
|
||||
"ai": "4.3.16"
|
||||
"zod": "3.25.49",
|
||||
"ai": "5.0.0-beta.7"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "3.5.3",
|
||||
"sst": "3.17.6"
|
||||
"sst": "3.17.8"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -29,10 +31,8 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"prettier": {
|
||||
"semi": false
|
||||
},
|
||||
"overrides": {
|
||||
"zod": "3.24.2"
|
||||
"semi": false,
|
||||
"printWidth": 120
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"esbuild",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto"
|
||||
type Env = {
|
||||
SYNC_SERVER: DurableObjectNamespace<SyncServer>
|
||||
Bucket: R2Bucket
|
||||
WEB_DOMAIN: string
|
||||
}
|
||||
|
||||
export class SyncServer extends DurableObject<Env> {
|
||||
@@ -35,13 +36,9 @@ export class SyncServer extends DurableObject<Env> {
|
||||
ws.close(code, "Durable Object is closing WebSocket")
|
||||
}
|
||||
|
||||
async publish(secret: string, key: string, content: any) {
|
||||
if (secret !== (await this.getSecret())) throw new Error("Invalid secret")
|
||||
async publish(key: string, content: any) {
|
||||
const sessionID = await this.getSessionID()
|
||||
if (
|
||||
!key.startsWith(`session/info/${sessionID}`) &&
|
||||
!key.startsWith(`session/message/${sessionID}/`)
|
||||
)
|
||||
if (!key.startsWith(`session/info/${sessionID}`) && !key.startsWith(`session/message/${sessionID}/`))
|
||||
return new Response("Error: Invalid key", { status: 400 })
|
||||
|
||||
// store message
|
||||
@@ -76,6 +73,10 @@ export class SyncServer extends DurableObject<Env> {
|
||||
.map(([key, content]) => ({ key, content }))
|
||||
}
|
||||
|
||||
public async assertSecret(secret: string) {
|
||||
if (secret !== (await this.getSecret())) throw new Error("Invalid secret")
|
||||
}
|
||||
|
||||
private async getSecret() {
|
||||
return this.ctx.storage.get<string>("secret")
|
||||
}
|
||||
@@ -84,15 +85,19 @@ export class SyncServer extends DurableObject<Env> {
|
||||
return this.ctx.storage.get<string>("sessionID")
|
||||
}
|
||||
|
||||
async clear(secret: string) {
|
||||
await this.assertSecret(secret)
|
||||
async clear() {
|
||||
const sessionID = await this.getSessionID()
|
||||
const list = await this.env.Bucket.list({
|
||||
prefix: `session/message/${sessionID}/`,
|
||||
limit: 1000,
|
||||
})
|
||||
for (const item of list.objects) {
|
||||
await this.env.Bucket.delete(item.key)
|
||||
}
|
||||
await this.env.Bucket.delete(`session/info/${sessionID}`)
|
||||
await this.ctx.storage.deleteAll()
|
||||
}
|
||||
|
||||
private async assertSecret(secret: string) {
|
||||
if (secret !== (await this.getSecret())) throw new Error("Invalid secret")
|
||||
}
|
||||
|
||||
static shortName(id: string) {
|
||||
return id.substring(id.length - 8)
|
||||
}
|
||||
@@ -120,7 +125,7 @@ export default {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
secret,
|
||||
url: "https://opencode.ai/s/" + short,
|
||||
url: `https://${env.WEB_DOMAIN}/s/${short}`,
|
||||
}),
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -134,7 +139,17 @@ export default {
|
||||
const secret = body.secret
|
||||
const id = env.SYNC_SERVER.idFromName(SyncServer.shortName(sessionID))
|
||||
const stub = env.SYNC_SERVER.get(id)
|
||||
await stub.clear(secret)
|
||||
await stub.assertSecret(secret)
|
||||
await stub.clear()
|
||||
return new Response(JSON.stringify({}), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
}
|
||||
|
||||
if (request.method === "POST" && method === "share_delete_admin") {
|
||||
const id = env.SYNC_SERVER.idFromName("oVF8Rsiv")
|
||||
const stub = env.SYNC_SERVER.get(id)
|
||||
await stub.clear()
|
||||
return new Response(JSON.stringify({}), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
@@ -150,7 +165,8 @@ export default {
|
||||
const name = SyncServer.shortName(body.sessionID)
|
||||
const id = env.SYNC_SERVER.idFromName(name)
|
||||
const stub = env.SYNC_SERVER.get(id)
|
||||
await stub.publish(body.secret, body.key, body.content)
|
||||
await stub.assertSecret(body.secret)
|
||||
await stub.publish(body.key, body.content)
|
||||
return new Response(JSON.stringify({}), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
@@ -165,8 +181,7 @@ export default {
|
||||
}
|
||||
const id = url.searchParams.get("id")
|
||||
console.log("share_poll", id)
|
||||
if (!id)
|
||||
return new Response("Error: Share ID is required", { status: 400 })
|
||||
if (!id) return new Response("Error: Share ID is required", { status: 400 })
|
||||
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
|
||||
return stub.fetch(request)
|
||||
}
|
||||
@@ -174,8 +189,7 @@ export default {
|
||||
if (request.method === "GET" && method === "share_data") {
|
||||
const id = url.searchParams.get("id")
|
||||
console.log("share_data", id)
|
||||
if (!id)
|
||||
return new Response("Error: Share ID is required", { status: 400 })
|
||||
if (!id) return new Response("Error: Share ID is required", { status: 400 })
|
||||
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
|
||||
const data = await stub.getData()
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
node_modules
|
||||
research
|
||||
dist
|
||||
gen
|
||||
|
||||
@@ -49,7 +49,7 @@ else
|
||||
done
|
||||
|
||||
if [ -z "$resolved" ]; then
|
||||
printf "It seems that your package manager failed to install the right version of the OpenCode CLI for your platform. You can try manually installing the \"%s\" package\n" "$name" >&2
|
||||
printf "It seems that your package manager failed to install the right version of the opencode CLI for your platform. You can try manually installing the \"%s\" package\n" "$name" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -48,9 +48,9 @@ set "current_dir=%parent_dir%"
|
||||
goto :search_loop
|
||||
|
||||
:not_found
|
||||
echo It seems that your package manager failed to install the right version of the OpenCode CLI for your platform. You can try manually installing the "%name%" package >&2
|
||||
echo It seems that your package manager failed to install the right version of the opencode CLI for your platform. You can try manually installing the "%name%" package >&2
|
||||
exit /b 1
|
||||
|
||||
:execute
|
||||
rem Execute the binary with all arguments
|
||||
"%resolved%" %*
|
||||
"%resolved%" %*
|
||||
|
||||
@@ -297,6 +297,13 @@
|
||||
},
|
||||
"description": "MCP (Model Context Protocol) server configurations"
|
||||
},
|
||||
"instructions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Additional instruction files or patterns to include"
|
||||
},
|
||||
"experimental": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"env-paths": "3.0.0",
|
||||
"hono": "4.7.10",
|
||||
"hono-openapi": "0.4.8",
|
||||
"isomorphic-git": "1.32.1",
|
||||
"open": "10.1.2",
|
||||
"remeda": "2.22.3",
|
||||
"ts-lsp-client": "1.0.3",
|
||||
|
||||
@@ -40,7 +40,7 @@ for (const [os, arch] of targets) {
|
||||
console.log(`building ${os}-${arch}`)
|
||||
const name = `${pkg.name}-${os}-${arch}`
|
||||
await $`mkdir -p dist/${name}/bin`
|
||||
await $`GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X main.Version=${version}" -o ../opencode/dist/${name}/bin/tui ../tui/cmd/opencode/main.go`.cwd(
|
||||
await $`CGO_ENABLED=0 GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X main.Version=${version}" -o ../opencode/dist/${name}/bin/tui ../tui/cmd/opencode/main.go`.cwd(
|
||||
"../tui",
|
||||
)
|
||||
await $`bun build --define OPENCODE_VERSION="'${version}'" --compile --minify --target=bun-${os}-${arch} --outfile=dist/${name}/bin/opencode ./src/index.ts ./dist/${name}/bin/tui`
|
||||
@@ -57,8 +57,7 @@ for (const [os, arch] of targets) {
|
||||
2,
|
||||
),
|
||||
)
|
||||
if (!dry)
|
||||
await $`cd dist/${name} && bun publish --access public --tag ${npmTag}`
|
||||
if (!dry) await $`cd dist/${name} && bun publish --access public --tag ${npmTag}`
|
||||
optionalDependencies[name] = version
|
||||
}
|
||||
|
||||
@@ -82,8 +81,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
||||
2,
|
||||
),
|
||||
)
|
||||
if (!dry)
|
||||
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${npmTag}`
|
||||
if (!dry) await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${npmTag}`
|
||||
|
||||
if (!snapshot) {
|
||||
// Github Release
|
||||
@@ -91,15 +89,11 @@ if (!snapshot) {
|
||||
await $`cd dist/${key}/bin && zip -r ../../${key}.zip *`
|
||||
}
|
||||
|
||||
const previous = await fetch(
|
||||
"https://api.github.com/repos/sst/opencode/releases/latest",
|
||||
)
|
||||
const previous = await fetch("https://api.github.com/repos/sst/opencode/releases/latest")
|
||||
.then((res) => res.json())
|
||||
.then((data) => data.tag_name)
|
||||
|
||||
const commits = await fetch(
|
||||
`https://api.github.com/repos/sst/opencode/compare/${previous}...HEAD`,
|
||||
)
|
||||
const commits = await fetch(`https://api.github.com/repos/sst/opencode/compare/${previous}...HEAD`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => data.commits || [])
|
||||
|
||||
@@ -110,32 +104,20 @@ if (!snapshot) {
|
||||
return (
|
||||
!lower.includes("ignore:") &&
|
||||
!lower.includes("ci:") &&
|
||||
!lower.includes("wip:") &&
|
||||
!lower.includes("docs:") &&
|
||||
!lower.includes("doc:")
|
||||
)
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
if (!dry)
|
||||
await $`gh release create v${version} --title "v${version}" --notes ${notes} ./dist/*.zip`
|
||||
if (!dry) await $`gh release create v${version} --title "v${version}" --notes ${notes} ./dist/*.zip`
|
||||
|
||||
// Calculate SHA values
|
||||
const arm64Sha =
|
||||
await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1`
|
||||
.text()
|
||||
.then((x) => x.trim())
|
||||
const x64Sha =
|
||||
await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`
|
||||
.text()
|
||||
.then((x) => x.trim())
|
||||
const macX64Sha =
|
||||
await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`
|
||||
.text()
|
||||
.then((x) => x.trim())
|
||||
const macArm64Sha =
|
||||
await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`
|
||||
.text()
|
||||
.then((x) => x.trim())
|
||||
const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
|
||||
const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
|
||||
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
|
||||
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
|
||||
|
||||
// AUR package
|
||||
const pkgbuild = [
|
||||
@@ -169,9 +151,7 @@ if (!snapshot) {
|
||||
for (const pkg of ["opencode", "opencode-bin"]) {
|
||||
await $`rm -rf ./dist/aur-${pkg}`
|
||||
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
|
||||
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(
|
||||
pkgbuild.replace("${pkg}", pkg),
|
||||
)
|
||||
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild.replace("${pkg}", pkg))
|
||||
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
|
||||
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
|
||||
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
|
||||
|
||||
@@ -36,29 +36,23 @@ export namespace App {
|
||||
services: Map<any, { state: any; shutdown?: (input: any) => Promise<void> }>
|
||||
}>("app")
|
||||
|
||||
export const use = ctx.use
|
||||
|
||||
const APP_JSON = "app.json"
|
||||
|
||||
export type Input = {
|
||||
cwd: string
|
||||
}
|
||||
|
||||
export async function provide<T>(
|
||||
input: Input,
|
||||
cb: (app: App.Info) => Promise<T>,
|
||||
) {
|
||||
export const provideExisting = ctx.provide
|
||||
export async function provide<T>(input: Input, cb: (app: App.Info) => Promise<T>) {
|
||||
log.info("creating", {
|
||||
cwd: input.cwd,
|
||||
})
|
||||
const git = await Filesystem.findUp(".git", input.cwd).then(([x]) =>
|
||||
x ? path.dirname(x) : undefined,
|
||||
)
|
||||
const git = await Filesystem.findUp(".git", input.cwd).then(([x]) => (x ? path.dirname(x) : undefined))
|
||||
log.info("git", { git })
|
||||
|
||||
const data = path.join(
|
||||
Global.Path.data,
|
||||
"project",
|
||||
git ? directory(git) : "global",
|
||||
)
|
||||
const data = path.join(Global.Path.data, "project", git ? directory(git) : "global")
|
||||
const stateFile = Bun.file(path.join(data, APP_JSON))
|
||||
const state = (await stateFile.json().catch(() => ({}))) as {
|
||||
initialized: number
|
||||
@@ -96,13 +90,16 @@ export namespace App {
|
||||
}
|
||||
|
||||
return ctx.provide(app, async () => {
|
||||
const result = await cb(app.info)
|
||||
for (const [key, entry] of app.services.entries()) {
|
||||
if (!entry.shutdown) continue
|
||||
log.info("shutdown", { name: key })
|
||||
await entry.shutdown?.(await entry.state)
|
||||
try {
|
||||
const result = await cb(app.info)
|
||||
return result
|
||||
} finally {
|
||||
for (const [key, entry] of app.services.entries()) {
|
||||
if (!entry.shutdown) continue
|
||||
log.info("shutdown", { name: key })
|
||||
await entry.shutdown?.(await entry.state)
|
||||
}
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -10,14 +10,8 @@ export namespace AuthAnthropic {
|
||||
url.searchParams.set("code", "true")
|
||||
url.searchParams.set("client_id", CLIENT_ID)
|
||||
url.searchParams.set("response_type", "code")
|
||||
url.searchParams.set(
|
||||
"redirect_uri",
|
||||
"https://console.anthropic.com/oauth/code/callback",
|
||||
)
|
||||
url.searchParams.set(
|
||||
"scope",
|
||||
"org:create_api_key user:profile user:inference",
|
||||
)
|
||||
url.searchParams.set("redirect_uri", "https://console.anthropic.com/oauth/code/callback")
|
||||
url.searchParams.set("scope", "org:create_api_key user:profile user:inference")
|
||||
url.searchParams.set("code_challenge", pkce.challenge)
|
||||
url.searchParams.set("code_challenge_method", "S256")
|
||||
url.searchParams.set("state", pkce.verifier)
|
||||
@@ -57,20 +51,17 @@ export namespace AuthAnthropic {
|
||||
const info = await Auth.get("anthropic")
|
||||
if (!info || info.type !== "oauth") return
|
||||
if (info.access && info.expires > Date.now()) return info.access
|
||||
const response = await fetch(
|
||||
"https://console.anthropic.com/v1/oauth/token",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: info.refresh,
|
||||
client_id: CLIENT_ID,
|
||||
}),
|
||||
const response = await fetch("https://console.anthropic.com/v1/oauth/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
body: JSON.stringify({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: info.refresh,
|
||||
client_id: CLIENT_ID,
|
||||
}),
|
||||
})
|
||||
if (!response.ok) return
|
||||
const json = await response.json()
|
||||
await Auth.set("anthropic", {
|
||||
|
||||
@@ -4,9 +4,7 @@ import path from "path"
|
||||
|
||||
export const AuthCopilot = lazy(async () => {
|
||||
const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts"))
|
||||
const response = fetch(
|
||||
"https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts",
|
||||
)
|
||||
const response = fetch("https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts")
|
||||
.then((x) => Bun.write(file, x))
|
||||
.catch(() => {})
|
||||
|
||||
|
||||
@@ -122,10 +122,7 @@ export namespace AuthGithubCopilot {
|
||||
return tokenData.token
|
||||
}
|
||||
|
||||
export const DeviceCodeError = NamedError.create(
|
||||
"DeviceCodeError",
|
||||
z.object({}),
|
||||
)
|
||||
export const DeviceCodeError = NamedError.create("DeviceCodeError", z.object({}))
|
||||
|
||||
export const TokenExchangeError = NamedError.create(
|
||||
"TokenExchangeError",
|
||||
|
||||
@@ -8,10 +8,7 @@ import { readableStreamToText } from "bun"
|
||||
export namespace BunProc {
|
||||
const log = Log.create({ service: "bun" })
|
||||
|
||||
export async function run(
|
||||
cmd: string[],
|
||||
options?: Bun.SpawnOptions.OptionsObject<any, any, any>,
|
||||
) {
|
||||
export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
|
||||
log.info("running", {
|
||||
cmd: [which(), ...cmd],
|
||||
...options,
|
||||
@@ -26,9 +23,17 @@ export namespace BunProc {
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
})
|
||||
const code = await result.exited;
|
||||
const stdout = result.stdout ? typeof result.stdout === "number" ? result.stdout : await readableStreamToText(result.stdout) : undefined
|
||||
const stderr = result.stderr ? typeof result.stderr === "number" ? result.stderr : await readableStreamToText(result.stderr) : undefined
|
||||
const code = await result.exited
|
||||
const stdout = result.stdout
|
||||
? typeof result.stdout === "number"
|
||||
? result.stdout
|
||||
: await readableStreamToText(result.stdout)
|
||||
: undefined
|
||||
const stderr = result.stderr
|
||||
? typeof result.stderr === "number"
|
||||
? result.stderr
|
||||
: await readableStreamToText(result.stderr)
|
||||
: undefined
|
||||
log.info("done", {
|
||||
code,
|
||||
stdout,
|
||||
@@ -61,7 +66,7 @@ export namespace BunProc {
|
||||
if (parsed.dependencies[pkg] === version) return mod
|
||||
parsed.dependencies[pkg] = version
|
||||
await Bun.write(pkgjson, JSON.stringify(parsed, null, 2))
|
||||
await BunProc.run(["install", "--registry=https://registry.npmjs.org"], {
|
||||
await BunProc.run(["install", "--cwd", Global.Path.cache, "--registry=https://registry.npmjs.org"], {
|
||||
cwd: Global.Path.cache,
|
||||
}).catch((e) => {
|
||||
throw new InstallFailedError(
|
||||
|
||||
@@ -18,10 +18,7 @@ export namespace Bus {
|
||||
|
||||
const registry = new Map<string, EventDefinition>()
|
||||
|
||||
export function event<Type extends string, Properties extends ZodType>(
|
||||
type: Type,
|
||||
properties: Properties,
|
||||
) {
|
||||
export function event<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
|
||||
const result = {
|
||||
type,
|
||||
properties,
|
||||
@@ -72,10 +69,7 @@ export namespace Bus {
|
||||
|
||||
export function subscribe<Definition extends EventDefinition>(
|
||||
def: Definition,
|
||||
callback: (event: {
|
||||
type: Definition["type"]
|
||||
properties: z.infer<Definition["properties"]>
|
||||
}) => void,
|
||||
callback: (event: { type: Definition["type"]; properties: z.infer<Definition["properties"]> }) => void,
|
||||
) {
|
||||
return raw(def.type, callback)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { App } from "../app/app"
|
||||
import { ConfigHooks } from "../config/hooks"
|
||||
import { FileWatcher } from "../file/watch"
|
||||
import { Format } from "../format"
|
||||
import { LSP } from "../lsp"
|
||||
import { Share } from "../share/share"
|
||||
|
||||
export async function bootstrap<T>(
|
||||
input: App.Input,
|
||||
cb: (app: App.Info) => Promise<T>,
|
||||
) {
|
||||
export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Promise<T>) {
|
||||
return App.provide(input, async (app) => {
|
||||
Share.init()
|
||||
Format.init()
|
||||
ConfigHooks.init()
|
||||
LSP.init()
|
||||
FileWatcher.init()
|
||||
|
||||
return cb(app)
|
||||
})
|
||||
|
||||
@@ -15,11 +15,7 @@ export const AuthCommand = cmd({
|
||||
command: "auth",
|
||||
describe: "manage credentials",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.command(AuthLoginCommand)
|
||||
.command(AuthLogoutCommand)
|
||||
.command(AuthListCommand)
|
||||
.demandCommand(),
|
||||
yargs.command(AuthLoginCommand).command(AuthLogoutCommand).command(AuthListCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
||||
@@ -31,9 +27,7 @@ export const AuthListCommand = cmd({
|
||||
UI.empty()
|
||||
const authPath = path.join(Global.Path.data, "auth.json")
|
||||
const homedir = os.homedir()
|
||||
const displayPath = authPath.startsWith(homedir)
|
||||
? authPath.replace(homedir, "~")
|
||||
: authPath
|
||||
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
|
||||
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
|
||||
const results = await Auth.all().then((x) => Object.entries(x))
|
||||
const database = await ModelsDev.get()
|
||||
@@ -114,8 +108,7 @@ export const AuthLoginCommand = cmd({
|
||||
if (provider === "other") {
|
||||
provider = await prompts.text({
|
||||
message: "Enter provider id",
|
||||
validate: (x) =>
|
||||
x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only",
|
||||
validate: (x) => (x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only"),
|
||||
})
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
provider = provider.replace(/^@ai-sdk\//, "")
|
||||
@@ -186,17 +179,13 @@ export const AuthLoginCommand = cmd({
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
const deviceInfo = await copilot.authorize()
|
||||
|
||||
prompts.note(
|
||||
`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`,
|
||||
)
|
||||
prompts.note(`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`)
|
||||
|
||||
const spinner = prompts.spinner()
|
||||
spinner.start("Waiting for authorization...")
|
||||
|
||||
while (true) {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, deviceInfo.interval * 1000),
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, deviceInfo.interval * 1000))
|
||||
const response = await copilot.poll(deviceInfo.device)
|
||||
if (response.status === "pending") continue
|
||||
if (response.status === "success") {
|
||||
@@ -248,12 +237,7 @@ export const AuthLogoutCommand = cmd({
|
||||
const providerID = await prompts.select({
|
||||
message: "Select provider",
|
||||
options: credentials.map(([key, value]) => ({
|
||||
label:
|
||||
(database[key]?.name || key) +
|
||||
UI.Style.TEXT_DIM +
|
||||
" (" +
|
||||
value.type +
|
||||
")",
|
||||
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
|
||||
value: key,
|
||||
})),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { File } from "../../../file"
|
||||
import { bootstrap } from "../../bootstrap"
|
||||
import { cmd } from "../cmd"
|
||||
|
||||
const FileReadCommand = cmd({
|
||||
command: "read <path>",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("path", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
description: "File path to read",
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
const content = await File.read(args.path)
|
||||
console.log(content)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const FileStatusCommand = cmd({
|
||||
command: "status",
|
||||
builder: (yargs) => yargs,
|
||||
async handler() {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
const status = await File.status()
|
||||
console.log(JSON.stringify(status, null, 2))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const FileCommand = cmd({
|
||||
command: "file",
|
||||
builder: (yargs) => yargs.command(FileReadCommand).command(FileStatusCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import { bootstrap } from "../../bootstrap"
|
||||
import { cmd } from "../cmd"
|
||||
import { FileCommand } from "./file"
|
||||
import { LSPCommand } from "./lsp"
|
||||
import { RipgrepCommand } from "./ripgrep"
|
||||
import { SnapshotCommand } from "./snapshot"
|
||||
|
||||
export const DebugCommand = cmd({
|
||||
command: "debug",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.command(LSPCommand)
|
||||
.command(RipgrepCommand)
|
||||
.command(FileCommand)
|
||||
.command(SnapshotCommand)
|
||||
.command({
|
||||
command: "wait",
|
||||
async handler() {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1_000 * 60 * 60 * 24))
|
||||
})
|
||||
},
|
||||
})
|
||||
.demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import { LSP } from "../../../lsp"
|
||||
import { bootstrap } from "../../bootstrap"
|
||||
import { cmd } from "../cmd"
|
||||
import { Log } from "../../../util/log"
|
||||
|
||||
export const LSPCommand = cmd({
|
||||
command: "lsp",
|
||||
builder: (yargs) => yargs.command(DiagnosticsCommand).command(SymbolsCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
||||
const DiagnosticsCommand = cmd({
|
||||
command: "diagnostics <file>",
|
||||
builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await LSP.touchFile(args.file, true)
|
||||
console.log(await LSP.diagnostics())
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const SymbolsCommand = cmd({
|
||||
command: "symbols <query>",
|
||||
builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await LSP.touchFile("./src/index.ts", true)
|
||||
using _ = Log.Default.time("symbols")
|
||||
const results = await LSP.workspaceSymbol(args.query)
|
||||
console.log(JSON.stringify(results, null, 2))
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
import { App } from "../../../app/app"
|
||||
import { Ripgrep } from "../../../file/ripgrep"
|
||||
import { bootstrap } from "../../bootstrap"
|
||||
import { cmd } from "../cmd"
|
||||
|
||||
export const RipgrepCommand = cmd({
|
||||
command: "rg",
|
||||
builder: (yargs) => yargs.command(TreeCommand).command(FilesCommand).command(SearchCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
||||
const TreeCommand = cmd({
|
||||
command: "tree",
|
||||
builder: (yargs) =>
|
||||
yargs.option("limit", {
|
||||
type: "number",
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
const app = App.info()
|
||||
console.log(await Ripgrep.tree({ cwd: app.path.cwd, limit: args.limit }))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const FilesCommand = cmd({
|
||||
command: "files",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.option("query", {
|
||||
type: "string",
|
||||
description: "Filter files by query",
|
||||
})
|
||||
.option("glob", {
|
||||
type: "string",
|
||||
description: "Glob pattern to match files",
|
||||
})
|
||||
.option("limit", {
|
||||
type: "number",
|
||||
description: "Limit number of results",
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
const app = App.info()
|
||||
const files = await Ripgrep.files({
|
||||
cwd: app.path.cwd,
|
||||
query: args.query,
|
||||
glob: args.glob,
|
||||
limit: args.limit,
|
||||
})
|
||||
console.log(files.join("\n"))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const SearchCommand = cmd({
|
||||
command: "search <pattern>",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional("pattern", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
description: "Search pattern",
|
||||
})
|
||||
.option("glob", {
|
||||
type: "array",
|
||||
description: "File glob patterns",
|
||||
})
|
||||
.option("limit", {
|
||||
type: "number",
|
||||
description: "Limit number of results",
|
||||
}),
|
||||
async handler(args) {
|
||||
const results = await Ripgrep.search({
|
||||
cwd: process.cwd(),
|
||||
pattern: args.pattern,
|
||||
glob: args.glob as string[] | undefined,
|
||||
limit: args.limit,
|
||||
})
|
||||
console.log(JSON.stringify(results, null, 2))
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Snapshot } from "../../../snapshot"
|
||||
import { bootstrap } from "../../bootstrap"
|
||||
import { cmd } from "../cmd"
|
||||
|
||||
export const SnapshotCommand = cmd({
|
||||
command: "snapshot",
|
||||
builder: (yargs) => yargs.command(SnapshotCreateCommand).command(SnapshotRestoreCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
||||
export const SnapshotCreateCommand = cmd({
|
||||
command: "create",
|
||||
async handler() {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
const result = await Snapshot.create("test")
|
||||
console.log(result)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const SnapshotRestoreCommand = cmd({
|
||||
command: "restore <commit>",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("commit", {
|
||||
type: "string",
|
||||
description: "commit",
|
||||
demandOption: true,
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await Snapshot.restore("test", args.commit)
|
||||
console.log("restored")
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -10,9 +10,6 @@ export const GenerateCommand = {
|
||||
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, "openapi.json"), JSON.stringify(specs, null, 2))
|
||||
},
|
||||
} satisfies CommandModule
|
||||
|
||||
@@ -2,12 +2,12 @@ import type { Argv } from "yargs"
|
||||
import { Bus } from "../../bus"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { Session } from "../../session"
|
||||
import { Message } from "../../session/message"
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import { Flag } from "../../flag/flag"
|
||||
import { Config } from "../../config/config"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { MessageV2 } from "../../session/message-v2"
|
||||
|
||||
const TOOL: Record<string, [string, string]> = {
|
||||
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||
@@ -84,21 +84,12 @@ export const RunCommand = cmd({
|
||||
const cfg = await Config.get()
|
||||
if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
|
||||
await Session.share(session.id)
|
||||
UI.println(
|
||||
UI.Style.TEXT_INFO_BOLD +
|
||||
"~ https://opencode.ai/s/" +
|
||||
session.id.slice(-8),
|
||||
)
|
||||
UI.println(UI.Style.TEXT_INFO_BOLD + "~ https://opencode.ai/s/" + session.id.slice(-8))
|
||||
}
|
||||
UI.empty()
|
||||
|
||||
const { providerID, modelID } = args.model
|
||||
? Provider.parseModel(args.model)
|
||||
: await Provider.defaultModel()
|
||||
UI.println(
|
||||
UI.Style.TEXT_NORMAL_BOLD + "@ ",
|
||||
UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`,
|
||||
)
|
||||
const { providerID, modelID } = args.model ? Provider.parseModel(args.model) : await Provider.defaultModel()
|
||||
UI.println(UI.Style.TEXT_NORMAL_BOLD + "@ ", UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`)
|
||||
UI.empty()
|
||||
|
||||
function printEvent(color: string, type: string, title: string) {
|
||||
@@ -110,24 +101,13 @@ export const RunCommand = cmd({
|
||||
)
|
||||
}
|
||||
|
||||
Bus.subscribe(Message.Event.PartUpdated, async (evt) => {
|
||||
Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
|
||||
if (evt.properties.sessionID !== session.id) return
|
||||
const part = evt.properties.part
|
||||
const message = await Session.getMessage(
|
||||
evt.properties.sessionID,
|
||||
evt.properties.messageID,
|
||||
)
|
||||
|
||||
if (
|
||||
part.type === "tool-invocation" &&
|
||||
part.toolInvocation.state === "result"
|
||||
) {
|
||||
const metadata = message.metadata.tool[part.toolInvocation.toolCallId]
|
||||
const [tool, color] = TOOL[part.toolInvocation.toolName] ?? [
|
||||
part.toolInvocation.toolName,
|
||||
UI.Style.TEXT_INFO_BOLD,
|
||||
]
|
||||
printEvent(color, tool, metadata?.title || "Unknown")
|
||||
if (part.type === "tool" && part.state.status === "completed") {
|
||||
const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD]
|
||||
printEvent(color, tool, part.state.title || "Unknown")
|
||||
}
|
||||
|
||||
if (part.type === "text") {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { App } from "../../app/app"
|
||||
import { LSP } from "../../lsp"
|
||||
import { cmd } from "./cmd"
|
||||
|
||||
export const ScrapCommand = cmd({
|
||||
command: "scrap <file>",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("file", { type: "string", demandOption: true }),
|
||||
async handler(args) {
|
||||
await App.provide({ cwd: process.cwd() }, async () => {
|
||||
await LSP.touchFile(args.file, true)
|
||||
console.log(await LSP.diagnostics())
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { App } from "../../app/app"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { Server } from "../../server/server"
|
||||
import { Share } from "../../share/share"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { cmd } from "./cmd"
|
||||
|
||||
export const ServeCommand = cmd({
|
||||
@@ -23,7 +23,7 @@ export const ServeCommand = cmd({
|
||||
describe: "starts a headless opencode server",
|
||||
handler: async (args) => {
|
||||
const cwd = process.cwd()
|
||||
await App.provide({ cwd }, async () => {
|
||||
await bootstrap({ cwd }, async () => {
|
||||
const providers = await Provider.list()
|
||||
if (Object.keys(providers).length === 0) {
|
||||
return "needs_provider"
|
||||
@@ -38,9 +38,7 @@ export const ServeCommand = cmd({
|
||||
hostname,
|
||||
})
|
||||
|
||||
console.log(
|
||||
`opencode server listening on http://${server.hostname}:${server.port}`,
|
||||
)
|
||||
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
||||
|
||||
await new Promise(() => {})
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import fs from "fs/promises"
|
||||
import { Installation } from "../../installation"
|
||||
import { Config } from "../../config/config"
|
||||
import { Bus } from "../../bus"
|
||||
import { Log } from "../../util/log"
|
||||
|
||||
export const TuiCommand = cmd({
|
||||
command: "$0 [project]",
|
||||
@@ -39,9 +40,7 @@ export const TuiCommand = cmd({
|
||||
})
|
||||
|
||||
let cmd = ["go", "run", "./main.go"]
|
||||
let cwd = Bun.fileURLToPath(
|
||||
new URL("../../../../tui/cmd/opencode", import.meta.url),
|
||||
)
|
||||
let cwd = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url))
|
||||
if (Bun.embeddedFiles.length > 0) {
|
||||
const blob = Bun.embeddedFiles[0] as File
|
||||
let binaryName = blob.name
|
||||
@@ -57,6 +56,9 @@ export const TuiCommand = cmd({
|
||||
cwd = process.cwd()
|
||||
cmd = [binary]
|
||||
}
|
||||
Log.Default.info("tui", {
|
||||
cmd,
|
||||
})
|
||||
const proc = Bun.spawn({
|
||||
cmd: [...cmd, ...process.argv.slice(2)],
|
||||
cwd,
|
||||
@@ -100,7 +102,7 @@ export const TuiCommand = cmd({
|
||||
UI.empty()
|
||||
UI.println(UI.logo(" "))
|
||||
const result = await Bun.spawn({
|
||||
cmd: [process.execPath, "auth", "login"],
|
||||
cmd: [...getOpencodeCommand(), "auth", "login"],
|
||||
cwd: process.cwd(),
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
@@ -112,3 +114,25 @@ export const TuiCommand = cmd({
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Get the correct command to run opencode CLI
|
||||
* In development: ["bun", "run", "packages/opencode/src/index.ts"]
|
||||
* In production: ["/path/to/opencode"]
|
||||
*/
|
||||
function getOpencodeCommand(): string[] {
|
||||
// Check if OPENCODE_BIN_PATH is set (used by shell wrapper scripts)
|
||||
if (process.env["OPENCODE_BIN_PATH"]) {
|
||||
return [process.env["OPENCODE_BIN_PATH"]]
|
||||
}
|
||||
|
||||
const execPath = process.execPath.toLowerCase()
|
||||
|
||||
if (Installation.isDev()) {
|
||||
// In development, use bun to run the TypeScript entry point
|
||||
return [execPath, "run", process.argv[1]]
|
||||
}
|
||||
|
||||
// In production, use the current executable path
|
||||
return [process.execPath]
|
||||
}
|
||||
|
||||
@@ -27,22 +27,26 @@ export const UpgradeCommand = {
|
||||
const detectedMethod = await Installation.method()
|
||||
const method = (args.method as Installation.Method) ?? detectedMethod
|
||||
if (method === "unknown") {
|
||||
prompts.log.error(
|
||||
`opencode is installed to ${process.execPath} and seems to be managed by a package manager`,
|
||||
)
|
||||
prompts.log.error(`opencode is installed to ${process.execPath} and seems to be managed by a package manager`)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
prompts.log.info("Using method: " + method)
|
||||
const target = args.target ?? (await Installation.latest())
|
||||
|
||||
if (Installation.VERSION === target) {
|
||||
prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
||||
prompts.log.info(`From ${Installation.VERSION} → ${target}`)
|
||||
const spinner = prompts.spinner()
|
||||
spinner.start("Upgrading...")
|
||||
const err = await Installation.upgrade(method, target).catch((err) => err)
|
||||
if (err) {
|
||||
spinner.stop("Upgrade failed")
|
||||
if (err instanceof Installation.UpgradeFailedError)
|
||||
prompts.log.error(err.data.stderr)
|
||||
if (err instanceof Installation.UpgradeFailedError) prompts.log.error(err.data.stderr)
|
||||
else if (err instanceof Error) prompts.log.error(err.message)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
|
||||
@@ -5,14 +5,11 @@ import { UI } from "./ui"
|
||||
export function FormatError(input: unknown) {
|
||||
if (MCP.Failed.isInstance(input))
|
||||
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
|
||||
if (Config.JsonError.isInstance(input))
|
||||
return `Config file at ${input.data.path} is not valid JSON`
|
||||
if (Config.JsonError.isInstance(input)) return `Config file at ${input.data.path} is not valid JSON`
|
||||
if (Config.InvalidError.isInstance(input))
|
||||
return [
|
||||
`Config file at ${input.data.path} is invalid`,
|
||||
...(input.data.issues?.map(
|
||||
(issue) => "↳ " + issue.message + " " + issue.path.join("."),
|
||||
) ?? []),
|
||||
...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []),
|
||||
].join("\n")
|
||||
|
||||
if (UI.CancelledError.isInstance(input)) return ""
|
||||
|
||||
@@ -29,18 +29,12 @@ export namespace Config {
|
||||
export const McpLocal = z
|
||||
.object({
|
||||
type: z.literal("local").describe("Type of MCP server connection"),
|
||||
command: z
|
||||
.string()
|
||||
.array()
|
||||
.describe("Command and arguments to run the MCP server"),
|
||||
command: z.string().array().describe("Command and arguments to run the MCP server"),
|
||||
environment: z
|
||||
.record(z.string(), z.string())
|
||||
.optional()
|
||||
.describe("Environment variables to set when running the MCP server"),
|
||||
enabled: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Enable or disable the MCP server on startup"),
|
||||
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
@@ -51,10 +45,7 @@ export namespace Config {
|
||||
.object({
|
||||
type: z.literal("remote").describe("Type of MCP server connection"),
|
||||
url: z.string().describe("URL of the remote MCP server"),
|
||||
enabled: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Enable or disable the MCP server on startup"),
|
||||
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
@@ -66,67 +57,31 @@ export namespace Config {
|
||||
|
||||
export const Keybinds = z
|
||||
.object({
|
||||
leader: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Leader key for keybind combinations"),
|
||||
leader: z.string().optional().describe("Leader key for keybind combinations"),
|
||||
help: z.string().optional().describe("Show help dialog"),
|
||||
editor_open: z.string().optional().describe("Open external editor"),
|
||||
session_new: z.string().optional().describe("Create a new session"),
|
||||
session_list: z.string().optional().describe("List all sessions"),
|
||||
session_share: z.string().optional().describe("Share current session"),
|
||||
session_interrupt: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Interrupt current session"),
|
||||
session_compact: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Toggle compact mode for session"),
|
||||
session_interrupt: z.string().optional().describe("Interrupt current session"),
|
||||
session_compact: z.string().optional().describe("Toggle compact mode for session"),
|
||||
tool_details: z.string().optional().describe("Show tool details"),
|
||||
model_list: z.string().optional().describe("List available models"),
|
||||
theme_list: z.string().optional().describe("List available themes"),
|
||||
project_init: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Initialize project configuration"),
|
||||
project_init: z.string().optional().describe("Initialize project configuration"),
|
||||
input_clear: z.string().optional().describe("Clear input field"),
|
||||
input_paste: z.string().optional().describe("Paste from clipboard"),
|
||||
input_submit: z.string().optional().describe("Submit input"),
|
||||
input_newline: z.string().optional().describe("Insert newline in input"),
|
||||
history_previous: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Navigate to previous history item"),
|
||||
history_next: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Navigate to next history item"),
|
||||
messages_page_up: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Scroll messages up by one page"),
|
||||
messages_page_down: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Scroll messages down by one page"),
|
||||
messages_half_page_up: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Scroll messages up by half page"),
|
||||
messages_half_page_down: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Scroll messages down by half page"),
|
||||
messages_previous: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Navigate to previous message"),
|
||||
history_previous: z.string().optional().describe("Navigate to previous history item"),
|
||||
history_next: z.string().optional().describe("Navigate to next history item"),
|
||||
messages_page_up: z.string().optional().describe("Scroll messages up by one page"),
|
||||
messages_page_down: z.string().optional().describe("Scroll messages down by one page"),
|
||||
messages_half_page_up: z.string().optional().describe("Scroll messages up by half page"),
|
||||
messages_half_page_down: z.string().optional().describe("Scroll messages down by half page"),
|
||||
messages_previous: z.string().optional().describe("Navigate to previous message"),
|
||||
messages_next: z.string().optional().describe("Navigate to next message"),
|
||||
messages_first: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Navigate to first message"),
|
||||
messages_first: z.string().optional().describe("Navigate to first message"),
|
||||
messages_last: z.string().optional().describe("Navigate to last message"),
|
||||
app_exit: z.string().optional().describe("Exit the application"),
|
||||
})
|
||||
@@ -136,33 +91,13 @@ export namespace Config {
|
||||
})
|
||||
export const Info = z
|
||||
.object({
|
||||
$schema: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("JSON schema reference for configuration validation"),
|
||||
theme: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Theme name to use for the interface"),
|
||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
||||
theme: z.string().optional().describe("Theme name to use for the interface"),
|
||||
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
||||
autoshare: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Share newly created sessions automatically"),
|
||||
autoupdate: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Automatically update to the latest version"),
|
||||
disabled_providers: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Disable providers that are loaded automatically"),
|
||||
model: z
|
||||
.string()
|
||||
.describe(
|
||||
"Model to use in the format of provider/model, eg anthropic/claude-2",
|
||||
)
|
||||
.optional(),
|
||||
autoshare: z.boolean().optional().describe("Share newly created sessions automatically"),
|
||||
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
|
||||
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
|
||||
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
|
||||
provider: z
|
||||
.record(
|
||||
ModelsDev.Provider.partial().extend({
|
||||
@@ -172,10 +107,8 @@ export namespace Config {
|
||||
)
|
||||
.optional()
|
||||
.describe("Custom provider configurations and model overrides"),
|
||||
mcp: z
|
||||
.record(z.string(), Mcp)
|
||||
.optional()
|
||||
.describe("MCP (Model Context Protocol) server configurations"),
|
||||
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
|
||||
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
|
||||
experimental: z
|
||||
.object({
|
||||
hook: z
|
||||
@@ -223,10 +156,7 @@ export namespace Config {
|
||||
if (provider && model) result.model = `${provider}/${model}`
|
||||
result["$schema"] = "https://opencode.ai/config.json"
|
||||
result = mergeDeep(result, rest)
|
||||
await Bun.write(
|
||||
path.join(Global.Path.config, "config.json"),
|
||||
JSON.stringify(result, null, 2),
|
||||
)
|
||||
await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
|
||||
await fs.unlink(path.join(Global.Path.config, "config"))
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
@@ -22,9 +22,7 @@ export namespace ConfigHooks {
|
||||
command: item.command,
|
||||
})
|
||||
Bun.spawn({
|
||||
cmd: item.command.map((x) =>
|
||||
x.replace("$FILE", payload.properties.file),
|
||||
),
|
||||
cmd: item.command.map((x) => x.replace("$FILE", payload.properties.file)),
|
||||
env: item.environment,
|
||||
cwd: app.path.cwd,
|
||||
stdout: "ignore",
|
||||
|
||||
-132
@@ -1,132 +0,0 @@
|
||||
import path from "path"
|
||||
import { Global } from "../global"
|
||||
import fs from "fs/promises"
|
||||
import { z } from "zod"
|
||||
import { NamedError } from "../util/error"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { $ } from "bun"
|
||||
import { Fzf } from "./fzf"
|
||||
|
||||
export namespace Ripgrep {
|
||||
const PLATFORM = {
|
||||
darwin: { platform: "apple-darwin", extension: "tar.gz" },
|
||||
linux: { platform: "unknown-linux-musl", extension: "tar.gz" },
|
||||
win32: { platform: "pc-windows-msvc", extension: "zip" },
|
||||
} as const
|
||||
|
||||
export const ExtractionFailedError = NamedError.create(
|
||||
"RipgrepExtractionFailedError",
|
||||
z.object({
|
||||
filepath: z.string(),
|
||||
stderr: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
export const UnsupportedPlatformError = NamedError.create(
|
||||
"RipgrepUnsupportedPlatformError",
|
||||
z.object({
|
||||
platform: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
export const DownloadFailedError = NamedError.create(
|
||||
"RipgrepDownloadFailedError",
|
||||
z.object({
|
||||
url: z.string(),
|
||||
status: z.number(),
|
||||
}),
|
||||
)
|
||||
|
||||
const state = lazy(async () => {
|
||||
let filepath = Bun.which("rg")
|
||||
if (filepath) return { filepath }
|
||||
filepath = path.join(
|
||||
Global.Path.bin,
|
||||
"rg" + (process.platform === "win32" ? ".exe" : ""),
|
||||
)
|
||||
|
||||
const file = Bun.file(filepath)
|
||||
if (!(await file.exists())) {
|
||||
const archMap = { x64: "x86_64", arm64: "aarch64" } as const
|
||||
const arch = archMap[process.arch as keyof typeof archMap] ?? process.arch
|
||||
|
||||
const config = PLATFORM[process.platform as keyof typeof PLATFORM]
|
||||
if (!config)
|
||||
throw new UnsupportedPlatformError({ platform: process.platform })
|
||||
|
||||
const version = "14.1.1"
|
||||
const filename = `ripgrep-${version}-${arch}-${config.platform}.${config.extension}`
|
||||
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}`
|
||||
|
||||
const response = await fetch(url)
|
||||
if (!response.ok)
|
||||
throw new DownloadFailedError({ url, status: response.status })
|
||||
|
||||
const buffer = await response.arrayBuffer()
|
||||
const archivePath = path.join(Global.Path.bin, filename)
|
||||
await Bun.write(archivePath, buffer)
|
||||
if (config.extension === "tar.gz") {
|
||||
const args = ["tar", "-xzf", archivePath, "--strip-components=1"]
|
||||
|
||||
if (process.platform === "darwin") args.push("--include=*/rg")
|
||||
if (process.platform === "linux") args.push("--wildcards", "*/rg")
|
||||
|
||||
const proc = Bun.spawn(args, {
|
||||
cwd: Global.Path.bin,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
})
|
||||
await proc.exited
|
||||
if (proc.exitCode !== 0)
|
||||
throw new ExtractionFailedError({
|
||||
filepath,
|
||||
stderr: await Bun.readableStreamToText(proc.stderr),
|
||||
})
|
||||
}
|
||||
if (config.extension === "zip") {
|
||||
const proc = Bun.spawn(
|
||||
["unzip", "-j", archivePath, "*/rg.exe", "-d", Global.Path.bin],
|
||||
{
|
||||
cwd: Global.Path.bin,
|
||||
stderr: "pipe",
|
||||
stdout: "ignore",
|
||||
},
|
||||
)
|
||||
await proc.exited
|
||||
if (proc.exitCode !== 0)
|
||||
throw new ExtractionFailedError({
|
||||
filepath: archivePath,
|
||||
stderr: await Bun.readableStreamToText(proc.stderr),
|
||||
})
|
||||
}
|
||||
await fs.unlink(archivePath)
|
||||
if (process.platform !== "win32") await fs.chmod(filepath, 0o755)
|
||||
}
|
||||
|
||||
return {
|
||||
filepath,
|
||||
}
|
||||
})
|
||||
|
||||
export async function filepath() {
|
||||
const { filepath } = await state()
|
||||
return filepath
|
||||
}
|
||||
|
||||
export async function files(input: {
|
||||
cwd: string
|
||||
query?: string
|
||||
glob?: string
|
||||
limit?: number
|
||||
}) {
|
||||
const commands = [
|
||||
`${await filepath()} --files --hidden --glob='!.git/*' ${input.glob ? `--glob='${input.glob}'` : ``}`,
|
||||
]
|
||||
if (input.query)
|
||||
commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
|
||||
if (input.limit) commands.push(`head -n ${input.limit}`)
|
||||
const joined = commands.join(" | ")
|
||||
const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text()
|
||||
return result.split("\n").filter(Boolean)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { z } from "zod"
|
||||
import { NamedError } from "../util/error"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { Log } from "../util/log"
|
||||
import { $ } from "bun"
|
||||
|
||||
export namespace Fzf {
|
||||
const log = Log.create({ service: "fzf" })
|
||||
@@ -46,10 +45,7 @@ export namespace Fzf {
|
||||
log.info("found", { filepath })
|
||||
return { filepath }
|
||||
}
|
||||
filepath = path.join(
|
||||
Global.Path.bin,
|
||||
"fzf" + (process.platform === "win32" ? ".exe" : ""),
|
||||
)
|
||||
filepath = path.join(Global.Path.bin, "fzf" + (process.platform === "win32" ? ".exe" : ""))
|
||||
|
||||
const file = Bun.file(filepath)
|
||||
if (!(await file.exists())) {
|
||||
@@ -57,18 +53,15 @@ export namespace Fzf {
|
||||
const arch = archMap[process.arch as keyof typeof archMap] ?? "amd64"
|
||||
|
||||
const config = PLATFORM[process.platform as keyof typeof PLATFORM]
|
||||
if (!config)
|
||||
throw new UnsupportedPlatformError({ platform: process.platform })
|
||||
if (!config) throw new UnsupportedPlatformError({ platform: process.platform })
|
||||
|
||||
const version = VERSION
|
||||
const platformName =
|
||||
process.platform === "win32" ? "windows" : process.platform
|
||||
const platformName = process.platform === "win32" ? "windows" : process.platform
|
||||
const filename = `fzf-${version}-${platformName}_${arch}.${config.extension}`
|
||||
const url = `https://github.com/junegunn/fzf/releases/download/v${version}/${filename}`
|
||||
|
||||
const response = await fetch(url)
|
||||
if (!response.ok)
|
||||
throw new DownloadFailedError({ url, status: response.status })
|
||||
if (!response.ok) throw new DownloadFailedError({ url, status: response.status })
|
||||
|
||||
const buffer = await response.arrayBuffer()
|
||||
const archivePath = path.join(Global.Path.bin, filename)
|
||||
@@ -87,14 +80,11 @@ export namespace Fzf {
|
||||
})
|
||||
}
|
||||
if (config.extension === "zip") {
|
||||
const proc = Bun.spawn(
|
||||
["unzip", "-j", archivePath, "fzf.exe", "-d", Global.Path.bin],
|
||||
{
|
||||
cwd: Global.Path.bin,
|
||||
stderr: "pipe",
|
||||
stdout: "ignore",
|
||||
},
|
||||
)
|
||||
const proc = Bun.spawn(["unzip", "-j", archivePath, "fzf.exe", "-d", Global.Path.bin], {
|
||||
cwd: Global.Path.bin,
|
||||
stderr: "pipe",
|
||||
stdout: "ignore",
|
||||
})
|
||||
await proc.exited
|
||||
if (proc.exitCode !== 0)
|
||||
throw new ExtractionFailedError({
|
||||
@@ -115,24 +105,4 @@ export namespace Fzf {
|
||||
const { filepath } = await state()
|
||||
return filepath
|
||||
}
|
||||
|
||||
export async function search(input: {
|
||||
cwd: string
|
||||
query: string
|
||||
limit?: number
|
||||
}) {
|
||||
const results = await $`${await filepath()} --filter=${input.query}`
|
||||
.quiet()
|
||||
.throws(false)
|
||||
.cwd(input.cwd)
|
||||
.text()
|
||||
const split = results
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((line) => line.length > 0)
|
||||
log.info("results", {
|
||||
count: split.length,
|
||||
})
|
||||
return split
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
import { z } from "zod"
|
||||
import { Bus } from "../bus"
|
||||
import { $ } from "bun"
|
||||
import { createPatch } from "diff"
|
||||
import path from "path"
|
||||
import * as git from "isomorphic-git"
|
||||
import { App } from "../app/app"
|
||||
import fs from "fs"
|
||||
import { Log } from "../util/log"
|
||||
|
||||
export namespace File {
|
||||
const log = Log.create({ service: "file" })
|
||||
|
||||
export const Event = {
|
||||
Edited: Bus.event(
|
||||
"file.edited",
|
||||
@@ -10,4 +19,92 @@ export namespace File {
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
export async function status() {
|
||||
const app = App.info()
|
||||
if (!app.git) return []
|
||||
|
||||
const diffOutput = await $`git diff --numstat HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
|
||||
|
||||
const changedFiles = []
|
||||
|
||||
if (diffOutput.trim()) {
|
||||
const lines = diffOutput.trim().split("\n")
|
||||
for (const line of lines) {
|
||||
const [added, removed, filepath] = line.split("\t")
|
||||
changedFiles.push({
|
||||
file: filepath,
|
||||
added: added === "-" ? 0 : parseInt(added, 10),
|
||||
removed: removed === "-" ? 0 : parseInt(removed, 10),
|
||||
status: "modified",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const untrackedOutput = await $`git ls-files --others --exclude-standard`.cwd(app.path.cwd).quiet().nothrow().text()
|
||||
|
||||
if (untrackedOutput.trim()) {
|
||||
const untrackedFiles = untrackedOutput.trim().split("\n")
|
||||
for (const filepath of untrackedFiles) {
|
||||
try {
|
||||
const content = await Bun.file(path.join(app.path.root, filepath)).text()
|
||||
const lines = content.split("\n").length
|
||||
changedFiles.push({
|
||||
file: filepath,
|
||||
added: lines,
|
||||
removed: 0,
|
||||
status: "added",
|
||||
})
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get deleted files
|
||||
const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
|
||||
|
||||
if (deletedOutput.trim()) {
|
||||
const deletedFiles = deletedOutput.trim().split("\n")
|
||||
for (const filepath of deletedFiles) {
|
||||
changedFiles.push({
|
||||
file: filepath,
|
||||
added: 0,
|
||||
removed: 0, // Could get original line count but would require another git command
|
||||
status: "deleted",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return changedFiles.map((x) => ({
|
||||
...x,
|
||||
file: path.relative(app.path.cwd, path.join(app.path.root, x.file)),
|
||||
}))
|
||||
}
|
||||
|
||||
export async function read(file: string) {
|
||||
using _ = log.time("read", { file })
|
||||
const app = App.info()
|
||||
const full = path.join(app.path.cwd, file)
|
||||
const content = await Bun.file(full)
|
||||
.text()
|
||||
.catch(() => "")
|
||||
.then((x) => x.trim())
|
||||
if (app.git) {
|
||||
const rel = path.relative(app.path.root, full)
|
||||
const diff = await git.status({
|
||||
fs,
|
||||
dir: app.path.root,
|
||||
filepath: rel,
|
||||
})
|
||||
if (diff !== "unmodified") {
|
||||
const original = await $`git show HEAD:${rel}`.cwd(app.path.root).quiet().nothrow().text()
|
||||
const patch = createPatch(file, original, content, "old", "new", {
|
||||
context: Infinity,
|
||||
})
|
||||
return { type: "patch", content: patch }
|
||||
}
|
||||
}
|
||||
return { type: "raw", content }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
// Ripgrep utility functions
|
||||
import path from "path"
|
||||
import { Global } from "../global"
|
||||
import fs from "fs/promises"
|
||||
import { z } from "zod"
|
||||
import { NamedError } from "../util/error"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { $ } from "bun"
|
||||
import { Fzf } from "./fzf"
|
||||
|
||||
export namespace Ripgrep {
|
||||
const Stats = z.object({
|
||||
elapsed: z.object({
|
||||
secs: z.number(),
|
||||
nanos: z.number(),
|
||||
human: z.string(),
|
||||
}),
|
||||
searches: z.number(),
|
||||
searches_with_match: z.number(),
|
||||
bytes_searched: z.number(),
|
||||
bytes_printed: z.number(),
|
||||
matched_lines: z.number(),
|
||||
matches: z.number(),
|
||||
})
|
||||
|
||||
const Begin = z.object({
|
||||
type: z.literal("begin"),
|
||||
data: z.object({
|
||||
path: z.object({
|
||||
text: z.string(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
export const Match = z.object({
|
||||
type: z.literal("match"),
|
||||
data: z.object({
|
||||
path: z.object({
|
||||
text: z.string(),
|
||||
}),
|
||||
lines: z.object({
|
||||
text: z.string(),
|
||||
}),
|
||||
line_number: z.number(),
|
||||
absolute_offset: z.number(),
|
||||
submatches: z.array(
|
||||
z.object({
|
||||
match: z.object({
|
||||
text: z.string(),
|
||||
}),
|
||||
start: z.number(),
|
||||
end: z.number(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
const End = z.object({
|
||||
type: z.literal("end"),
|
||||
data: z.object({
|
||||
path: z.object({
|
||||
text: z.string(),
|
||||
}),
|
||||
binary_offset: z.number().nullable(),
|
||||
stats: Stats,
|
||||
}),
|
||||
})
|
||||
|
||||
const Summary = z.object({
|
||||
type: z.literal("summary"),
|
||||
data: z.object({
|
||||
elapsed_total: z.object({
|
||||
human: z.string(),
|
||||
nanos: z.number(),
|
||||
secs: z.number(),
|
||||
}),
|
||||
stats: Stats,
|
||||
}),
|
||||
})
|
||||
|
||||
const Result = z.union([Begin, Match, End, Summary])
|
||||
|
||||
export type Result = z.infer<typeof Result>
|
||||
export type Match = z.infer<typeof Match>
|
||||
export type Begin = z.infer<typeof Begin>
|
||||
export type End = z.infer<typeof End>
|
||||
export type Summary = z.infer<typeof Summary>
|
||||
const PLATFORM = {
|
||||
"arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" },
|
||||
"arm64-linux": {
|
||||
platform: "aarch64-unknown-linux-gnu",
|
||||
extension: "tar.gz",
|
||||
},
|
||||
"x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" },
|
||||
"x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" },
|
||||
"x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" },
|
||||
} as const
|
||||
|
||||
export const ExtractionFailedError = NamedError.create(
|
||||
"RipgrepExtractionFailedError",
|
||||
z.object({
|
||||
filepath: z.string(),
|
||||
stderr: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
export const UnsupportedPlatformError = NamedError.create(
|
||||
"RipgrepUnsupportedPlatformError",
|
||||
z.object({
|
||||
platform: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
export const DownloadFailedError = NamedError.create(
|
||||
"RipgrepDownloadFailedError",
|
||||
z.object({
|
||||
url: z.string(),
|
||||
status: z.number(),
|
||||
}),
|
||||
)
|
||||
|
||||
const state = lazy(async () => {
|
||||
let filepath = Bun.which("rg")
|
||||
if (filepath) return { filepath }
|
||||
filepath = path.join(Global.Path.bin, "rg" + (process.platform === "win32" ? ".exe" : ""))
|
||||
|
||||
const file = Bun.file(filepath)
|
||||
if (!(await file.exists())) {
|
||||
const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM
|
||||
const config = PLATFORM[platformKey]
|
||||
if (!config) throw new UnsupportedPlatformError({ platform: platformKey })
|
||||
|
||||
const version = "14.1.1"
|
||||
const filename = `ripgrep-${version}-${config.platform}.${config.extension}`
|
||||
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}`
|
||||
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) throw new DownloadFailedError({ url, status: response.status })
|
||||
|
||||
const buffer = await response.arrayBuffer()
|
||||
const archivePath = path.join(Global.Path.bin, filename)
|
||||
await Bun.write(archivePath, buffer)
|
||||
if (config.extension === "tar.gz") {
|
||||
const args = ["tar", "-xzf", archivePath, "--strip-components=1"]
|
||||
|
||||
if (platformKey.endsWith("-darwin")) args.push("--include=*/rg")
|
||||
if (platformKey.endsWith("-linux")) args.push("--wildcards", "*/rg")
|
||||
|
||||
const proc = Bun.spawn(args, {
|
||||
cwd: Global.Path.bin,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
})
|
||||
await proc.exited
|
||||
if (proc.exitCode !== 0)
|
||||
throw new ExtractionFailedError({
|
||||
filepath,
|
||||
stderr: await Bun.readableStreamToText(proc.stderr),
|
||||
})
|
||||
}
|
||||
if (config.extension === "zip") {
|
||||
const proc = Bun.spawn(["unzip", "-j", archivePath, "*/rg.exe", "-d", Global.Path.bin], {
|
||||
cwd: Global.Path.bin,
|
||||
stderr: "pipe",
|
||||
stdout: "ignore",
|
||||
})
|
||||
await proc.exited
|
||||
if (proc.exitCode !== 0)
|
||||
throw new ExtractionFailedError({
|
||||
filepath: archivePath,
|
||||
stderr: await Bun.readableStreamToText(proc.stderr),
|
||||
})
|
||||
}
|
||||
await fs.unlink(archivePath)
|
||||
if (!platformKey.endsWith("-win32")) await fs.chmod(filepath, 0o755)
|
||||
}
|
||||
|
||||
return {
|
||||
filepath,
|
||||
}
|
||||
})
|
||||
|
||||
export async function filepath() {
|
||||
const { filepath } = await state()
|
||||
return filepath
|
||||
}
|
||||
|
||||
export async function files(input: { cwd: string; query?: string; glob?: string; limit?: number }) {
|
||||
const commands = [
|
||||
`${await filepath()} --files --hidden --glob='!.git/*' ${input.glob ? `--glob='${input.glob}'` : ``}`,
|
||||
]
|
||||
if (input.query) commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
|
||||
if (input.limit) commands.push(`head -n ${input.limit}`)
|
||||
const joined = commands.join(" | ")
|
||||
const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text()
|
||||
return result.split("\n").filter(Boolean)
|
||||
}
|
||||
|
||||
export async function tree(input: { cwd: string; limit?: number }) {
|
||||
const files = await Ripgrep.files({ cwd: input.cwd })
|
||||
interface Node {
|
||||
path: string[]
|
||||
children: Node[]
|
||||
}
|
||||
|
||||
function getPath(node: Node, parts: string[], create: boolean) {
|
||||
if (parts.length === 0) return node
|
||||
let current = node
|
||||
for (const part of parts) {
|
||||
let existing = current.children.find((x) => x.path.at(-1) === part)
|
||||
if (!existing) {
|
||||
if (!create) return
|
||||
existing = {
|
||||
path: current.path.concat(part),
|
||||
children: [],
|
||||
}
|
||||
current.children.push(existing)
|
||||
}
|
||||
current = existing
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
const root: Node = {
|
||||
path: [],
|
||||
children: [],
|
||||
}
|
||||
for (const file of files) {
|
||||
const parts = file.split(path.sep)
|
||||
getPath(root, parts, true)
|
||||
}
|
||||
|
||||
function sort(node: Node) {
|
||||
node.children.sort((a, b) => {
|
||||
if (!a.children.length && b.children.length) return 1
|
||||
if (!b.children.length && a.children.length) return -1
|
||||
return a.path.at(-1)!.localeCompare(b.path.at(-1)!)
|
||||
})
|
||||
for (const child of node.children) {
|
||||
sort(child)
|
||||
}
|
||||
}
|
||||
sort(root)
|
||||
|
||||
let current = [root]
|
||||
const result: Node = {
|
||||
path: [],
|
||||
children: [],
|
||||
}
|
||||
|
||||
let processed = 0
|
||||
const limit = input.limit ?? 50
|
||||
while (current.length > 0) {
|
||||
const next = []
|
||||
for (const node of current) {
|
||||
if (node.children.length) next.push(...node.children)
|
||||
}
|
||||
const max = Math.max(...current.map((x) => x.children.length))
|
||||
for (let i = 0; i < max && processed < limit; i++) {
|
||||
for (const node of current) {
|
||||
const child = node.children[i]
|
||||
if (!child) continue
|
||||
getPath(result, child.path, true)
|
||||
processed++
|
||||
if (processed >= limit) break
|
||||
}
|
||||
}
|
||||
if (processed >= limit) {
|
||||
for (const node of [...current, ...next]) {
|
||||
const compare = getPath(result, node.path, false)
|
||||
if (!compare) continue
|
||||
if (compare?.children.length !== node.children.length) {
|
||||
const diff = node.children.length - compare.children.length
|
||||
compare.children.push({
|
||||
path: compare.path.concat(`[${diff} truncated]`),
|
||||
children: [],
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
current = next
|
||||
}
|
||||
|
||||
const lines: string[] = []
|
||||
|
||||
function render(node: Node, depth: number) {
|
||||
const indent = "\t".repeat(depth)
|
||||
lines.push(indent + node.path.at(-1) + (node.children.length ? "/" : ""))
|
||||
for (const child of node.children) {
|
||||
render(child, depth + 1)
|
||||
}
|
||||
}
|
||||
result.children.map((x) => render(x, 0))
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
export async function search(input: { cwd: string; pattern: string; glob?: string[]; limit?: number }) {
|
||||
const args = [`${await filepath()}`, "--json", "--hidden", "--glob='!.git/*'"]
|
||||
|
||||
if (input.glob) {
|
||||
for (const g of input.glob) {
|
||||
args.push(`--glob=${g}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (input.limit) {
|
||||
args.push(`--max-count=${input.limit}`)
|
||||
}
|
||||
|
||||
args.push(input.pattern)
|
||||
|
||||
const command = args.join(" ")
|
||||
const result = await $`${{ raw: command }}`.cwd(input.cwd).quiet().nothrow()
|
||||
if (result.exitCode !== 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const lines = result.text().trim().split("\n").filter(Boolean)
|
||||
// Parse JSON lines from ripgrep output
|
||||
|
||||
return lines
|
||||
.map((line) => JSON.parse(line))
|
||||
.map((parsed) => Result.parse(parsed))
|
||||
.filter((r) => r.type === "match")
|
||||
.map((r) => r.data)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { App } from "../app/app"
|
||||
import { Log } from "../util/log"
|
||||
|
||||
export namespace FileTime {
|
||||
const log = Log.create({ service: "file.time" })
|
||||
export const state = App.state("tool.filetimes", () => {
|
||||
const read: {
|
||||
[sessionID: string]: {
|
||||
@@ -13,6 +15,7 @@ export namespace FileTime {
|
||||
})
|
||||
|
||||
export function read(sessionID: string, file: string) {
|
||||
log.info("read", { sessionID, file })
|
||||
const { read } = state()
|
||||
read[sessionID] = read[sessionID] || {}
|
||||
read[sessionID][file] = new Date()
|
||||
@@ -24,10 +27,7 @@ export namespace FileTime {
|
||||
|
||||
export async function assert(sessionID: string, filepath: string) {
|
||||
const time = get(sessionID, filepath)
|
||||
if (!time)
|
||||
throw new Error(
|
||||
`You must read the file ${filepath} before overwriting it. Use the Read tool first`,
|
||||
)
|
||||
if (!time) throw new Error(`You must read the file ${filepath} before overwriting it. Use the Read tool first`)
|
||||
const stats = await Bun.file(filepath).stat()
|
||||
if (stats.mtime.getTime() > time.getTime()) {
|
||||
throw new Error(
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { z } from "zod"
|
||||
import { Bus } from "../bus"
|
||||
import fs from "fs"
|
||||
import { App } from "../app/app"
|
||||
import { Log } from "../util/log"
|
||||
import { Flag } from "../flag/flag"
|
||||
|
||||
export namespace FileWatcher {
|
||||
const log = Log.create({ service: "file.watcher" })
|
||||
|
||||
export const Event = {
|
||||
Updated: Bus.event(
|
||||
"file.watcher.updated",
|
||||
z.object({
|
||||
file: z.string(),
|
||||
event: z.union([z.literal("rename"), z.literal("change")]),
|
||||
}),
|
||||
),
|
||||
}
|
||||
const state = App.state(
|
||||
"file.watcher",
|
||||
() => {
|
||||
const app = App.use()
|
||||
if (!app.info.git) return {}
|
||||
try {
|
||||
const watcher = fs.watch(app.info.path.cwd, { recursive: true }, (event, file) => {
|
||||
log.info("change", { file, event })
|
||||
if (!file) return
|
||||
// for some reason async local storage is lost here
|
||||
// https://github.com/oven-sh/bun/issues/20754
|
||||
App.provideExisting(app, async () => {
|
||||
Bus.publish(Event.Updated, {
|
||||
file,
|
||||
event,
|
||||
})
|
||||
})
|
||||
})
|
||||
return { watcher }
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
async (state) => {
|
||||
state.watcher?.close()
|
||||
},
|
||||
)
|
||||
|
||||
export function init() {
|
||||
if (Flag.OPENCODE_DISABLE_WATCHER) return
|
||||
state()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export namespace Flag {
|
||||
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
|
||||
export const OPENCODE_DISABLE_WATCHER = truthy("OPENCODE_DISABLE_WATCHER")
|
||||
|
||||
function truthy(key: string) {
|
||||
const value = process.env[key]?.toLowerCase()
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { App } from "../app/app"
|
||||
import { BunProc } from "../bun"
|
||||
|
||||
export interface Info {
|
||||
name: string
|
||||
command: string[]
|
||||
environment?: Record<string, string>
|
||||
extensions: string[]
|
||||
enabled(): Promise<boolean>
|
||||
}
|
||||
|
||||
export const gofmt: Info = {
|
||||
name: "gofmt",
|
||||
command: ["gofmt", "-w", "$FILE"],
|
||||
extensions: [".go"],
|
||||
async enabled() {
|
||||
return Bun.which("gofmt") !== null
|
||||
},
|
||||
}
|
||||
|
||||
export const mix: Info = {
|
||||
name: "mix",
|
||||
command: ["mix", "format", "$FILE"],
|
||||
extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
|
||||
async enabled() {
|
||||
return Bun.which("mix") !== null
|
||||
},
|
||||
}
|
||||
|
||||
export const prettier: Info = {
|
||||
name: "prettier",
|
||||
command: [BunProc.which(), "run", "prettier", "--write", "$FILE"],
|
||||
environment: {
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
extensions: [
|
||||
".js",
|
||||
".jsx",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".ts",
|
||||
".tsx",
|
||||
".mts",
|
||||
".cts",
|
||||
".html",
|
||||
".htm",
|
||||
".css",
|
||||
".scss",
|
||||
".sass",
|
||||
".less",
|
||||
".vue",
|
||||
".svelte",
|
||||
".json",
|
||||
".jsonc",
|
||||
".yaml",
|
||||
".yml",
|
||||
".toml",
|
||||
".xml",
|
||||
".md",
|
||||
".mdx",
|
||||
".graphql",
|
||||
".gql",
|
||||
],
|
||||
async enabled() {
|
||||
// this is more complicated because we only want to use prettier if it's
|
||||
// being used with the current project
|
||||
try {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [BunProc.which(), "run", "prettier", "--version"],
|
||||
cwd: App.info().path.cwd,
|
||||
env: {
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
return exit === 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const zig: Info = {
|
||||
name: "zig",
|
||||
command: ["zig", "fmt", "$FILE"],
|
||||
extensions: [".zig", ".zon"],
|
||||
async enabled() {
|
||||
return Bun.which("zig") !== null
|
||||
},
|
||||
}
|
||||
|
||||
export const clang: Info = {
|
||||
name: "clang-format",
|
||||
command: ["clang-format", "-i", "$FILE"],
|
||||
extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
|
||||
async enabled() {
|
||||
return Bun.which("clang-format") !== null
|
||||
},
|
||||
}
|
||||
|
||||
export const ktlint: Info = {
|
||||
name: "ktlint",
|
||||
command: ["ktlint", "-F", "$FILE"],
|
||||
extensions: [".kt", ".kts"],
|
||||
async enabled() {
|
||||
return Bun.which("ktlint") !== null
|
||||
},
|
||||
}
|
||||
|
||||
export const ruff: Info = {
|
||||
name: "ruff",
|
||||
command: ["ruff", "format", "$FILE"],
|
||||
extensions: [".py", ".pyi"],
|
||||
async enabled() {
|
||||
return Bun.which("ruff") !== null
|
||||
},
|
||||
}
|
||||
|
||||
export const rubocop: Info = {
|
||||
name: "rubocop",
|
||||
command: ["rubocop", "--autocorrect", "$FILE"],
|
||||
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
||||
async enabled() {
|
||||
return Bun.which("rubocop") !== null
|
||||
},
|
||||
}
|
||||
|
||||
export const standardrb: Info = {
|
||||
name: "standardrb",
|
||||
command: ["standardrb", "--fix", "$FILE"],
|
||||
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
||||
async enabled() {
|
||||
return Bun.which("standardrb") !== null
|
||||
},
|
||||
}
|
||||
|
||||
export const htmlbeautifier: Info = {
|
||||
name: "htmlbeautifier",
|
||||
command: ["htmlbeautifier", "$FILE"],
|
||||
extensions: [".erb", ".html.erb"],
|
||||
async enabled() {
|
||||
return Bun.which("htmlbeautifier") !== null
|
||||
},
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { App } from "../app/app"
|
||||
import { BunProc } from "../bun"
|
||||
import { Bus } from "../bus"
|
||||
import { File } from "../file"
|
||||
import { Log } from "../util/log"
|
||||
import path from "path"
|
||||
|
||||
import * as Formatter from "./formatter"
|
||||
|
||||
export namespace Format {
|
||||
const log = Log.create({ service: "format" })
|
||||
|
||||
@@ -16,7 +17,7 @@ export namespace Format {
|
||||
}
|
||||
})
|
||||
|
||||
async function isEnabled(item: Definition) {
|
||||
async function isEnabled(item: Formatter.Info) {
|
||||
const s = state()
|
||||
let status = s.enabled[item.name]
|
||||
if (status === undefined) {
|
||||
@@ -28,9 +29,9 @@ export namespace Format {
|
||||
|
||||
async function getFormatter(ext: string) {
|
||||
const result = []
|
||||
for (const item of FORMATTERS) {
|
||||
for (const item of Object.values(Formatter)) {
|
||||
if (!item.extensions.includes(ext)) continue
|
||||
if (!isEnabled(item)) continue
|
||||
if (!(await isEnabled(item))) continue
|
||||
result.push(item)
|
||||
}
|
||||
return result
|
||||
@@ -61,105 +62,4 @@ export namespace Format {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface Definition {
|
||||
name: string
|
||||
command: string[]
|
||||
environment?: Record<string, string>
|
||||
extensions: string[]
|
||||
enabled(): Promise<boolean>
|
||||
}
|
||||
|
||||
const FORMATTERS: Definition[] = [
|
||||
{
|
||||
name: "prettier",
|
||||
command: [BunProc.which(), "run", "prettier", "--write", "$FILE"],
|
||||
environment: {
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
extensions: [
|
||||
".js",
|
||||
".jsx",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".ts",
|
||||
".tsx",
|
||||
".mts",
|
||||
".cts",
|
||||
".html",
|
||||
".htm",
|
||||
".css",
|
||||
".scss",
|
||||
".sass",
|
||||
".less",
|
||||
".vue",
|
||||
".svelte",
|
||||
".json",
|
||||
".jsonc",
|
||||
".yaml",
|
||||
".yml",
|
||||
".toml",
|
||||
".xml",
|
||||
".md",
|
||||
".mdx",
|
||||
".graphql",
|
||||
".gql",
|
||||
],
|
||||
async enabled() {
|
||||
try {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [BunProc.which(), "run", "prettier", "--version"],
|
||||
cwd: App.info().path.cwd,
|
||||
env: {
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
return exit === 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mix",
|
||||
command: ["mix", "format", "$FILE"],
|
||||
extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
|
||||
async enabled() {
|
||||
try {
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["mix", "--version"],
|
||||
cwd: App.info().path.cwd,
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
return exit === 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gofmt",
|
||||
command: ["gofmt", "-w", "$FILE"],
|
||||
extensions: [".go"],
|
||||
async enabled() {
|
||||
try {
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["gofmt", "-h"],
|
||||
cwd: App.info().path.cwd,
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
return exit === 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -26,11 +26,7 @@ export namespace Identifier {
|
||||
return generateID(prefix, true, given)
|
||||
}
|
||||
|
||||
function generateID(
|
||||
prefix: keyof typeof prefixes,
|
||||
descending: boolean,
|
||||
given?: string,
|
||||
): string {
|
||||
function generateID(prefix: keyof typeof prefixes, descending: boolean, given?: string): string {
|
||||
if (!given) {
|
||||
return generateNewID(prefix, descending)
|
||||
}
|
||||
@@ -42,8 +38,7 @@ export namespace Identifier {
|
||||
}
|
||||
|
||||
function randomBase62(length: number): string {
|
||||
const chars =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
let result = ""
|
||||
const bytes = randomBytes(length)
|
||||
for (let i = 0; i < length; i++) {
|
||||
@@ -52,10 +47,7 @@ export namespace Identifier {
|
||||
return result
|
||||
}
|
||||
|
||||
function generateNewID(
|
||||
prefix: keyof typeof prefixes,
|
||||
descending: boolean,
|
||||
): string {
|
||||
function generateNewID(prefix: keyof typeof prefixes, descending: boolean): string {
|
||||
const currentTimestamp = Date.now()
|
||||
|
||||
if (currentTimestamp !== lastTimestamp) {
|
||||
@@ -73,11 +65,6 @@ export namespace Identifier {
|
||||
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
|
||||
}
|
||||
|
||||
return (
|
||||
prefixes[prefix] +
|
||||
"_" +
|
||||
timeBytes.toString("hex") +
|
||||
randomBase62(LENGTH - 12)
|
||||
)
|
||||
return prefixes[prefix] + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import yargs from "yargs"
|
||||
import { hideBin } from "yargs/helpers"
|
||||
import { RunCommand } from "./cli/cmd/run"
|
||||
import { GenerateCommand } from "./cli/cmd/generate"
|
||||
import { ScrapCommand } from "./cli/cmd/scrap"
|
||||
import { Log } from "./util/log"
|
||||
import { AuthCommand } from "./cli/cmd/auth"
|
||||
import { UpgradeCommand } from "./cli/cmd/upgrade"
|
||||
@@ -14,6 +13,7 @@ import { NamedError } from "./util/error"
|
||||
import { FormatError } from "./cli/error"
|
||||
import { ServeCommand } from "./cli/cmd/serve"
|
||||
import { TuiCommand } from "./cli/cmd/tui"
|
||||
import { DebugCommand } from "./cli/cmd/debug"
|
||||
|
||||
const cancel = new AbortController()
|
||||
|
||||
@@ -49,16 +49,13 @@ const cli = yargs(hideBin(process.argv))
|
||||
.command(TuiCommand)
|
||||
.command(RunCommand)
|
||||
.command(GenerateCommand)
|
||||
.command(ScrapCommand)
|
||||
.command(DebugCommand)
|
||||
.command(AuthCommand)
|
||||
.command(UpgradeCommand)
|
||||
.command(ServeCommand)
|
||||
.command(ModelsCommand)
|
||||
.fail((msg) => {
|
||||
if (
|
||||
msg.startsWith("Unknown argument") ||
|
||||
msg.startsWith("Not enough non-option arguments")
|
||||
) {
|
||||
if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {
|
||||
cli.showHelp("log")
|
||||
}
|
||||
})
|
||||
@@ -97,10 +94,7 @@ try {
|
||||
Log.Default.error("fatal", data)
|
||||
const formatted = FormatError(e)
|
||||
if (formatted) UI.error(formatted)
|
||||
if (formatted === undefined)
|
||||
UI.error(
|
||||
"Unexpected error, check log file at " + Log.file() + " for more details",
|
||||
)
|
||||
if (formatted === undefined) UI.error("Unexpected error, check log file at " + Log.file() + " for more details")
|
||||
process.exitCode = 1
|
||||
}
|
||||
|
||||
|
||||
@@ -135,8 +135,7 @@ export namespace Installation {
|
||||
})
|
||||
}
|
||||
|
||||
export const VERSION =
|
||||
typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
|
||||
export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
|
||||
|
||||
export async function latest() {
|
||||
return fetch("https://api.github.com/repos/sst/opencode/releases/latest")
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import path from "path"
|
||||
import {
|
||||
createMessageConnection,
|
||||
StreamMessageReader,
|
||||
StreamMessageWriter,
|
||||
} from "vscode-jsonrpc/node"
|
||||
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
|
||||
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
|
||||
import { App } from "../app/app"
|
||||
import { Log } from "../util/log"
|
||||
@@ -12,6 +8,7 @@ import { Bus } from "../bus"
|
||||
import z from "zod"
|
||||
import type { LSPServer } from "./server"
|
||||
import { NamedError } from "../util/error"
|
||||
import { withTimeout } from "../util/timeout"
|
||||
|
||||
export namespace LSPClient {
|
||||
const log = Log.create({ service: "lsp.client" })
|
||||
@@ -52,7 +49,9 @@ export namespace LSPClient {
|
||||
log.info("textDocument/publishDiagnostics", {
|
||||
path,
|
||||
})
|
||||
const exists = diagnostics.has(path)
|
||||
diagnostics.set(path, params.diagnostics)
|
||||
if (!exists && serverID === "typescript") return
|
||||
Bus.publish(Event.Diagnostics, { path, serverID })
|
||||
})
|
||||
connection.onRequest("workspace/configuration", async () => {
|
||||
@@ -61,8 +60,9 @@ export namespace LSPClient {
|
||||
connection.listen()
|
||||
|
||||
log.info("sending initialize", { id: serverID })
|
||||
await Promise.race([
|
||||
await withTimeout(
|
||||
connection.sendRequest("initialize", {
|
||||
rootUri: "file://" + app.path.cwd,
|
||||
processId: server.process.pid,
|
||||
workspaceFolders: [
|
||||
{
|
||||
@@ -88,14 +88,21 @@ export namespace LSPClient {
|
||||
},
|
||||
},
|
||||
}),
|
||||
new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new InitializeError({ serverID }))
|
||||
}, 5_000)
|
||||
}),
|
||||
])
|
||||
5_000,
|
||||
).catch((err) => {
|
||||
log.error("initialize error", { error: err })
|
||||
throw new InitializeError(
|
||||
{ serverID },
|
||||
{
|
||||
cause: err,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
await connection.sendNotification("initialized", {})
|
||||
log.info("initialized")
|
||||
log.info("initialized", {
|
||||
serverID,
|
||||
})
|
||||
|
||||
const files: {
|
||||
[path: string]: number
|
||||
@@ -110,82 +117,63 @@ export namespace LSPClient {
|
||||
},
|
||||
notify: {
|
||||
async open(input: { path: string }) {
|
||||
input.path = path.isAbsolute(input.path)
|
||||
? input.path
|
||||
: path.resolve(app.path.cwd, input.path)
|
||||
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path)
|
||||
const file = Bun.file(input.path)
|
||||
const text = await file.text()
|
||||
const version = files[input.path]
|
||||
if (version === undefined) {
|
||||
log.info("textDocument/didOpen", input)
|
||||
if (version !== undefined) {
|
||||
diagnostics.delete(input.path)
|
||||
const extension = path.extname(input.path)
|
||||
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
|
||||
await connection.sendNotification("textDocument/didOpen", {
|
||||
await connection.sendNotification("textDocument/didClose", {
|
||||
textDocument: {
|
||||
uri: `file://` + input.path,
|
||||
languageId,
|
||||
version: 0,
|
||||
text,
|
||||
},
|
||||
})
|
||||
files[input.path] = 0
|
||||
return
|
||||
}
|
||||
|
||||
log.info("textDocument/didChange", input)
|
||||
log.info("textDocument/didOpen", input)
|
||||
diagnostics.delete(input.path)
|
||||
await connection.sendNotification("textDocument/didChange", {
|
||||
const extension = path.extname(input.path)
|
||||
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
|
||||
await connection.sendNotification("textDocument/didOpen", {
|
||||
textDocument: {
|
||||
uri: `file://` + input.path,
|
||||
version: ++files[input.path],
|
||||
languageId,
|
||||
version: 0,
|
||||
text,
|
||||
},
|
||||
contentChanges: [
|
||||
{
|
||||
text,
|
||||
},
|
||||
],
|
||||
})
|
||||
files[input.path] = 0
|
||||
return
|
||||
},
|
||||
},
|
||||
get diagnostics() {
|
||||
return diagnostics
|
||||
},
|
||||
async waitForDiagnostics(input: { path: string }) {
|
||||
input.path = path.isAbsolute(input.path)
|
||||
? input.path
|
||||
: path.resolve(app.path.cwd, input.path)
|
||||
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path)
|
||||
log.info("waiting for diagnostics", input)
|
||||
let unsub: () => void
|
||||
let timeout: NodeJS.Timeout
|
||||
return await Promise.race([
|
||||
new Promise<void>(async (resolve) => {
|
||||
return await withTimeout(
|
||||
new Promise<void>((resolve) => {
|
||||
unsub = Bus.subscribe(Event.Diagnostics, (event) => {
|
||||
if (
|
||||
event.properties.path === input.path &&
|
||||
event.properties.serverID === result.serverID
|
||||
) {
|
||||
if (event.properties.path === input.path && event.properties.serverID === result.serverID) {
|
||||
log.info("got diagnostics", input)
|
||||
clearTimeout(timeout)
|
||||
unsub?.()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}),
|
||||
new Promise<void>((resolve) => {
|
||||
timeout = setTimeout(() => {
|
||||
log.info("timed out refreshing diagnostics", input)
|
||||
unsub?.()
|
||||
resolve()
|
||||
}, 5000)
|
||||
}),
|
||||
])
|
||||
3000,
|
||||
)
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
unsub?.()
|
||||
})
|
||||
},
|
||||
async shutdown() {
|
||||
log.info("shutting down")
|
||||
log.info("shutting down", { serverID })
|
||||
connection.end()
|
||||
connection.dispose()
|
||||
server.process.kill("SIGKILL")
|
||||
log.info("shutdown", { serverID })
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,19 +3,58 @@ import { Log } from "../util/log"
|
||||
import { LSPClient } from "./client"
|
||||
import path from "path"
|
||||
import { LSPServer } from "./server"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { z } from "zod"
|
||||
|
||||
export namespace LSP {
|
||||
const log = Log.create({ service: "lsp" })
|
||||
|
||||
export const Symbol = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
kind: z.number(),
|
||||
location: z.object({
|
||||
uri: z.string(),
|
||||
range: z.object({
|
||||
start: z.object({
|
||||
line: z.number(),
|
||||
character: z.number(),
|
||||
}),
|
||||
end: z.object({
|
||||
line: z.number(),
|
||||
character: z.number(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
ref: "LSP.Symbol",
|
||||
})
|
||||
export type Symbol = z.infer<typeof Symbol>
|
||||
|
||||
const state = App.state(
|
||||
"lsp",
|
||||
async () => {
|
||||
async (app) => {
|
||||
log.info("initializing")
|
||||
const clients = new Map<string, LSPClient.Info>()
|
||||
const skip = new Set<string>()
|
||||
for (const server of Object.values(LSPServer)) {
|
||||
for (const extension of server.extensions) {
|
||||
const [file] = await Ripgrep.files({
|
||||
cwd: app.path.cwd,
|
||||
glob: "*" + extension,
|
||||
})
|
||||
if (!file) continue
|
||||
const handle = await server.spawn(App.info())
|
||||
if (!handle) break
|
||||
const client = await LSPClient.create(server.id, handle).catch((err) => log.error("", { error: err }))
|
||||
if (!client) break
|
||||
clients.set(server.id, client)
|
||||
break
|
||||
}
|
||||
}
|
||||
log.info("initialized")
|
||||
return {
|
||||
clients,
|
||||
skip,
|
||||
}
|
||||
},
|
||||
async (state) => {
|
||||
@@ -25,35 +64,21 @@ export namespace LSP {
|
||||
},
|
||||
)
|
||||
|
||||
export async function init() {
|
||||
return state()
|
||||
}
|
||||
|
||||
export async function touchFile(input: string, waitForDiagnostics?: boolean) {
|
||||
const extension = path.parse(input).ext
|
||||
const s = await state()
|
||||
const matches = LSPServer.All.filter((x) =>
|
||||
x.extensions.includes(extension),
|
||||
)
|
||||
for (const match of matches) {
|
||||
if (s.skip.has(match.id)) continue
|
||||
const existing = s.clients.get(match.id)
|
||||
if (existing) continue
|
||||
const handle = await match.spawn(App.info())
|
||||
if (!handle) {
|
||||
s.skip.add(match.id)
|
||||
continue
|
||||
}
|
||||
const client = await LSPClient.create(match.id, handle).catch(() => {})
|
||||
if (!client) {
|
||||
s.skip.add(match.id)
|
||||
continue
|
||||
}
|
||||
s.clients.set(match.id, client)
|
||||
}
|
||||
if (waitForDiagnostics) {
|
||||
await run(async (client) => {
|
||||
const wait = client.waitForDiagnostics({ path: input })
|
||||
await client.notify.open({ path: input })
|
||||
return wait
|
||||
})
|
||||
}
|
||||
const matches = Object.values(LSPServer)
|
||||
.filter((x) => x.extensions.includes(extension))
|
||||
.map((x) => x.id)
|
||||
await run(async (client) => {
|
||||
if (!matches.includes(client.serverID)) return
|
||||
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
|
||||
await client.notify.open({ path: input })
|
||||
return wait
|
||||
})
|
||||
}
|
||||
|
||||
export async function diagnostics() {
|
||||
@@ -68,11 +93,7 @@ export namespace LSP {
|
||||
return results
|
||||
}
|
||||
|
||||
export async function hover(input: {
|
||||
file: string
|
||||
line: number
|
||||
character: number
|
||||
}) {
|
||||
export async function hover(input: { file: string; line: number; character: number }) {
|
||||
return run((client) => {
|
||||
return client.connection.sendRequest("textDocument/hover", {
|
||||
textDocument: {
|
||||
@@ -86,9 +107,15 @@ export namespace LSP {
|
||||
})
|
||||
}
|
||||
|
||||
async function run<T>(
|
||||
input: (client: LSPClient.Info) => Promise<T>,
|
||||
): Promise<T[]> {
|
||||
export async function workspaceSymbol(query: string) {
|
||||
return run((client) =>
|
||||
client.connection.sendRequest("workspace/symbol", {
|
||||
query,
|
||||
}),
|
||||
).then((result) => result.flat() as LSP.Symbol[])
|
||||
}
|
||||
|
||||
async function run<T>(input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
|
||||
const clients = await state().then((x) => [...x.clients.values()])
|
||||
const tasks = clients.map((x) => input(x))
|
||||
return Promise.all(tasks)
|
||||
|
||||
@@ -63,6 +63,14 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
|
||||
".cshtml": "razor",
|
||||
".razor": "razor",
|
||||
".rb": "ruby",
|
||||
".rake": "ruby",
|
||||
".gemspec": "ruby",
|
||||
".ru": "ruby",
|
||||
".erb": "erb",
|
||||
".html.erb": "erb",
|
||||
".js.erb": "erb",
|
||||
".css.erb": "erb",
|
||||
".json.erb": "erb",
|
||||
".rs": "rust",
|
||||
".scss": "scss",
|
||||
".sass": "sass",
|
||||
|
||||
@@ -4,6 +4,8 @@ import path from "path"
|
||||
import { Global } from "../global"
|
||||
import { Log } from "../util/log"
|
||||
import { BunProc } from "../bun"
|
||||
import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
|
||||
export namespace LSPServer {
|
||||
const log = Log.create({ service: "lsp.server" })
|
||||
@@ -19,78 +21,164 @@ export namespace LSPServer {
|
||||
spawn(app: App.Info): Promise<Handle | undefined>
|
||||
}
|
||||
|
||||
export const All: Info[] = [
|
||||
{
|
||||
id: "typescript",
|
||||
extensions: [
|
||||
".ts",
|
||||
".tsx",
|
||||
".js",
|
||||
".jsx",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".mts",
|
||||
".cts",
|
||||
],
|
||||
async spawn(app) {
|
||||
const tsserver = await Bun.resolve(
|
||||
"typescript/lib/tsserver.js",
|
||||
app.path.cwd,
|
||||
).catch(() => {})
|
||||
if (!tsserver) return
|
||||
const proc = spawn(
|
||||
BunProc.which(),
|
||||
["x", "typescript-language-server", "--stdio"],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
export const Typescript: Info = {
|
||||
id: "typescript",
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
||||
async spawn(app) {
|
||||
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {})
|
||||
if (!tsserver) return
|
||||
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
|
||||
env: {
|
||||
...process.env,
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
})
|
||||
return {
|
||||
process: proc,
|
||||
initialization: {
|
||||
tsserver: {
|
||||
path: tsserver,
|
||||
},
|
||||
)
|
||||
return {
|
||||
process: proc,
|
||||
initialization: {
|
||||
tsserver: {
|
||||
path: tsserver,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "golang",
|
||||
extensions: [".go"],
|
||||
async spawn() {
|
||||
let bin = Bun.which("gopls", {
|
||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||
}
|
||||
|
||||
export const Gopls: Info = {
|
||||
id: "golang",
|
||||
extensions: [".go"],
|
||||
async spawn() {
|
||||
let bin = Bun.which("gopls", {
|
||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||
})
|
||||
if (!bin) {
|
||||
if (!Bun.which("go")) return
|
||||
log.info("installing gopls")
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
|
||||
env: { ...process.env, GOBIN: Global.Path.bin },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
stdin: "pipe",
|
||||
})
|
||||
if (!bin) {
|
||||
log.info("installing gopls")
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
|
||||
env: { ...process.env, GOBIN: Global.Path.bin },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
stdin: "pipe",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0) {
|
||||
log.error("Failed to install gopls")
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0) {
|
||||
log.error("Failed to install gopls")
|
||||
return
|
||||
}
|
||||
bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : ""))
|
||||
log.info(`installed gopls`, {
|
||||
bin,
|
||||
})
|
||||
}
|
||||
return {
|
||||
process: spawn(bin!),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const RubyLsp: Info = {
|
||||
id: "ruby-lsp",
|
||||
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
||||
async spawn() {
|
||||
let bin = Bun.which("ruby-lsp", {
|
||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||
})
|
||||
if (!bin) {
|
||||
const ruby = Bun.which("ruby")
|
||||
const gem = Bun.which("gem")
|
||||
if (!ruby || !gem) {
|
||||
log.info("Ruby not found, please install Ruby first")
|
||||
return
|
||||
}
|
||||
log.info("installing ruby-lsp")
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["gem", "install", "ruby-lsp", "--bindir", Global.Path.bin],
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
stdin: "pipe",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0) {
|
||||
log.error("Failed to install ruby-lsp")
|
||||
return
|
||||
}
|
||||
bin = path.join(Global.Path.bin, "ruby-lsp" + (process.platform === "win32" ? ".exe" : ""))
|
||||
log.info(`installed ruby-lsp`, {
|
||||
bin,
|
||||
})
|
||||
}
|
||||
return {
|
||||
process: spawn(bin!, ["--stdio"]),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const Pyright: Info = {
|
||||
id: "pyright",
|
||||
extensions: [".py", ".pyi"],
|
||||
async spawn() {
|
||||
const proc = spawn(BunProc.which(), ["x", "pyright-langserver", "--stdio"], {
|
||||
env: {
|
||||
...process.env,
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
})
|
||||
return {
|
||||
process: proc,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const ElixirLS: Info = {
|
||||
id: "elixir-ls",
|
||||
extensions: [".ex", ".exs"],
|
||||
async spawn() {
|
||||
let binary = Bun.which("elixir-ls")
|
||||
if (!binary) {
|
||||
const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
|
||||
binary = path.join(
|
||||
Global.Path.bin,
|
||||
"elixir-ls-master",
|
||||
"release",
|
||||
process.platform === "win32" ? "language_server.bar" : "language_server.sh",
|
||||
)
|
||||
|
||||
if (!(await Bun.file(binary).exists())) {
|
||||
const elixir = Bun.which("elixir")
|
||||
if (!elixir) {
|
||||
log.error("elixir is required to run elixir-ls")
|
||||
return
|
||||
}
|
||||
bin = path.join(
|
||||
Global.Path.bin,
|
||||
"gopls" + (process.platform === "win32" ? ".exe" : ""),
|
||||
)
|
||||
log.info(`installed gopls`, {
|
||||
bin,
|
||||
|
||||
log.info("downloading elixir-ls from GitHub releases")
|
||||
|
||||
const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
|
||||
if (!response.ok) return
|
||||
const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
|
||||
await Bun.file(zipPath).write(response)
|
||||
|
||||
await $`unzip -o -q ${zipPath}`.cwd(Global.Path.bin).nothrow()
|
||||
|
||||
await fs.rm(zipPath, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
})
|
||||
|
||||
await $`mix deps.get && mix compile && mix elixir_ls.release2 -o release`
|
||||
.quiet()
|
||||
.cwd(path.join(Global.Path.bin, "elixir-ls-master"))
|
||||
.env({ MIX_ENV: "prod", ...process.env })
|
||||
|
||||
log.info(`installed elixir-ls`, {
|
||||
path: elixirLsPath,
|
||||
})
|
||||
}
|
||||
return {
|
||||
process: spawn(bin!),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
process: spawn(binary),
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ export namespace ModelsDev {
|
||||
|
||||
export const Model = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
release_date: z.string(),
|
||||
attachment: z.boolean(),
|
||||
reasoning: z.boolean(),
|
||||
temperature: z.boolean(),
|
||||
@@ -25,7 +27,6 @@ export namespace ModelsDev {
|
||||
context: z.number(),
|
||||
output: z.number(),
|
||||
}),
|
||||
id: z.string(),
|
||||
options: z.record(z.any()),
|
||||
})
|
||||
.openapi({
|
||||
|
||||
@@ -11,8 +11,6 @@ import { WebFetchTool } from "../tool/webfetch"
|
||||
import { GlobTool } from "../tool/glob"
|
||||
import { GrepTool } from "../tool/grep"
|
||||
import { ListTool } from "../tool/ls"
|
||||
import { LspDiagnosticTool } from "../tool/lsp-diagnostics"
|
||||
import { LspHoverTool } from "../tool/lsp-hover"
|
||||
import { PatchTool } from "../tool/patch"
|
||||
import { ReadTool } from "../tool/read"
|
||||
import type { Tool } from "../tool/tool"
|
||||
@@ -23,6 +21,7 @@ import { AuthCopilot } from "../auth/copilot"
|
||||
import { ModelsDev } from "./models"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Auth } from "../auth"
|
||||
// import { TaskTool } from "../tool/task"
|
||||
|
||||
export namespace Provider {
|
||||
const log = Log.create({ service: "provider" })
|
||||
@@ -92,19 +91,26 @@ export namespace Provider {
|
||||
if (!info || info.type !== "oauth") return
|
||||
if (!info.access || info.expires < Date.now()) {
|
||||
const tokens = await copilot.access(info.refresh)
|
||||
if (!tokens)
|
||||
throw new Error("GitHub Copilot authentication expired")
|
||||
if (!tokens) throw new Error("GitHub Copilot authentication expired")
|
||||
await Auth.set("github-copilot", {
|
||||
type: "oauth",
|
||||
...tokens,
|
||||
})
|
||||
info.access = tokens.access
|
||||
}
|
||||
let isAgentCall = false
|
||||
try {
|
||||
const body = typeof init.body === "string" ? JSON.parse(init.body) : init.body
|
||||
if (body?.messages) {
|
||||
isAgentCall = body.messages.some((msg: any) => msg.role && ["tool", "assistant"].includes(msg.role))
|
||||
}
|
||||
} catch {}
|
||||
const headers = {
|
||||
...init.headers,
|
||||
...copilot.HEADERS,
|
||||
Authorization: `Bearer ${info.access}`,
|
||||
"Openai-Intent": "conversation-edits",
|
||||
"X-Initiator": isAgentCall ? "agent" : "user",
|
||||
}
|
||||
delete headers["x-api-key"]
|
||||
return fetch(input, {
|
||||
@@ -125,14 +131,11 @@ export namespace Provider {
|
||||
}
|
||||
},
|
||||
"amazon-bedrock": async () => {
|
||||
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"])
|
||||
return { autoload: false }
|
||||
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"]) return { autoload: false }
|
||||
|
||||
const region = process.env["AWS_REGION"] ?? "us-east-1"
|
||||
|
||||
const { fromNodeProviderChain } = await import(
|
||||
await BunProc.install("@aws-sdk/credential-providers")
|
||||
)
|
||||
const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
|
||||
return {
|
||||
autoload: true,
|
||||
options: {
|
||||
@@ -140,14 +143,60 @@ export namespace Provider {
|
||||
credentialProvider: fromNodeProviderChain(),
|
||||
},
|
||||
async getModel(sdk: any, modelID: string) {
|
||||
if (modelID.includes("claude")) {
|
||||
const prefix = region.split("-")[0]
|
||||
modelID = `${prefix}.${modelID}`
|
||||
let regionPrefix = region.split("-")[0]
|
||||
|
||||
switch (regionPrefix) {
|
||||
case "us": {
|
||||
const modelRequiresPrefix = ["claude", "deepseek"].some((m) => modelID.includes(m))
|
||||
if (modelRequiresPrefix) {
|
||||
modelID = `${regionPrefix}.${modelID}`
|
||||
}
|
||||
break
|
||||
}
|
||||
case "eu": {
|
||||
const regionRequiresPrefix = [
|
||||
"eu-west-1",
|
||||
"eu-west-3",
|
||||
"eu-north-1",
|
||||
"eu-central-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
].some((r) => region.includes(r))
|
||||
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) =>
|
||||
modelID.includes(m),
|
||||
)
|
||||
if (regionRequiresPrefix && modelRequiresPrefix) {
|
||||
modelID = `${regionPrefix}.${modelID}`
|
||||
}
|
||||
break
|
||||
}
|
||||
case "ap": {
|
||||
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
|
||||
modelID.includes(m),
|
||||
)
|
||||
if (modelRequiresPrefix) {
|
||||
regionPrefix = "apac"
|
||||
modelID = `${regionPrefix}.${modelID}`
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return sdk.languageModel(modelID)
|
||||
},
|
||||
}
|
||||
},
|
||||
openrouter: async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
options: {
|
||||
headers: {
|
||||
"HTTP-Referer": "https://opencode.ai/",
|
||||
"X-Title": "opencode",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const state = App.state("provider", async () => {
|
||||
@@ -162,10 +211,7 @@ export namespace Provider {
|
||||
options: Record<string, any>
|
||||
}
|
||||
} = {}
|
||||
const models = new Map<
|
||||
string,
|
||||
{ info: ModelsDev.Model; language: LanguageModel }
|
||||
>()
|
||||
const models = new Map<string, { info: ModelsDev.Model; language: LanguageModel }>()
|
||||
const sdk = new Map<string, SDK>()
|
||||
|
||||
log.info("init")
|
||||
@@ -203,6 +249,7 @@ export namespace Provider {
|
||||
npm: provider.npm ?? existing?.npm,
|
||||
name: provider.name ?? existing?.name ?? providerID,
|
||||
env: provider.env ?? existing?.env ?? [],
|
||||
api: provider.api ?? existing?.api,
|
||||
models: existing?.models ?? {},
|
||||
}
|
||||
|
||||
@@ -211,6 +258,7 @@ export namespace Provider {
|
||||
const parsedModel: ModelsDev.Model = {
|
||||
id: modelID,
|
||||
name: model.name ?? existing?.name ?? modelID,
|
||||
release_date: model.release_date ?? existing?.release_date,
|
||||
attachment: model.attachment ?? existing?.attachment ?? false,
|
||||
reasoning: model.reasoning ?? existing?.reasoning ?? false,
|
||||
temperature: model.temperature ?? existing?.temperature ?? false,
|
||||
@@ -238,15 +286,18 @@ export namespace Provider {
|
||||
database[providerID] = parsed
|
||||
}
|
||||
|
||||
const disabled = await Config.get().then(
|
||||
(cfg) => new Set(cfg.disabled_providers ?? []),
|
||||
)
|
||||
const disabled = await Config.get().then((cfg) => new Set(cfg.disabled_providers ?? []))
|
||||
// load env
|
||||
for (const [providerID, provider] of Object.entries(database)) {
|
||||
if (disabled.has(providerID)) continue
|
||||
if (provider.env.some((item) => process.env[item])) {
|
||||
mergeProvider(providerID, {}, "env")
|
||||
}
|
||||
const apiKey = provider.env.map((item) => process.env[item]).at(0)
|
||||
if (!apiKey) continue
|
||||
mergeProvider(
|
||||
providerID,
|
||||
// only include apiKey if there's only one potential option
|
||||
provider.env.length === 1 ? { apiKey } : {},
|
||||
"env",
|
||||
)
|
||||
}
|
||||
|
||||
// load apikeys
|
||||
@@ -262,12 +313,7 @@ export namespace Provider {
|
||||
if (disabled.has(providerID)) continue
|
||||
const result = await fn(database[providerID])
|
||||
if (result && (result.autoload || providers[providerID])) {
|
||||
mergeProvider(
|
||||
providerID,
|
||||
result.options ?? {},
|
||||
"custom",
|
||||
result.getModel,
|
||||
)
|
||||
mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +350,7 @@ export namespace Provider {
|
||||
const existing = s.sdk.get(provider.id)
|
||||
if (existing) return existing
|
||||
const pkg = provider.npm ?? provider.id
|
||||
const mod = await import(await BunProc.install(pkg, "latest"))
|
||||
const mod = await import(await BunProc.install(pkg, "beta"))
|
||||
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
||||
const loaded = fn(s.providers[provider.id]?.options)
|
||||
s.sdk.set(provider.id, loaded)
|
||||
@@ -331,9 +377,7 @@ export namespace Provider {
|
||||
const sdk = await getSDK(provider.info)
|
||||
|
||||
try {
|
||||
const language = provider.getModel
|
||||
? await provider.getModel(sdk, modelID)
|
||||
: sdk.languageModel(modelID)
|
||||
const language = provider.getModel ? await provider.getModel(sdk, modelID) : sdk.languageModel(modelID)
|
||||
log.info("found", { providerID, modelID })
|
||||
s.models.set(key, {
|
||||
info,
|
||||
@@ -360,10 +404,7 @@ export namespace Provider {
|
||||
export function sort(models: ModelsDev.Model[]) {
|
||||
return sortBy(
|
||||
models,
|
||||
[
|
||||
(model) => priority.findIndex((filter) => model.id.includes(filter)),
|
||||
"desc",
|
||||
],
|
||||
[(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
|
||||
[(model) => (model.id.includes("latest") ? 0 : 1), "asc"],
|
||||
[(model) => model.id, "desc"],
|
||||
)
|
||||
@@ -374,11 +415,7 @@ export namespace Provider {
|
||||
if (cfg.model) return parseModel(cfg.model)
|
||||
const provider = await list()
|
||||
.then((val) => Object.values(val))
|
||||
.then((x) =>
|
||||
x.find(
|
||||
(p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id),
|
||||
),
|
||||
)
|
||||
.then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)))
|
||||
if (!provider) throw new Error("no providers found")
|
||||
const [model] = sort(Object.values(provider.info.models))
|
||||
if (!model) throw new Error("no models found")
|
||||
@@ -403,16 +440,15 @@ export namespace Provider {
|
||||
GlobTool,
|
||||
GrepTool,
|
||||
ListTool,
|
||||
LspDiagnosticTool,
|
||||
LspHoverTool,
|
||||
// LspDiagnosticTool,
|
||||
// LspHoverTool,
|
||||
PatchTool,
|
||||
ReadTool,
|
||||
EditTool,
|
||||
// MultiEditTool,
|
||||
WriteTool,
|
||||
TodoWriteTool,
|
||||
// TaskTool,
|
||||
TodoReadTool,
|
||||
// TaskTool,
|
||||
]
|
||||
|
||||
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
|
||||
@@ -462,9 +498,11 @@ export namespace Provider {
|
||||
|
||||
if (schema instanceof z.ZodUnion) {
|
||||
return z.union(
|
||||
schema.options.map((option: z.ZodTypeAny) =>
|
||||
optionalToNullable(option),
|
||||
) as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]],
|
||||
schema.options.map((option: z.ZodTypeAny) => optionalToNullable(option)) as [
|
||||
z.ZodTypeAny,
|
||||
z.ZodTypeAny,
|
||||
...z.ZodTypeAny[],
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import type { LanguageModelV1Prompt } from "ai"
|
||||
import type { ModelMessage } from "ai"
|
||||
import { unique } from "remeda"
|
||||
|
||||
export namespace ProviderTransform {
|
||||
export function message(
|
||||
msgs: LanguageModelV1Prompt,
|
||||
providerID: string,
|
||||
modelID: string,
|
||||
) {
|
||||
export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
|
||||
if (providerID === "anthropic" || modelID.includes("anthropic")) {
|
||||
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
||||
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
||||
|
||||
for (const msg of unique([...system, ...final])) {
|
||||
msg.providerMetadata = {
|
||||
...msg.providerMetadata,
|
||||
msg.providerOptions = {
|
||||
...msg.providerOptions,
|
||||
anthropic: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
openaiCompatible: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,8 +24,8 @@ export namespace ProviderTransform {
|
||||
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
||||
|
||||
for (const msg of unique([...system, ...final])) {
|
||||
msg.providerMetadata = {
|
||||
...msg.providerMetadata,
|
||||
msg.providerOptions = {
|
||||
...msg.providerOptions,
|
||||
bedrock: {
|
||||
cachePoint: { type: "ephemeral" },
|
||||
},
|
||||
|
||||
@@ -6,14 +6,16 @@ import { streamSSE } from "hono/streaming"
|
||||
import { Session } from "../session"
|
||||
import { resolver, validator as zValidator } from "hono-openapi/zod"
|
||||
import { z } from "zod"
|
||||
import { Message } from "../session/message"
|
||||
import { Provider } from "../provider/provider"
|
||||
import { App } from "../app/app"
|
||||
import { mapValues } from "remeda"
|
||||
import { NamedError } from "../util/error"
|
||||
import { ModelsDev } from "../provider/models"
|
||||
import { Ripgrep } from "../external/ripgrep"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { Config } from "../config/config"
|
||||
import { File } from "../file"
|
||||
import { LSP } from "../lsp"
|
||||
import { MessageV2 } from "../session/message-v2"
|
||||
|
||||
const ERRORS = {
|
||||
400: {
|
||||
@@ -49,12 +51,9 @@ export namespace Server {
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
return c.json(
|
||||
new NamedError.Unknown({ message: err.toString() }).toObject(),
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
)
|
||||
return c.json(new NamedError.Unknown({ message: err.toString() }).toObject(), {
|
||||
status: 400,
|
||||
})
|
||||
})
|
||||
.use(async (c, next) => {
|
||||
log.info("request", {
|
||||
@@ -73,7 +72,7 @@ export namespace Server {
|
||||
documentation: {
|
||||
info: {
|
||||
title: "opencode",
|
||||
version: "0.0.2",
|
||||
version: "0.0.3",
|
||||
description: "opencode api",
|
||||
},
|
||||
openapi: "3.0.0",
|
||||
@@ -405,7 +404,7 @@ export namespace Server {
|
||||
description: "List of messages",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Message.Info.array()),
|
||||
schema: resolver(MessageV2.Info.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -431,7 +430,7 @@ export namespace Server {
|
||||
description: "Created message",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Message.Info),
|
||||
schema: resolver(MessageV2.Assistant),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -448,7 +447,7 @@ export namespace Server {
|
||||
z.object({
|
||||
providerID: z.string(),
|
||||
modelID: z.string(),
|
||||
parts: Message.MessagePart.array(),
|
||||
parts: MessageV2.UserPart.array(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -479,25 +478,52 @@ export namespace Server {
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const providers = await Provider.list().then((x) =>
|
||||
mapValues(x, (item) => item.info),
|
||||
)
|
||||
const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info))
|
||||
return c.json({
|
||||
providers: Object.values(providers),
|
||||
default: mapValues(
|
||||
providers,
|
||||
(item) => Provider.sort(Object.values(item.models))[0].id,
|
||||
),
|
||||
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
||||
})
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/file",
|
||||
"/find",
|
||||
describeRoute({
|
||||
description: "Search for files",
|
||||
description: "Find text in files",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Search for files",
|
||||
description: "Matches",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Ripgrep.Match.shape.data.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"query",
|
||||
z.object({
|
||||
pattern: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const app = App.info()
|
||||
const pattern = c.req.valid("query").pattern
|
||||
const result = await Ripgrep.search({
|
||||
cwd: app.path.cwd,
|
||||
pattern,
|
||||
limit: 10,
|
||||
})
|
||||
return c.json(result)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/find/file",
|
||||
describeRoute({
|
||||
description: "Find files",
|
||||
responses: {
|
||||
200: {
|
||||
description: "File paths",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.string().array()),
|
||||
@@ -523,6 +549,98 @@ export namespace Server {
|
||||
return c.json(result)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/find/symbol",
|
||||
describeRoute({
|
||||
description: "Find workspace symbols",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Symbols",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.unknown().array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"query",
|
||||
z.object({
|
||||
query: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const query = c.req.valid("query").query
|
||||
const result = await LSP.workspaceSymbol(query)
|
||||
return c.json(result)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/file",
|
||||
describeRoute({
|
||||
description: "Read a file",
|
||||
responses: {
|
||||
200: {
|
||||
description: "File content",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z.object({
|
||||
type: z.enum(["raw", "patch"]),
|
||||
content: z.string(),
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"query",
|
||||
z.object({
|
||||
path: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const path = c.req.valid("query").path
|
||||
const content = await File.read(path)
|
||||
log.info("read file", {
|
||||
path,
|
||||
content: content.content,
|
||||
})
|
||||
return c.json(content)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/file/status",
|
||||
describeRoute({
|
||||
description: "Get file status",
|
||||
responses: {
|
||||
200: {
|
||||
description: "File status",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z
|
||||
.object({
|
||||
file: z.string(),
|
||||
added: z.number().int(),
|
||||
removed: z.number().int(),
|
||||
status: z.enum(["added", "deleted", "modified"]),
|
||||
})
|
||||
.array(),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const content = await File.status()
|
||||
return c.json(content)
|
||||
},
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,398 @@
|
||||
import z from "zod"
|
||||
import { Bus } from "../bus"
|
||||
import { Provider } from "../provider/provider"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Message } from "./message"
|
||||
import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
|
||||
|
||||
export namespace MessageV2 {
|
||||
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
|
||||
|
||||
export const ToolStatePending = z
|
||||
.object({
|
||||
status: z.literal("pending"),
|
||||
})
|
||||
.openapi({
|
||||
ref: "ToolStatePending",
|
||||
})
|
||||
|
||||
export type ToolStatePending = z.infer<typeof ToolStatePending>
|
||||
|
||||
export const ToolStateRunning = z
|
||||
.object({
|
||||
status: z.literal("running"),
|
||||
input: z.any(),
|
||||
title: z.string().optional(),
|
||||
metadata: z.record(z.any()).optional(),
|
||||
time: z.object({
|
||||
start: z.number(),
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
ref: "ToolStateRunning",
|
||||
})
|
||||
export type ToolStateRunning = z.infer<typeof ToolStateRunning>
|
||||
|
||||
export const ToolStateCompleted = z
|
||||
.object({
|
||||
status: z.literal("completed"),
|
||||
input: z.record(z.any()),
|
||||
output: z.string(),
|
||||
title: z.string(),
|
||||
metadata: z.record(z.any()),
|
||||
time: z.object({
|
||||
start: z.number(),
|
||||
end: z.number(),
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
ref: "ToolStateCompleted",
|
||||
})
|
||||
export type ToolStateCompleted = z.infer<typeof ToolStateCompleted>
|
||||
|
||||
export const ToolStateError = z
|
||||
.object({
|
||||
status: z.literal("error"),
|
||||
input: z.record(z.any()),
|
||||
error: z.string(),
|
||||
time: z.object({
|
||||
start: z.number(),
|
||||
end: z.number(),
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
ref: "ToolStateError",
|
||||
})
|
||||
export type ToolStateError = z.infer<typeof ToolStateError>
|
||||
|
||||
export const ToolState = z
|
||||
.discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError])
|
||||
.openapi({
|
||||
ref: "ToolState",
|
||||
})
|
||||
|
||||
export const TextPart = z
|
||||
.object({
|
||||
type: z.literal("text"),
|
||||
text: z.string(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "TextPart",
|
||||
})
|
||||
export type TextPart = z.infer<typeof TextPart>
|
||||
|
||||
export const ToolPart = z
|
||||
.object({
|
||||
type: z.literal("tool"),
|
||||
id: z.string(),
|
||||
tool: z.string(),
|
||||
state: ToolState,
|
||||
})
|
||||
.openapi({
|
||||
ref: "ToolPart",
|
||||
})
|
||||
export type ToolPart = z.infer<typeof ToolPart>
|
||||
|
||||
export const FilePart = z
|
||||
.object({
|
||||
type: z.literal("file"),
|
||||
mime: z.string(),
|
||||
filename: z.string().optional(),
|
||||
url: z.string(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "FilePart",
|
||||
})
|
||||
export type FilePart = z.infer<typeof FilePart>
|
||||
|
||||
export const StepStartPart = z
|
||||
.object({
|
||||
type: z.literal("step-start"),
|
||||
})
|
||||
.openapi({
|
||||
ref: "StepStartPart",
|
||||
})
|
||||
export type StepStartPart = z.infer<typeof StepStartPart>
|
||||
|
||||
const Base = z.object({
|
||||
id: z.string(),
|
||||
sessionID: z.string(),
|
||||
})
|
||||
|
||||
export const UserPart = z.discriminatedUnion("type", [TextPart, FilePart]).openapi({
|
||||
ref: "UserMessagePart",
|
||||
})
|
||||
export type UserPart = z.infer<typeof UserPart>
|
||||
|
||||
export const User = Base.extend({
|
||||
role: z.literal("user"),
|
||||
parts: z.array(UserPart),
|
||||
time: z.object({
|
||||
created: z.number(),
|
||||
}),
|
||||
}).openapi({
|
||||
ref: "UserMessage",
|
||||
})
|
||||
export type User = z.infer<typeof User>
|
||||
|
||||
export const AssistantPart = z.discriminatedUnion("type", [TextPart, ToolPart, StepStartPart]).openapi({
|
||||
ref: "AssistantMessagePart",
|
||||
})
|
||||
export type AssistantPart = z.infer<typeof AssistantPart>
|
||||
|
||||
export const Assistant = Base.extend({
|
||||
role: z.literal("assistant"),
|
||||
parts: z.array(AssistantPart),
|
||||
time: z.object({
|
||||
created: z.number(),
|
||||
completed: z.number().optional(),
|
||||
}),
|
||||
error: z
|
||||
.discriminatedUnion("name", [Provider.AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema])
|
||||
.optional(),
|
||||
system: z.string().array(),
|
||||
modelID: z.string(),
|
||||
providerID: z.string(),
|
||||
path: z.object({
|
||||
cwd: z.string(),
|
||||
root: z.string(),
|
||||
}),
|
||||
cost: z.number(),
|
||||
summary: z.boolean().optional(),
|
||||
tokens: z.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
reasoning: z.number(),
|
||||
cache: z.object({
|
||||
read: z.number(),
|
||||
write: z.number(),
|
||||
}),
|
||||
}),
|
||||
}).openapi({
|
||||
ref: "AssistantMessage",
|
||||
})
|
||||
export type Assistant = z.infer<typeof Assistant>
|
||||
|
||||
export const Info = z.discriminatedUnion("role", [User, Assistant]).openapi({
|
||||
ref: "Message",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
export const Event = {
|
||||
Updated: Bus.event(
|
||||
"message.updated",
|
||||
z.object({
|
||||
info: Info,
|
||||
}),
|
||||
),
|
||||
Removed: Bus.event(
|
||||
"message.removed",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
}),
|
||||
),
|
||||
PartUpdated: Bus.event(
|
||||
"message.part.updated",
|
||||
z.object({
|
||||
part: AssistantPart,
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
export function fromV1(v1: Message.Info) {
|
||||
if (v1.role === "assistant") {
|
||||
const result: Assistant = {
|
||||
id: v1.id,
|
||||
sessionID: v1.metadata.sessionID,
|
||||
role: "assistant",
|
||||
time: {
|
||||
created: v1.metadata.time.created,
|
||||
completed: v1.metadata.time.completed,
|
||||
},
|
||||
cost: v1.metadata.assistant!.cost,
|
||||
path: v1.metadata.assistant!.path,
|
||||
summary: v1.metadata.assistant!.summary,
|
||||
tokens: v1.metadata.assistant!.tokens,
|
||||
modelID: v1.metadata.assistant!.modelID,
|
||||
providerID: v1.metadata.assistant!.providerID,
|
||||
system: v1.metadata.assistant!.system,
|
||||
error: v1.metadata.error,
|
||||
parts: v1.parts.flatMap((part): AssistantPart[] => {
|
||||
if (part.type === "text") {
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: part.text,
|
||||
},
|
||||
]
|
||||
}
|
||||
if (part.type === "step-start") {
|
||||
return [
|
||||
{
|
||||
type: "step-start",
|
||||
},
|
||||
]
|
||||
}
|
||||
if (part.type === "tool-invocation") {
|
||||
return [
|
||||
{
|
||||
type: "tool",
|
||||
id: part.toolInvocation.toolCallId,
|
||||
tool: part.toolInvocation.toolName,
|
||||
state: (() => {
|
||||
if (part.toolInvocation.state === "partial-call") {
|
||||
return {
|
||||
status: "pending",
|
||||
}
|
||||
}
|
||||
|
||||
const { title, time, ...metadata } = v1.metadata.tool[part.toolInvocation.toolCallId] ?? {}
|
||||
if (part.toolInvocation.state === "call") {
|
||||
return {
|
||||
status: "running",
|
||||
input: part.toolInvocation.args,
|
||||
time: {
|
||||
start: time?.start,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (part.toolInvocation.state === "result") {
|
||||
return {
|
||||
status: "completed",
|
||||
input: part.toolInvocation.args,
|
||||
output: part.toolInvocation.result,
|
||||
title,
|
||||
time,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
throw new Error("unknown tool invocation state")
|
||||
})(),
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
}),
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
if (v1.role === "user") {
|
||||
const result: User = {
|
||||
id: v1.id,
|
||||
sessionID: v1.metadata.sessionID,
|
||||
role: "user",
|
||||
time: {
|
||||
created: v1.metadata.time.created,
|
||||
},
|
||||
parts: v1.parts.flatMap((part): UserPart[] => {
|
||||
if (part.type === "text") {
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: part.text,
|
||||
},
|
||||
]
|
||||
}
|
||||
if (part.type === "file") {
|
||||
return [
|
||||
{
|
||||
type: "file",
|
||||
mime: part.mediaType,
|
||||
filename: part.filename,
|
||||
url: part.url,
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
}),
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export function toModelMessage(input: Info[]): ModelMessage[] {
|
||||
const result: UIMessage[] = []
|
||||
|
||||
for (const msg of input) {
|
||||
if (msg.parts.length === 0) continue
|
||||
if (msg.role === "user") {
|
||||
result.push({
|
||||
id: msg.id,
|
||||
role: "user",
|
||||
parts: msg.parts.flatMap((part): UIMessage["parts"] => {
|
||||
if (part.type === "text")
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: part.text,
|
||||
},
|
||||
]
|
||||
if (part.type === "file")
|
||||
return [
|
||||
{
|
||||
type: "file",
|
||||
url: part.url,
|
||||
mediaType: part.mime,
|
||||
filename: part.filename,
|
||||
},
|
||||
]
|
||||
return []
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
if (msg.role === "assistant") {
|
||||
result.push({
|
||||
id: msg.id,
|
||||
role: "assistant",
|
||||
parts: msg.parts.flatMap((part): UIMessage["parts"] => {
|
||||
if (part.type === "text")
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: part.text,
|
||||
},
|
||||
]
|
||||
if (part.type === "step-start")
|
||||
return [
|
||||
{
|
||||
type: "step-start",
|
||||
},
|
||||
]
|
||||
if (part.type === "tool") {
|
||||
if (part.state.status === "completed")
|
||||
return [
|
||||
{
|
||||
type: ("tool-" + part.tool) as `tool-${string}`,
|
||||
state: "output-available",
|
||||
toolCallId: part.id,
|
||||
input: part.state.input,
|
||||
output: part.state.output,
|
||||
},
|
||||
]
|
||||
if (part.state.status === "error")
|
||||
return [
|
||||
{
|
||||
type: ("tool-" + part.tool) as `tool-${string}`,
|
||||
state: "output-error",
|
||||
toolCallId: part.id,
|
||||
input: part.state.input,
|
||||
errorText: part.state.error,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return []
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return convertToModelMessages(result)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
import z from "zod"
|
||||
import { Bus } from "../bus"
|
||||
import { Provider } from "../provider/provider"
|
||||
import { NamedError } from "../util/error"
|
||||
|
||||
export namespace Message {
|
||||
export const OutputLengthError = NamedError.create(
|
||||
"MessageOutputLengthError",
|
||||
z.object({}),
|
||||
)
|
||||
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
|
||||
|
||||
export const ToolCall = z
|
||||
.object({
|
||||
@@ -49,11 +45,9 @@ export namespace Message {
|
||||
})
|
||||
export type ToolResult = z.infer<typeof ToolResult>
|
||||
|
||||
export const ToolInvocation = z
|
||||
.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult])
|
||||
.openapi({
|
||||
ref: "ToolInvocation",
|
||||
})
|
||||
export const ToolInvocation = z.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]).openapi({
|
||||
ref: "ToolInvocation",
|
||||
})
|
||||
export type ToolInvocation = z.infer<typeof ToolInvocation>
|
||||
|
||||
export const TextPart = z
|
||||
@@ -122,14 +116,7 @@ export namespace Message {
|
||||
export type StepStartPart = z.infer<typeof StepStartPart>
|
||||
|
||||
export const MessagePart = z
|
||||
.discriminatedUnion("type", [
|
||||
TextPart,
|
||||
ReasoningPart,
|
||||
ToolInvocationPart,
|
||||
SourceUrlPart,
|
||||
FilePart,
|
||||
StepStartPart,
|
||||
])
|
||||
.discriminatedUnion("type", [TextPart, ReasoningPart, ToolInvocationPart, SourceUrlPart, FilePart, StepStartPart])
|
||||
.openapi({
|
||||
ref: "MessagePart",
|
||||
})
|
||||
@@ -159,6 +146,7 @@ export namespace Message {
|
||||
z
|
||||
.object({
|
||||
title: z.string(),
|
||||
snapshot: z.string().optional(),
|
||||
time: z.object({
|
||||
start: z.number(),
|
||||
end: z.number(),
|
||||
@@ -188,6 +176,7 @@ export namespace Message {
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
snapshot: z.string().optional(),
|
||||
})
|
||||
.openapi({ ref: "MessageMetadata" }),
|
||||
})
|
||||
@@ -195,21 +184,4 @@ export namespace Message {
|
||||
ref: "Message",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
export const Event = {
|
||||
Updated: Bus.event(
|
||||
"message.updated",
|
||||
z.object({
|
||||
info: Info,
|
||||
}),
|
||||
),
|
||||
PartUpdated: Bus.event(
|
||||
"message.part.updated",
|
||||
z.object({
|
||||
part: MessagePart,
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ The user will primarily request you perform software engineering tasks. This inc
|
||||
- Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially.
|
||||
- Implement the solution using all tools available to you
|
||||
- Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
|
||||
- VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) with Bash if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to CLAUDE.md so that you will know to run it next time.
|
||||
- VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) with Bash if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to AGENTS.md so that you will know to run it next time.
|
||||
NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.
|
||||
|
||||
- Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are NOT part of the user's provided input or the tool result.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { App } from "../app/app"
|
||||
import { Ripgrep } from "../external/ripgrep"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { Global } from "../global"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Config } from "../config/config"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
|
||||
@@ -27,55 +28,6 @@ export namespace SystemPrompt {
|
||||
|
||||
export async function environment() {
|
||||
const app = App.info()
|
||||
|
||||
;async () => {
|
||||
const files = await Ripgrep.files({
|
||||
cwd: app.path.cwd,
|
||||
})
|
||||
type Node = {
|
||||
children: Record<string, Node>
|
||||
}
|
||||
const root: Node = {
|
||||
children: {},
|
||||
}
|
||||
for (const file of files) {
|
||||
const parts = file.split("/")
|
||||
let node = root
|
||||
for (const part of parts) {
|
||||
const existing = node.children[part]
|
||||
if (existing) {
|
||||
node = existing
|
||||
continue
|
||||
}
|
||||
node.children[part] = {
|
||||
children: {},
|
||||
}
|
||||
node = node.children[part]
|
||||
}
|
||||
}
|
||||
|
||||
function render(path: string[], node: Node): string {
|
||||
// if (path.length === 3) return "\t".repeat(path.length) + "..."
|
||||
const lines: string[] = []
|
||||
const entries = Object.entries(node.children).sort(([a], [b]) =>
|
||||
a.localeCompare(b),
|
||||
)
|
||||
|
||||
for (const [name, child] of entries) {
|
||||
const currentPath = [...path, name]
|
||||
const indent = "\t".repeat(path.length)
|
||||
const hasChildren = Object.keys(child.children).length > 0
|
||||
lines.push(`${indent}${name}` + (hasChildren ? "/" : ""))
|
||||
|
||||
if (hasChildren) lines.push(render(currentPath, child))
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
const result = render([], root)
|
||||
return result
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
`Here is some useful information about the environment you are running in:`,
|
||||
@@ -85,9 +37,16 @@ export namespace SystemPrompt {
|
||||
` Platform: ${process.platform}`,
|
||||
` Today's date: ${new Date().toDateString()}`,
|
||||
`</env>`,
|
||||
// `<project>`,
|
||||
// ` ${app.git ? await tree() : ""}`,
|
||||
// `</project>`,
|
||||
`<project>`,
|
||||
` ${
|
||||
app.git
|
||||
? await Ripgrep.tree({
|
||||
cwd: app.path.cwd,
|
||||
limit: 200,
|
||||
})
|
||||
: ""
|
||||
}`,
|
||||
`</project>`,
|
||||
].join("\n"),
|
||||
]
|
||||
}
|
||||
@@ -97,8 +56,10 @@ export namespace SystemPrompt {
|
||||
"CLAUDE.md",
|
||||
"CONTEXT.md", // deprecated
|
||||
]
|
||||
|
||||
export async function custom() {
|
||||
const { cwd, root } = App.info().path
|
||||
const config = await Config.get()
|
||||
const found = []
|
||||
for (const item of CUSTOM_FILES) {
|
||||
const matches = await Filesystem.findUp(item, cwd, root)
|
||||
@@ -114,6 +75,18 @@ export namespace SystemPrompt {
|
||||
.text()
|
||||
.catch(() => ""),
|
||||
)
|
||||
|
||||
if (config.instructions) {
|
||||
for (const instruction of config.instructions) {
|
||||
try {
|
||||
const matches = await Filesystem.globUp(instruction, cwd, root)
|
||||
found.push(...matches.map((x) => Bun.file(x).text()))
|
||||
} catch {
|
||||
continue // Skip invalid glob patterns
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(found).then((result) => result.filter(Boolean))
|
||||
}
|
||||
|
||||
|
||||
@@ -53,9 +53,7 @@ export namespace Share {
|
||||
|
||||
export const URL =
|
||||
process.env["OPENCODE_API"] ??
|
||||
(Installation.isSnapshot() || Installation.isDev()
|
||||
? "https://api.dev.opencode.ai"
|
||||
: "https://api.opencode.ai")
|
||||
(Installation.isSnapshot() || Installation.isDev() ? "https://api.dev.opencode.ai" : "https://api.opencode.ai")
|
||||
|
||||
export async function create(sessionID: string) {
|
||||
return fetch(`${URL}/share_create`, {
|
||||
@@ -66,10 +64,10 @@ export namespace Share {
|
||||
.then((x) => x as { url: string; secret: string })
|
||||
}
|
||||
|
||||
export async function remove(id: string) {
|
||||
export async function remove(sessionID: string, secret: string) {
|
||||
return fetch(`${URL}/share_delete`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id }),
|
||||
body: JSON.stringify({ sessionID, secret }),
|
||||
}).then((x) => x.json())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { App } from "../app/app"
|
||||
import { $ } from "bun"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { Log } from "../util/log"
|
||||
|
||||
export namespace Snapshot {
|
||||
const log = Log.create({ service: "snapshot" })
|
||||
|
||||
export async function create(sessionID: string) {
|
||||
return
|
||||
log.info("creating snapshot")
|
||||
const app = App.info()
|
||||
const git = gitdir(sessionID)
|
||||
|
||||
// not a git repo, check if too big to snapshot
|
||||
if (!app.git) {
|
||||
const files = await Ripgrep.files({
|
||||
cwd: app.path.cwd,
|
||||
limit: 1000,
|
||||
})
|
||||
log.info("found files", { count: files.length })
|
||||
if (files.length > 1000) return
|
||||
}
|
||||
|
||||
if (await fs.mkdir(git, { recursive: true })) {
|
||||
await $`git init`
|
||||
.env({
|
||||
...process.env,
|
||||
GIT_DIR: git,
|
||||
GIT_WORK_TREE: app.path.root,
|
||||
})
|
||||
.quiet()
|
||||
.nothrow()
|
||||
log.info("initialized")
|
||||
}
|
||||
|
||||
await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow()
|
||||
log.info("added files")
|
||||
|
||||
const result =
|
||||
await $`git --git-dir ${git} commit --allow-empty -m "snapshot" --author="opencode <mail@opencode.ai>"`
|
||||
.quiet()
|
||||
.cwd(app.path.cwd)
|
||||
.nothrow()
|
||||
log.info("commit")
|
||||
|
||||
const match = result.stdout.toString().match(/\[.+ ([a-f0-9]+)\]/)
|
||||
if (!match) return
|
||||
return match![1]
|
||||
}
|
||||
|
||||
export async function restore(sessionID: string, commit: string) {
|
||||
log.info("restore", { commit })
|
||||
const app = App.info()
|
||||
const git = gitdir(sessionID)
|
||||
await $`git --git-dir=${git} checkout ${commit} --force`.quiet().cwd(app.path.root)
|
||||
}
|
||||
|
||||
function gitdir(sessionID: string) {
|
||||
const app = App.info()
|
||||
return path.join(app.path.data, "snapshot", sessionID)
|
||||
}
|
||||
}
|
||||
@@ -4,44 +4,77 @@ import { Bus } from "../bus"
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
import fs from "fs/promises"
|
||||
import { MessageV2 } from "../session/message-v2"
|
||||
|
||||
export namespace Storage {
|
||||
const log = Log.create({ service: "storage" })
|
||||
|
||||
export const Event = {
|
||||
Write: Bus.event(
|
||||
"storage.write",
|
||||
z.object({ key: z.string(), content: z.any() }),
|
||||
),
|
||||
Write: Bus.event("storage.write", z.object({ key: z.string(), content: z.any() })),
|
||||
}
|
||||
|
||||
const state = App.state("storage", () => {
|
||||
type Migration = (dir: string) => Promise<void>
|
||||
|
||||
const MIGRATIONS: Migration[] = [
|
||||
async (dir: string) => {
|
||||
const files = new Bun.Glob("session/message/*/*.json").scanSync({
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
})
|
||||
for (const file of files) {
|
||||
const content = await Bun.file(file).json()
|
||||
if (!content.metadata) continue
|
||||
log.info("migrating to v2 message", { file })
|
||||
try {
|
||||
const result = MessageV2.fromV1(content)
|
||||
await Bun.write(file, JSON.stringify(result, null, 2))
|
||||
} catch (e) {
|
||||
await fs.rename(file, file.replace("storage", "broken"))
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
const state = App.state("storage", async () => {
|
||||
const app = App.info()
|
||||
const dir = path.join(app.path.data, "storage")
|
||||
log.info("init", { path: dir })
|
||||
const migration = await Bun.file(path.join(dir, "migration"))
|
||||
.json()
|
||||
.then((x) => parseInt(x))
|
||||
.catch(() => 0)
|
||||
for (let index = migration; index < MIGRATIONS.length; index++) {
|
||||
log.info("running migration", { index })
|
||||
const migration = MIGRATIONS[index]
|
||||
await migration(dir)
|
||||
await Bun.write(path.join(dir, "migration"), (index + 1).toString())
|
||||
}
|
||||
return {
|
||||
dir,
|
||||
}
|
||||
})
|
||||
|
||||
export async function remove(key: string) {
|
||||
const target = path.join(state().dir, key + ".json")
|
||||
const dir = await state().then((x) => x.dir)
|
||||
const target = path.join(dir, key + ".json")
|
||||
await fs.unlink(target).catch(() => {})
|
||||
}
|
||||
|
||||
export async function removeDir(key: string) {
|
||||
const target = path.join(state().dir, key)
|
||||
const dir = await state().then((x) => x.dir)
|
||||
const target = path.join(dir, key)
|
||||
await fs.rm(target, { recursive: true, force: true }).catch(() => {})
|
||||
}
|
||||
|
||||
export async function readJSON<T>(key: string) {
|
||||
return Bun.file(path.join(state().dir, key + ".json")).json() as Promise<T>
|
||||
const dir = await state().then((x) => x.dir)
|
||||
return Bun.file(path.join(dir, key + ".json")).json() as Promise<T>
|
||||
}
|
||||
|
||||
export async function writeJSON<T>(key: string, content: T) {
|
||||
const target = path.join(state().dir, key + ".json")
|
||||
const dir = await state().then((x) => x.dir)
|
||||
const target = path.join(dir, key + ".json")
|
||||
const tmp = target + Date.now() + ".tmp"
|
||||
await Bun.write(tmp, JSON.stringify(content))
|
||||
await Bun.write(tmp, JSON.stringify(content, null, 2))
|
||||
await fs.rename(tmp, target).catch(() => {})
|
||||
await fs.unlink(tmp).catch(() => {})
|
||||
Bus.publish(Event.Write, { key, content })
|
||||
@@ -49,9 +82,10 @@ export namespace Storage {
|
||||
|
||||
const glob = new Bun.Glob("**/*")
|
||||
export async function* list(prefix: string) {
|
||||
const dir = await state().then((x) => x.dir)
|
||||
try {
|
||||
for await (const item of glob.scan({
|
||||
cwd: path.join(state().dir, prefix),
|
||||
cwd: path.join(dir, prefix),
|
||||
onlyFiles: true,
|
||||
})) {
|
||||
const result = path.join(prefix, item.slice(0, -5))
|
||||
|
||||
@@ -1,27 +1,9 @@
|
||||
import { z } from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import DESCRIPTION from "./bash.txt"
|
||||
import { App } from "../app/app"
|
||||
|
||||
const MAX_OUTPUT_LENGTH = 30000
|
||||
const BANNED_COMMANDS = [
|
||||
"alias",
|
||||
"curl",
|
||||
"curlie",
|
||||
"wget",
|
||||
"axel",
|
||||
"aria2c",
|
||||
"nc",
|
||||
"telnet",
|
||||
"lynx",
|
||||
"w3m",
|
||||
"links",
|
||||
"httpie",
|
||||
"xh",
|
||||
"http-prompt",
|
||||
"chrome",
|
||||
"firefox",
|
||||
"safari",
|
||||
]
|
||||
const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
||||
const MAX_TIMEOUT = 10 * 60 * 1000
|
||||
|
||||
@@ -30,12 +12,7 @@ export const BashTool = Tool.define({
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
command: z.string().describe("The command to execute"),
|
||||
timeout: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(MAX_TIMEOUT)
|
||||
.describe("Optional timeout in milliseconds")
|
||||
.optional(),
|
||||
timeout: z.number().min(0).max(MAX_TIMEOUT).describe("Optional timeout in milliseconds").optional(),
|
||||
description: z
|
||||
.string()
|
||||
.describe(
|
||||
@@ -44,11 +21,10 @@ export const BashTool = Tool.define({
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
|
||||
if (BANNED_COMMANDS.some((item) => params.command.startsWith(item)))
|
||||
throw new Error(`Command '${params.command}' is not allowed`)
|
||||
|
||||
const process = Bun.spawn({
|
||||
cmd: ["bash", "-c", params.command],
|
||||
cwd: App.info().path.cwd,
|
||||
maxBuffer: MAX_OUTPUT_LENGTH,
|
||||
signal: ctx.abort,
|
||||
timeout: timeout,
|
||||
@@ -60,21 +36,14 @@ export const BashTool = Tool.define({
|
||||
const stderr = await new Response(process.stderr).text()
|
||||
|
||||
return {
|
||||
title: params.command,
|
||||
metadata: {
|
||||
stderr,
|
||||
stdout,
|
||||
exit: process.exitCode,
|
||||
description: params.description,
|
||||
title: params.command,
|
||||
},
|
||||
output: [
|
||||
`<stdout>`,
|
||||
stdout ?? "",
|
||||
`</stdout>`,
|
||||
`<stderr>`,
|
||||
stderr ?? "",
|
||||
`</stderr>`,
|
||||
].join("\n"),
|
||||
output: [`<stdout>`, stdout ?? "", `</stdout>`, `<stderr>`, stderr ?? "", `</stderr>`].join("\n"),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -20,15 +20,8 @@ export const EditTool = Tool.define({
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||
oldString: z.string().describe("The text to replace"),
|
||||
newString: z
|
||||
.string()
|
||||
.describe(
|
||||
"The text to replace it with (must be different from old_string)",
|
||||
),
|
||||
replaceAll: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Replace all occurrences of old_string (default false)"),
|
||||
newString: z.string().describe("The text to replace it with (must be different from old_string)"),
|
||||
replaceAll: z.boolean().optional().describe("Replace all occurrences of old_string (default false)"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
if (!params.filePath) {
|
||||
@@ -40,9 +33,7 @@ export const EditTool = Tool.define({
|
||||
}
|
||||
|
||||
const app = App.info()
|
||||
const filepath = path.isAbsolute(params.filePath)
|
||||
? params.filePath
|
||||
: path.join(app.path.cwd, params.filePath)
|
||||
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
|
||||
|
||||
await Permission.ask({
|
||||
id: "edit",
|
||||
@@ -70,17 +61,11 @@ export const EditTool = Tool.define({
|
||||
const file = Bun.file(filepath)
|
||||
const stats = await file.stat().catch(() => {})
|
||||
if (!stats) throw new Error(`File ${filepath} not found`)
|
||||
if (stats.isDirectory())
|
||||
throw new Error(`Path is a directory, not a file: ${filepath}`)
|
||||
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filepath}`)
|
||||
await FileTime.assert(ctx.sessionID, filepath)
|
||||
contentOld = await file.text()
|
||||
|
||||
contentNew = replace(
|
||||
contentOld,
|
||||
params.oldString,
|
||||
params.newString,
|
||||
params.replaceAll,
|
||||
)
|
||||
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
|
||||
await file.write(contentNew)
|
||||
await Bus.publish(File.Event.Edited, {
|
||||
file: filepath,
|
||||
@@ -88,9 +73,7 @@ export const EditTool = Tool.define({
|
||||
contentNew = await file.text()
|
||||
})()
|
||||
|
||||
const diff = trimDiff(
|
||||
createTwoFilesPatch(filepath, filepath, contentOld, contentNew),
|
||||
)
|
||||
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, contentNew))
|
||||
|
||||
FileTime.read(ctx.sessionID, filepath)
|
||||
|
||||
@@ -110,17 +93,14 @@ export const EditTool = Tool.define({
|
||||
metadata: {
|
||||
diagnostics,
|
||||
diff,
|
||||
title: `${path.relative(app.path.root, filepath)}`,
|
||||
},
|
||||
title: `${path.relative(app.path.root, filepath)}`,
|
||||
output,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export type Replacer = (
|
||||
content: string,
|
||||
find: string,
|
||||
) => Generator<string, void, unknown>
|
||||
export type Replacer = (content: string, find: string) => Generator<string, void, unknown>
|
||||
|
||||
export const SimpleReplacer: Replacer = function* (_content, find) {
|
||||
yield find
|
||||
@@ -208,10 +188,7 @@ export const BlockAnchorReplacer: Replacer = function* (content, find) {
|
||||
}
|
||||
}
|
||||
|
||||
export const WhitespaceNormalizedReplacer: Replacer = function* (
|
||||
content,
|
||||
find,
|
||||
) {
|
||||
export const WhitespaceNormalizedReplacer: Replacer = function* (content, find) {
|
||||
const normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim()
|
||||
const normalizedFind = normalizeWhitespace(find)
|
||||
|
||||
@@ -229,9 +206,7 @@ export const WhitespaceNormalizedReplacer: Replacer = function* (
|
||||
// Find the actual substring in the original line that matches
|
||||
const words = find.trim().split(/\s+/)
|
||||
if (words.length > 0) {
|
||||
const pattern = words
|
||||
.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
||||
.join("\\s+")
|
||||
const pattern = words.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+")
|
||||
try {
|
||||
const regex = new RegExp(pattern)
|
||||
const match = line.match(regex)
|
||||
@@ -270,9 +245,7 @@ export const IndentationFlexibleReplacer: Replacer = function* (content, find) {
|
||||
}),
|
||||
)
|
||||
|
||||
return lines
|
||||
.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent)))
|
||||
.join("\n")
|
||||
return lines.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent))).join("\n")
|
||||
}
|
||||
|
||||
const normalizedFind = removeIndentation(find)
|
||||
@@ -423,10 +396,7 @@ export const ContextAwareReplacer: Replacer = function* (content, find) {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
totalNonEmptyLines === 0 ||
|
||||
matchingLines / totalNonEmptyLines >= 0.5
|
||||
) {
|
||||
if (totalNonEmptyLines === 0 || matchingLines / totalNonEmptyLines >= 0.5) {
|
||||
yield block
|
||||
break // Only match the first occurrence
|
||||
}
|
||||
@@ -473,12 +443,7 @@ function trimDiff(diff: string): string {
|
||||
return trimmedLines.join("\n")
|
||||
}
|
||||
|
||||
export function replace(
|
||||
content: string,
|
||||
oldString: string,
|
||||
newString: string,
|
||||
replaceAll = false,
|
||||
): string {
|
||||
export function replace(content: string, oldString: string, newString: string, replaceAll = false): string {
|
||||
if (oldString === newString) {
|
||||
throw new Error("oldString and newString must be different")
|
||||
}
|
||||
@@ -489,10 +454,10 @@ export function replace(
|
||||
BlockAnchorReplacer,
|
||||
WhitespaceNormalizedReplacer,
|
||||
IndentationFlexibleReplacer,
|
||||
EscapeNormalizedReplacer,
|
||||
TrimmedBoundaryReplacer,
|
||||
ContextAwareReplacer,
|
||||
MultiOccurrenceReplacer,
|
||||
// EscapeNormalizedReplacer,
|
||||
// TrimmedBoundaryReplacer,
|
||||
// ContextAwareReplacer,
|
||||
// MultiOccurrenceReplacer,
|
||||
]) {
|
||||
for (const search of replacer(content, oldString)) {
|
||||
const index = content.indexOf(search)
|
||||
@@ -502,11 +467,7 @@ export function replace(
|
||||
}
|
||||
const lastIndex = content.lastIndexOf(search)
|
||||
if (index !== lastIndex) continue
|
||||
return (
|
||||
content.substring(0, index) +
|
||||
newString +
|
||||
content.substring(index + search.length)
|
||||
)
|
||||
return content.substring(0, index) + newString + content.substring(index + search.length)
|
||||
}
|
||||
}
|
||||
throw new Error("oldString not found in content or was found multiple times")
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from "path"
|
||||
import { Tool } from "./tool"
|
||||
import { App } from "../app/app"
|
||||
import DESCRIPTION from "./glob.txt"
|
||||
import { Ripgrep } from "../external/ripgrep"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
|
||||
export const GlobTool = Tool.define({
|
||||
id: "glob",
|
||||
@@ -20,9 +20,7 @@ export const GlobTool = Tool.define({
|
||||
async execute(params) {
|
||||
const app = App.info()
|
||||
let search = params.path ?? app.path.cwd
|
||||
search = path.isAbsolute(search)
|
||||
? search
|
||||
: path.resolve(app.path.cwd, search)
|
||||
search = path.isAbsolute(search) ? search : path.resolve(app.path.cwd, search)
|
||||
|
||||
const limit = 100
|
||||
const files = []
|
||||
@@ -53,17 +51,15 @@ export const GlobTool = Tool.define({
|
||||
output.push(...files.map((f) => f.path))
|
||||
if (truncated) {
|
||||
output.push("")
|
||||
output.push(
|
||||
"(Results are truncated. Consider using a more specific path or pattern.)",
|
||||
)
|
||||
output.push("(Results are truncated. Consider using a more specific path or pattern.)")
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: path.relative(app.path.root, search),
|
||||
metadata: {
|
||||
count: files.length,
|
||||
truncated,
|
||||
title: path.relative(app.path.root, search),
|
||||
},
|
||||
output: output.join("\n"),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import { App } from "../app/app"
|
||||
import { Ripgrep } from "../external/ripgrep"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
|
||||
import DESCRIPTION from "./grep.txt"
|
||||
|
||||
@@ -9,21 +9,9 @@ export const GrepTool = Tool.define({
|
||||
id: "grep",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
pattern: z
|
||||
.string()
|
||||
.describe("The regex pattern to search for in file contents"),
|
||||
path: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"The directory to search in. Defaults to the current working directory.",
|
||||
),
|
||||
include: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
|
||||
),
|
||||
pattern: z.string().describe("The regex pattern to search for in file contents"),
|
||||
path: z.string().optional().describe("The directory to search in. Defaults to the current working directory."),
|
||||
include: z.string().optional().describe('File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")'),
|
||||
}),
|
||||
async execute(params) {
|
||||
if (!params.pattern) {
|
||||
@@ -51,7 +39,8 @@ export const GrepTool = Tool.define({
|
||||
|
||||
if (exitCode === 1) {
|
||||
return {
|
||||
metadata: { matches: 0, truncated: false, title: params.pattern },
|
||||
title: params.pattern,
|
||||
metadata: { matches: 0, truncated: false },
|
||||
output: "No files found",
|
||||
}
|
||||
}
|
||||
@@ -93,7 +82,8 @@ export const GrepTool = Tool.define({
|
||||
|
||||
if (finalMatches.length === 0) {
|
||||
return {
|
||||
metadata: { matches: 0, truncated: false, title: params.pattern },
|
||||
title: params.pattern,
|
||||
metadata: { matches: 0, truncated: false },
|
||||
output: "No files found",
|
||||
}
|
||||
}
|
||||
@@ -114,16 +104,14 @@ export const GrepTool = Tool.define({
|
||||
|
||||
if (truncated) {
|
||||
outputLines.push("")
|
||||
outputLines.push(
|
||||
"(Results are truncated. Consider using a more specific path or pattern.)",
|
||||
)
|
||||
outputLines.push("(Results are truncated. Consider using a more specific path or pattern.)")
|
||||
}
|
||||
|
||||
return {
|
||||
title: params.pattern,
|
||||
metadata: {
|
||||
matches: finalMatches.length,
|
||||
truncated,
|
||||
title: params.pattern,
|
||||
},
|
||||
output: outputLines.join("\n"),
|
||||
}
|
||||
|
||||
@@ -24,16 +24,8 @@ export const ListTool = Tool.define({
|
||||
id: "list",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
path: z
|
||||
.string()
|
||||
.describe(
|
||||
"The absolute path to the directory to list (must be absolute, not relative)",
|
||||
)
|
||||
.optional(),
|
||||
ignore: z
|
||||
.array(z.string())
|
||||
.describe("List of glob patterns to ignore")
|
||||
.optional(),
|
||||
path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(),
|
||||
ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(),
|
||||
}),
|
||||
async execute(params) {
|
||||
const app = App.info()
|
||||
@@ -44,8 +36,7 @@ export const ListTool = Tool.define({
|
||||
|
||||
for await (const file of glob.scan({ cwd: searchPath, dot: true })) {
|
||||
if (IGNORE_PATTERNS.some((p) => file.includes(p))) continue
|
||||
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
|
||||
continue
|
||||
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file))) continue
|
||||
files.push(file)
|
||||
if (files.length >= LIMIT) break
|
||||
}
|
||||
@@ -99,10 +90,10 @@ export const ListTool = Tool.define({
|
||||
const output = `${searchPath}/\n` + renderDir(".", 0)
|
||||
|
||||
return {
|
||||
title: path.relative(app.path.root, searchPath),
|
||||
metadata: {
|
||||
count: files.length,
|
||||
truncated: files.length >= LIMIT,
|
||||
title: path.relative(app.path.root, searchPath),
|
||||
},
|
||||
output,
|
||||
}
|
||||
|
||||
@@ -13,20 +13,16 @@ export const LspDiagnosticTool = Tool.define({
|
||||
}),
|
||||
execute: async (args) => {
|
||||
const app = App.info()
|
||||
const normalized = path.isAbsolute(args.path)
|
||||
? args.path
|
||||
: path.join(app.path.cwd, args.path)
|
||||
const normalized = path.isAbsolute(args.path) ? args.path : path.join(app.path.cwd, args.path)
|
||||
await LSP.touchFile(normalized, true)
|
||||
const diagnostics = await LSP.diagnostics()
|
||||
const file = diagnostics[normalized]
|
||||
return {
|
||||
title: path.relative(app.path.root, normalized),
|
||||
metadata: {
|
||||
diagnostics,
|
||||
title: path.relative(app.path.root, normalized),
|
||||
},
|
||||
output: file?.length
|
||||
? file.map(LSP.Diagnostic.pretty).join("\n")
|
||||
: "No errors found",
|
||||
output: file?.length ? file.map(LSP.Diagnostic.pretty).join("\n") : "No errors found",
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -15,9 +15,7 @@ export const LspHoverTool = Tool.define({
|
||||
}),
|
||||
execute: async (args) => {
|
||||
const app = App.info()
|
||||
const file = path.isAbsolute(args.file)
|
||||
? args.file
|
||||
: path.join(app.path.cwd, args.file)
|
||||
const file = path.isAbsolute(args.file) ? args.file : path.join(app.path.cwd, args.file)
|
||||
await LSP.touchFile(file, true)
|
||||
const result = await LSP.hover({
|
||||
...args,
|
||||
@@ -25,14 +23,9 @@ export const LspHoverTool = Tool.define({
|
||||
})
|
||||
|
||||
return {
|
||||
title: path.relative(app.path.root, file) + ":" + args.line + ":" + args.character,
|
||||
metadata: {
|
||||
result,
|
||||
title:
|
||||
path.relative(app.path.root, file) +
|
||||
":" +
|
||||
args.line +
|
||||
":" +
|
||||
args.character,
|
||||
},
|
||||
output: JSON.stringify(result, null, 2),
|
||||
}
|
||||
|
||||
@@ -10,9 +10,7 @@ export const MultiEditTool = Tool.define({
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||
edits: z
|
||||
.array(EditTool.parameters)
|
||||
.describe("Array of edit operations to perform sequentially on the file"),
|
||||
edits: z.array(EditTool.parameters).describe("Array of edit operations to perform sequentially on the file"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const results = []
|
||||
@@ -30,9 +28,9 @@ export const MultiEditTool = Tool.define({
|
||||
}
|
||||
const app = App.info()
|
||||
return {
|
||||
title: path.relative(app.path.root, params.filePath),
|
||||
metadata: {
|
||||
results: results.map((r) => r.metadata),
|
||||
title: path.relative(app.path.root, params.filePath),
|
||||
},
|
||||
output: results.at(-1)!.output,
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ import { FileTime } from "../file/time"
|
||||
import DESCRIPTION from "./patch.txt"
|
||||
|
||||
const PatchParams = z.object({
|
||||
patchText: z
|
||||
.string()
|
||||
.describe("The full patch text that describes all changes to be made"),
|
||||
patchText: z.string().describe("The full patch text that describes all changes to be made"),
|
||||
})
|
||||
|
||||
interface Change {
|
||||
@@ -42,10 +40,7 @@ function identifyFilesNeeded(patchText: string): string[] {
|
||||
const files: string[] = []
|
||||
const lines = patchText.split("\n")
|
||||
for (const line of lines) {
|
||||
if (
|
||||
line.startsWith("*** Update File:") ||
|
||||
line.startsWith("*** Delete File:")
|
||||
) {
|
||||
if (line.startsWith("*** Update File:") || line.startsWith("*** Delete File:")) {
|
||||
const filePath = line.split(":", 2)[1]?.trim()
|
||||
if (filePath) files.push(filePath)
|
||||
}
|
||||
@@ -65,10 +60,7 @@ function identifyFilesAdded(patchText: string): string[] {
|
||||
return files
|
||||
}
|
||||
|
||||
function textToPatch(
|
||||
patchText: string,
|
||||
_currentFiles: Record<string, string>,
|
||||
): [PatchOperation[], number] {
|
||||
function textToPatch(patchText: string, _currentFiles: Record<string, string>): [PatchOperation[], number] {
|
||||
const operations: PatchOperation[] = []
|
||||
const lines = patchText.split("\n")
|
||||
let i = 0
|
||||
@@ -93,11 +85,7 @@ function textToPatch(
|
||||
const changes: PatchChange[] = []
|
||||
i++
|
||||
|
||||
while (
|
||||
i < lines.length &&
|
||||
!lines[i].startsWith("@@") &&
|
||||
!lines[i].startsWith("***")
|
||||
) {
|
||||
while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("***")) {
|
||||
const changeLine = lines[i]
|
||||
if (changeLine.startsWith(" ")) {
|
||||
changes.push({ type: "keep", content: changeLine.substring(1) })
|
||||
@@ -151,10 +139,7 @@ function textToPatch(
|
||||
return [operations, fuzz]
|
||||
}
|
||||
|
||||
function patchToCommit(
|
||||
operations: PatchOperation[],
|
||||
currentFiles: Record<string, string>,
|
||||
): Commit {
|
||||
function patchToCommit(operations: PatchOperation[], currentFiles: Record<string, string>): Commit {
|
||||
const changes: Record<string, Change> = {}
|
||||
|
||||
for (const op of operations) {
|
||||
@@ -173,9 +158,7 @@ function patchToCommit(
|
||||
const lines = originalContent.split("\n")
|
||||
|
||||
for (const hunk of op.hunks) {
|
||||
const contextIndex = lines.findIndex((line) =>
|
||||
line.includes(hunk.contextLine),
|
||||
)
|
||||
const contextIndex = lines.findIndex((line) => line.includes(hunk.contextLine))
|
||||
if (contextIndex === -1) {
|
||||
throw new Error(`Context line not found: ${hunk.contextLine}`)
|
||||
}
|
||||
@@ -204,11 +187,7 @@ function patchToCommit(
|
||||
return { changes }
|
||||
}
|
||||
|
||||
function generateDiff(
|
||||
oldContent: string,
|
||||
newContent: string,
|
||||
filePath: string,
|
||||
): [string, number, number] {
|
||||
function generateDiff(oldContent: string, newContent: string, filePath: string): [string, number, number] {
|
||||
// Mock implementation - would need actual diff generation
|
||||
const lines1 = oldContent.split("\n")
|
||||
const lines2 = newContent.split("\n")
|
||||
@@ -296,9 +275,7 @@ export const PatchTool = Tool.define({
|
||||
// Process the patch
|
||||
const [patch, fuzz] = textToPatch(params.patchText, currentFiles)
|
||||
if (fuzz > 3) {
|
||||
throw new Error(
|
||||
`patch contains fuzzy matches (fuzz level: ${fuzz}). Please make your context lines more precise`,
|
||||
)
|
||||
throw new Error(`patch contains fuzzy matches (fuzz level: ${fuzz}). Please make your context lines more precise`)
|
||||
}
|
||||
|
||||
// Convert patch to commit
|
||||
@@ -343,11 +320,7 @@ export const PatchTool = Tool.define({
|
||||
const newContent = change.new_content || ""
|
||||
|
||||
// Calculate diff statistics
|
||||
const [, additions, removals] = generateDiff(
|
||||
oldContent,
|
||||
newContent,
|
||||
filePath,
|
||||
)
|
||||
const [, additions, removals] = generateDiff(oldContent, newContent, filePath)
|
||||
totalAdditions += additions
|
||||
totalRemovals += removals
|
||||
|
||||
@@ -358,11 +331,11 @@ export const PatchTool = Tool.define({
|
||||
const output = result
|
||||
|
||||
return {
|
||||
title: `${filesToRead.length} files`,
|
||||
metadata: {
|
||||
changed: changedFiles,
|
||||
additions: totalAdditions,
|
||||
removals: totalRemovals,
|
||||
title: `${filesToRead.length} files`,
|
||||
},
|
||||
output,
|
||||
}
|
||||
|
||||
@@ -16,14 +16,8 @@ export const ReadTool = Tool.define({
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The path to the file to read"),
|
||||
offset: z
|
||||
.number()
|
||||
.describe("The line number to start reading from (0-based)")
|
||||
.optional(),
|
||||
limit: z
|
||||
.number()
|
||||
.describe("The number of lines to read (defaults to 2000)")
|
||||
.optional(),
|
||||
offset: z.number().describe("The line number to start reading from (0-based)").optional(),
|
||||
limit: z.number().describe("The number of lines to read (defaults to 2000)").optional(),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
let filePath = params.filePath
|
||||
@@ -40,16 +34,13 @@ export const ReadTool = Tool.define({
|
||||
const suggestions = dirEntries
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.toLowerCase().includes(base.toLowerCase()) ||
|
||||
base.toLowerCase().includes(entry.toLowerCase()),
|
||||
entry.toLowerCase().includes(base.toLowerCase()) || base.toLowerCase().includes(entry.toLowerCase()),
|
||||
)
|
||||
.map((entry) => path.join(dir, entry))
|
||||
.slice(0, 3)
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
throw new Error(
|
||||
`File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`,
|
||||
)
|
||||
throw new Error(`File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
|
||||
}
|
||||
|
||||
throw new Error(`File not found: ${filePath}`)
|
||||
@@ -57,21 +48,14 @@ export const ReadTool = Tool.define({
|
||||
const stats = await file.stat()
|
||||
|
||||
if (stats.size > MAX_READ_SIZE)
|
||||
throw new Error(
|
||||
`File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`,
|
||||
)
|
||||
throw new Error(`File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`)
|
||||
const limit = params.limit ?? DEFAULT_READ_LIMIT
|
||||
const offset = params.offset || 0
|
||||
const isImage = isImageFile(filePath)
|
||||
if (isImage)
|
||||
throw new Error(
|
||||
`This is an image file of type: ${isImage}\nUse a different tool to process images`,
|
||||
)
|
||||
if (isImage) throw new Error(`This is an image file of type: ${isImage}\nUse a different tool to process images`)
|
||||
const lines = await file.text().then((text) => text.split("\n"))
|
||||
const raw = lines.slice(offset, offset + limit).map((line) => {
|
||||
return line.length > MAX_LINE_LENGTH
|
||||
? line.substring(0, MAX_LINE_LENGTH) + "..."
|
||||
: line
|
||||
return line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line
|
||||
})
|
||||
const content = raw.map((line, index) => {
|
||||
return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`
|
||||
@@ -82,21 +66,19 @@ export const ReadTool = Tool.define({
|
||||
output += content.join("\n")
|
||||
|
||||
if (lines.length > offset + content.length) {
|
||||
output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${
|
||||
offset + content.length
|
||||
})`
|
||||
output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${offset + content.length})`
|
||||
}
|
||||
output += "\n</file>"
|
||||
|
||||
// just warms the lsp client
|
||||
await LSP.touchFile(filePath, true)
|
||||
await LSP.touchFile(filePath, false)
|
||||
FileTime.read(ctx.sessionID, filePath)
|
||||
|
||||
return {
|
||||
title: path.relative(App.info().path.root, filePath),
|
||||
output,
|
||||
metadata: {
|
||||
preview,
|
||||
title: path.relative(App.info().path.root, filePath),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage:
|
||||
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
|
||||
- Any lines longer than 2000 characters will be truncated
|
||||
- Results are returned using cat -n format, with line numbers starting at 1
|
||||
- This tool allows OpenCode to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as OpenCode is a multimodal LLM.
|
||||
- This tool allows opencode to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as opencode is a multimodal LLM.
|
||||
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
|
||||
- You will regularly be asked to read screenshots. If the user provides a path to a screenshot ALWAYS use this tool to view the file at the path. This tool will work with all temporary file paths like /var/folders/123/abc/T/TemporaryItems/NSIRD_screencaptureui_ZfB1tD/Screenshot.png
|
||||
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
|
||||
|
||||
@@ -3,41 +3,36 @@ import DESCRIPTION from "./task.txt"
|
||||
import { z } from "zod"
|
||||
import { Session } from "../session"
|
||||
import { Bus } from "../bus"
|
||||
import { Message } from "../session/message"
|
||||
import { MessageV2 } from "../session/message-v2"
|
||||
|
||||
export const TaskTool = Tool.define({
|
||||
id: "task",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
description: z
|
||||
.string()
|
||||
.describe("A short (3-5 words) description of the task"),
|
||||
description: z.string().describe("A short (3-5 words) description of the task"),
|
||||
prompt: z.string().describe("The task for the agent to perform"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const session = await Session.create(ctx.sessionID)
|
||||
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
|
||||
const metadata = msg.metadata.assistant!
|
||||
const msg = (await Session.getMessage(ctx.sessionID, ctx.messageID)) as MessageV2.Assistant
|
||||
|
||||
function summary(input: Message.Info) {
|
||||
function summary(input: MessageV2.Info) {
|
||||
const result = []
|
||||
|
||||
for (const part of input.parts) {
|
||||
if (part.type === "tool-invocation") {
|
||||
result.push({
|
||||
toolInvocation: part.toolInvocation,
|
||||
metadata: input.metadata.tool[part.toolInvocation.toolCallId],
|
||||
})
|
||||
if (part.type === "tool" && part.state.status === "completed") {
|
||||
result.push(part)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const unsub = Bus.subscribe(Message.Event.Updated, async (evt) => {
|
||||
if (evt.properties.info.metadata.sessionID !== session.id) return
|
||||
const unsub = Bus.subscribe(MessageV2.Event.Updated, async (evt) => {
|
||||
if (evt.properties.info.sessionID !== session.id) return
|
||||
ctx.metadata({
|
||||
title: params.description,
|
||||
summary: summary(evt.properties.info),
|
||||
metadata: {
|
||||
summary: summary(evt.properties.info),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,8 +41,8 @@ export const TaskTool = Tool.define({
|
||||
})
|
||||
const result = await Session.chat({
|
||||
sessionID: session.id,
|
||||
modelID: metadata.modelID,
|
||||
providerID: metadata.providerID,
|
||||
modelID: msg.modelID,
|
||||
providerID: msg.providerID,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
@@ -57,8 +52,8 @@ export const TaskTool = Tool.define({
|
||||
})
|
||||
unsub()
|
||||
return {
|
||||
title: params.description,
|
||||
metadata: {
|
||||
title: params.description,
|
||||
summary: summary(result),
|
||||
},
|
||||
output: result.parts.findLast((x) => x.type === "text")!.text,
|
||||
|
||||
@@ -5,12 +5,8 @@ import { App } from "../app/app"
|
||||
|
||||
const TodoInfo = z.object({
|
||||
content: z.string().min(1).describe("Brief description of the task"),
|
||||
status: z
|
||||
.enum(["pending", "in_progress", "completed"])
|
||||
.describe("Current status of the task"),
|
||||
priority: z
|
||||
.enum(["high", "medium", "low"])
|
||||
.describe("Priority level of the task"),
|
||||
status: z.enum(["pending", "in_progress", "completed"]).describe("Current status of the task"),
|
||||
priority: z.enum(["high", "medium", "low"]).describe("Priority level of the task"),
|
||||
id: z.string().describe("Unique identifier for the todo item"),
|
||||
})
|
||||
type TodoInfo = z.infer<typeof TodoInfo>
|
||||
@@ -32,9 +28,9 @@ export const TodoWriteTool = Tool.define({
|
||||
const todos = state()
|
||||
todos[opts.sessionID] = params.todos
|
||||
return {
|
||||
title: `${params.todos.filter((x) => x.status !== "completed").length} todos`,
|
||||
output: JSON.stringify(params.todos, null, 2),
|
||||
metadata: {
|
||||
title: `${params.todos.filter((x) => x.status !== "completed").length} todos`,
|
||||
todos: params.todos,
|
||||
},
|
||||
}
|
||||
@@ -48,9 +44,9 @@ export const TodoReadTool = Tool.define({
|
||||
async execute(_params, opts) {
|
||||
const todos = state()[opts.sessionID] ?? []
|
||||
return {
|
||||
title: `${todos.filter((x) => x.status !== "completed").length} todos`,
|
||||
metadata: {
|
||||
todos,
|
||||
title: `${todos.filter((x) => x.status !== "completed").length} todos`,
|
||||
},
|
||||
output: JSON.stringify(todos, null, 2),
|
||||
}
|
||||
|
||||
@@ -2,19 +2,15 @@ import type { StandardSchemaV1 } from "@standard-schema/spec"
|
||||
|
||||
export namespace Tool {
|
||||
interface Metadata {
|
||||
title: string
|
||||
[key: string]: any
|
||||
}
|
||||
export type Context<M extends Metadata = Metadata> = {
|
||||
sessionID: string
|
||||
messageID: string
|
||||
abort: AbortSignal
|
||||
metadata(meta: M): void
|
||||
metadata(input: { title?: string; metadata?: M }): void
|
||||
}
|
||||
export interface Info<
|
||||
Parameters extends StandardSchemaV1 = StandardSchemaV1,
|
||||
M extends Metadata = Metadata,
|
||||
> {
|
||||
export interface Info<Parameters extends StandardSchemaV1 = StandardSchemaV1, M extends Metadata = Metadata> {
|
||||
id: string
|
||||
description: string
|
||||
parameters: Parameters
|
||||
@@ -22,15 +18,15 @@ export namespace Tool {
|
||||
args: StandardSchemaV1.InferOutput<Parameters>,
|
||||
ctx: Context,
|
||||
): Promise<{
|
||||
title: string
|
||||
metadata: M
|
||||
output: string
|
||||
}>
|
||||
}
|
||||
|
||||
export function define<
|
||||
Parameters extends StandardSchemaV1,
|
||||
Result extends Metadata,
|
||||
>(input: Info<Parameters, Result>): Info<Parameters, Result> {
|
||||
export function define<Parameters extends StandardSchemaV1, Result extends Metadata>(
|
||||
input: Info<Parameters, Result>,
|
||||
): Info<Parameters, Result> {
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,7 @@ export const WebFetchTool = Tool.define({
|
||||
url: z.string().describe("The URL to fetch content from"),
|
||||
format: z
|
||||
.enum(["text", "markdown", "html"])
|
||||
.describe(
|
||||
"The format to return the content in (text, markdown, or html)",
|
||||
),
|
||||
.describe("The format to return the content in (text, markdown, or html)"),
|
||||
timeout: z
|
||||
.number()
|
||||
.min(0)
|
||||
@@ -26,17 +24,11 @@ export const WebFetchTool = Tool.define({
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
// Validate URL
|
||||
if (
|
||||
!params.url.startsWith("http://") &&
|
||||
!params.url.startsWith("https://")
|
||||
) {
|
||||
if (!params.url.startsWith("http://") && !params.url.startsWith("https://")) {
|
||||
throw new Error("URL must start with http:// or https://")
|
||||
}
|
||||
|
||||
const timeout = Math.min(
|
||||
(params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000,
|
||||
MAX_TIMEOUT,
|
||||
)
|
||||
const timeout = Math.min((params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000, MAX_TIMEOUT)
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
@@ -46,8 +38,7 @@ export const WebFetchTool = Tool.define({
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
Accept:
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
},
|
||||
})
|
||||
@@ -79,16 +70,14 @@ export const WebFetchTool = Tool.define({
|
||||
const text = await extractTextFromHTML(content)
|
||||
return {
|
||||
output: text,
|
||||
metadata: {
|
||||
title,
|
||||
},
|
||||
title,
|
||||
metadata: {},
|
||||
}
|
||||
}
|
||||
return {
|
||||
output: content,
|
||||
metadata: {
|
||||
title,
|
||||
},
|
||||
title,
|
||||
metadata: {},
|
||||
}
|
||||
|
||||
case "markdown":
|
||||
@@ -96,32 +85,28 @@ export const WebFetchTool = Tool.define({
|
||||
const markdown = convertHTMLToMarkdown(content)
|
||||
return {
|
||||
output: markdown,
|
||||
metadata: {
|
||||
title,
|
||||
},
|
||||
title,
|
||||
metadata: {},
|
||||
}
|
||||
}
|
||||
return {
|
||||
output: "```\n" + content + "\n```",
|
||||
metadata: {
|
||||
title,
|
||||
},
|
||||
title,
|
||||
metadata: {},
|
||||
}
|
||||
|
||||
case "html":
|
||||
return {
|
||||
output: content,
|
||||
metadata: {
|
||||
title,
|
||||
},
|
||||
title,
|
||||
metadata: {},
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
output: content,
|
||||
metadata: {
|
||||
title,
|
||||
},
|
||||
title,
|
||||
metadata: {},
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -143,16 +128,7 @@ async function extractTextFromHTML(html: string) {
|
||||
.on("*", {
|
||||
element(element) {
|
||||
// Reset skip flag when entering other elements
|
||||
if (
|
||||
![
|
||||
"script",
|
||||
"style",
|
||||
"noscript",
|
||||
"iframe",
|
||||
"object",
|
||||
"embed",
|
||||
].includes(element.tagName)
|
||||
) {
|
||||
if (!["script", "style", "noscript", "iframe", "object", "embed"].includes(element.tagName)) {
|
||||
skipContent = false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,18 +13,12 @@ export const WriteTool = Tool.define({
|
||||
id: "write",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z
|
||||
.string()
|
||||
.describe(
|
||||
"The absolute path to the file to write (must be absolute, not relative)",
|
||||
),
|
||||
filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"),
|
||||
content: z.string().describe("The content to write to the file"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const app = App.info()
|
||||
const filepath = path.isAbsolute(params.filePath)
|
||||
? params.filePath
|
||||
: path.join(app.path.cwd, params.filePath)
|
||||
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
|
||||
|
||||
const file = Bun.file(filepath)
|
||||
const exists = await file.exists()
|
||||
@@ -33,9 +27,7 @@ export const WriteTool = Tool.define({
|
||||
await Permission.ask({
|
||||
id: "write",
|
||||
sessionID: ctx.sessionID,
|
||||
title: exists
|
||||
? "Overwrite this file: " + filepath
|
||||
: "Create new file: " + filepath,
|
||||
title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
|
||||
metadata: {
|
||||
filePath: filepath,
|
||||
content: params.content,
|
||||
@@ -62,11 +54,11 @@ export const WriteTool = Tool.define({
|
||||
}
|
||||
|
||||
return {
|
||||
title: path.relative(app.path.root, filepath),
|
||||
metadata: {
|
||||
diagnostics,
|
||||
filepath,
|
||||
exists: exists,
|
||||
title: path.relative(app.path.root, filepath),
|
||||
},
|
||||
output,
|
||||
}
|
||||
|
||||
@@ -7,10 +7,7 @@ export abstract class NamedError extends Error {
|
||||
abstract schema(): ZodSchema
|
||||
abstract toObject(): { name: string; data: any }
|
||||
|
||||
static create<Name extends string, Data extends ZodSchema>(
|
||||
name: Name,
|
||||
data: Data,
|
||||
) {
|
||||
static create<Name extends string, Data extends ZodSchema>(name: Name, data: Data) {
|
||||
const schema = z
|
||||
.object({
|
||||
name: z.literal(name),
|
||||
|
||||
@@ -15,4 +15,28 @@ export namespace Filesystem {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export async function globUp(pattern: string, start: string, stop?: string) {
|
||||
let current = start
|
||||
const result = []
|
||||
while (true) {
|
||||
try {
|
||||
const glob = new Bun.Glob(pattern)
|
||||
for await (const match of glob.scan({
|
||||
cwd: current,
|
||||
onlyFiles: true,
|
||||
dot: true,
|
||||
})) {
|
||||
result.push(join(current, match))
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid glob patterns
|
||||
}
|
||||
if (stop === current) break
|
||||
const parent = dirname(current)
|
||||
if (parent === current) break
|
||||
current = parent
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export function lazy<T>(fn: () => T) {
|
||||
|
||||
return (): T => {
|
||||
if (loaded) return value as T
|
||||
loaded = true
|
||||
value = fn()
|
||||
return value as T
|
||||
}
|
||||
|
||||
@@ -19,10 +19,7 @@ export namespace Log {
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
cleanup(dir)
|
||||
if (options.print) return
|
||||
logpath = path.join(
|
||||
dir,
|
||||
new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
|
||||
)
|
||||
logpath = path.join(dir, new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log")
|
||||
const logfile = Bun.file(logpath)
|
||||
await fs.truncate(logpath).catch(() => {})
|
||||
const writer = logfile.writer()
|
||||
@@ -43,9 +40,7 @@ export namespace Log {
|
||||
|
||||
const filesToDelete = files.slice(0, -10)
|
||||
|
||||
await Promise.all(
|
||||
filesToDelete.map((file) => fs.unlink(file).catch(() => {})),
|
||||
)
|
||||
await Promise.all(filesToDelete.map((file) => fs.unlink(file).catch(() => {})))
|
||||
}
|
||||
|
||||
let last = Date.now()
|
||||
@@ -63,11 +58,7 @@ export namespace Log {
|
||||
const next = new Date()
|
||||
const diff = next.getTime() - last
|
||||
last = next.getTime()
|
||||
return (
|
||||
[next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message]
|
||||
.filter(Boolean)
|
||||
.join(" ") + "\n"
|
||||
)
|
||||
return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
|
||||
}
|
||||
const result = {
|
||||
info(message?: any, extra?: Record<string, any>) {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||
let timeout: NodeJS.Timeout
|
||||
return Promise.race([
|
||||
promise.then((result) => {
|
||||
clearTimeout(timeout)
|
||||
return result
|
||||
}),
|
||||
new Promise<never>((_, reject) => {
|
||||
timeout = setTimeout(() => {
|
||||
reject(new Error(`Operation timed out after ${ms}ms`))
|
||||
}, ms)
|
||||
}),
|
||||
])
|
||||
}
|
||||
@@ -17,12 +17,7 @@ const testCases: TestCase[] = [
|
||||
replace: 'console.log("universe");',
|
||||
},
|
||||
{
|
||||
content: [
|
||||
"if (condition) {",
|
||||
" doSomething();",
|
||||
" doSomethingElse();",
|
||||
"}",
|
||||
].join("\n"),
|
||||
content: ["if (condition) {", " doSomething();", " doSomethingElse();", "}"].join("\n"),
|
||||
find: [" doSomething();", " doSomethingElse();"].join("\n"),
|
||||
replace: [" doNewThing();", " doAnotherThing();"].join("\n"),
|
||||
},
|
||||
@@ -53,15 +48,8 @@ const testCases: TestCase[] = [
|
||||
" return result;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"function calculate(a, b) {",
|
||||
" // different middle content",
|
||||
" return result;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["function calculate(a, b) {", " return a * b * 2;", "}"].join(
|
||||
"\n",
|
||||
),
|
||||
find: ["function calculate(a, b) {", " // different middle content", " return result;", "}"].join("\n"),
|
||||
replace: ["function calculate(a, b) {", " return a * b * 2;", "}"].join("\n"),
|
||||
},
|
||||
{
|
||||
content: [
|
||||
@@ -76,13 +64,7 @@ const testCases: TestCase[] = [
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: ["class MyClass {", " // different implementation", "}"].join("\n"),
|
||||
replace: [
|
||||
"class MyClass {",
|
||||
" constructor() {",
|
||||
" this.value = 42;",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["class MyClass {", " constructor() {", " this.value = 42;", " }", "}"].join("\n"),
|
||||
},
|
||||
|
||||
// WhitespaceNormalizedReplacer cases
|
||||
@@ -104,48 +86,21 @@ const testCases: TestCase[] = [
|
||||
|
||||
// IndentationFlexibleReplacer cases
|
||||
{
|
||||
content: [
|
||||
" function nested() {",
|
||||
' console.log("deeply nested");',
|
||||
" return true;",
|
||||
" }",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"function nested() {",
|
||||
' console.log("deeply nested");',
|
||||
" return true;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: [
|
||||
"function nested() {",
|
||||
' console.log("updated");',
|
||||
" return false;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
content: [" function nested() {", ' console.log("deeply nested");', " return true;", " }"].join(
|
||||
"\n",
|
||||
),
|
||||
find: ["function nested() {", ' console.log("deeply nested");', " return true;", "}"].join("\n"),
|
||||
replace: ["function nested() {", ' console.log("updated");', " return false;", "}"].join("\n"),
|
||||
},
|
||||
{
|
||||
content: [
|
||||
" if (true) {",
|
||||
' console.log("level 1");',
|
||||
' console.log("level 2");',
|
||||
" }",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"if (true) {",
|
||||
'console.log("level 1");',
|
||||
' console.log("level 2");',
|
||||
"}",
|
||||
].join("\n"),
|
||||
content: [" if (true) {", ' console.log("level 1");', ' console.log("level 2");', " }"].join("\n"),
|
||||
find: ["if (true) {", 'console.log("level 1");', ' console.log("level 2");', "}"].join("\n"),
|
||||
replace: ["if (true) {", 'console.log("updated");', "}"].join("\n"),
|
||||
},
|
||||
|
||||
// replaceAll option cases
|
||||
{
|
||||
content: [
|
||||
'console.log("test");',
|
||||
'console.log("test");',
|
||||
'console.log("test");',
|
||||
].join("\n"),
|
||||
content: ['console.log("test");', 'console.log("test");', 'console.log("test");'].join("\n"),
|
||||
find: 'console.log("test");',
|
||||
replace: 'console.log("updated");',
|
||||
all: true,
|
||||
@@ -213,9 +168,7 @@ const testCases: TestCase[] = [
|
||||
|
||||
// MultiOccurrenceReplacer cases (with replaceAll)
|
||||
{
|
||||
content: ["debug('start');", "debug('middle');", "debug('end');"].join(
|
||||
"\n",
|
||||
),
|
||||
content: ["debug('start');", "debug('middle');", "debug('end');"].join("\n"),
|
||||
find: "debug",
|
||||
replace: "log",
|
||||
all: true,
|
||||
@@ -239,9 +192,7 @@ const testCases: TestCase[] = [
|
||||
replace: "const value = 24;",
|
||||
},
|
||||
{
|
||||
content: ["", " if (condition) {", " doSomething();", " }", ""].join(
|
||||
"\n",
|
||||
),
|
||||
content: ["", " if (condition) {", " doSomething();", " }", ""].join("\n"),
|
||||
find: ["if (condition) {", " doSomething();", "}"].join("\n"),
|
||||
replace: ["if (condition) {", " doNothing();", "}"].join("\n"),
|
||||
},
|
||||
@@ -262,9 +213,7 @@ const testCases: TestCase[] = [
|
||||
" return result;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["function calculate(a, b) {", " return (a + b) * 2;", "}"].join(
|
||||
"\n",
|
||||
),
|
||||
replace: ["function calculate(a, b) {", " return (a + b) * 2;", "}"].join("\n"),
|
||||
},
|
||||
{
|
||||
content: [
|
||||
@@ -278,15 +227,8 @@ const testCases: TestCase[] = [
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"class TestClass {",
|
||||
" // different implementation",
|
||||
" // with multiple lines",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["class TestClass {", " getValue() { return 42; }", "}"].join(
|
||||
"\n",
|
||||
),
|
||||
find: ["class TestClass {", " // different implementation", " // with multiple lines", "}"].join("\n"),
|
||||
replace: ["class TestClass {", " getValue() { return 42; }", "}"].join("\n"),
|
||||
},
|
||||
|
||||
// Combined edge cases for new replacers
|
||||
@@ -296,9 +238,7 @@ const testCases: TestCase[] = [
|
||||
replace: 'console.log("updated");',
|
||||
},
|
||||
{
|
||||
content: [" ", "function test() {", " return 'value';", "}", " "].join(
|
||||
"\n",
|
||||
),
|
||||
content: [" ", "function test() {", " return 'value';", "}", " "].join("\n"),
|
||||
find: ["function test() {", "return 'value';", "}"].join("\n"),
|
||||
replace: ["function test() {", "return 'new value';", "}"].join("\n"),
|
||||
},
|
||||
@@ -346,13 +286,7 @@ const testCases: TestCase[] = [
|
||||
|
||||
// ContextAwareReplacer - test with trailing newline in find string
|
||||
{
|
||||
content: [
|
||||
"class Test {",
|
||||
" method1() {",
|
||||
" return 1;",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
content: ["class Test {", " method1() {", " return 1;", " }", "}"].join("\n"),
|
||||
find: [
|
||||
"class Test {",
|
||||
" // different content",
|
||||
@@ -401,12 +335,7 @@ describe("EditTool Replacers", () => {
|
||||
replace(testCase.content, testCase.find, testCase.replace, testCase.all)
|
||||
}).toThrow()
|
||||
} else {
|
||||
const result = replace(
|
||||
testCase.content,
|
||||
testCase.find,
|
||||
testCase.replace,
|
||||
testCase.all,
|
||||
)
|
||||
const result = replace(testCase.content, testCase.find, testCase.replace, testCase.all)
|
||||
expect(result).toContain(testCase.replace)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -42,10 +42,7 @@ describe("tool.glob", () => {
|
||||
describe("tool.ls", () => {
|
||||
test("basic", async () => {
|
||||
const result = await App.provide({ cwd: process.cwd() }, async () => {
|
||||
return await ListTool.execute(
|
||||
{ path: "./example", ignore: [".git"] },
|
||||
ctx,
|
||||
)
|
||||
return await ListTool.execute({ path: "./example", ignore: [".git"] }, ctx)
|
||||
})
|
||||
expect(result.output).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/tui"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
|
||||
var Version = "dev"
|
||||
@@ -78,15 +77,15 @@ func main() {
|
||||
tea.WithMouseCellMotion(),
|
||||
)
|
||||
|
||||
evts, err := client.Event(httpClient, url, ctx)
|
||||
if err != nil {
|
||||
slog.Error("Failed to subscribe to events", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for item := range evts {
|
||||
program.Send(item)
|
||||
stream := httpClient.Event.ListStreaming(ctx)
|
||||
for stream.Next() {
|
||||
evt := stream.Current().AsUnion()
|
||||
program.Send(evt)
|
||||
}
|
||||
if err := stream.Err(); err != nil {
|
||||
slog.Error("Error streaming events", "error", err)
|
||||
program.Send(err)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
+4
-4
@@ -10,22 +10,23 @@ require (
|
||||
github.com/charmbracelet/glamour v0.10.0
|
||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
|
||||
github.com/charmbracelet/x/ansi v0.8.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lithammer/fuzzysearch v1.1.8
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
|
||||
github.com/muesli/reflow v0.3.0
|
||||
github.com/muesli/termenv v0.16.0
|
||||
github.com/oapi-codegen/runtime v1.1.1
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
|
||||
github.com/sst/opencode-sdk-go v0.1.0-alpha.5
|
||||
github.com/sst/opencode-sdk-go v0.1.0-alpha.8
|
||||
github.com/tidwall/gjson v1.14.4
|
||||
rsc.io/qr v0.2.0
|
||||
)
|
||||
|
||||
replace github.com/sst/opencode-sdk-go => ./sdk
|
||||
|
||||
require golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/atombender/go-jsonschema v0.20.0 // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||
@@ -37,7 +38,6 @@ require (
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/goccy/go-yaml v1.17.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/invopop/yaml v0.3.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
|
||||
@@ -4,15 +4,12 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.18.0 h1:6h53Q4hW83SuF+jcsp7CVhLsMozzvQvO8HBbKQW+gn4=
|
||||
github.com/alecthomas/chroma/v2 v2.18.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
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=
|
||||
@@ -23,7 +20,6 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE=
|
||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
|
||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3 h1:5A2e3myxXMpCES+kjEWgGsaf9VgZXjZbLi5iMTH7j40=
|
||||
@@ -110,7 +106,6 @@ github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
|
||||
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -148,8 +143,6 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 h1:ykgG34472DWey7TSjd8vIfNykXgjOgYJZoQbKfEeY/Q=
|
||||
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1/go.mod h1:N5+lY1tiTDV3V1BeHtOxeWXHoPVeApvsvjJqegfoaz8=
|
||||
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
|
||||
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
@@ -190,12 +183,8 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/sst/opencode-sdk-go v0.1.0-alpha.5 h1:iZjdSHLo6jOMjUbDH5JWi+44v76yNbEktsRqG/Qxrco=
|
||||
github.com/sst/opencode-sdk-go v0.1.0-alpha.5/go.mod h1:uagorfAHZsVy6vf0xY6TlQraM4uCILdZ5tKKhl1oToM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
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=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
|
||||
@@ -20,8 +20,6 @@ import (
|
||||
"github.com/sst/opencode/internal/util"
|
||||
)
|
||||
|
||||
var RootPath string
|
||||
|
||||
type App struct {
|
||||
Info opencode.App
|
||||
Version string
|
||||
@@ -32,11 +30,12 @@ type App struct {
|
||||
Provider *opencode.Provider
|
||||
Model *opencode.Model
|
||||
Session *opencode.Session
|
||||
Messages []opencode.Message
|
||||
Messages []opencode.MessageUnion
|
||||
Commands commands.CommandRegistry
|
||||
}
|
||||
|
||||
type SessionSelectedMsg = *opencode.Session
|
||||
type SessionLoadedMsg struct{}
|
||||
type ModelSelectedMsg struct {
|
||||
Provider opencode.Provider
|
||||
Model opencode.Model
|
||||
@@ -45,13 +44,13 @@ type SessionClearedMsg struct{}
|
||||
type CompactSessionMsg struct{}
|
||||
type SendMsg struct {
|
||||
Text string
|
||||
Attachments []Attachment
|
||||
}
|
||||
type CompletionDialogTriggeredMsg struct {
|
||||
InitialValue string
|
||||
Attachments []opencode.FilePartParam
|
||||
}
|
||||
type OptimisticMessageAddedMsg struct {
|
||||
Message opencode.Message
|
||||
Message opencode.MessageUnion
|
||||
}
|
||||
type FileRenderedMsg struct {
|
||||
FilePath string
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -60,7 +59,8 @@ func New(
|
||||
appInfo opencode.App,
|
||||
httpClient *opencode.Client,
|
||||
) (*App, error) {
|
||||
RootPath = appInfo.Path.Root
|
||||
util.RootPath = appInfo.Path.Root
|
||||
util.CwdPath = appInfo.Path.Cwd
|
||||
|
||||
configInfo, err := httpClient.Config.Get(ctx)
|
||||
if err != nil {
|
||||
@@ -116,13 +116,30 @@ func New(
|
||||
State: appState,
|
||||
Client: httpClient,
|
||||
Session: &opencode.Session{},
|
||||
Messages: []opencode.Message{},
|
||||
Messages: []opencode.MessageUnion{},
|
||||
Commands: commands.LoadFromConfig(configInfo),
|
||||
}
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (a *App) Key(commandName commands.CommandName) string {
|
||||
t := theme.CurrentTheme()
|
||||
base := styles.NewStyle().Background(t.Background()).Foreground(t.Text()).Bold(true).Render
|
||||
muted := styles.NewStyle().
|
||||
Background(t.Background()).
|
||||
Foreground(t.TextMuted()).
|
||||
Faint(true).
|
||||
Render
|
||||
command := a.Commands[commandName]
|
||||
kb := command.Keybindings[0]
|
||||
key := kb.Key
|
||||
if kb.RequiresLeader {
|
||||
key = a.Config.Keybinds.Leader + " " + kb.Key
|
||||
}
|
||||
return base(key) + muted(" "+command.Description)
|
||||
}
|
||||
|
||||
func (a *App) InitializeProvider() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
providersResponse, err := a.Client.Config.Providers(context.Background())
|
||||
@@ -185,7 +202,10 @@ func (a *App) InitializeProvider() tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func getDefaultModel(response *opencode.ConfigProvidersResponse, provider opencode.Provider) *opencode.Model {
|
||||
func getDefaultModel(
|
||||
response *opencode.ConfigProvidersResponse,
|
||||
provider opencode.Provider,
|
||||
) *opencode.Model {
|
||||
if match, ok := response.Default[provider.ID]; ok {
|
||||
model := provider.Models[match]
|
||||
return &model
|
||||
@@ -197,20 +217,16 @@ func getDefaultModel(response *opencode.ConfigProvidersResponse, provider openco
|
||||
return nil
|
||||
}
|
||||
|
||||
type Attachment struct {
|
||||
FilePath string
|
||||
FileName string
|
||||
MimeType string
|
||||
Content []byte
|
||||
}
|
||||
|
||||
func (a *App) IsBusy() bool {
|
||||
if len(a.Messages) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
lastMessage := a.Messages[len(a.Messages)-1]
|
||||
return lastMessage.Metadata.Time.Completed == 0
|
||||
if casted, ok := lastMessage.(opencode.AssistantMessage); ok {
|
||||
return casted.Time.Completed == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) SaveState() {
|
||||
@@ -276,29 +292,43 @@ func (a *App) CreateSession(ctx context.Context) (*opencode.Session, error) {
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (a *App) SendChatMessage(ctx context.Context, text string, attachments []Attachment) tea.Cmd {
|
||||
func (a *App) SendChatMessage(
|
||||
ctx context.Context,
|
||||
text string,
|
||||
attachments []opencode.FilePartParam,
|
||||
) (*App, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
if a.Session.ID == "" {
|
||||
session, err := a.CreateSession(ctx)
|
||||
if err != nil {
|
||||
return toast.NewErrorToast(err.Error())
|
||||
return a, toast.NewErrorToast(err.Error())
|
||||
}
|
||||
a.Session = session
|
||||
cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
|
||||
}
|
||||
|
||||
optimisticMessage := opencode.Message{
|
||||
ID: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()),
|
||||
Role: opencode.MessageRoleUser,
|
||||
Parts: []opencode.MessagePart{{
|
||||
Type: opencode.MessagePartTypeText,
|
||||
Text: text,
|
||||
}},
|
||||
Metadata: opencode.MessageMetadata{
|
||||
SessionID: a.Session.ID,
|
||||
Time: opencode.MessageMetadataTime{
|
||||
Created: float64(time.Now().Unix()),
|
||||
},
|
||||
optimisticParts := []opencode.UserMessagePart{{
|
||||
Type: opencode.UserMessagePartTypeText,
|
||||
Text: text,
|
||||
}}
|
||||
if len(attachments) > 0 {
|
||||
for _, attachment := range attachments {
|
||||
optimisticParts = append(optimisticParts, opencode.UserMessagePart{
|
||||
Type: opencode.UserMessagePartTypeFile,
|
||||
Filename: attachment.Filename.Value,
|
||||
Mime: attachment.Mime.Value,
|
||||
URL: attachment.URL.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
optimisticMessage := opencode.UserMessage{
|
||||
ID: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()),
|
||||
Role: opencode.UserMessageRoleUser,
|
||||
Parts: optimisticParts,
|
||||
SessionID: a.Session.ID,
|
||||
Time: opencode.UserMessageTime{
|
||||
Created: float64(time.Now().Unix()),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -306,13 +336,25 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
|
||||
cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage}))
|
||||
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
parts := []opencode.UserMessagePartUnionParam{
|
||||
opencode.TextPartParam{
|
||||
Type: opencode.F(opencode.TextPartTypeText),
|
||||
Text: opencode.F(text),
|
||||
},
|
||||
}
|
||||
if len(attachments) > 0 {
|
||||
for _, attachment := range attachments {
|
||||
parts = append(parts, opencode.FilePartParam{
|
||||
Mime: attachment.Mime,
|
||||
Type: attachment.Type,
|
||||
URL: attachment.URL,
|
||||
Filename: attachment.Filename,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{
|
||||
Parts: opencode.F([]opencode.MessagePartUnionParam{
|
||||
opencode.TextPartParam{
|
||||
Type: opencode.F(opencode.TextPartTypeText),
|
||||
Text: opencode.F(text),
|
||||
},
|
||||
}),
|
||||
Parts: opencode.F(parts),
|
||||
ProviderID: opencode.F(a.Provider.ID),
|
||||
ModelID: opencode.F(a.Model.ID),
|
||||
})
|
||||
@@ -326,7 +368,7 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
|
||||
|
||||
// The actual response will come through SSE
|
||||
// For now, just return success
|
||||
return tea.Batch(cmds...)
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (a *App) Cancel(ctx context.Context, sessionID string) error {
|
||||
|
||||
@@ -75,18 +75,21 @@ const (
|
||||
SessionNewCommand CommandName = "session_new"
|
||||
SessionListCommand CommandName = "session_list"
|
||||
SessionShareCommand CommandName = "session_share"
|
||||
SessionUnshareCommand CommandName = "session_unshare"
|
||||
SessionInterruptCommand CommandName = "session_interrupt"
|
||||
SessionCompactCommand CommandName = "session_compact"
|
||||
ToolDetailsCommand CommandName = "tool_details"
|
||||
ModelListCommand CommandName = "model_list"
|
||||
ThemeListCommand CommandName = "theme_list"
|
||||
FileListCommand CommandName = "file_list"
|
||||
FileCloseCommand CommandName = "file_close"
|
||||
FileSearchCommand CommandName = "file_search"
|
||||
FileDiffToggleCommand CommandName = "file_diff_toggle"
|
||||
ProjectInitCommand CommandName = "project_init"
|
||||
InputClearCommand CommandName = "input_clear"
|
||||
InputPasteCommand CommandName = "input_paste"
|
||||
InputSubmitCommand CommandName = "input_submit"
|
||||
InputNewlineCommand CommandName = "input_newline"
|
||||
HistoryPreviousCommand CommandName = "history_previous"
|
||||
HistoryNextCommand CommandName = "history_next"
|
||||
MessagesPageUpCommand CommandName = "messages_page_up"
|
||||
MessagesPageDownCommand CommandName = "messages_page_down"
|
||||
MessagesHalfPageUpCommand CommandName = "messages_half_page_up"
|
||||
@@ -95,6 +98,9 @@ const (
|
||||
MessagesNextCommand CommandName = "messages_next"
|
||||
MessagesFirstCommand CommandName = "messages_first"
|
||||
MessagesLastCommand CommandName = "messages_last"
|
||||
MessagesLayoutToggleCommand CommandName = "messages_layout_toggle"
|
||||
MessagesCopyCommand CommandName = "messages_copy"
|
||||
MessagesRevertCommand CommandName = "messages_revert"
|
||||
AppExitCommand CommandName = "app_exit"
|
||||
)
|
||||
|
||||
@@ -155,6 +161,12 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
Keybindings: parseBindings("<leader>s"),
|
||||
Trigger: "share",
|
||||
},
|
||||
{
|
||||
Name: SessionUnshareCommand,
|
||||
Description: "unshare session",
|
||||
Keybindings: parseBindings("<leader>u"),
|
||||
Trigger: "unshare",
|
||||
},
|
||||
{
|
||||
Name: SessionInterruptCommand,
|
||||
Description: "interrupt session",
|
||||
@@ -184,6 +196,27 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
Keybindings: parseBindings("<leader>t"),
|
||||
Trigger: "themes",
|
||||
},
|
||||
{
|
||||
Name: FileListCommand,
|
||||
Description: "list files",
|
||||
Keybindings: parseBindings("<leader>f"),
|
||||
Trigger: "files",
|
||||
},
|
||||
{
|
||||
Name: FileCloseCommand,
|
||||
Description: "close file",
|
||||
Keybindings: parseBindings("esc"),
|
||||
},
|
||||
{
|
||||
Name: FileSearchCommand,
|
||||
Description: "search file",
|
||||
Keybindings: parseBindings("<leader>/"),
|
||||
},
|
||||
{
|
||||
Name: FileDiffToggleCommand,
|
||||
Description: "split/unified diff",
|
||||
Keybindings: parseBindings("<leader>v"),
|
||||
},
|
||||
{
|
||||
Name: ProjectInitCommand,
|
||||
Description: "create/update AGENTS.md",
|
||||
@@ -210,16 +243,6 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
Description: "insert newline",
|
||||
Keybindings: parseBindings("shift+enter", "ctrl+j"),
|
||||
},
|
||||
// {
|
||||
// Name: HistoryPreviousCommand,
|
||||
// Description: "previous prompt",
|
||||
// Keybindings: parseBindings("up"),
|
||||
// },
|
||||
// {
|
||||
// Name: HistoryNextCommand,
|
||||
// Description: "next prompt",
|
||||
// Keybindings: parseBindings("down"),
|
||||
// },
|
||||
{
|
||||
Name: MessagesPageUpCommand,
|
||||
Description: "page up",
|
||||
@@ -243,12 +266,12 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
{
|
||||
Name: MessagesPreviousCommand,
|
||||
Description: "previous message",
|
||||
Keybindings: parseBindings("ctrl+alt+k"),
|
||||
Keybindings: parseBindings("ctrl+up"),
|
||||
},
|
||||
{
|
||||
Name: MessagesNextCommand,
|
||||
Description: "next message",
|
||||
Keybindings: parseBindings("ctrl+alt+j"),
|
||||
Keybindings: parseBindings("ctrl+down"),
|
||||
},
|
||||
{
|
||||
Name: MessagesFirstCommand,
|
||||
@@ -260,6 +283,21 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
Description: "last message",
|
||||
Keybindings: parseBindings("ctrl+alt+g"),
|
||||
},
|
||||
{
|
||||
Name: MessagesLayoutToggleCommand,
|
||||
Description: "toggle layout",
|
||||
Keybindings: parseBindings("<leader>p"),
|
||||
},
|
||||
{
|
||||
Name: MessagesCopyCommand,
|
||||
Description: "copy message",
|
||||
Keybindings: parseBindings("<leader>y"),
|
||||
},
|
||||
{
|
||||
Name: MessagesRevertCommand,
|
||||
Description: "revert message",
|
||||
Keybindings: parseBindings("<leader>r"),
|
||||
},
|
||||
{
|
||||
Name: AppExitCommand,
|
||||
Description: "exit the app",
|
||||
|
||||
@@ -25,13 +25,6 @@ func (c *CommandCompletionProvider) GetId() string {
|
||||
return "commands"
|
||||
}
|
||||
|
||||
func (c *CommandCompletionProvider) GetEntry() dialog.CompletionItemI {
|
||||
return dialog.NewCompletionItem(dialog.CompletionItem{
|
||||
Title: "Commands",
|
||||
Value: "commands",
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CommandCompletionProvider) GetEmptyMessage() string {
|
||||
return "no matching commands"
|
||||
}
|
||||
|
||||
@@ -2,64 +2,114 @@ package completions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
type filesAndFoldersContextGroup struct {
|
||||
app *app.App
|
||||
prefix string
|
||||
app *app.App
|
||||
gitFiles []dialog.CompletionItemI
|
||||
}
|
||||
|
||||
func (cg *filesAndFoldersContextGroup) GetId() string {
|
||||
return cg.prefix
|
||||
}
|
||||
|
||||
func (cg *filesAndFoldersContextGroup) GetEntry() dialog.CompletionItemI {
|
||||
return dialog.NewCompletionItem(dialog.CompletionItem{
|
||||
Title: "Files & Folders",
|
||||
Value: "files",
|
||||
})
|
||||
return "files"
|
||||
}
|
||||
|
||||
func (cg *filesAndFoldersContextGroup) GetEmptyMessage() string {
|
||||
return "no matching files"
|
||||
}
|
||||
|
||||
func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) {
|
||||
files, err := cg.app.Client.File.Search(
|
||||
context.Background(),
|
||||
opencode.FileSearchParams{Query: opencode.F(query)},
|
||||
)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
func (cg *filesAndFoldersContextGroup) getGitFiles() []dialog.CompletionItemI {
|
||||
t := theme.CurrentTheme()
|
||||
items := make([]dialog.CompletionItemI, 0)
|
||||
base := styles.NewStyle().Background(t.BackgroundElement())
|
||||
green := base.Foreground(t.Success()).Render
|
||||
red := base.Foreground(t.Error()).Render
|
||||
|
||||
status, _ := cg.app.Client.File.Status(context.Background())
|
||||
if status != nil {
|
||||
files := *status
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i].Added+files[i].Removed > files[j].Added+files[j].Removed
|
||||
})
|
||||
|
||||
for _, file := range files {
|
||||
title := file.File
|
||||
if file.Added > 0 {
|
||||
title += green(" +" + strconv.Itoa(int(file.Added)))
|
||||
}
|
||||
if file.Removed > 0 {
|
||||
title += red(" -" + strconv.Itoa(int(file.Removed)))
|
||||
}
|
||||
item := dialog.NewCompletionItem(dialog.CompletionItem{
|
||||
Title: title,
|
||||
Value: file.File,
|
||||
})
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
return *files, nil
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
|
||||
matches, err := cg.getFiles(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (cg *filesAndFoldersContextGroup) GetChildEntries(
|
||||
query string,
|
||||
) ([]dialog.CompletionItemI, error) {
|
||||
items := make([]dialog.CompletionItemI, 0)
|
||||
|
||||
query = strings.TrimSpace(query)
|
||||
if query == "" {
|
||||
items = append(items, cg.gitFiles...)
|
||||
}
|
||||
|
||||
items := make([]dialog.CompletionItemI, 0, len(matches))
|
||||
for _, file := range matches {
|
||||
item := dialog.NewCompletionItem(dialog.CompletionItem{
|
||||
Title: file,
|
||||
Value: file,
|
||||
})
|
||||
items = append(items, item)
|
||||
files, err := cg.app.Client.Find.Files(
|
||||
context.Background(),
|
||||
opencode.FindFilesParams{Query: opencode.F(query)},
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get completion items", "error", err)
|
||||
return items, err
|
||||
}
|
||||
if files == nil {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
for _, file := range *files {
|
||||
exists := false
|
||||
for _, existing := range cg.gitFiles {
|
||||
if existing.GetValue() == file {
|
||||
if query != "" {
|
||||
items = append(items, existing)
|
||||
}
|
||||
exists = true
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
item := dialog.NewCompletionItem(dialog.CompletionItem{
|
||||
Title: file,
|
||||
Value: file,
|
||||
})
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func NewFileAndFolderContextGroup(app *app.App) dialog.CompletionProvider {
|
||||
return &filesAndFoldersContextGroup{
|
||||
app: app,
|
||||
prefix: "file",
|
||||
cg := &filesAndFoldersContextGroup{
|
||||
app: app,
|
||||
}
|
||||
go func() {
|
||||
cg.gitFiles = cg.getGitFiles()
|
||||
}()
|
||||
return cg
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package completions
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
)
|
||||
|
||||
type CompletionManager struct {
|
||||
providers map[string]dialog.CompletionProvider
|
||||
}
|
||||
|
||||
func NewCompletionManager(app *app.App) *CompletionManager {
|
||||
return &CompletionManager{
|
||||
providers: map[string]dialog.CompletionProvider{
|
||||
"files": NewFileAndFolderContextGroup(app),
|
||||
"commands": NewCommandCompletionProvider(app),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *CompletionManager) DefaultProvider() dialog.CompletionProvider {
|
||||
return m.providers["commands"]
|
||||
}
|
||||
|
||||
func (m *CompletionManager) GetProvider(input string) dialog.CompletionProvider {
|
||||
if strings.HasPrefix(input, "/") {
|
||||
return m.providers["commands"]
|
||||
}
|
||||
return m.providers["files"]
|
||||
}
|
||||
@@ -3,17 +3,19 @@ package chat
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/commands"
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
"github.com/sst/opencode/internal/components/textarea"
|
||||
"github.com/sst/opencode/internal/image"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
@@ -21,9 +23,8 @@ import (
|
||||
|
||||
type EditorComponent interface {
|
||||
tea.Model
|
||||
tea.ViewModel
|
||||
layout.Sizeable
|
||||
Content() string
|
||||
View(width int) string
|
||||
Content(width int) string
|
||||
Lines() int
|
||||
Value() string
|
||||
Focused() bool
|
||||
@@ -33,19 +34,12 @@ type EditorComponent interface {
|
||||
Clear() (tea.Model, tea.Cmd)
|
||||
Paste() (tea.Model, tea.Cmd)
|
||||
Newline() (tea.Model, tea.Cmd)
|
||||
Previous() (tea.Model, tea.Cmd)
|
||||
Next() (tea.Model, tea.Cmd)
|
||||
SetInterruptKeyInDebounce(inDebounce bool)
|
||||
}
|
||||
|
||||
type editorComponent struct {
|
||||
app *app.App
|
||||
width, height int
|
||||
textarea textarea.Model
|
||||
attachments []app.Attachment
|
||||
history []string
|
||||
historyIndex int
|
||||
currentMessage string
|
||||
spinner spinner.Model
|
||||
interruptKeyInDebounce bool
|
||||
}
|
||||
@@ -74,17 +68,54 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.spinner = createSpinner()
|
||||
return m, tea.Batch(m.spinner.Tick, m.textarea.Focus())
|
||||
case dialog.CompletionSelectedMsg:
|
||||
if msg.IsCommand {
|
||||
switch msg.ProviderID {
|
||||
case "commands":
|
||||
commandName := strings.TrimPrefix(msg.CompletionValue, "/")
|
||||
updated, cmd := m.Clear()
|
||||
m = updated.(*editorComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
cmds = append(cmds, util.CmdHandler(commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)])))
|
||||
return m, tea.Batch(cmds...)
|
||||
} else {
|
||||
existingValue := m.textarea.Value()
|
||||
case "files":
|
||||
atIndex := m.textarea.LastRuneIndex('@')
|
||||
if atIndex == -1 {
|
||||
// Should not happen, but as a fallback, just insert.
|
||||
m.textarea.InsertString(msg.CompletionValue + " ")
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Replace the current token (after last space)
|
||||
// The range to replace is from the '@' up to the current cursor position.
|
||||
// Replace the search term (e.g., "@search") with an empty string first.
|
||||
cursorCol := m.textarea.CursorColumn()
|
||||
m.textarea.ReplaceRange(atIndex, cursorCol, "")
|
||||
|
||||
// Now, insert the attachment at the position where the '@' was.
|
||||
// The cursor is now at `atIndex` after the replacement.
|
||||
filePath := msg.CompletionValue
|
||||
extension := filepath.Ext(filePath)
|
||||
mediaType := ""
|
||||
switch extension {
|
||||
case ".jpg":
|
||||
mediaType = "image/jpeg"
|
||||
case ".png", ".jpeg", ".gif", ".webp":
|
||||
mediaType = "image/" + extension[1:]
|
||||
case ".pdf":
|
||||
mediaType = "application/pdf"
|
||||
default:
|
||||
mediaType = "text/plain"
|
||||
}
|
||||
attachment := &textarea.Attachment{
|
||||
ID: uuid.NewString(),
|
||||
Display: "@" + filePath,
|
||||
URL: fmt.Sprintf("file://./%s", filePath),
|
||||
Filename: filePath,
|
||||
MediaType: mediaType,
|
||||
}
|
||||
m.textarea.InsertAttachment(attachment)
|
||||
m.textarea.InsertString(" ")
|
||||
return m, nil
|
||||
default:
|
||||
existingValue := m.textarea.Value()
|
||||
lastSpaceIndex := strings.LastIndex(existingValue, " ")
|
||||
if lastSpaceIndex == -1 {
|
||||
m.textarea.SetValue(msg.CompletionValue + " ")
|
||||
@@ -105,7 +136,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *editorComponent) Content() string {
|
||||
func (m *editorComponent) Content(width int) string {
|
||||
t := theme.CurrentTheme()
|
||||
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
|
||||
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
|
||||
@@ -114,6 +145,7 @@ func (m *editorComponent) Content() string {
|
||||
Bold(true)
|
||||
prompt := promptStyle.Render(">")
|
||||
|
||||
m.textarea.SetWidth(width - 6)
|
||||
textarea := lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
prompt,
|
||||
@@ -121,7 +153,7 @@ func (m *editorComponent) Content() string {
|
||||
)
|
||||
textarea = styles.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Width(m.width).
|
||||
Width(width).
|
||||
PaddingTop(1).
|
||||
PaddingBottom(1).
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
@@ -135,7 +167,15 @@ func (m *editorComponent) Content() string {
|
||||
if m.app.IsBusy() {
|
||||
keyText := m.getInterruptKeyText()
|
||||
if m.interruptKeyInDebounce {
|
||||
hint = muted("working") + m.spinner.View() + muted(" ") + base(keyText+" again") + muted(" interrupt")
|
||||
hint = muted(
|
||||
"working",
|
||||
) + m.spinner.View() + muted(
|
||||
" ",
|
||||
) + base(
|
||||
keyText+" again",
|
||||
) + muted(
|
||||
" interrupt",
|
||||
)
|
||||
} else {
|
||||
hint = muted("working") + m.spinner.View() + muted(" ") + base(keyText) + muted(" interrupt")
|
||||
}
|
||||
@@ -146,7 +186,7 @@ func (m *editorComponent) Content() string {
|
||||
model = muted(m.app.Provider.Name) + base(" "+m.app.Model.Name)
|
||||
}
|
||||
|
||||
space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
|
||||
space := width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
|
||||
spacer := styles.NewStyle().Background(t.Background()).Width(space).Render("")
|
||||
|
||||
info := hint + spacer + model
|
||||
@@ -156,11 +196,18 @@ func (m *editorComponent) Content() string {
|
||||
return content
|
||||
}
|
||||
|
||||
func (m *editorComponent) View() string {
|
||||
func (m *editorComponent) View(width int) string {
|
||||
if m.Lines() > 1 {
|
||||
return ""
|
||||
return lipgloss.Place(
|
||||
width,
|
||||
5,
|
||||
lipgloss.Center,
|
||||
lipgloss.Center,
|
||||
"",
|
||||
styles.WhitespaceStyle(theme.CurrentTheme().Background()),
|
||||
)
|
||||
}
|
||||
return m.Content()
|
||||
return m.Content(width)
|
||||
}
|
||||
|
||||
func (m *editorComponent) Focused() bool {
|
||||
@@ -175,16 +222,6 @@ func (m *editorComponent) Blur() {
|
||||
m.textarea.Blur()
|
||||
}
|
||||
|
||||
func (m *editorComponent) GetSize() (width, height int) {
|
||||
return m.width, m.height
|
||||
}
|
||||
|
||||
func (m *editorComponent) SetSize(width, height int) tea.Cmd {
|
||||
m.width = width
|
||||
m.height = height
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *editorComponent) Lines() int {
|
||||
return m.textarea.LineCount()
|
||||
}
|
||||
@@ -200,29 +237,29 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
|
||||
}
|
||||
if len(value) > 0 && value[len(value)-1] == '\\' {
|
||||
// If the last character is a backslash, remove it and add a newline
|
||||
m.textarea.SetValue(value[:len(value)-1] + "\n")
|
||||
m.textarea.ReplaceRange(len(value)-1, len(value), "")
|
||||
m.textarea.InsertString("\n")
|
||||
return m, nil
|
||||
}
|
||||
|
||||
var cmds []tea.Cmd
|
||||
|
||||
attachments := m.textarea.GetAttachments()
|
||||
fileParts := make([]opencode.FilePartParam, 0)
|
||||
for _, attachment := range attachments {
|
||||
fileParts = append(fileParts, opencode.FilePartParam{
|
||||
Type: opencode.F(opencode.FilePartTypeFile),
|
||||
Mime: opencode.F(attachment.MediaType),
|
||||
URL: opencode.F(attachment.URL),
|
||||
Filename: opencode.F(attachment.Filename),
|
||||
})
|
||||
}
|
||||
|
||||
updated, cmd := m.Clear()
|
||||
m = updated.(*editorComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
attachments := m.attachments
|
||||
|
||||
// Save to history if not empty and not a duplicate of the last entry
|
||||
if value != "" {
|
||||
if len(m.history) == 0 || m.history[len(m.history)-1] != value {
|
||||
m.history = append(m.history, value)
|
||||
}
|
||||
m.historyIndex = len(m.history)
|
||||
m.currentMessage = ""
|
||||
}
|
||||
|
||||
m.attachments = nil
|
||||
|
||||
cmds = append(cmds, util.CmdHandler(app.SendMsg{Text: value, Attachments: attachments}))
|
||||
cmds = append(cmds, util.CmdHandler(app.SendMsg{Text: value, Attachments: fileParts}))
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
@@ -232,18 +269,23 @@ func (m *editorComponent) Clear() (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m *editorComponent) Paste() (tea.Model, tea.Cmd) {
|
||||
imageBytes, text, err := image.GetImageFromClipboard()
|
||||
_, text, err := image.GetImageFromClipboard()
|
||||
if err != nil {
|
||||
slog.Error(err.Error())
|
||||
return m, nil
|
||||
}
|
||||
if len(imageBytes) != 0 {
|
||||
attachmentName := fmt.Sprintf("clipboard-image-%d", len(m.attachments))
|
||||
attachment := app.Attachment{FilePath: attachmentName, FileName: attachmentName, Content: imageBytes, MimeType: "image/png"}
|
||||
m.attachments = append(m.attachments, attachment)
|
||||
} else {
|
||||
m.textarea.SetValue(m.textarea.Value() + text)
|
||||
}
|
||||
// if len(imageBytes) != 0 {
|
||||
// attachmentName := fmt.Sprintf("clipboard-image-%d", len(m.attachments))
|
||||
// attachment := app.Attachment{
|
||||
// FilePath: attachmentName,
|
||||
// FileName: attachmentName,
|
||||
// Content: imageBytes,
|
||||
// MimeType: "image/png",
|
||||
// }
|
||||
// m.attachments = append(m.attachments, attachment)
|
||||
// } else {
|
||||
m.textarea.InsertString(text)
|
||||
// }
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -252,48 +294,6 @@ func (m *editorComponent) Newline() (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *editorComponent) Previous() (tea.Model, tea.Cmd) {
|
||||
currentLine := m.textarea.Line()
|
||||
|
||||
// Only navigate history if we're at the first line
|
||||
if currentLine == 0 && len(m.history) > 0 {
|
||||
// Save current message if we're just starting to navigate
|
||||
if m.historyIndex == len(m.history) {
|
||||
m.currentMessage = m.textarea.Value()
|
||||
}
|
||||
|
||||
// Go to previous message in history
|
||||
if m.historyIndex > 0 {
|
||||
m.historyIndex--
|
||||
m.textarea.SetValue(m.history[m.historyIndex])
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *editorComponent) Next() (tea.Model, tea.Cmd) {
|
||||
currentLine := m.textarea.Line()
|
||||
value := m.textarea.Value()
|
||||
lines := strings.Split(value, "\n")
|
||||
totalLines := len(lines)
|
||||
|
||||
// Only navigate history if we're at the last line
|
||||
if currentLine == totalLines-1 {
|
||||
if m.historyIndex < len(m.history)-1 {
|
||||
// Go to next message in history
|
||||
m.historyIndex++
|
||||
m.textarea.SetValue(m.history[m.historyIndex])
|
||||
} else if m.historyIndex == len(m.history)-1 {
|
||||
// Return to the current message being composed
|
||||
m.historyIndex = len(m.history)
|
||||
m.textarea.SetValue(m.currentMessage)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *editorComponent) SetInterruptKeyInDebounce(inDebounce bool) {
|
||||
m.interruptKeyInDebounce = inDebounce
|
||||
}
|
||||
@@ -316,18 +316,31 @@ func createTextArea(existing *textarea.Model) textarea.Model {
|
||||
|
||||
ta.Styles.Blurred.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Blurred.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
|
||||
ta.Styles.Blurred.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Blurred.Placeholder = styles.NewStyle().
|
||||
Foreground(textMutedColor).
|
||||
Background(bgColor).
|
||||
Lipgloss()
|
||||
ta.Styles.Blurred.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Focused.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Focused.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
|
||||
ta.Styles.Focused.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Focused.Placeholder = styles.NewStyle().
|
||||
Foreground(textMutedColor).
|
||||
Background(bgColor).
|
||||
Lipgloss()
|
||||
ta.Styles.Focused.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Attachment = styles.NewStyle().
|
||||
Foreground(t.Secondary()).
|
||||
Background(bgColor).
|
||||
Lipgloss()
|
||||
ta.Styles.SelectedAttachment = styles.NewStyle().
|
||||
Foreground(t.Text()).
|
||||
Background(t.Secondary()).
|
||||
Lipgloss()
|
||||
ta.Styles.Cursor.Color = t.Primary()
|
||||
|
||||
ta.Prompt = " "
|
||||
ta.ShowLineNumbers = false
|
||||
ta.CharLimit = -1
|
||||
ta.SetWidth(layout.Current.Container.Width - 6)
|
||||
|
||||
if existing != nil {
|
||||
ta.SetValue(existing.Value())
|
||||
@@ -335,7 +348,6 @@ func createTextArea(existing *textarea.Model) textarea.Model {
|
||||
ta.SetHeight(existing.Height())
|
||||
}
|
||||
|
||||
// ta.Focus()
|
||||
return ta
|
||||
}
|
||||
|
||||
@@ -360,9 +372,6 @@ func NewEditorComponent(app *app.App) EditorComponent {
|
||||
return &editorComponent{
|
||||
app: app,
|
||||
textarea: ta,
|
||||
history: []string{},
|
||||
historyIndex: 0,
|
||||
currentMessage: "",
|
||||
spinner: s,
|
||||
interruptKeyInDebounce: false,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,13 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/spinner"
|
||||
"github.com/charmbracelet/bubbles/v2/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/commands"
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
@@ -21,73 +17,93 @@ import (
|
||||
|
||||
type MessagesComponent interface {
|
||||
tea.Model
|
||||
tea.ViewModel
|
||||
View(width, height int) string
|
||||
SetWidth(width int) tea.Cmd
|
||||
PageUp() (tea.Model, tea.Cmd)
|
||||
PageDown() (tea.Model, tea.Cmd)
|
||||
HalfPageUp() (tea.Model, tea.Cmd)
|
||||
HalfPageDown() (tea.Model, tea.Cmd)
|
||||
First() (tea.Model, tea.Cmd)
|
||||
Last() (tea.Model, tea.Cmd)
|
||||
// Previous() (tea.Model, tea.Cmd)
|
||||
// Next() (tea.Model, tea.Cmd)
|
||||
Previous() (tea.Model, tea.Cmd)
|
||||
Next() (tea.Model, tea.Cmd)
|
||||
ToolDetailsVisible() bool
|
||||
Selected() string
|
||||
}
|
||||
|
||||
type messagesComponent struct {
|
||||
width, height int
|
||||
width int
|
||||
app *app.App
|
||||
viewport viewport.Model
|
||||
spinner spinner.Model
|
||||
attachments viewport.Model
|
||||
commands commands.CommandsComponent
|
||||
cache *MessageCache
|
||||
rendering bool
|
||||
showToolDetails bool
|
||||
tail bool
|
||||
partCount int
|
||||
lineCount int
|
||||
selectedPart int
|
||||
selectedText string
|
||||
}
|
||||
type renderFinishedMsg struct{}
|
||||
type selectedMessagePartChangedMsg struct {
|
||||
part int
|
||||
}
|
||||
|
||||
type ToggleToolDetailsMsg struct{}
|
||||
|
||||
func (m *messagesComponent) Init() tea.Cmd {
|
||||
return tea.Batch(m.viewport.Init(), m.spinner.Tick, m.commands.Init())
|
||||
return tea.Batch(m.viewport.Init())
|
||||
}
|
||||
|
||||
func (m *messagesComponent) Selected() string {
|
||||
return m.selectedText
|
||||
}
|
||||
|
||||
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
switch msg.(type) {
|
||||
switch msg := msg.(type) {
|
||||
case app.SendMsg:
|
||||
m.viewport.GotoBottom()
|
||||
m.tail = true
|
||||
m.selectedPart = -1
|
||||
return m, nil
|
||||
case app.OptimisticMessageAddedMsg:
|
||||
m.renderView()
|
||||
if m.tail {
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
return m, nil
|
||||
m.tail = true
|
||||
m.rendering = true
|
||||
return m, m.Reload()
|
||||
case dialog.ThemeSelectedMsg:
|
||||
m.cache.Clear()
|
||||
m.rendering = true
|
||||
return m, m.Reload()
|
||||
case ToggleToolDetailsMsg:
|
||||
m.showToolDetails = !m.showToolDetails
|
||||
m.rendering = true
|
||||
return m, m.Reload()
|
||||
case app.SessionSelectedMsg:
|
||||
case app.SessionLoadedMsg, app.SessionClearedMsg:
|
||||
m.cache.Clear()
|
||||
m.tail = true
|
||||
m.rendering = true
|
||||
return m, m.Reload()
|
||||
case app.SessionClearedMsg:
|
||||
m.cache.Clear()
|
||||
cmd := m.Reload()
|
||||
return m, cmd
|
||||
case renderFinishedMsg:
|
||||
m.rendering = false
|
||||
if m.tail {
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
case opencode.EventListResponseEventSessionUpdated, opencode.EventListResponseEventMessageUpdated:
|
||||
m.renderView()
|
||||
if m.tail {
|
||||
m.viewport.GotoBottom()
|
||||
case selectedMessagePartChangedMsg:
|
||||
return m, m.Reload()
|
||||
case opencode.EventListResponseEventSessionUpdated:
|
||||
if msg.Properties.Info.ID == m.app.Session.ID {
|
||||
m.renderView(m.width)
|
||||
if m.tail {
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
}
|
||||
case opencode.EventListResponseEventMessageUpdated:
|
||||
if msg.Properties.Info.SessionID == m.app.Session.ID {
|
||||
m.renderView(m.width)
|
||||
if m.tail {
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,189 +112,264 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.tail = m.viewport.AtBottom()
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
spinner, cmd := m.spinner.Update(msg)
|
||||
m.spinner = spinner
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
updated, cmd := m.commands.Update(msg)
|
||||
m.commands = updated.(commands.CommandsComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
type blockType int
|
||||
|
||||
const (
|
||||
none blockType = iota
|
||||
userTextBlock
|
||||
assistantTextBlock
|
||||
toolInvocationBlock
|
||||
errorBlock
|
||||
)
|
||||
|
||||
func (m *messagesComponent) renderView() {
|
||||
if m.width == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
func (m *messagesComponent) renderView(width int) {
|
||||
measure := util.Measure("messages.renderView")
|
||||
defer measure("messageCount", len(m.app.Messages))
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
blocks := make([]string, 0)
|
||||
previousBlockType := none
|
||||
m.partCount = 0
|
||||
m.lineCount = 0
|
||||
|
||||
orphanedToolCalls := make([]opencode.ToolPart, 0)
|
||||
|
||||
for _, message := range m.app.Messages {
|
||||
var content string
|
||||
var cached bool
|
||||
lastToolIndex := 0
|
||||
lastToolIndices := []int{}
|
||||
for i, p := range message.Parts {
|
||||
switch p.Type {
|
||||
case opencode.MessagePartTypeText:
|
||||
lastToolIndices = append(lastToolIndices, lastToolIndex)
|
||||
case opencode.MessagePartTypeToolInvocation:
|
||||
lastToolIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
author := ""
|
||||
switch message.Role {
|
||||
case opencode.MessageRoleUser:
|
||||
author = m.app.Info.User
|
||||
case opencode.MessageRoleAssistant:
|
||||
author = message.Metadata.Assistant.ModelID
|
||||
}
|
||||
|
||||
for i, p := range message.Parts {
|
||||
switch part := p.AsUnion().(type) {
|
||||
// case client.MessagePartStepStart:
|
||||
// messages = append(messages, "")
|
||||
case opencode.TextPart:
|
||||
key := m.cache.GenerateKey(message.ID, p.Text, layout.Current.Viewport.Width)
|
||||
content, cached = m.cache.Get(key)
|
||||
if !cached {
|
||||
content = renderText(message, p.Text, author)
|
||||
m.cache.Set(key, content)
|
||||
}
|
||||
if previousBlockType != none {
|
||||
blocks = append(blocks, "")
|
||||
}
|
||||
blocks = append(blocks, content)
|
||||
if message.Role == opencode.MessageRoleUser {
|
||||
previousBlockType = userTextBlock
|
||||
} else if message.Role == opencode.MessageRoleAssistant {
|
||||
previousBlockType = assistantTextBlock
|
||||
}
|
||||
case opencode.ToolInvocationPart:
|
||||
isLastToolInvocation := slices.Contains(lastToolIndices, i)
|
||||
metadata := opencode.MessageMetadataTool{}
|
||||
|
||||
toolCallID := part.ToolInvocation.ToolCallID
|
||||
// var toolCallID string
|
||||
// var result *string
|
||||
// switch toolCall := part.ToolInvocation.AsUnion().(type) {
|
||||
// case opencode.ToolCall:
|
||||
// toolCallID = toolCall.ToolCallID
|
||||
// case opencode.ToolPartialCall:
|
||||
// toolCallID = toolCall.ToolCallID
|
||||
// case opencode.ToolResult:
|
||||
// toolCallID = toolCall.ToolCallID
|
||||
// result = &toolCall.Result
|
||||
// }
|
||||
|
||||
if _, ok := message.Metadata.Tool[toolCallID]; ok {
|
||||
metadata = message.Metadata.Tool[toolCallID]
|
||||
}
|
||||
|
||||
var result *string
|
||||
if part.ToolInvocation.Result != "" {
|
||||
result = &part.ToolInvocation.Result
|
||||
}
|
||||
|
||||
if part.ToolInvocation.State == "result" {
|
||||
key := m.cache.GenerateKey(message.ID,
|
||||
part.ToolInvocation.ToolCallID,
|
||||
m.showToolDetails,
|
||||
layout.Current.Viewport.Width,
|
||||
switch casted := message.(type) {
|
||||
case opencode.UserMessage:
|
||||
userLoop:
|
||||
for partIndex, part := range casted.Parts {
|
||||
switch part := part.AsUnion().(type) {
|
||||
case opencode.TextPart:
|
||||
remainingParts := casted.Parts[partIndex+1:]
|
||||
fileParts := make([]opencode.FilePart, 0)
|
||||
for _, part := range remainingParts {
|
||||
switch part := part.AsUnion().(type) {
|
||||
case opencode.FilePart:
|
||||
fileParts = append(fileParts, part)
|
||||
}
|
||||
}
|
||||
flexItems := []layout.FlexItem{}
|
||||
if len(fileParts) > 0 {
|
||||
fileStyle := styles.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Padding(0, 1)
|
||||
mediaTypeStyle := styles.NewStyle().Background(t.Secondary()).Foreground(t.BackgroundPanel()).Padding(0, 1)
|
||||
for _, filePart := range fileParts {
|
||||
mediaType := ""
|
||||
switch filePart.Mime {
|
||||
case "text/plain":
|
||||
mediaType = "txt"
|
||||
case "image/png", "image/jpeg", "image/gif", "image/webp":
|
||||
mediaType = "img"
|
||||
mediaTypeStyle = mediaTypeStyle.Background(t.Accent())
|
||||
case "application/pdf":
|
||||
mediaType = "pdf"
|
||||
mediaTypeStyle = mediaTypeStyle.Background(t.Primary())
|
||||
}
|
||||
flexItems = append(flexItems, layout.FlexItem{
|
||||
View: mediaTypeStyle.Render(mediaType) + fileStyle.Render(filePart.Filename),
|
||||
})
|
||||
}
|
||||
}
|
||||
bgColor := t.BackgroundPanel()
|
||||
files := layout.Render(
|
||||
layout.FlexOptions{
|
||||
Background: &bgColor,
|
||||
Width: width - 6,
|
||||
Direction: layout.Column,
|
||||
},
|
||||
flexItems...,
|
||||
)
|
||||
|
||||
key := m.cache.GenerateKey(casted.ID, part.Text, width, m.selectedPart == m.partCount, files)
|
||||
content, cached = m.cache.Get(key)
|
||||
if !cached {
|
||||
content = renderToolInvocation(
|
||||
part,
|
||||
result,
|
||||
metadata,
|
||||
content = renderText(
|
||||
m.app,
|
||||
message,
|
||||
part.Text,
|
||||
m.app.Info.User,
|
||||
m.showToolDetails,
|
||||
isLastToolInvocation,
|
||||
false,
|
||||
message.Metadata,
|
||||
m.partCount == m.selectedPart,
|
||||
width,
|
||||
files,
|
||||
)
|
||||
m.cache.Set(key, content)
|
||||
}
|
||||
} else {
|
||||
// if the tool call isn't finished, don't cache
|
||||
content = renderToolInvocation(
|
||||
part,
|
||||
result,
|
||||
metadata,
|
||||
m.showToolDetails,
|
||||
isLastToolInvocation,
|
||||
false,
|
||||
message.Metadata,
|
||||
)
|
||||
if content != "" {
|
||||
m = m.updateSelected(content, part.Text)
|
||||
blocks = append(blocks, content)
|
||||
}
|
||||
// Only render the first text part
|
||||
break userLoop
|
||||
}
|
||||
}
|
||||
|
||||
if previousBlockType != toolInvocationBlock && m.showToolDetails {
|
||||
blocks = append(blocks, "")
|
||||
case opencode.AssistantMessage:
|
||||
hasTextPart := false
|
||||
for partIndex, p := range casted.Parts {
|
||||
switch part := p.AsUnion().(type) {
|
||||
case opencode.TextPart:
|
||||
hasTextPart = true
|
||||
finished := casted.Time.Completed > 0
|
||||
remainingParts := casted.Parts[partIndex+1:]
|
||||
toolCallParts := make([]opencode.ToolPart, 0)
|
||||
|
||||
// sometimes tool calls happen without an assistant message
|
||||
// these should be included in this assistant message as well
|
||||
if len(orphanedToolCalls) > 0 {
|
||||
toolCallParts = append(toolCallParts, orphanedToolCalls...)
|
||||
orphanedToolCalls = make([]opencode.ToolPart, 0)
|
||||
}
|
||||
|
||||
remaining := true
|
||||
for _, part := range remainingParts {
|
||||
if !remaining {
|
||||
break
|
||||
}
|
||||
switch part := part.AsUnion().(type) {
|
||||
case opencode.TextPart:
|
||||
// we only want tool calls associated with the current text part.
|
||||
// if we hit another text part, we're done.
|
||||
remaining = false
|
||||
case opencode.ToolPart:
|
||||
toolCallParts = append(toolCallParts, part)
|
||||
if part.State.Status != opencode.ToolPartStateStatusCompleted || part.State.Status != opencode.ToolPartStateStatusError {
|
||||
// i don't think there's a case where a tool call isn't in result state
|
||||
// and the message time is 0, but just in case
|
||||
finished = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if finished {
|
||||
key := m.cache.GenerateKey(casted.ID, p.Text, width, m.showToolDetails, m.selectedPart == m.partCount)
|
||||
content, cached = m.cache.Get(key)
|
||||
if !cached {
|
||||
content = renderText(
|
||||
m.app,
|
||||
message,
|
||||
p.Text,
|
||||
casted.ModelID,
|
||||
m.showToolDetails,
|
||||
m.partCount == m.selectedPart,
|
||||
width,
|
||||
"",
|
||||
toolCallParts...,
|
||||
)
|
||||
m.cache.Set(key, content)
|
||||
}
|
||||
} else {
|
||||
content = renderText(
|
||||
m.app,
|
||||
message,
|
||||
p.Text,
|
||||
casted.ModelID,
|
||||
m.showToolDetails,
|
||||
m.partCount == m.selectedPart,
|
||||
width,
|
||||
"",
|
||||
toolCallParts...,
|
||||
)
|
||||
}
|
||||
if content != "" {
|
||||
m = m.updateSelected(content, p.Text)
|
||||
blocks = append(blocks, content)
|
||||
}
|
||||
case opencode.ToolPart:
|
||||
if !m.showToolDetails {
|
||||
if !hasTextPart {
|
||||
orphanedToolCalls = append(orphanedToolCalls, part)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if part.State.Status == opencode.ToolPartStateStatusCompleted || part.State.Status == opencode.ToolPartStateStatusError {
|
||||
key := m.cache.GenerateKey(casted.ID,
|
||||
part.ID,
|
||||
m.showToolDetails,
|
||||
width,
|
||||
m.partCount == m.selectedPart,
|
||||
)
|
||||
content, cached = m.cache.Get(key)
|
||||
if !cached {
|
||||
content = renderToolDetails(
|
||||
m.app,
|
||||
part,
|
||||
m.partCount == m.selectedPart,
|
||||
width,
|
||||
)
|
||||
m.cache.Set(key, content)
|
||||
}
|
||||
} else {
|
||||
// if the tool call isn't finished, don't cache
|
||||
content = renderToolDetails(
|
||||
m.app,
|
||||
part,
|
||||
m.partCount == m.selectedPart,
|
||||
width,
|
||||
)
|
||||
}
|
||||
if content != "" {
|
||||
m = m.updateSelected(content, "")
|
||||
blocks = append(blocks, content)
|
||||
}
|
||||
}
|
||||
blocks = append(blocks, content)
|
||||
previousBlockType = toolInvocationBlock
|
||||
}
|
||||
}
|
||||
|
||||
error := ""
|
||||
switch err := message.Metadata.Error.AsUnion().(type) {
|
||||
case nil:
|
||||
default:
|
||||
clientError := err.(opencode.UnknownError)
|
||||
error = clientError.Data.Message
|
||||
if assistant, ok := message.(opencode.AssistantMessage); ok {
|
||||
switch err := assistant.Error.AsUnion().(type) {
|
||||
case nil:
|
||||
case opencode.AssistantMessageErrorMessageOutputLengthError:
|
||||
error = "Message output length exceeded"
|
||||
case opencode.ProviderAuthError:
|
||||
error = err.Data.Message
|
||||
case opencode.UnknownError:
|
||||
error = err.Data.Message
|
||||
}
|
||||
}
|
||||
|
||||
if error != "" {
|
||||
error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
|
||||
error = renderContentBlock(
|
||||
m.app,
|
||||
error,
|
||||
false,
|
||||
width,
|
||||
WithBorderColor(t.Error()),
|
||||
)
|
||||
blocks = append(blocks, error)
|
||||
previousBlockType = errorBlock
|
||||
m.lineCount += lipgloss.Height(error) + 1
|
||||
}
|
||||
}
|
||||
|
||||
centered := []string{}
|
||||
for _, block := range blocks {
|
||||
centered = append(centered, lipgloss.PlaceHorizontal(
|
||||
m.width,
|
||||
lipgloss.Center,
|
||||
block,
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
))
|
||||
m.viewport.SetContent("\n" + strings.Join(blocks, "\n\n"))
|
||||
if m.selectedPart == m.partCount {
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
|
||||
m.viewport.SetHeight(m.height - lipgloss.Height(m.header()))
|
||||
m.viewport.SetContent("\n" + strings.Join(centered, "\n") + "\n")
|
||||
}
|
||||
|
||||
func (m *messagesComponent) header() string {
|
||||
func (m *messagesComponent) updateSelected(content string, selectedText string) *messagesComponent {
|
||||
if m.selectedPart == m.partCount {
|
||||
m.viewport.SetYOffset(m.lineCount - (m.viewport.Height() / 2) + 4)
|
||||
m.selectedText = selectedText
|
||||
}
|
||||
m.partCount++
|
||||
m.lineCount += lipgloss.Height(content) + 1
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *messagesComponent) header(width int) string {
|
||||
if m.app.Session.ID == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
width := layout.Current.Container.Width
|
||||
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
|
||||
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
|
||||
headerLines := []string{}
|
||||
headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width-6, t.Background()))
|
||||
headerLines = append(
|
||||
headerLines,
|
||||
util.ToMarkdown("# "+m.app.Session.Title, width-6, t.Background()),
|
||||
)
|
||||
if m.app.Session.Share.URL != "" {
|
||||
headerLines = append(headerLines, muted(m.app.Session.Share.URL))
|
||||
headerLines = append(headerLines, muted(m.app.Session.Share.URL+" /unshare"))
|
||||
} else {
|
||||
headerLines = append(headerLines, base("/share")+muted(" to create a shareable link"))
|
||||
}
|
||||
@@ -299,98 +390,29 @@ func (m *messagesComponent) header() string {
|
||||
return "\n" + header + "\n"
|
||||
}
|
||||
|
||||
func (m *messagesComponent) View() string {
|
||||
if len(m.app.Messages) == 0 {
|
||||
return m.home()
|
||||
}
|
||||
func (m *messagesComponent) View(width, height int) string {
|
||||
t := theme.CurrentTheme()
|
||||
if m.rendering {
|
||||
return lipgloss.Place(
|
||||
m.width,
|
||||
m.height,
|
||||
width,
|
||||
height,
|
||||
lipgloss.Center,
|
||||
lipgloss.Center,
|
||||
"Loading session...",
|
||||
styles.NewStyle().Background(t.Background()).Render(""),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
}
|
||||
t := theme.CurrentTheme()
|
||||
return lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
lipgloss.PlaceHorizontal(
|
||||
m.width,
|
||||
lipgloss.Center,
|
||||
m.header(),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
),
|
||||
m.viewport.View(),
|
||||
)
|
||||
}
|
||||
header := m.header(width)
|
||||
m.viewport.SetWidth(width)
|
||||
m.viewport.SetHeight(height - lipgloss.Height(header))
|
||||
|
||||
func (m *messagesComponent) home() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.NewStyle().Background(t.Background())
|
||||
base := baseStyle.Render
|
||||
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
|
||||
|
||||
open := `
|
||||
█▀▀█ █▀▀█ █▀▀ █▀▀▄
|
||||
█░░█ █░░█ █▀▀ █░░█
|
||||
▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ `
|
||||
code := `
|
||||
█▀▀ █▀▀█ █▀▀▄ █▀▀
|
||||
█░░ █░░█ █░░█ █▀▀
|
||||
▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`
|
||||
|
||||
logo := lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
muted(open),
|
||||
base(code),
|
||||
)
|
||||
// cwd := app.Info.Path.Cwd
|
||||
// config := app.Info.Path.Config
|
||||
|
||||
versionStyle := styles.NewStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
return styles.NewStyle().
|
||||
Background(t.Background()).
|
||||
Width(lipgloss.Width(logo)).
|
||||
Align(lipgloss.Right)
|
||||
version := versionStyle.Render(m.app.Version)
|
||||
|
||||
logoAndVersion := strings.Join([]string{logo, version}, "\n")
|
||||
logoAndVersion = lipgloss.PlaceHorizontal(
|
||||
m.width,
|
||||
lipgloss.Center,
|
||||
logoAndVersion,
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
m.commands.SetBackgroundColor(t.Background())
|
||||
commands := lipgloss.PlaceHorizontal(
|
||||
m.width,
|
||||
lipgloss.Center,
|
||||
m.commands.View(),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
|
||||
lines := []string{}
|
||||
lines = append(lines, logoAndVersion)
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, "")
|
||||
// lines = append(lines, base("cwd ")+muted(cwd))
|
||||
// lines = append(lines, base("config ")+muted(config))
|
||||
// lines = append(lines, "")
|
||||
lines = append(lines, commands)
|
||||
|
||||
return lipgloss.Place(
|
||||
m.width,
|
||||
m.height,
|
||||
lipgloss.Center,
|
||||
lipgloss.Center,
|
||||
baseStyle.Render(strings.Join(lines, "\n")),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
Render(header + "\n" + m.viewport.View())
|
||||
}
|
||||
|
||||
func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
|
||||
if m.width == width && m.height == height {
|
||||
func (m *messagesComponent) SetWidth(width int) tea.Cmd {
|
||||
if m.width == width {
|
||||
return nil
|
||||
}
|
||||
// Clear cache on resize since width affects rendering
|
||||
@@ -398,24 +420,14 @@ func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
|
||||
m.cache.Clear()
|
||||
}
|
||||
m.width = width
|
||||
m.height = height
|
||||
m.viewport.SetWidth(width)
|
||||
m.viewport.SetHeight(height - lipgloss.Height(m.header()))
|
||||
m.attachments.SetWidth(width + 40)
|
||||
m.attachments.SetHeight(3)
|
||||
m.commands.SetSize(width, height)
|
||||
m.renderView()
|
||||
m.renderView(width)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *messagesComponent) GetSize() (int, int) {
|
||||
return m.width, m.height
|
||||
}
|
||||
|
||||
func (m *messagesComponent) Reload() tea.Cmd {
|
||||
m.rendering = true
|
||||
return func() tea.Msg {
|
||||
m.renderView()
|
||||
m.renderView(m.width)
|
||||
return renderFinishedMsg{}
|
||||
}
|
||||
}
|
||||
@@ -440,16 +452,45 @@ func (m *messagesComponent) HalfPageDown() (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *messagesComponent) First() (tea.Model, tea.Cmd) {
|
||||
m.viewport.GotoTop()
|
||||
func (m *messagesComponent) Previous() (tea.Model, tea.Cmd) {
|
||||
m.tail = false
|
||||
return m, nil
|
||||
if m.selectedPart < 0 {
|
||||
m.selectedPart = m.partCount
|
||||
}
|
||||
m.selectedPart--
|
||||
if m.selectedPart < 0 {
|
||||
m.selectedPart = 0
|
||||
}
|
||||
return m, util.CmdHandler(selectedMessagePartChangedMsg{
|
||||
part: m.selectedPart,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *messagesComponent) Next() (tea.Model, tea.Cmd) {
|
||||
m.tail = false
|
||||
m.selectedPart++
|
||||
if m.selectedPart >= m.partCount {
|
||||
m.selectedPart = m.partCount
|
||||
}
|
||||
return m, util.CmdHandler(selectedMessagePartChangedMsg{
|
||||
part: m.selectedPart,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *messagesComponent) First() (tea.Model, tea.Cmd) {
|
||||
m.selectedPart = 0
|
||||
m.tail = false
|
||||
return m, util.CmdHandler(selectedMessagePartChangedMsg{
|
||||
part: m.selectedPart,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *messagesComponent) Last() (tea.Model, tea.Cmd) {
|
||||
m.viewport.GotoBottom()
|
||||
m.selectedPart = m.partCount - 1
|
||||
m.tail = true
|
||||
return m, nil
|
||||
return m, util.CmdHandler(selectedMessagePartChangedMsg{
|
||||
part: m.selectedPart,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *messagesComponent) ToolDetailsVisible() bool {
|
||||
@@ -457,31 +498,15 @@ func (m *messagesComponent) ToolDetailsVisible() bool {
|
||||
}
|
||||
|
||||
func NewMessagesComponent(app *app.App) MessagesComponent {
|
||||
customSpinner := spinner.Spinner{
|
||||
Frames: []string{" ", "┃", "┃"},
|
||||
FPS: time.Second / 3,
|
||||
}
|
||||
s := spinner.New(spinner.WithSpinner(customSpinner))
|
||||
|
||||
vp := viewport.New()
|
||||
attachments := viewport.New()
|
||||
vp.KeyMap = viewport.KeyMap{}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
commandsView := commands.New(
|
||||
app,
|
||||
commands.WithBackground(t.Background()),
|
||||
commands.WithLimit(6),
|
||||
)
|
||||
|
||||
return &messagesComponent{
|
||||
app: app,
|
||||
viewport: vp,
|
||||
spinner: s,
|
||||
attachments: attachments,
|
||||
commands: commandsView,
|
||||
showToolDetails: true,
|
||||
cache: NewMessageCache(),
|
||||
tail: true,
|
||||
selectedPart: -1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,13 @@ import (
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/commands"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
type CommandsComponent interface {
|
||||
tea.Model
|
||||
tea.ViewModel
|
||||
layout.Sizeable
|
||||
SetSize(width, height int) tea.Cmd
|
||||
SetBackgroundColor(color compat.AdaptiveColor)
|
||||
}
|
||||
|
||||
@@ -36,27 +34,10 @@ func (c *commandsComponent) SetSize(width, height int) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *commandsComponent) GetSize() (int, int) {
|
||||
return c.width, c.height
|
||||
}
|
||||
|
||||
func (c *commandsComponent) SetBackgroundColor(color compat.AdaptiveColor) {
|
||||
c.background = &color
|
||||
}
|
||||
|
||||
func (c *commandsComponent) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *commandsComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
c.width = msg.Width
|
||||
c.height = msg.Height
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *commandsComponent) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user