mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-05-03 07:11:31 +08:00
Compare commits
159 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e07384364 | ||
|
|
4c4739c422 | ||
|
|
2d8b90a6ff | ||
|
|
a2fa7ffa42 | ||
|
|
f7d6175283 | ||
|
|
9ed187ee52 | ||
|
|
14d81e574b | ||
|
|
6efe8cc8df | ||
|
|
daa5fc916a | ||
|
|
c659496b96 | ||
|
|
21fbf21cb6 | ||
|
|
f31cbf2744 | ||
|
|
8322f18e03 | ||
|
|
562bdb95e2 | ||
|
|
a57ce8365d | ||
|
|
0da83ae67e | ||
|
|
662d022a48 | ||
|
|
9efef03919 | ||
|
|
7a9fb3fa92 | ||
|
|
ea96ead346 | ||
|
|
6100a77b85 | ||
|
|
c7a59ee2b1 | ||
|
|
a272b58fe9 | ||
|
|
9948fcf1b6 | ||
|
|
0d50c867ff | ||
|
|
27f7e02f12 | ||
|
|
0f93ecd564 | ||
|
|
da909d9684 | ||
|
|
facd851b11 | ||
|
|
c51de945a5 | ||
|
|
9253a3ca9e | ||
|
|
7cfa297a78 | ||
|
|
661b74def6 | ||
|
|
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 | ||
|
|
248325925f | ||
|
|
ca48a4f0fb | ||
|
|
98ee5a3d87 | ||
|
|
67480e5a1c | ||
|
|
2581a9b54c | ||
|
|
14a293e124 | ||
|
|
780419ecae | ||
|
|
f0962e2d9c |
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@@ -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
|
||||
32
.github/workflows/stats.yml
vendored
Normal file
32
.github/workflows/stats.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: stats
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 12 * * *" # Run daily at 12:00 UTC
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
jobs:
|
||||
stats:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Run stats script
|
||||
run: bun scripts/stats.ts
|
||||
|
||||
- name: Commit stats
|
||||
run: |
|
||||
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 "ignore: update download stats $(date -I)"
|
||||
git push
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ node_modules
|
||||
.env
|
||||
.idea
|
||||
.vscode
|
||||
openapi.json
|
||||
|
||||
11
README.md
11
README.md
@@ -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)
|
||||
|
||||
13
STATS.md
Normal file
13
STATS.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Download Stats
|
||||
|
||||
| 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) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
114
bun.lock
114
bun.lock
@@ -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: {
|
||||
@@ -36,6 +39,8 @@ new sst.cloudflare.x.Astro("Web", {
|
||||
domain,
|
||||
path: "packages/web",
|
||||
environment: {
|
||||
// For astro config
|
||||
SST_STAGE: $app.stage,
|
||||
VITE_API_URL: api.url,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"weather": {
|
||||
"type": "local",
|
||||
"command": ["opencode", "x", "@h1deya/mcp-server-weather"]
|
||||
}
|
||||
},
|
||||
"experimental": {
|
||||
"hook": {
|
||||
"file_edited": {
|
||||
|
||||
14
package.json
14
package.json
@@ -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": "./scripts/stainless",
|
||||
"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
packages/opencode/.gitignore
vendored
1
packages/opencode/.gitignore
vendored
@@ -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%" %*
|
||||
|
||||
@@ -14,111 +14,267 @@
|
||||
"properties": {
|
||||
"leader": {
|
||||
"type": "string",
|
||||
"description": "Leader key for keybind combinations"
|
||||
"default": "ctrl+x",
|
||||
"description": "Leader key for keybind combinations\n\ndefault: `ctrl+x`",
|
||||
"examples": [
|
||||
"ctrl+x"
|
||||
]
|
||||
},
|
||||
"help": {
|
||||
"app_help": {
|
||||
"type": "string",
|
||||
"description": "Show help dialog"
|
||||
"default": "<leader>h",
|
||||
"description": "Show help dialog\n\ndefault: `<leader>h`",
|
||||
"examples": [
|
||||
"<leader>h"
|
||||
]
|
||||
},
|
||||
"editor_open": {
|
||||
"type": "string",
|
||||
"description": "Open external editor"
|
||||
"default": "<leader>e",
|
||||
"description": "Open external editor\n\ndefault: `<leader>e`",
|
||||
"examples": [
|
||||
"<leader>e"
|
||||
]
|
||||
},
|
||||
"session_new": {
|
||||
"type": "string",
|
||||
"description": "Create a new session"
|
||||
"default": "<leader>n",
|
||||
"description": "Create a new session\n\ndefault: `<leader>n`",
|
||||
"examples": [
|
||||
"<leader>n"
|
||||
]
|
||||
},
|
||||
"session_list": {
|
||||
"type": "string",
|
||||
"description": "List all sessions"
|
||||
"default": "<leader>l",
|
||||
"description": "List all sessions\n\ndefault: `<leader>l`",
|
||||
"examples": [
|
||||
"<leader>l"
|
||||
]
|
||||
},
|
||||
"session_share": {
|
||||
"type": "string",
|
||||
"description": "Share current session"
|
||||
"default": "<leader>s",
|
||||
"description": "Share current session\n\ndefault: `<leader>s`",
|
||||
"examples": [
|
||||
"<leader>s"
|
||||
]
|
||||
},
|
||||
"session_unshare": {
|
||||
"type": "string",
|
||||
"default": "<leader>u",
|
||||
"description": "Unshare current session\n\ndefault: `<leader>u`",
|
||||
"examples": [
|
||||
"<leader>u"
|
||||
]
|
||||
},
|
||||
"session_interrupt": {
|
||||
"type": "string",
|
||||
"description": "Interrupt current session"
|
||||
"default": "esc",
|
||||
"description": "Interrupt current session\n\ndefault: `esc`",
|
||||
"examples": [
|
||||
"esc"
|
||||
]
|
||||
},
|
||||
"session_compact": {
|
||||
"type": "string",
|
||||
"description": "Toggle compact mode for session"
|
||||
"default": "<leader>c",
|
||||
"description": "Compact the session\n\ndefault: `<leader>c`",
|
||||
"examples": [
|
||||
"<leader>c"
|
||||
]
|
||||
},
|
||||
"tool_details": {
|
||||
"type": "string",
|
||||
"description": "Show tool details"
|
||||
"default": "<leader>d",
|
||||
"description": "Toggle tool details\n\ndefault: `<leader>d`",
|
||||
"examples": [
|
||||
"<leader>d"
|
||||
]
|
||||
},
|
||||
"model_list": {
|
||||
"type": "string",
|
||||
"description": "List available models"
|
||||
"default": "<leader>m",
|
||||
"description": "List available models\n\ndefault: `<leader>m`",
|
||||
"examples": [
|
||||
"<leader>m"
|
||||
]
|
||||
},
|
||||
"theme_list": {
|
||||
"type": "string",
|
||||
"description": "List available themes"
|
||||
"default": "<leader>t",
|
||||
"description": "List available themes\n\ndefault: `<leader>t`",
|
||||
"examples": [
|
||||
"<leader>t"
|
||||
]
|
||||
},
|
||||
"file_list": {
|
||||
"type": "string",
|
||||
"default": "<leader>f",
|
||||
"description": "List files\n\ndefault: `<leader>f`",
|
||||
"examples": [
|
||||
"<leader>f"
|
||||
]
|
||||
},
|
||||
"file_close": {
|
||||
"type": "string",
|
||||
"default": "esc",
|
||||
"description": "Close file\n\ndefault: `esc`",
|
||||
"examples": [
|
||||
"esc"
|
||||
]
|
||||
},
|
||||
"file_search": {
|
||||
"type": "string",
|
||||
"default": "<leader>/",
|
||||
"description": "Search file\n\ndefault: `<leader>/`",
|
||||
"examples": [
|
||||
"<leader>/"
|
||||
]
|
||||
},
|
||||
"file_diff_toggle": {
|
||||
"type": "string",
|
||||
"default": "<leader>v",
|
||||
"description": "Split/unified diff\n\ndefault: `<leader>v`",
|
||||
"examples": [
|
||||
"<leader>v"
|
||||
]
|
||||
},
|
||||
"project_init": {
|
||||
"type": "string",
|
||||
"description": "Initialize project configuration"
|
||||
"default": "<leader>i",
|
||||
"description": "Create/update AGENTS.md\n\ndefault: `<leader>i`",
|
||||
"examples": [
|
||||
"<leader>i"
|
||||
]
|
||||
},
|
||||
"input_clear": {
|
||||
"type": "string",
|
||||
"description": "Clear input field"
|
||||
"default": "ctrl+c",
|
||||
"description": "Clear input field\n\ndefault: `ctrl+c`",
|
||||
"examples": [
|
||||
"ctrl+c"
|
||||
]
|
||||
},
|
||||
"input_paste": {
|
||||
"type": "string",
|
||||
"description": "Paste from clipboard"
|
||||
"default": "ctrl+v",
|
||||
"description": "Paste from clipboard\n\ndefault: `ctrl+v`",
|
||||
"examples": [
|
||||
"ctrl+v"
|
||||
]
|
||||
},
|
||||
"input_submit": {
|
||||
"type": "string",
|
||||
"description": "Submit input"
|
||||
"default": "enter",
|
||||
"description": "Submit input\n\ndefault: `enter`",
|
||||
"examples": [
|
||||
"enter"
|
||||
]
|
||||
},
|
||||
"input_newline": {
|
||||
"type": "string",
|
||||
"description": "Insert newline in input"
|
||||
},
|
||||
"history_previous": {
|
||||
"type": "string",
|
||||
"description": "Navigate to previous history item"
|
||||
},
|
||||
"history_next": {
|
||||
"type": "string",
|
||||
"description": "Navigate to next history item"
|
||||
"default": "shift+enter,ctrl+j",
|
||||
"description": "Insert newline in input\n\ndefault: `shift+enter,ctrl+j`",
|
||||
"examples": [
|
||||
"shift+enter,ctrl+j"
|
||||
]
|
||||
},
|
||||
"messages_page_up": {
|
||||
"type": "string",
|
||||
"description": "Scroll messages up by one page"
|
||||
"default": "pgup",
|
||||
"description": "Scroll messages up by one page\n\ndefault: `pgup`",
|
||||
"examples": [
|
||||
"pgup"
|
||||
]
|
||||
},
|
||||
"messages_page_down": {
|
||||
"type": "string",
|
||||
"description": "Scroll messages down by one page"
|
||||
"default": "pgdown",
|
||||
"description": "Scroll messages down by one page\n\ndefault: `pgdown`",
|
||||
"examples": [
|
||||
"pgdown"
|
||||
]
|
||||
},
|
||||
"messages_half_page_up": {
|
||||
"type": "string",
|
||||
"description": "Scroll messages up by half page"
|
||||
"default": "ctrl+alt+u",
|
||||
"description": "Scroll messages up by half page\n\ndefault: `ctrl+alt+u`",
|
||||
"examples": [
|
||||
"ctrl+alt+u"
|
||||
]
|
||||
},
|
||||
"messages_half_page_down": {
|
||||
"type": "string",
|
||||
"description": "Scroll messages down by half page"
|
||||
"default": "ctrl+alt+d",
|
||||
"description": "Scroll messages down by half page\n\ndefault: `ctrl+alt+d`",
|
||||
"examples": [
|
||||
"ctrl+alt+d"
|
||||
]
|
||||
},
|
||||
"messages_previous": {
|
||||
"type": "string",
|
||||
"description": "Navigate to previous message"
|
||||
"default": "ctrl+up",
|
||||
"description": "Navigate to previous message\n\ndefault: `ctrl+up`",
|
||||
"examples": [
|
||||
"ctrl+up"
|
||||
]
|
||||
},
|
||||
"messages_next": {
|
||||
"type": "string",
|
||||
"description": "Navigate to next message"
|
||||
"default": "ctrl+down",
|
||||
"description": "Navigate to next message\n\ndefault: `ctrl+down`",
|
||||
"examples": [
|
||||
"ctrl+down"
|
||||
]
|
||||
},
|
||||
"messages_first": {
|
||||
"type": "string",
|
||||
"description": "Navigate to first message"
|
||||
"default": "ctrl+g",
|
||||
"description": "Navigate to first message\n\ndefault: `ctrl+g`",
|
||||
"examples": [
|
||||
"ctrl+g"
|
||||
]
|
||||
},
|
||||
"messages_last": {
|
||||
"type": "string",
|
||||
"description": "Navigate to last message"
|
||||
"default": "ctrl+alt+g",
|
||||
"description": "Navigate to last message\n\ndefault: `ctrl+alt+g`",
|
||||
"examples": [
|
||||
"ctrl+alt+g"
|
||||
]
|
||||
},
|
||||
"messages_layout_toggle": {
|
||||
"type": "string",
|
||||
"default": "<leader>p",
|
||||
"description": "Toggle layout\n\ndefault: `<leader>p`",
|
||||
"examples": [
|
||||
"<leader>p"
|
||||
]
|
||||
},
|
||||
"messages_copy": {
|
||||
"type": "string",
|
||||
"default": "<leader>y",
|
||||
"description": "Copy message\n\ndefault: `<leader>y`",
|
||||
"examples": [
|
||||
"<leader>y"
|
||||
]
|
||||
},
|
||||
"messages_revert": {
|
||||
"type": "string",
|
||||
"default": "<leader>r",
|
||||
"description": "Revert message\n\ndefault: `<leader>r`",
|
||||
"examples": [
|
||||
"<leader>r"
|
||||
]
|
||||
},
|
||||
"app_exit": {
|
||||
"type": "string",
|
||||
"description": "Exit the application"
|
||||
"default": "ctrl+c,<leader>q",
|
||||
"description": "Exit the application\n\ndefault: `ctrl+c,<leader>q`",
|
||||
"examples": [
|
||||
"ctrl+c,<leader>q"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -171,9 +327,15 @@
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"release_date": {
|
||||
"type": "string"
|
||||
},
|
||||
"attachment": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -202,7 +364,10 @@
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["input", "output"],
|
||||
"required": [
|
||||
"input",
|
||||
"output"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"limit": {
|
||||
@@ -215,12 +380,12 @@
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["context", "output"],
|
||||
"required": [
|
||||
"context",
|
||||
"output"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
@@ -234,7 +399,9 @@
|
||||
"additionalProperties": {}
|
||||
}
|
||||
},
|
||||
"required": ["models"],
|
||||
"required": [
|
||||
"models"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"description": "Custom provider configurations and model overrides"
|
||||
@@ -264,9 +431,16 @@
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Environment variables to set when running the MCP server"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable or disable the MCP server on startup"
|
||||
}
|
||||
},
|
||||
"required": ["type", "command"],
|
||||
"required": [
|
||||
"type",
|
||||
"command"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
@@ -280,15 +454,29 @@
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "URL of the remote MCP server"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable or disable the MCP server on startup"
|
||||
}
|
||||
},
|
||||
"required": ["type", "url"],
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"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": {
|
||||
@@ -315,7 +503,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["command"],
|
||||
"required": [
|
||||
"command"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
@@ -338,7 +528,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["command"],
|
||||
"required": [
|
||||
"command"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
@@ -351,4 +543,4 @@
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||
}
|
||||
}
|
||||
@@ -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}"`
|
||||
|
||||
@@ -4,5 +4,30 @@ import "zod-openapi/extend"
|
||||
import { Config } from "../src/config/config"
|
||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
|
||||
const result = zodToJsonSchema(Config.Info)
|
||||
const result = zodToJsonSchema(Config.Info, {
|
||||
/**
|
||||
* We'll use the `default` values of the field as the only value in `examples`.
|
||||
* This will ensure no docs are needed to be read, as the configuration is
|
||||
* self-documenting.
|
||||
*
|
||||
* See https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.9.5
|
||||
*/
|
||||
postProcess(jsonSchema) {
|
||||
const schema = jsonSchema as typeof jsonSchema & {
|
||||
examples?: unknown[]
|
||||
}
|
||||
if (schema && typeof schema === "object" && "type" in schema && schema.type === "string" && schema?.default) {
|
||||
if (!schema.examples) {
|
||||
schema.examples = [schema.default]
|
||||
}
|
||||
|
||||
schema.description = [schema.description || "", `default: \`${schema.default}\``]
|
||||
.filter(Boolean)
|
||||
.join("\n\n")
|
||||
.trim()
|
||||
}
|
||||
|
||||
return jsonSchema
|
||||
},
|
||||
})
|
||||
await Bun.write("config.schema.json", JSON.stringify(result, null, 2))
|
||||
|
||||
@@ -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,15 @@
|
||||
import { App } from "../app/app"
|
||||
import { ConfigHooks } from "../config/hooks"
|
||||
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()
|
||||
|
||||
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,
|
||||
})),
|
||||
})
|
||||
|
||||
36
packages/opencode/src/cli/cmd/debug/file.ts
Normal file
36
packages/opencode/src/cli/cmd/debug/file.ts
Normal file
@@ -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() {},
|
||||
})
|
||||
26
packages/opencode/src/cli/cmd/debug/index.ts
Normal file
26
packages/opencode/src/cli/cmd/debug/index.ts
Normal file
@@ -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() {},
|
||||
})
|
||||
34
packages/opencode/src/cli/cmd/debug/lsp.ts
Normal file
34
packages/opencode/src/cli/cmd/debug/lsp.ts
Normal file
@@ -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))
|
||||
})
|
||||
},
|
||||
})
|
||||
82
packages/opencode/src/cli/cmd/debug/ripgrep.ts
Normal file
82
packages/opencode/src/cli/cmd/debug/ripgrep.ts
Normal file
@@ -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))
|
||||
},
|
||||
})
|
||||
35
packages/opencode/src/cli/cmd/debug/snapshot.ts
Normal file
35
packages/opencode/src/cli/cmd/debug/snapshot.ts
Normal file
@@ -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],
|
||||
@@ -54,7 +54,10 @@ export const RunCommand = cmd({
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
const message = args.message.join(" ")
|
||||
let message = args.message.join(" ")
|
||||
|
||||
if (!process.stdin.isTTY) message += "\n" + (await Bun.stdin.text())
|
||||
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
const session = await (async () => {
|
||||
if (args.continue) {
|
||||
@@ -78,27 +81,19 @@ export const RunCommand = cmd({
|
||||
UI.empty()
|
||||
UI.println(UI.logo())
|
||||
UI.empty()
|
||||
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
|
||||
const displayMessage = message.length > 300 ? message.slice(0, 300) + "..." : message
|
||||
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", displayMessage)
|
||||
UI.empty()
|
||||
|
||||
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 +105,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") {
|
||||
@@ -141,6 +125,20 @@ export const RunCommand = cmd({
|
||||
}
|
||||
})
|
||||
|
||||
let errorMsg: string | undefined
|
||||
Bus.subscribe(Session.Event.Error, async (evt) => {
|
||||
const { sessionID, error } = evt.properties
|
||||
if (sessionID !== session.id || !error) return
|
||||
let err = String(error.name)
|
||||
|
||||
if ("data" in error && error.data && "message" in error.data) {
|
||||
err = error.data.message
|
||||
}
|
||||
errorMsg = errorMsg ? errorMsg + "\n" + err : err
|
||||
|
||||
UI.error(err)
|
||||
})
|
||||
|
||||
const result = await Session.chat({
|
||||
sessionID: session.id,
|
||||
providerID,
|
||||
@@ -156,6 +154,7 @@ export const RunCommand = cmd({
|
||||
if (isPiped) {
|
||||
const match = result.parts.findLast((x) => x.type === "text")
|
||||
if (match) process.stdout.write(match.text)
|
||||
if (errorMsg) process.stdout.write(errorMsg)
|
||||
}
|
||||
UI.empty()
|
||||
})
|
||||
|
||||
@@ -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,15 +9,28 @@ import fs from "fs/promises"
|
||||
import { Installation } from "../../installation"
|
||||
import { Config } from "../../config/config"
|
||||
import { Bus } from "../../bus"
|
||||
import { Log } from "../../util/log"
|
||||
import { FileWatcher } from "../../file/watch"
|
||||
|
||||
export const TuiCommand = cmd({
|
||||
command: "$0 [project]",
|
||||
describe: "start opencode tui",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("project", {
|
||||
type: "string",
|
||||
describe: "path to start opencode in",
|
||||
}),
|
||||
yargs
|
||||
.positional("project", {
|
||||
type: "string",
|
||||
describe: "path to start opencode in",
|
||||
})
|
||||
.option("model", {
|
||||
type: "string",
|
||||
alias: ["m"],
|
||||
describe: "model to use in the format of provider/model",
|
||||
})
|
||||
.option("prompt", {
|
||||
alias: ["p"],
|
||||
type: "string",
|
||||
describe: "prompt to use",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
while (true) {
|
||||
const cwd = args.project ? path.resolve(args.project) : process.cwd()
|
||||
@@ -28,6 +41,7 @@ export const TuiCommand = cmd({
|
||||
return
|
||||
}
|
||||
const result = await bootstrap({ cwd }, async (app) => {
|
||||
FileWatcher.init()
|
||||
const providers = await Provider.list()
|
||||
if (Object.keys(providers).length === 0) {
|
||||
return "needs_provider"
|
||||
@@ -39,9 +53,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,14 +69,22 @@ export const TuiCommand = cmd({
|
||||
cwd = process.cwd()
|
||||
cmd = [binary]
|
||||
}
|
||||
Log.Default.info("tui", {
|
||||
cmd,
|
||||
})
|
||||
const proc = Bun.spawn({
|
||||
cmd: [...cmd, ...process.argv.slice(2)],
|
||||
cmd: [
|
||||
...cmd,
|
||||
...(args.model ? ["--model", args.model] : []),
|
||||
...(args.prompt ? ["--prompt", args.prompt] : []),
|
||||
],
|
||||
cwd,
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
stdin: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
CGO_ENABLED: "0",
|
||||
OPENCODE_SERVER: server.url.toString(),
|
||||
OPENCODE_APP_INFO: JSON.stringify(app),
|
||||
},
|
||||
@@ -100,7 +120,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 +132,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,14 +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"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
@@ -47,6 +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"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
@@ -58,69 +57,43 @@ export namespace Config {
|
||||
|
||||
export const Keybinds = z
|
||||
.object({
|
||||
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"),
|
||||
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"),
|
||||
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"),
|
||||
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
|
||||
app_help: z.string().optional().default("<leader>h").describe("Show help dialog"),
|
||||
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
|
||||
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
|
||||
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
|
||||
session_share: z.string().optional().default("<leader>s").describe("Share current session"),
|
||||
session_unshare: z.string().optional().default("<leader>u").describe("Unshare current session"),
|
||||
session_interrupt: z.string().optional().default("esc").describe("Interrupt current session"),
|
||||
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
|
||||
tool_details: z.string().optional().default("<leader>d").describe("Toggle tool details"),
|
||||
model_list: z.string().optional().default("<leader>m").describe("List available models"),
|
||||
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
|
||||
file_list: z.string().optional().default("<leader>f").describe("List files"),
|
||||
file_close: z.string().optional().default("esc").describe("Close file"),
|
||||
file_search: z.string().optional().default("<leader>/").describe("Search file"),
|
||||
file_diff_toggle: z.string().optional().default("<leader>v").describe("Split/unified diff"),
|
||||
project_init: z.string().optional().default("<leader>i").describe("Create/update AGENTS.md"),
|
||||
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
|
||||
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
|
||||
input_submit: z.string().optional().default("enter").describe("Submit input"),
|
||||
input_newline: z.string().optional().default("shift+enter,ctrl+j").describe("Insert newline in input"),
|
||||
messages_page_up: z.string().optional().default("pgup").describe("Scroll messages up by one page"),
|
||||
messages_page_down: z.string().optional().default("pgdown").describe("Scroll messages down by one page"),
|
||||
messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
|
||||
messages_half_page_down: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+alt+d")
|
||||
.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_last: z.string().optional().describe("Navigate to last message"),
|
||||
app_exit: z.string().optional().describe("Exit the application"),
|
||||
messages_previous: z.string().optional().default("ctrl+up").describe("Navigate to previous message"),
|
||||
messages_next: z.string().optional().default("ctrl+down").describe("Navigate to next message"),
|
||||
messages_first: z.string().optional().default("ctrl+g").describe("Navigate to first message"),
|
||||
messages_last: z.string().optional().default("ctrl+alt+g").describe("Navigate to last message"),
|
||||
messages_layout_toggle: z.string().optional().default("<leader>p").describe("Toggle layout"),
|
||||
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
|
||||
messages_revert: z.string().optional().default("<leader>r").describe("Revert message"),
|
||||
app_exit: z.string().optional().default("ctrl+c,<leader>q").describe("Exit the application"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
@@ -128,33 +101,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({
|
||||
@@ -164,10 +117,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
|
||||
@@ -215,10 +166,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
packages/opencode/src/external/ripgrep.ts
vendored
132
packages/opencode/src/external/ripgrep.ts
vendored
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
329
packages/opencode/src/file/ripgrep.ts
Normal file
329
packages/opencode/src/file/ripgrep.ts
Normal file
@@ -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 = [
|
||||
`${$.escape(await filepath())} --files --follow --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(
|
||||
|
||||
52
packages/opencode/src/file/watch.ts
Normal file
52
packages/opencode/src/file/watch.ts
Normal file
@@ -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()
|
||||
|
||||
146
packages/opencode/src/format/formatter.ts
Normal file
146
packages/opencode/src/format/formatter.ts
Normal file
@@ -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",
|
||||
@@ -86,4 +94,6 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
|
||||
".yml": "yaml",
|
||||
".mjs": "javascript",
|
||||
".cjs": "javascript",
|
||||
".zig": "zig",
|
||||
".zon": "zig",
|
||||
} as const
|
||||
|
||||
@@ -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,266 @@ 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),
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export const Zls: Info = {
|
||||
id: "zls",
|
||||
extensions: [".zig", ".zon"],
|
||||
async spawn() {
|
||||
let bin = Bun.which("zls", {
|
||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||
})
|
||||
|
||||
if (!bin) {
|
||||
const zig = Bun.which("zig")
|
||||
if (!zig) {
|
||||
log.error("Zig is required to use zls. Please install Zig first.")
|
||||
return
|
||||
}
|
||||
|
||||
log.info("downloading zls from GitHub releases")
|
||||
|
||||
const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
|
||||
if (!releaseResponse.ok) {
|
||||
log.error("Failed to fetch zls release info")
|
||||
return
|
||||
}
|
||||
|
||||
const release = await releaseResponse.json()
|
||||
|
||||
const platform = process.platform
|
||||
const arch = process.arch
|
||||
let assetName = ""
|
||||
|
||||
let zlsArch: string = arch
|
||||
if (arch === "arm64") zlsArch = "aarch64"
|
||||
else if (arch === "x64") zlsArch = "x86_64"
|
||||
else if (arch === "ia32") zlsArch = "x86"
|
||||
|
||||
let zlsPlatform: string = platform
|
||||
if (platform === "darwin") zlsPlatform = "macos"
|
||||
else if (platform === "win32") zlsPlatform = "windows"
|
||||
|
||||
const ext = platform === "win32" ? "zip" : "tar.xz"
|
||||
|
||||
assetName = `zls-${zlsArch}-${zlsPlatform}.${ext}`
|
||||
|
||||
const supportedCombos = [
|
||||
"zls-x86_64-linux.tar.xz",
|
||||
"zls-x86_64-macos.tar.xz",
|
||||
"zls-x86_64-windows.zip",
|
||||
"zls-aarch64-linux.tar.xz",
|
||||
"zls-aarch64-macos.tar.xz",
|
||||
"zls-aarch64-windows.zip",
|
||||
"zls-x86-linux.tar.xz",
|
||||
"zls-x86-windows.zip",
|
||||
]
|
||||
|
||||
if (!supportedCombos.includes(assetName)) {
|
||||
log.error("Unsupported platform/architecture for zls", { platform, arch, assetName })
|
||||
return
|
||||
}
|
||||
|
||||
const asset = release.assets?.find((a: any) => a.name === assetName)
|
||||
|
||||
if (!asset) {
|
||||
log.error("Could not find zls download for platform", { platform, arch, assetName })
|
||||
return
|
||||
}
|
||||
|
||||
const downloadUrl = asset.browser_download_url
|
||||
log.info("downloading zls", { url: downloadUrl })
|
||||
|
||||
const response = await fetch(downloadUrl)
|
||||
if (!response.ok) {
|
||||
log.error("Failed to download zls")
|
||||
return
|
||||
}
|
||||
|
||||
const isZip = assetName.endsWith(".zip")
|
||||
const archivePath = path.join(Global.Path.bin, isZip ? "zls.zip" : "zls.tar.xz")
|
||||
await Bun.file(archivePath).write(response)
|
||||
|
||||
if (isZip) {
|
||||
await $`unzip -o -q ${archivePath} -d ${Global.Path.bin}`.nothrow()
|
||||
} else {
|
||||
await $`tar -xf ${archivePath} -C ${Global.Path.bin}`.quiet()
|
||||
}
|
||||
|
||||
await fs.rm(archivePath, { force: true })
|
||||
|
||||
if (platform !== "win32") {
|
||||
bin = path.join(Global.Path.bin, "zls")
|
||||
await $`chmod +x ${bin}`.quiet()
|
||||
} else {
|
||||
bin = path.join(Global.Path.bin, "zls.exe")
|
||||
}
|
||||
|
||||
log.info("installed zls", { bin })
|
||||
}
|
||||
|
||||
return {
|
||||
process: spawn(bin!),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ export namespace MCP {
|
||||
[name: string]: Awaited<ReturnType<typeof experimental_createMCPClient>>
|
||||
} = {}
|
||||
for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) {
|
||||
if (mcp.enabled === false) {
|
||||
log.info("mcp server disabled", { key })
|
||||
continue
|
||||
}
|
||||
log.info("found", { key, type: mcp.type })
|
||||
if (mcp.type === "remote") {
|
||||
const client = await experimental_createMCPClient({
|
||||
|
||||
@@ -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,34 @@ 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
|
||||
}
|
||||
const headers = {
|
||||
let isAgentCall = false
|
||||
let isVisionRequest = 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))
|
||||
isVisionRequest = body.messages.some(
|
||||
(msg: any) =>
|
||||
Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"),
|
||||
)
|
||||
}
|
||||
} catch {}
|
||||
const headers: Record<string, string> = {
|
||||
...init.headers,
|
||||
...copilot.HEADERS,
|
||||
Authorization: `Bearer ${info.access}`,
|
||||
"Openai-Intent": "conversation-edits",
|
||||
"X-Initiator": isAgentCall ? "agent" : "user",
|
||||
}
|
||||
if (isVisionRequest) {
|
||||
headers["Copilot-Vision-Request"] = "true"
|
||||
}
|
||||
delete headers["x-api-key"]
|
||||
return fetch(input, {
|
||||
@@ -125,14 +139,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 +151,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 +219,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")
|
||||
@@ -185,6 +239,7 @@ export namespace Provider {
|
||||
source,
|
||||
info,
|
||||
options,
|
||||
getModel,
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -202,6 +257,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 ?? {},
|
||||
}
|
||||
|
||||
@@ -210,6 +266,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,
|
||||
@@ -237,15 +294,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
|
||||
@@ -261,12 +321,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +358,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)
|
||||
@@ -330,9 +385,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,
|
||||
@@ -359,10 +412,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"],
|
||||
)
|
||||
@@ -373,11 +423,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")
|
||||
@@ -402,16 +448,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[]> = {
|
||||
@@ -461,9 +506,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,34 @@
|
||||
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" },
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
if (providerID === "amazon-bedrock" || 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.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
404
packages/opencode/src/session/message-v2.ts
Normal file
404
packages/opencode/src/session/message-v2.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
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 AbortedError = NamedError.create("MessageAbortedError", 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,
|
||||
AbortedError.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())
|
||||
}
|
||||
}
|
||||
|
||||
65
packages/opencode/src/snapshot/index.ts
Normal file
65
packages/opencode/src/snapshot/index.ts
Normal file
@@ -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,80 @@ 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) => {
|
||||
try {
|
||||
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"))
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
]
|
||||
|
||||
const state = App.state("storage", async () => {
|
||||
const app = App.info()
|
||||
const dir = path.join(app.path.data, "storage")
|
||||
log.info("init", { path: dir })
|
||||
const dir = path.normalize(path.join(app.path.data, "storage"))
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
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 +85,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),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,12 +2,12 @@ Reads a file from the local filesystem. You can access any file directly by usin
|
||||
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
|
||||
|
||||
Usage:
|
||||
- The file_path parameter must be an absolute path, not a relative path
|
||||
- The filePath parameter must be an absolute path, not a relative path
|
||||
- By default, it reads up to 2000 lines starting from the beginning of the file
|
||||
- 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", "cancelled"]).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>) {
|
||||
|
||||
14
packages/opencode/src/util/timeout.ts
Normal file
14
packages/opencode/src/util/timeout.ts
Normal file
@@ -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()
|
||||
})
|
||||
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
flag "github.com/spf13/pflag"
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"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"
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
var Version = "dev"
|
||||
@@ -24,6 +25,10 @@ func main() {
|
||||
version = "v" + Version
|
||||
}
|
||||
|
||||
var model *string = flag.String("model", "", "model to begin with")
|
||||
var prompt *string = flag.String("prompt", "", "prompt to begin with")
|
||||
flag.Parse()
|
||||
|
||||
url := os.Getenv("OPENCODE_SERVER")
|
||||
|
||||
appInfoStr := os.Getenv("OPENCODE_APP_INFO")
|
||||
@@ -62,11 +67,18 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = clipboard.Init()
|
||||
if err != nil {
|
||||
slog.Error("Failed to initialize clipboard", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Create main context for the application
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
app_, err := app.New(ctx, version, appInfo, httpClient)
|
||||
app_, err := app.New(ctx, version, appInfo, httpClient, model, prompt)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -78,15 +90,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)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -10,22 +10,25 @@ 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/tidwall/gjson v1.14.4
|
||||
github.com/sst/opencode-sdk-go v0.1.0-alpha.8
|
||||
golang.design/x/clipboard v0.7.1
|
||||
rsc.io/qr v0.2.0
|
||||
)
|
||||
|
||||
replace github.com/sst/opencode-sdk-go => ./sdk
|
||||
|
||||
replace golang.design/x/clipboard v0.7.1 => github.com/sst/clipboard v0.0.0-20250708194754-36e9fc12a4e8
|
||||
|
||||
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 +40,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
|
||||
@@ -50,23 +52,25 @@ require (
|
||||
github.com/sosodev/duration v1.3.1 // indirect
|
||||
github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/tidwall/gjson v1.14.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
golang.org/x/exp/shiny v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.3.1 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
@@ -78,16 +82,16 @@ require (
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||
golang.org/x/image v0.26.0
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/term v0.31.0 // indirect
|
||||
golang.org/x/text v0.24.0
|
||||
golang.org/x/image v0.28.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.26.0
|
||||
gopkg.in/yaml.v3 v3.0.1 // 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=
|
||||
@@ -58,8 +54,6 @@ github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
|
||||
@@ -110,7 +104,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 +141,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 +181,10 @@ 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/sst/clipboard v0.0.0-20250708194754-36e9fc12a4e8 h1:GNr7eAnMwAzUNk7eOsw4Flr9c9Uk55Wnf4840DCckwc=
|
||||
github.com/sst/clipboard v0.0.0-20250708194754-36e9fc12a4e8/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg=
|
||||
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=
|
||||
@@ -229,14 +218,17 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
|
||||
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
|
||||
golang.org/x/exp/shiny v0.0.0-20250620022241-b7579e27df2b h1:zELBzk+7ERc6m8BxhzU2VYjp03wlEvi+cIgYQR5H3CI=
|
||||
golang.org/x/exp/shiny v0.0.0-20250620022241-b7579e27df2b/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
|
||||
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
||||
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
|
||||
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f h1:/n+PL2HlfqeSiDCuhdBbRNlGS/g2fM4OHufalHaTVG8=
|
||||
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f/go.mod h1:ESkJ836Z6LpG6mTVAhA48LpfW/8fNR0ifStlH2axyfg=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -247,15 +239,15 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -274,28 +266,28 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -18,25 +18,28 @@ import (
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
var RootPath string
|
||||
|
||||
type App struct {
|
||||
Info opencode.App
|
||||
Version string
|
||||
StatePath string
|
||||
Config *opencode.Config
|
||||
Client *opencode.Client
|
||||
State *config.State
|
||||
Provider *opencode.Provider
|
||||
Model *opencode.Model
|
||||
Session *opencode.Session
|
||||
Messages []opencode.Message
|
||||
Commands commands.CommandRegistry
|
||||
Info opencode.App
|
||||
Version string
|
||||
StatePath string
|
||||
Config *opencode.Config
|
||||
Client *opencode.Client
|
||||
State *config.State
|
||||
Provider *opencode.Provider
|
||||
Model *opencode.Model
|
||||
Session *opencode.Session
|
||||
Messages []opencode.MessageUnion
|
||||
Commands commands.CommandRegistry
|
||||
InitialModel *string
|
||||
InitialPrompt *string
|
||||
compactCancel context.CancelFunc
|
||||
}
|
||||
|
||||
type SessionSelectedMsg = *opencode.Session
|
||||
type SessionLoadedMsg struct{}
|
||||
type ModelSelectedMsg struct {
|
||||
Provider opencode.Provider
|
||||
Model opencode.Model
|
||||
@@ -45,13 +48,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(
|
||||
@@ -59,8 +62,11 @@ func New(
|
||||
version string,
|
||||
appInfo opencode.App,
|
||||
httpClient *opencode.Client,
|
||||
model *string,
|
||||
prompt *string,
|
||||
) (*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 {
|
||||
@@ -109,83 +115,141 @@ func New(
|
||||
slog.Debug("Loaded config", "config", configInfo)
|
||||
|
||||
app := &App{
|
||||
Info: appInfo,
|
||||
Version: version,
|
||||
StatePath: appStatePath,
|
||||
Config: configInfo,
|
||||
State: appState,
|
||||
Client: httpClient,
|
||||
Session: &opencode.Session{},
|
||||
Messages: []opencode.Message{},
|
||||
Commands: commands.LoadFromConfig(configInfo),
|
||||
Info: appInfo,
|
||||
Version: version,
|
||||
StatePath: appStatePath,
|
||||
Config: configInfo,
|
||||
State: appState,
|
||||
Client: httpClient,
|
||||
Session: &opencode.Session{},
|
||||
Messages: []opencode.MessageUnion{},
|
||||
Commands: commands.LoadFromConfig(configInfo),
|
||||
InitialModel: model,
|
||||
InitialPrompt: prompt,
|
||||
}
|
||||
|
||||
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) SetClipboard(text string) tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
clipboard.Write(clipboard.FmtText, []byte(text))
|
||||
return nil
|
||||
})
|
||||
// try to set the clipboard using OSC52 for terminals that support it
|
||||
cmds = append(cmds, tea.SetClipboard(text))
|
||||
return tea.Sequence(cmds...)
|
||||
}
|
||||
|
||||
func (a *App) InitializeProvider() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
providersResponse, err := a.Client.Config.Providers(context.Background())
|
||||
if err != nil {
|
||||
slog.Error("Failed to list providers", "error", err)
|
||||
// TODO: notify user
|
||||
return nil
|
||||
}
|
||||
providers := providersResponse.Providers
|
||||
var defaultProvider *opencode.Provider
|
||||
var defaultModel *opencode.Model
|
||||
providersResponse, err := a.Client.Config.Providers(context.Background())
|
||||
if err != nil {
|
||||
slog.Error("Failed to list providers", "error", err)
|
||||
// TODO: notify user
|
||||
return nil
|
||||
}
|
||||
providers := providersResponse.Providers
|
||||
var defaultProvider *opencode.Provider
|
||||
var defaultModel *opencode.Model
|
||||
|
||||
var anthropic *opencode.Provider
|
||||
for _, provider := range providers {
|
||||
if provider.ID == "anthropic" {
|
||||
anthropic = &provider
|
||||
var anthropic *opencode.Provider
|
||||
for _, provider := range providers {
|
||||
if provider.ID == "anthropic" {
|
||||
anthropic = &provider
|
||||
}
|
||||
}
|
||||
|
||||
// default to anthropic if available
|
||||
if anthropic != nil {
|
||||
defaultProvider = anthropic
|
||||
defaultModel = getDefaultModel(providersResponse, *anthropic)
|
||||
}
|
||||
|
||||
for _, provider := range providers {
|
||||
if defaultProvider == nil || defaultModel == nil {
|
||||
defaultProvider = &provider
|
||||
defaultModel = getDefaultModel(providersResponse, provider)
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
if len(providers) == 0 {
|
||||
slog.Error("No providers configured")
|
||||
return nil
|
||||
}
|
||||
|
||||
var currentProvider *opencode.Provider
|
||||
var currentModel *opencode.Model
|
||||
for _, provider := range providers {
|
||||
if provider.ID == a.State.Provider {
|
||||
currentProvider = &provider
|
||||
|
||||
for _, model := range provider.Models {
|
||||
if model.ID == a.State.Model {
|
||||
currentModel = &model
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if currentProvider == nil || currentModel == nil {
|
||||
currentProvider = defaultProvider
|
||||
currentModel = defaultModel
|
||||
}
|
||||
|
||||
// default to anthropic if available
|
||||
if anthropic != nil {
|
||||
defaultProvider = anthropic
|
||||
defaultModel = getDefaultModel(providersResponse, *anthropic)
|
||||
}
|
||||
|
||||
var initialProvider *opencode.Provider
|
||||
var initialModel *opencode.Model
|
||||
if a.InitialModel != nil && *a.InitialModel != "" {
|
||||
splits := strings.Split(*a.InitialModel, "/")
|
||||
for _, provider := range providers {
|
||||
if defaultProvider == nil || defaultModel == nil {
|
||||
defaultProvider = &provider
|
||||
defaultModel = getDefaultModel(providersResponse, provider)
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
if len(providers) == 0 {
|
||||
slog.Error("No providers configured")
|
||||
return nil
|
||||
}
|
||||
|
||||
var currentProvider *opencode.Provider
|
||||
var currentModel *opencode.Model
|
||||
for _, provider := range providers {
|
||||
if provider.ID == a.State.Provider {
|
||||
currentProvider = &provider
|
||||
|
||||
if provider.ID == splits[0] {
|
||||
initialProvider = &provider
|
||||
for _, model := range provider.Models {
|
||||
if model.ID == a.State.Model {
|
||||
currentModel = &model
|
||||
modelID := strings.Join(splits[1:], "/")
|
||||
if model.ID == modelID {
|
||||
initialModel = &model
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if currentProvider == nil || currentModel == nil {
|
||||
currentProvider = defaultProvider
|
||||
currentModel = defaultModel
|
||||
}
|
||||
|
||||
return ModelSelectedMsg{
|
||||
Provider: *currentProvider,
|
||||
Model: *currentModel,
|
||||
}
|
||||
}
|
||||
|
||||
if initialProvider != nil && initialModel != nil {
|
||||
currentProvider = initialProvider
|
||||
currentModel = initialModel
|
||||
}
|
||||
|
||||
var cmds []tea.Cmd
|
||||
cmds = append(cmds, util.CmdHandler(ModelSelectedMsg{
|
||||
Provider: *currentProvider,
|
||||
Model: *currentModel,
|
||||
}))
|
||||
if a.InitialPrompt != nil && *a.InitialPrompt != "" {
|
||||
cmds = append(cmds, util.CmdHandler(SendMsg{Text: *a.InitialPrompt}))
|
||||
}
|
||||
return tea.Sequence(cmds...)
|
||||
}
|
||||
|
||||
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 +261,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() {
|
||||
@@ -247,13 +307,26 @@ func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
|
||||
}
|
||||
|
||||
func (a *App) CompactSession(ctx context.Context) tea.Cmd {
|
||||
if a.compactCancel != nil {
|
||||
a.compactCancel()
|
||||
}
|
||||
|
||||
compactCtx, cancel := context.WithCancel(ctx)
|
||||
a.compactCancel = cancel
|
||||
|
||||
go func() {
|
||||
_, err := a.Client.Session.Summarize(ctx, a.Session.ID, opencode.SessionSummarizeParams{
|
||||
defer func() {
|
||||
a.compactCancel = nil
|
||||
}()
|
||||
|
||||
_, err := a.Client.Session.Summarize(compactCtx, a.Session.ID, opencode.SessionSummarizeParams{
|
||||
ProviderID: opencode.F(a.Provider.ID),
|
||||
ModelID: opencode.F(a.Model.ID),
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("Failed to compact session", "error", err)
|
||||
if compactCtx.Err() != context.Canceled {
|
||||
slog.Error("Failed to compact session", "error", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
@@ -276,29 +349,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 +393,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,10 +425,16 @@ 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 {
|
||||
// Cancel any running compact operation
|
||||
if a.compactCancel != nil {
|
||||
a.compactCancel()
|
||||
a.compactCancel = nil
|
||||
}
|
||||
|
||||
_, err := a.Client.Session.Abort(ctx, sessionID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to cancel session", "error", err)
|
||||
|
||||
@@ -29,7 +29,7 @@ type Command struct {
|
||||
Name CommandName
|
||||
Description string
|
||||
Keybindings []Keybinding
|
||||
Trigger string
|
||||
Trigger []string
|
||||
}
|
||||
|
||||
func (c Command) Keys() []string {
|
||||
@@ -40,6 +40,21 @@ func (c Command) Keys() []string {
|
||||
return keys
|
||||
}
|
||||
|
||||
func (c Command) HasTrigger() bool {
|
||||
return len(c.Trigger) > 0
|
||||
}
|
||||
|
||||
func (c Command) PrimaryTrigger() string {
|
||||
if len(c.Trigger) > 0 {
|
||||
return c.Trigger[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c Command) MatchesTrigger(trigger string) bool {
|
||||
return slices.Contains(c.Trigger, trigger)
|
||||
}
|
||||
|
||||
type CommandRegistry map[CommandName]Command
|
||||
|
||||
func (r CommandRegistry) Sorted() []Command {
|
||||
@@ -75,18 +90,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 +113,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"
|
||||
)
|
||||
|
||||
@@ -129,31 +150,37 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
Name: AppHelpCommand,
|
||||
Description: "show help",
|
||||
Keybindings: parseBindings("<leader>h"),
|
||||
Trigger: "help",
|
||||
Trigger: []string{"help"},
|
||||
},
|
||||
{
|
||||
Name: EditorOpenCommand,
|
||||
Description: "open editor",
|
||||
Keybindings: parseBindings("<leader>e"),
|
||||
Trigger: "editor",
|
||||
Trigger: []string{"editor"},
|
||||
},
|
||||
{
|
||||
Name: SessionNewCommand,
|
||||
Description: "new session",
|
||||
Keybindings: parseBindings("<leader>n"),
|
||||
Trigger: "new",
|
||||
Trigger: []string{"new", "clear"},
|
||||
},
|
||||
{
|
||||
Name: SessionListCommand,
|
||||
Description: "list sessions",
|
||||
Keybindings: parseBindings("<leader>l"),
|
||||
Trigger: "sessions",
|
||||
Trigger: []string{"sessions", "resume", "continue"},
|
||||
},
|
||||
{
|
||||
Name: SessionShareCommand,
|
||||
Description: "share session",
|
||||
Keybindings: parseBindings("<leader>s"),
|
||||
Trigger: "share",
|
||||
Trigger: []string{"share"},
|
||||
},
|
||||
{
|
||||
Name: SessionUnshareCommand,
|
||||
Description: "unshare session",
|
||||
Keybindings: parseBindings("<leader>u"),
|
||||
Trigger: []string{"unshare"},
|
||||
},
|
||||
{
|
||||
Name: SessionInterruptCommand,
|
||||
@@ -164,31 +191,52 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
Name: SessionCompactCommand,
|
||||
Description: "compact the session",
|
||||
Keybindings: parseBindings("<leader>c"),
|
||||
Trigger: "compact",
|
||||
Trigger: []string{"compact", "summarize"},
|
||||
},
|
||||
{
|
||||
Name: ToolDetailsCommand,
|
||||
Description: "toggle tool details",
|
||||
Keybindings: parseBindings("<leader>d"),
|
||||
Trigger: "details",
|
||||
Trigger: []string{"details"},
|
||||
},
|
||||
{
|
||||
Name: ModelListCommand,
|
||||
Description: "list models",
|
||||
Keybindings: parseBindings("<leader>m"),
|
||||
Trigger: "models",
|
||||
Trigger: []string{"models"},
|
||||
},
|
||||
{
|
||||
Name: ThemeListCommand,
|
||||
Description: "list themes",
|
||||
Keybindings: parseBindings("<leader>t"),
|
||||
Trigger: "themes",
|
||||
Trigger: []string{"themes"},
|
||||
},
|
||||
{
|
||||
Name: FileListCommand,
|
||||
Description: "list files",
|
||||
Keybindings: parseBindings("<leader>f"),
|
||||
Trigger: []string{"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",
|
||||
Keybindings: parseBindings("<leader>i"),
|
||||
Trigger: "init",
|
||||
Trigger: []string{"init"},
|
||||
},
|
||||
{
|
||||
Name: InputClearCommand,
|
||||
@@ -198,7 +246,7 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
{
|
||||
Name: InputPasteCommand,
|
||||
Description: "paste content",
|
||||
Keybindings: parseBindings("ctrl+v"),
|
||||
Keybindings: parseBindings("ctrl+v", "super+v"),
|
||||
},
|
||||
{
|
||||
Name: InputSubmitCommand,
|
||||
@@ -210,16 +258,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 +281,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,11 +298,26 @@ 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",
|
||||
Keybindings: parseBindings("ctrl+c", "<leader>q"),
|
||||
Trigger: "exit",
|
||||
Trigger: []string{"exit", "quit"},
|
||||
},
|
||||
}
|
||||
registry := make(CommandRegistry)
|
||||
|
||||
@@ -25,20 +25,13 @@ 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"
|
||||
}
|
||||
|
||||
func getCommandCompletionItem(cmd commands.Command, space int, t theme.Theme) dialog.CompletionItemI {
|
||||
spacer := strings.Repeat(" ", space)
|
||||
title := " /" + cmd.Trigger + styles.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description)
|
||||
title := " /" + cmd.PrimaryTrigger() + styles.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description)
|
||||
value := string(cmd.Name)
|
||||
return dialog.NewCompletionItem(dialog.CompletionItem{
|
||||
Title: title,
|
||||
@@ -52,8 +45,8 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
|
||||
|
||||
space := 1
|
||||
for _, cmd := range c.app.Commands {
|
||||
if lipgloss.Width(cmd.Trigger) > space {
|
||||
space = lipgloss.Width(cmd.Trigger)
|
||||
if cmd.HasTrigger() && lipgloss.Width(cmd.PrimaryTrigger()) > space {
|
||||
space = lipgloss.Width(cmd.PrimaryTrigger())
|
||||
}
|
||||
}
|
||||
space += 2
|
||||
@@ -63,10 +56,10 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
|
||||
// If no query, return all commands
|
||||
items := []dialog.CompletionItemI{}
|
||||
for _, cmd := range sorted {
|
||||
if cmd.Trigger == "" {
|
||||
if !cmd.HasTrigger() {
|
||||
continue
|
||||
}
|
||||
space := space - lipgloss.Width(cmd.Trigger)
|
||||
space := space - lipgloss.Width(cmd.PrimaryTrigger())
|
||||
items = append(items, getCommandCompletionItem(cmd, space, t))
|
||||
}
|
||||
return items, nil
|
||||
@@ -77,12 +70,15 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
|
||||
commandMap := make(map[string]dialog.CompletionItemI)
|
||||
|
||||
for _, cmd := range sorted {
|
||||
if cmd.Trigger == "" {
|
||||
if !cmd.HasTrigger() {
|
||||
continue
|
||||
}
|
||||
space := space - lipgloss.Width(cmd.Trigger)
|
||||
commandNames = append(commandNames, cmd.Trigger)
|
||||
commandMap[cmd.Trigger] = getCommandCompletionItem(cmd, space, t)
|
||||
space := space - lipgloss.Width(cmd.PrimaryTrigger())
|
||||
// Add all triggers as searchable options
|
||||
for _, trigger := range cmd.Trigger {
|
||||
commandNames = append(commandNames, trigger)
|
||||
commandMap[trigger] = getCommandCompletionItem(cmd, space, t)
|
||||
}
|
||||
}
|
||||
|
||||
// Find fuzzy matches
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -1,29 +1,33 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"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"
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
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 +37,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
|
||||
}
|
||||
@@ -69,22 +66,116 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
cmds = append(cmds, cmd)
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
case tea.PasteMsg:
|
||||
text := string(msg)
|
||||
text = strings.ReplaceAll(text, "\\", "")
|
||||
text, err := strconv.Unquote(`"` + text + `"`)
|
||||
if err != nil {
|
||||
slog.Error("Failed to unquote text", "error", err)
|
||||
m.textarea.InsertRunesFromUserInput([]rune(msg))
|
||||
return m, nil
|
||||
}
|
||||
if _, err := os.Stat(text); err != nil {
|
||||
slog.Error("Failed to paste file", "error", err)
|
||||
m.textarea.InsertRunesFromUserInput([]rune(msg))
|
||||
return m, nil
|
||||
}
|
||||
|
||||
filePath := text
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
|
||||
mediaType := ""
|
||||
switch ext {
|
||||
case ".jpg":
|
||||
mediaType = "image/jpeg"
|
||||
case ".png", ".jpeg", ".gif", ".webp":
|
||||
mediaType = "image/" + ext[1:]
|
||||
case ".pdf":
|
||||
mediaType = "application/pdf"
|
||||
default:
|
||||
mediaType = "text/plain"
|
||||
}
|
||||
|
||||
fileBytes, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
slog.Error("Failed to read file", "error", err)
|
||||
m.textarea.InsertRunesFromUserInput([]rune(msg))
|
||||
return m, nil
|
||||
}
|
||||
base64EncodedFile := base64.StdEncoding.EncodeToString(fileBytes)
|
||||
url := fmt.Sprintf("data:%s;base64,%s", mediaType, base64EncodedFile)
|
||||
attachmentCount := len(m.textarea.GetAttachments())
|
||||
attachmentIndex := attachmentCount + 1
|
||||
label := "File"
|
||||
if strings.HasPrefix(mediaType, "image/") {
|
||||
label = "Image"
|
||||
}
|
||||
|
||||
attachment := &textarea.Attachment{
|
||||
ID: uuid.NewString(),
|
||||
MediaType: mediaType,
|
||||
Display: fmt.Sprintf("[%s #%d]", label, attachmentIndex),
|
||||
URL: url,
|
||||
Filename: filePath,
|
||||
}
|
||||
m.textarea.InsertAttachment(attachment)
|
||||
m.textarea.InsertString(" ")
|
||||
case tea.ClipboardMsg:
|
||||
text := string(msg)
|
||||
m.textarea.InsertRunesFromUserInput([]rune(text))
|
||||
case dialog.ThemeSelectedMsg:
|
||||
m.textarea = createTextArea(&m.textarea)
|
||||
m.textarea = m.resetTextareaStyles()
|
||||
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 +196,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 +205,7 @@ func (m *editorComponent) Content() string {
|
||||
Bold(true)
|
||||
prompt := promptStyle.Render(">")
|
||||
|
||||
m.textarea.SetWidth(width - 6)
|
||||
textarea := lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
prompt,
|
||||
@@ -121,7 +213,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 +227,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 +246,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 +256,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 +282,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 +297,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,19 +329,31 @@ func (m *editorComponent) Clear() (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m *editorComponent) Paste() (tea.Model, tea.Cmd) {
|
||||
imageBytes, text, err := image.GetImageFromClipboard()
|
||||
if err != nil {
|
||||
slog.Error(err.Error())
|
||||
imageBytes := clipboard.Read(clipboard.FmtImage)
|
||||
if imageBytes != nil {
|
||||
attachmentCount := len(m.textarea.GetAttachments())
|
||||
attachmentIndex := attachmentCount + 1
|
||||
base64EncodedFile := base64.StdEncoding.EncodeToString(imageBytes)
|
||||
attachment := &textarea.Attachment{
|
||||
ID: uuid.NewString(),
|
||||
MediaType: "image/png",
|
||||
Display: fmt.Sprintf("[Image #%d]", attachmentIndex),
|
||||
Filename: fmt.Sprintf("image-%d.png", attachmentIndex),
|
||||
URL: fmt.Sprintf("data:image/png;base64,%s", base64EncodedFile),
|
||||
}
|
||||
m.textarea.InsertAttachment(attachment)
|
||||
m.textarea.InsertString(" ")
|
||||
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)
|
||||
|
||||
textBytes := clipboard.Read(clipboard.FmtText)
|
||||
if textBytes != nil {
|
||||
m.textarea.InsertRunesFromUserInput([]rune(string(textBytes)))
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
|
||||
// fallback to reading the clipboard using OSC52
|
||||
return m, tea.ReadClipboard
|
||||
}
|
||||
|
||||
func (m *editorComponent) Newline() (tea.Model, tea.Cmd) {
|
||||
@@ -252,48 +361,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
|
||||
}
|
||||
@@ -306,36 +373,37 @@ func (m *editorComponent) getSubmitKeyText() string {
|
||||
return m.app.Commands[commands.InputSubmitCommand].Keys()[0]
|
||||
}
|
||||
|
||||
func createTextArea(existing *textarea.Model) textarea.Model {
|
||||
func (m *editorComponent) resetTextareaStyles() textarea.Model {
|
||||
t := theme.CurrentTheme()
|
||||
bgColor := t.BackgroundElement()
|
||||
textColor := t.Text()
|
||||
textMutedColor := t.TextMuted()
|
||||
|
||||
ta := textarea.New()
|
||||
ta := m.textarea
|
||||
|
||||
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())
|
||||
// ta.SetWidth(existing.Width())
|
||||
ta.SetHeight(existing.Height())
|
||||
}
|
||||
|
||||
// ta.Focus()
|
||||
return ta
|
||||
}
|
||||
|
||||
@@ -355,15 +423,19 @@ func createSpinner() spinner.Model {
|
||||
|
||||
func NewEditorComponent(app *app.App) EditorComponent {
|
||||
s := createSpinner()
|
||||
ta := createTextArea(nil)
|
||||
|
||||
return &editorComponent{
|
||||
ta := textarea.New()
|
||||
ta.Prompt = " "
|
||||
ta.ShowLineNumbers = false
|
||||
ta.CharLimit = -1
|
||||
|
||||
m := &editorComponent{
|
||||
app: app,
|
||||
textarea: ta,
|
||||
history: []string{},
|
||||
historyIndex: 0,
|
||||
currentMessage: "",
|
||||
spinner: s,
|
||||
interruptKeyInDebounce: false,
|
||||
}
|
||||
m.resetTextareaStyles()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user