mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 11:39:45 -04:00
Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eebca580e3 | |||
| e73a7c23d0 | |||
| 11de2e59f3 | |||
| f4b69df7a3 | |||
| 83b9b67c4c | |||
| d9de78cfe8 | |||
| ef6bff6386 | |||
| cb03655aac | |||
| 012a292948 | |||
| d2e2eae4b8 | |||
| fd84e8d405 | |||
| 567a1964c0 | |||
| 564418f1ff | |||
| d7c4faec58 | |||
| 34982b5d18 | |||
| 5b5bd146ea | |||
| 836c2060c7 | |||
| 6357136ca5 | |||
| 0a0b363587 | |||
| f5f6167146 | |||
| f1684c9e15 | |||
| f597b7287b | |||
| 95fabc1407 | |||
| 315c366e11 | |||
| 5d68a7c2e0 | |||
| 1b2d3bf659 | |||
| 24e4f5b051 | |||
| 2992c5a6bf | |||
| ca2660ccf8 | |||
| 6b4bd590ac | |||
| 60ba42af15 | |||
| f22827bdfa | |||
| f9b5b6d129 | |||
| cc66e06101 | |||
| d4c8d95ec6 | |||
| 0fd312346b | |||
| b80046120c | |||
| 07ed2a8391 | |||
| e9f52934e9 | |||
| 732b67f8ce | |||
| d47bb96784 | |||
| 6456350564 | |||
| c5c0a2ca6e | |||
| 3706b2bca7 | |||
| 1f57b9a70f | |||
| 004f53f741 | |||
| cf29ec0a59 | |||
| b5e08acdf7 | |||
| 7ddeeeb4f8 | |||
| 0f1697b2ab | |||
| 6e626afdcb | |||
| 0fe94c1616 | |||
| a42b004c72 | |||
| 35f57768fd | |||
| 9a90ce84fb | |||
| 133fe41cd5 | |||
| 74c1085103 | |||
| 497fc170fd | |||
| 3edab60560 | |||
| 3f2ac2b9b0 | |||
| 1577b44087 | |||
| 39f52f48f2 | |||
| 4fadbcfb90 | |||
| 08c5c401ba | |||
| ba2e86c7ef | |||
| 6d056789c7 | |||
| 5d508cc9c2 | |||
| d9233872b9 | |||
| aa4dba1541 | |||
| 947a3e8aff | |||
| 9a3186317b | |||
| b1e584ca1d | |||
| bca523eb63 | |||
| 2ff4cd2c2b | |||
| d686269377 | |||
| 491abd6b5b | |||
| 4518f96e3d | |||
| a9dcbedf99 | |||
| 9231043eb4 | |||
| 04dcd87170 | |||
| c31fd9ed79 | |||
| 2989d92794 | |||
| 256d074411 | |||
| 8b01676ec0 | |||
| 34c6c8494a | |||
| 522bed6b7d | |||
| dda672284c | |||
| 6018364164 | |||
| bc0d438cee | |||
| abef91c223 | |||
| 1bbf6d38e5 | |||
| c9c9db1e8d | |||
| b11fe9fbc6 | |||
| 60f3d413de | |||
| 1df2d78b85 | |||
| 2286a872c1 | |||
| 8a83301e0d | |||
| 9bc40f00e3 | |||
| c3c440948a | |||
| aa10f8a7f6 | |||
| a2db58f125 | |||
| 574be9febf | |||
| 5b05ede748 | |||
| 4032426185 | |||
| 8d8045ff95 | |||
| b3c8bec019 | |||
| 25f43adaa0 | |||
| 4913ee6afd | |||
| c59ded82b3 | |||
| 40bdbf92a3 | |||
| ad76d7e57d | |||
| 863ae6fa7d | |||
| 8f230ad4b4 | |||
| c0f90eb564 | |||
| 50fb337270 | |||
| e08ec077b0 | |||
| 796245d146 |
@@ -32,14 +32,12 @@ jobs:
|
||||
"webfetch": "deny"
|
||||
}
|
||||
run: |
|
||||
opencode run -m anthropic/claude-sonnet-4-20250514 "A new issue has been created: '${{ github.event.issue.title }}'
|
||||
opencode run -m anthropic/claude-sonnet-4-20250514 "A new issue has been created:'
|
||||
|
||||
Issue number:
|
||||
${{ github.event.issue.number }}
|
||||
Issue body:
|
||||
${{ github.event.issue.body }}
|
||||
|
||||
Please search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
|
||||
Lookup this issue and search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
|
||||
Consider:
|
||||
1. Similar titles or descriptions
|
||||
2. Same error messages or symptoms
|
||||
|
||||
@@ -8,22 +8,20 @@ jobs:
|
||||
opencode:
|
||||
if: |
|
||||
contains(github.event.comment.body, ' /oc') ||
|
||||
startsWith(github.event.comment.body, '/oc') ||
|
||||
contains(github.event.comment.body, ' /opencode') ||
|
||||
startsWith(github.event.comment.body, '/opencode')
|
||||
contains(github.event.comment.body, ' /opencode')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
with:
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
model: opencode/sonic
|
||||
@@ -24,3 +24,8 @@ example, if the page title is "Models", avoid using a section title like "Add
|
||||
new models". This might be unavoidable in some cases, but try to avoid it.
|
||||
|
||||
Check out the /packages/web/src/content/docs/docs/index.mdx as an example.
|
||||
|
||||
For JS or TS code snippets remove trailing semicolons and any trailing commas
|
||||
that might not be needed.
|
||||
|
||||
If you are making a commit prefix the commit message with `docs:`
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
commit and push
|
||||
|
||||
make sure it includes a prefix like
|
||||
docs:
|
||||
tui:
|
||||
core:
|
||||
ci:
|
||||
ignore:
|
||||
wip:
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: hello world
|
||||
---
|
||||
|
||||
hey there $ARGUMENTS
|
||||
|
||||
!`ls`
|
||||
check out @README.md
|
||||
@@ -107,4 +107,4 @@ The other confusingly named repo has no relation to this one. You can [read the
|
||||
|
||||
---
|
||||
|
||||
**Join our community** [Discord](https://discord.gg/opencode) | [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/anomaly_inv)
|
||||
|
||||
@@ -52,3 +52,11 @@
|
||||
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
|
||||
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
|
||||
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
|
||||
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
|
||||
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
|
||||
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
|
||||
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
|
||||
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
|
||||
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
|
||||
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
|
||||
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opencode/cloud-core": "workspace:*",
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
"@solidjs/router": "^0.15.0",
|
||||
"@solidjs/start": "^1.1.0",
|
||||
@@ -25,7 +26,7 @@
|
||||
},
|
||||
"cloud/core": {
|
||||
"name": "@opencode/cloud-core",
|
||||
"version": "0.5.5",
|
||||
"version": "0.5.15",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"drizzle-orm": "0.41.0",
|
||||
@@ -39,7 +40,7 @@
|
||||
},
|
||||
"cloud/function": {
|
||||
"name": "@opencode/cloud-function",
|
||||
"version": "0.5.5",
|
||||
"version": "0.5.15",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -59,7 +60,7 @@
|
||||
},
|
||||
"cloud/web": {
|
||||
"name": "@opencode/cloud-web",
|
||||
"version": "0.5.5",
|
||||
"version": "0.5.15",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "0.13.9",
|
||||
"@openauthjs/solid": "0.0.0-20250322224806",
|
||||
@@ -78,7 +79,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode/function",
|
||||
"version": "0.5.5",
|
||||
"version": "0.5.15",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
@@ -93,7 +94,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "0.5.5",
|
||||
"version": "0.5.15",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -143,19 +144,22 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "0.5.5",
|
||||
"version": "0.5.15",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.80.1",
|
||||
"@hey-api/openapi-ts": "0.81.0",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "0.5.5",
|
||||
"version": "0.5.15",
|
||||
"dependencies": {
|
||||
"@hey-api/openapi-ts": "0.80.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.80.1",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -164,7 +168,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode/web",
|
||||
"version": "0.5.5",
|
||||
"version": "0.5.15",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -186,7 +190,7 @@
|
||||
"sharp": "0.32.5",
|
||||
"shiki": "3.4.2",
|
||||
"solid-js": "1.9.7",
|
||||
"toolbeam-docs-theme": "0.4.3",
|
||||
"toolbeam-docs-theme": "0.4.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
@@ -203,6 +207,9 @@
|
||||
"web-tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
],
|
||||
"overrides": {
|
||||
"zod": "3.25.76",
|
||||
},
|
||||
"catalog": {
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
@@ -488,7 +495,7 @@
|
||||
|
||||
"@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.0.6", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0", "lodash": "^4.17.21" } }, "sha512-yktiFZoWPtEW8QKS65eqKwA5MTKp88CyiL8q72WynrBs/73SAaxlSWlA2zW/DZlywZ5hX1OYzrCC0wFdvO9c2w=="],
|
||||
|
||||
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.80.1", "", { "dependencies": { "@hey-api/json-schema-ref-parser": "1.0.6", "ansi-colors": "4.1.3", "c12": "2.0.1", "color-support": "1.1.3", "commander": "13.0.0", "handlebars": "4.7.8", "open": "10.1.2", "semver": "7.7.2" }, "peerDependencies": { "typescript": "^5.5.3" }, "bin": { "openapi-ts": "bin/index.cjs" } }, "sha512-AC478kg36vmmrseLZNFonZ/cmXXmDzW5yWz4PVg1S8ebJsRtVRJ/QU+mtnXfzf9avN2P0pz/AO4WAe4jyFY2gA=="],
|
||||
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.81.0", "", { "dependencies": { "@hey-api/json-schema-ref-parser": "1.0.6", "ansi-colors": "4.1.3", "c12": "2.0.1", "color-support": "1.1.3", "commander": "13.0.0", "handlebars": "4.7.8", "js-yaml": "4.1.0", "open": "10.1.2", "semver": "7.7.2" }, "peerDependencies": { "typescript": "^5.5.3" }, "bin": { "openapi-ts": "bin/index.cjs" } }, "sha512-PoJukNBkUfHOoMDpN33bBETX49TUhy7Hu8Sa0jslOvFndvZ5VjQr4Nl/Dzjb9LG1Lp5HjybyTJMA6a1zYk/q6A=="],
|
||||
|
||||
"@hono/zod-validator": ["@hono/zod-validator@0.4.2", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g=="],
|
||||
|
||||
@@ -2804,7 +2811,7 @@
|
||||
|
||||
"toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="],
|
||||
|
||||
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.3", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-3um/NsSq4xFeKbKrNGPHIzfTixwnEVvroqA8Q+lecnYHHJ5TtiYTggHDqewOW+I67t0J1IVBwVKUPjxiQfIcog=="],
|
||||
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.6", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-s4yKn3PYnmPCl6MHPkQ2MedSgVh7FMAP64DiXnAfCls/H/RR6iTe0/SFgPTprz+HXXaKvv2iPo3XuvmDRY6cxQ=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
@@ -3062,8 +3069,6 @@
|
||||
|
||||
"@astrojs/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
"@astrojs/sitemap/zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="],
|
||||
|
||||
"@astrojs/solid-js/vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
||||
|
||||
"@astrojs/solid-js/vite-plugin-solid": ["vite-plugin-solid@2.11.8", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-hFrCxBfv3B1BmFqnJF4JOCYpjrmi/zwyeKjcomQ0khh8HFyQ8SbuBWQ7zGojfrz6HUOBFrJBNySDi/JgAHytWg=="],
|
||||
@@ -3160,8 +3165,6 @@
|
||||
|
||||
"@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
"@modelcontextprotocol/sdk/zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="],
|
||||
|
||||
"@netlify/dev-utils/find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g=="],
|
||||
|
||||
"@netlify/dev-utils/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
|
||||
@@ -3208,6 +3211,8 @@
|
||||
|
||||
"@openauthjs/solid/@openauthjs/openauth": ["@openauthjs/openauth@0.4.2", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-8+Bia559iffrZXfQ0LWXrVVVriochS88pDtB8indyQ1S+40MQgDBu8aBzKt+fgSrTmoQGCTT+wlOXgbjc9qIcw=="],
|
||||
|
||||
"@opencode-ai/sdk/@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.80.1", "", { "dependencies": { "@hey-api/json-schema-ref-parser": "1.0.6", "ansi-colors": "4.1.3", "c12": "2.0.1", "color-support": "1.1.3", "commander": "13.0.0", "handlebars": "4.7.8", "open": "10.1.2", "semver": "7.7.2" }, "peerDependencies": { "typescript": "^5.5.3" }, "bin": { "openapi-ts": "bin/index.cjs" } }, "sha512-AC478kg36vmmrseLZNFonZ/cmXXmDzW5yWz4PVg1S8ebJsRtVRJ/QU+mtnXfzf9avN2P0pz/AO4WAe4jyFY2gA=="],
|
||||
|
||||
"@opencode/cloud-web/solid-js": ["solid-js@1.9.5", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "^1.1.0", "seroval-plugins": "^1.1.0" } }, "sha512-ogI3DaFcyn6UhYhrgcyRAMbu/buBJitYQASZz5WzfQVPP10RD2AbCoRZ517psnezrasyCbWzIxZ6kVqet768xw=="],
|
||||
|
||||
"@opentelemetry/instrumentation-grpc/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.27.0", "", {}, "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="],
|
||||
@@ -3338,8 +3343,6 @@
|
||||
|
||||
"astro/vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
||||
|
||||
"astro/zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="],
|
||||
|
||||
"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=="],
|
||||
@@ -3416,8 +3419,6 @@
|
||||
|
||||
"miniflare/youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
|
||||
|
||||
"miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
|
||||
|
||||
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
|
||||
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
@@ -3462,8 +3463,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -3882,8 +3881,6 @@
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
|
||||
|
||||
"prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"dev": "vinxi dev --host 0.0.0.0",
|
||||
"build": "vinxi build",
|
||||
"start": "vinxi start",
|
||||
"version": "0.5.7"
|
||||
"version": "0.5.28"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -14,7 +14,8 @@
|
||||
"@solidjs/router": "^0.15.0",
|
||||
"@solidjs/start": "^1.1.0",
|
||||
"solid-js": "^1.9.5",
|
||||
"vinxi": "^0.5.7"
|
||||
"vinxi": "^0.5.7",
|
||||
"@opencode/cloud-core": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MetaProvider, Title } from "@solidjs/meta";
|
||||
import { Router } from "@solidjs/router";
|
||||
import { FileRoutes } from "@solidjs/start/router";
|
||||
import { Suspense } from "solid-js";
|
||||
import { ErrorBoundary, Suspense } from "solid-js";
|
||||
import "@ibm/plex/css/ibm-plex.css";
|
||||
import "./app.css";
|
||||
|
||||
@@ -11,7 +11,9 @@ export default function App() {
|
||||
root={props => (
|
||||
<MetaProvider>
|
||||
<Title>SolidStart - Basic</Title>
|
||||
<Suspense>{props.children}</Suspense>
|
||||
<ErrorBoundary fallback={<div>Something went wrong</div>}>
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</ErrorBoundary>
|
||||
</MetaProvider>
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,90 @@
|
||||
|
||||
|
||||
import { useSession } from "vinxi/http"
|
||||
import { createClient } from "@openauthjs/openauth/client"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import { and, Database, eq, inArray } from "@opencode/cloud-core/drizzle/index.js"
|
||||
import { WorkspaceTable } from "@opencode/cloud-core/schema/workspace.sql.js"
|
||||
import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
|
||||
import { query, redirect } from "@solidjs/router"
|
||||
import { AccountTable } from "@opencode/cloud-core/schema/account.sql.js"
|
||||
import { Actor } from "@opencode/cloud-core/actor.js"
|
||||
|
||||
export async function withActor<T>(fn: () => T) {
|
||||
const actor = await getActor()
|
||||
return Actor.provide(actor.type, actor.properties, fn)
|
||||
}
|
||||
|
||||
export const getActor = query(async (): Promise<Actor.Info> => {
|
||||
"use server"
|
||||
const evt = getRequestEvent()
|
||||
const url = new URL(evt!.request.headers.get("referer") ?? evt!.request.url)
|
||||
const auth = await useAuthSession()
|
||||
const [workspaceHint] = url.pathname.split("/").filter((x) => x.length > 0)
|
||||
if (!workspaceHint) {
|
||||
if (auth.data.current) {
|
||||
const current = auth.data.account[auth.data.current]
|
||||
return {
|
||||
type: "account",
|
||||
properties: {
|
||||
email: current.email,
|
||||
accountID: current.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
if (Object.keys(auth.data.account).length > 0) {
|
||||
const current = Object.values(auth.data.account)[0]
|
||||
await auth.update(val => ({
|
||||
...val,
|
||||
current: current.id,
|
||||
}))
|
||||
return {
|
||||
type: "account",
|
||||
properties: {
|
||||
email: current.email,
|
||||
accountID: current.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: "public",
|
||||
properties: {},
|
||||
}
|
||||
}
|
||||
const accounts = Object.keys(auth.data.account)
|
||||
const result = await Database.transaction(async (tx) => {
|
||||
return await tx.select({
|
||||
user: UserTable
|
||||
})
|
||||
.from(AccountTable)
|
||||
.innerJoin(UserTable, and(eq(UserTable.email, AccountTable.email)))
|
||||
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
|
||||
.where(
|
||||
and(
|
||||
inArray(AccountTable.id, accounts),
|
||||
eq(WorkspaceTable.id, workspaceHint),
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
.execute()
|
||||
.then((x) => x[0])
|
||||
})
|
||||
if (result) {
|
||||
return {
|
||||
type: "user",
|
||||
properties: {
|
||||
userID: result.user.id,
|
||||
workspaceID: result.user.workspaceID,
|
||||
},
|
||||
}
|
||||
}
|
||||
throw redirect("/auth/authorize")
|
||||
}, "actor")
|
||||
|
||||
|
||||
export const AuthClient = createClient({
|
||||
clientID: "app",
|
||||
issuer: "https://auth.dev.opencode.ai",
|
||||
issuer: import.meta.env.VITE_AUTH_URL,
|
||||
})
|
||||
|
||||
export interface AuthSession {
|
||||
@@ -15,7 +96,6 @@ export interface AuthSession {
|
||||
}
|
||||
|
||||
export function useAuthSession() {
|
||||
"use server"
|
||||
|
||||
return useSession<AuthSession>({
|
||||
password: "0".repeat(32),
|
||||
@@ -26,3 +106,4 @@ export function useAuthSession() {
|
||||
|
||||
export function AuthProvider() {
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createAsync, query } from "@solidjs/router"
|
||||
import { getActor, withActor } from "~/context/auth"
|
||||
|
||||
const getPosts = query(async () => {
|
||||
"use server"
|
||||
return withActor(() => {
|
||||
return "ok"
|
||||
})
|
||||
}, "posts")
|
||||
|
||||
|
||||
export default function () {
|
||||
const actor = createAsync(async () => getActor())
|
||||
return <div>{JSON.stringify(actor())}</div>
|
||||
}
|
||||
@@ -5,11 +5,6 @@ export async function GET(input: APIEvent) {
|
||||
const url = new URL(input.request.url)
|
||||
const code = url.searchParams.get("code")
|
||||
if (!code) throw new Error("No code found")
|
||||
const redirectURI = `${url.origin}${url.pathname}`
|
||||
console.log({
|
||||
redirectURI,
|
||||
code,
|
||||
})
|
||||
const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`)
|
||||
if (result.err) {
|
||||
throw new Error(result.err.message)
|
||||
|
||||
@@ -6,6 +6,9 @@ import IMG_SPLASH from "../asset/screenshot-splash.webp"
|
||||
import IMG_VSCODE from "../asset/screenshot-vscode.webp"
|
||||
import IMG_GITHUB from "../asset/screenshot-github.webp"
|
||||
import { IconCopy, IconCheck } from "../component/icon"
|
||||
import { createAsync, query, redirect, RouteDefinition } from "@solidjs/router"
|
||||
import { getActor, withActor } from "~/context/auth"
|
||||
import { Account } from "@opencode/cloud-core/account.js"
|
||||
|
||||
function CopyStatus() {
|
||||
return (
|
||||
@@ -16,7 +19,22 @@ function CopyStatus() {
|
||||
)
|
||||
}
|
||||
|
||||
const isLoggedIn = query(async () => {
|
||||
"use server"
|
||||
const actor = await getActor()
|
||||
if (actor.type === "account") {
|
||||
const workspaces = await withActor(() => Account.workspaces())
|
||||
throw redirect("/" + workspaces[0].id)
|
||||
}
|
||||
return
|
||||
}, "isLoggedIn")
|
||||
|
||||
|
||||
|
||||
export default function Home() {
|
||||
createAsync(() => isLoggedIn(), {
|
||||
deferStream: true,
|
||||
})
|
||||
onMount(() => {
|
||||
const commands = document.querySelectorAll("[data-copy]")
|
||||
for (const button of commands) {
|
||||
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/cloud-core",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.28",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -20,7 +20,6 @@ export namespace Actor {
|
||||
properties: {
|
||||
userID: string
|
||||
workspaceID: string
|
||||
email: string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Resource } from "sst"
|
||||
export * from "drizzle-orm"
|
||||
import postgres from "postgres"
|
||||
|
||||
function createClient() {
|
||||
const createClient = memo(() => {
|
||||
const client = postgres({
|
||||
idle_timeout: 30000,
|
||||
connect_timeout: 30000,
|
||||
@@ -19,12 +19,13 @@ function createClient() {
|
||||
})
|
||||
|
||||
return drizzle(client, {})
|
||||
}
|
||||
})
|
||||
|
||||
import { PgTransaction, type PgTransactionConfig } from "drizzle-orm/pg-core"
|
||||
import type { ExtractTablesWithRelations } from "drizzle-orm"
|
||||
import type { PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js"
|
||||
import { Context } from "../context"
|
||||
import { memo } from "../util/memo"
|
||||
|
||||
export namespace Database {
|
||||
export type Transaction = PgTransaction<
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export function memo<T>(fn: () => T) {
|
||||
let value: T | undefined
|
||||
let loaded = false
|
||||
|
||||
return (): T => {
|
||||
if (loaded) return value as T
|
||||
loaded = true
|
||||
value = fn()
|
||||
return value as T
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/cloud-function",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.28",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -2,11 +2,12 @@ import { Resource } from "sst"
|
||||
import { z } from "zod"
|
||||
import { issuer } from "@openauthjs/openauth"
|
||||
import { createSubjects } from "@openauthjs/openauth/subject"
|
||||
import { CodeProvider } from "@openauthjs/openauth/provider/code"
|
||||
import { GithubProvider } from "@openauthjs/openauth/provider/github"
|
||||
import { GoogleOidcProvider } from "@openauthjs/openauth/provider/google"
|
||||
import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"
|
||||
import { Account } from "@opencode/cloud-core/account.js"
|
||||
import { Workspace } from "@opencode/cloud-core/workspace.js"
|
||||
import { Actor } from "@opencode/cloud-core/actor.js"
|
||||
|
||||
type Env = {
|
||||
AuthStorage: KVNamespace
|
||||
@@ -117,6 +118,12 @@ export default {
|
||||
email: email!,
|
||||
})
|
||||
}
|
||||
await Actor.provide("account", { accountID, email }, async () => {
|
||||
const workspaces = await Account.workspaces()
|
||||
if (workspaces.length === 0) {
|
||||
await Workspace.create()
|
||||
}
|
||||
})
|
||||
return ctx.subject("account", accountID, { accountID, email })
|
||||
},
|
||||
}).fetch(request, env, ctx)
|
||||
|
||||
Vendored
-4
@@ -14,10 +14,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Console": {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"DATABASE_PASSWORD": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/cloud-web",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.28",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"type": "module",
|
||||
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
|
||||
/// <reference path="../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
+2
-2
@@ -25,8 +25,8 @@ export const api = new sst.cloudflare.Worker("Api", {
|
||||
])
|
||||
args.migrations = {
|
||||
// Note: when releasing the next tag, make sure all stages use tag v2
|
||||
oldTag: $app.stage === "production" ? "" : "v1",
|
||||
newTag: $app.stage === "production" ? "" : "v1",
|
||||
oldTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
|
||||
newTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
|
||||
//newSqliteClasses: ["SyncServer"],
|
||||
}
|
||||
},
|
||||
|
||||
+14
-1
@@ -10,7 +10,7 @@ const DATABASE_USERNAME = new sst.Secret("DATABASE_USERNAME")
|
||||
const DATABASE_PASSWORD = new sst.Secret("DATABASE_PASSWORD")
|
||||
export const database = new sst.Linkable("Database", {
|
||||
properties: {
|
||||
host: "aws-us-east-2-1.pg.psdb.cloud",
|
||||
host: `aws-us-east-2-${$app.stage === "thdxr" ? "2" : "1"}.pg.psdb.cloud`,
|
||||
database: "postgres",
|
||||
username: DATABASE_USERNAME.value,
|
||||
password: DATABASE_PASSWORD.value,
|
||||
@@ -106,6 +106,7 @@ export const gateway = new sst.cloudflare.Worker("GatewayApi", {
|
||||
// CONSOLE
|
||||
////////////////
|
||||
|
||||
/*
|
||||
export const console = new sst.cloudflare.x.StaticSite("Console", {
|
||||
domain: `console.${domain}`,
|
||||
path: "cloud/web",
|
||||
@@ -119,3 +120,15 @@ export const console = new sst.cloudflare.x.StaticSite("Console", {
|
||||
VITE_AUTH_URL: auth.url.apply((url) => url!),
|
||||
},
|
||||
})
|
||||
*/
|
||||
|
||||
new sst.x.DevCommand("Solid", {
|
||||
link: [database],
|
||||
dev: {
|
||||
directory: "cloud/app",
|
||||
command: "bun dev",
|
||||
},
|
||||
environment: {
|
||||
VITE_AUTH_URL: auth.url.apply((url) => url!),
|
||||
},
|
||||
})
|
||||
|
||||
+1
-4
@@ -1,10 +1,7 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
|
||||
"mcp": {
|
||||
"context7": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.context7.com/sse"
|
||||
},
|
||||
"weather": {
|
||||
"type": "local",
|
||||
"command": ["opencode", "x", "@h1deya/mcp-server-weather"]
|
||||
|
||||
@@ -51,5 +51,8 @@
|
||||
"tree-sitter-bash",
|
||||
"web-tree-sitter"
|
||||
],
|
||||
"overrides": {
|
||||
"zod": "3.25.76"
|
||||
},
|
||||
"patchedDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/function",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.28",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
Vendored
-4
@@ -14,10 +14,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Console": {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"DATABASE_PASSWORD": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.28",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -97,12 +97,11 @@ if (!snapshot) {
|
||||
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 - commented out as AUR is down
|
||||
const pkgbuild = [
|
||||
const binaryPkgbuild = [
|
||||
"# Maintainer: dax",
|
||||
"# Maintainer: adam",
|
||||
"",
|
||||
"pkgname='${pkg}'",
|
||||
"pkgname='opencode-bin'",
|
||||
`pkgver=${version.split("-")[0]}`,
|
||||
"options=('!debug' '!strip')",
|
||||
"pkgrel=1",
|
||||
@@ -126,17 +125,63 @@ if (!snapshot) {
|
||||
"",
|
||||
].join("\n")
|
||||
|
||||
for (const pkg of ["opencode-bin"]) {
|
||||
// Source-based PKGBUILD for opencode
|
||||
const sourcePkgbuild = [
|
||||
"# Maintainer: dax",
|
||||
"# Maintainer: adam",
|
||||
"",
|
||||
"pkgname='opencode'",
|
||||
`pkgver=${version.split("-")[0]}`,
|
||||
"options=('!debug' '!strip')",
|
||||
"pkgrel=1",
|
||||
"pkgdesc='The AI coding agent built for the terminal.'",
|
||||
"url='https://github.com/sst/opencode'",
|
||||
"arch=('aarch64' 'x86_64')",
|
||||
"license=('MIT')",
|
||||
"provides=('opencode')",
|
||||
"conflicts=('opencode-bin')",
|
||||
"depends=('fzf' 'ripgrep')",
|
||||
"makedepends=('git' 'bun-bin' 'go')",
|
||||
"",
|
||||
`source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v${version}.tar.gz")`,
|
||||
`sha256sums=('SKIP')`,
|
||||
"",
|
||||
"build() {",
|
||||
` cd "opencode-\${pkgver}"`,
|
||||
` bun install`,
|
||||
" cd packages/tui",
|
||||
` CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=\${pkgver}" -o tui cmd/opencode/main.go`,
|
||||
" cd ../opencode",
|
||||
` bun build --define OPENCODE_TUI_PATH="'$(realpath ../tui/tui)'" --define OPENCODE_VERSION="'\${pkgver}'" --compile --target=bun-linux-x64 --outfile=opencode ./src/index.ts`,
|
||||
"}",
|
||||
"",
|
||||
"package() {",
|
||||
` cd "opencode-\${pkgver}/packages/opencode"`,
|
||||
' install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"',
|
||||
"}",
|
||||
"",
|
||||
].join("\n")
|
||||
|
||||
for (const [pkg, pkgbuild] of [
|
||||
["opencode-bin", binaryPkgbuild],
|
||||
["opencode", sourcePkgbuild],
|
||||
]) {
|
||||
await $`rm -rf ./dist/aur-${pkg}`
|
||||
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
|
||||
while (true) {
|
||||
try {
|
||||
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
|
||||
break
|
||||
} catch (e) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
await $`cd ./dist/aur-${pkg} && git checkout master`
|
||||
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild.replace("${pkg}", pkg))
|
||||
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild)
|
||||
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}"`
|
||||
if (!dry) await $`cd ./dist/aur-${pkg} && git push`
|
||||
}
|
||||
*/
|
||||
|
||||
// Homebrew formula
|
||||
const homebrewFormula = [
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Config } from "../src/config/config"
|
||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
|
||||
const file = process.argv[2]
|
||||
console.log(file)
|
||||
|
||||
const result = zodToJsonSchema(Config.Info, {
|
||||
/**
|
||||
@@ -31,5 +32,13 @@ const result = zodToJsonSchema(Config.Info, {
|
||||
|
||||
return jsonSchema
|
||||
},
|
||||
})
|
||||
}) as Record<string, unknown> & {
|
||||
allowComments?: boolean
|
||||
allowTrailingCommas?: boolean
|
||||
}
|
||||
|
||||
// used for json lsps since config supports jsonc
|
||||
result.allowComments = true
|
||||
result.allowTrailingCommas = true
|
||||
|
||||
await Bun.write(file, JSON.stringify(result, null, 2))
|
||||
|
||||
@@ -48,6 +48,15 @@ export namespace Agent {
|
||||
}
|
||||
const agentPermission = mergeAgentPermissions(defaultPermission, cfg.permission ?? {})
|
||||
|
||||
const planPermission = mergeAgentPermissions(
|
||||
{
|
||||
edit: "ask",
|
||||
bash: "ask",
|
||||
webfetch: "allow",
|
||||
},
|
||||
cfg.permission ?? {},
|
||||
)
|
||||
|
||||
const result: Record<string, Info> = {
|
||||
general: {
|
||||
name: "general",
|
||||
@@ -74,11 +83,8 @@ export namespace Agent {
|
||||
plan: {
|
||||
name: "plan",
|
||||
options: {},
|
||||
permission: agentPermission,
|
||||
permission: planPermission,
|
||||
tools: {
|
||||
write: false,
|
||||
edit: false,
|
||||
patch: false,
|
||||
...defaultTools,
|
||||
},
|
||||
mode: "primary",
|
||||
@@ -100,7 +106,7 @@ export namespace Agent {
|
||||
tools: {},
|
||||
builtIn: false,
|
||||
}
|
||||
const { model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
|
||||
const { name, model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
|
||||
item.options = {
|
||||
...item.options,
|
||||
...extra,
|
||||
@@ -120,6 +126,8 @@ export namespace Agent {
|
||||
if (temperature != undefined) item.temperature = temperature
|
||||
if (top_p != undefined) item.topP = top_p
|
||||
if (mode) item.mode = mode
|
||||
// just here for consistency & to prevent it from being added as an option
|
||||
if (name) item.name = name
|
||||
|
||||
if (permission ?? cfg.permission) {
|
||||
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
|
||||
|
||||
@@ -8,9 +8,9 @@ import { Snapshot } from "../snapshot"
|
||||
|
||||
export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Promise<T>) {
|
||||
return App.provide(input, async (app) => {
|
||||
await Plugin.init()
|
||||
Share.init()
|
||||
Format.init()
|
||||
Plugin.init()
|
||||
ConfigHooks.init()
|
||||
LSP.init()
|
||||
Snapshot.init()
|
||||
|
||||
@@ -46,7 +46,10 @@ const AgentCreateCommand = cmd({
|
||||
const spinner = prompts.spinner()
|
||||
|
||||
spinner.start("Generating agent configuration...")
|
||||
const generated = await Agent.generate({ description: query })
|
||||
const generated = await Agent.generate({ description: query }).catch((error) => {
|
||||
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
|
||||
throw new UI.CancelledError()
|
||||
})
|
||||
spinner.stop(`Agent ${generated.identifier} generated`)
|
||||
|
||||
const availableTools = [
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Auth } from "../../auth"
|
||||
import { cmd } from "./cmd"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import open from "open"
|
||||
import { UI } from "../ui"
|
||||
import { ModelsDev } from "../../provider/models"
|
||||
import { map, pipe, sortBy, values } from "remeda"
|
||||
@@ -159,9 +158,6 @@ export const AuthLoginCommand = cmd({
|
||||
const authorize = await method.authorize()
|
||||
|
||||
if (authorize.url) {
|
||||
try {
|
||||
await open(authorize.url)
|
||||
} catch (e) {}
|
||||
prompts.log.info("Go to: " + authorize.url)
|
||||
}
|
||||
|
||||
|
||||
@@ -67,11 +67,17 @@ export const RunCommand = cmd({
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
const session = await (async () => {
|
||||
if (args.continue) {
|
||||
const list = Session.list()
|
||||
const first = await list.next()
|
||||
await list.return()
|
||||
if (first.done) return
|
||||
return first.value
|
||||
const it = Session.list()
|
||||
try {
|
||||
for await (const s of it) {
|
||||
if (s.parentID === undefined) {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return
|
||||
} finally {
|
||||
await it.return()
|
||||
}
|
||||
}
|
||||
|
||||
if (args.session) return Session.get(args.session)
|
||||
@@ -165,12 +171,8 @@ export const RunCommand = cmd({
|
||||
const result = await Session.chat({
|
||||
sessionID: session.id,
|
||||
messageID,
|
||||
...(agent.model
|
||||
? agent.model
|
||||
: {
|
||||
providerID,
|
||||
modelID,
|
||||
}),
|
||||
providerID,
|
||||
modelID,
|
||||
agent: agent.name,
|
||||
parts: [
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ export const ServeCommand = cmd({
|
||||
alias: ["p"],
|
||||
type: "number",
|
||||
describe: "port to listen on",
|
||||
default: 4096,
|
||||
default: 0,
|
||||
})
|
||||
.option("hostname", {
|
||||
alias: ["h"],
|
||||
|
||||
@@ -82,11 +82,17 @@ export const TuiCommand = cmd({
|
||||
const result = await bootstrap({ cwd }, async (app) => {
|
||||
const sessionID = await (async () => {
|
||||
if (args.continue) {
|
||||
const list = Session.list()
|
||||
const first = await list.next()
|
||||
await list.return()
|
||||
if (first.done) return
|
||||
return first.value.id
|
||||
const it = Session.list()
|
||||
try {
|
||||
for await (const s of it) {
|
||||
if (s.parentID === undefined) {
|
||||
return s.id
|
||||
}
|
||||
}
|
||||
return
|
||||
} finally {
|
||||
await it.return()
|
||||
}
|
||||
}
|
||||
if (args.session) {
|
||||
return args.session
|
||||
|
||||
@@ -45,7 +45,7 @@ export const UpgradeCommand = {
|
||||
spinner.start("Upgrading...")
|
||||
const err = await Installation.upgrade(method, target).catch((err) => err)
|
||||
if (err) {
|
||||
spinner.stop("Upgrade failed")
|
||||
spinner.stop("Upgrade failed", 1)
|
||||
if (err instanceof Installation.UpgradeFailedError) prompts.log.error(err.data.stderr)
|
||||
else if (err instanceof Error) prompts.log.error(err.message)
|
||||
prompts.outro("Done")
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import z from "zod"
|
||||
import { App } from "../app/app"
|
||||
import { Config } from "../config/config"
|
||||
|
||||
export namespace Command {
|
||||
export const Info = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
agent: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
template: z.string(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Command",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
const state = App.state("command", async () => {
|
||||
const cfg = await Config.get()
|
||||
|
||||
const result: Record<string, Info> = {}
|
||||
|
||||
for (const [name, command] of Object.entries(cfg.command ?? {})) {
|
||||
result[name] = {
|
||||
name,
|
||||
agent: command.agent,
|
||||
model: command.model,
|
||||
description: command.description,
|
||||
template: command.template,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
export async function get(name: string) {
|
||||
return state().then((x) => x[name])
|
||||
}
|
||||
|
||||
export async function list() {
|
||||
return state().then((x) => Object.values(x))
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ export namespace Config {
|
||||
if (agentFolderPath.includes("/")) {
|
||||
const relativePath = agentFolderPath.replace(".md", "")
|
||||
const pathParts = relativePath.split("/")
|
||||
agentName = pathParts.slice(0, -1).join("/").toUpperCase() + "/" + pathParts[pathParts.length - 1].toUpperCase()
|
||||
agentName = pathParts.slice(0, -1).join("/") + "/" + pathParts[pathParts.length - 1]
|
||||
}
|
||||
|
||||
const config = {
|
||||
@@ -107,6 +107,32 @@ export namespace Config {
|
||||
}
|
||||
throw new InvalidError({ path: item }, { cause: parsed.error })
|
||||
}
|
||||
|
||||
// Load command markdown files
|
||||
result.command = result.command || {}
|
||||
const markdownCommands = [
|
||||
...(await Filesystem.globUp("command/*.md", Global.Path.config, Global.Path.config)),
|
||||
...(await Filesystem.globUp(".opencode/command/*.md", app.path.cwd, app.path.root)),
|
||||
]
|
||||
for (const item of markdownCommands) {
|
||||
const content = await Bun.file(item).text()
|
||||
const md = matter(content)
|
||||
if (!md.data) continue
|
||||
|
||||
const config = {
|
||||
name: path.basename(item, ".md"),
|
||||
...md.data,
|
||||
template: md.content.trim(),
|
||||
}
|
||||
const parsed = Command.safeParse(config)
|
||||
if (parsed.success) {
|
||||
result.command = mergeDeep(result.command, {
|
||||
[config.name]: parsed.data,
|
||||
})
|
||||
continue
|
||||
}
|
||||
throw new InvalidError({ path: item }, { cause: parsed.error })
|
||||
}
|
||||
// Migrate deprecated mode field to agent field
|
||||
for (const [name, mode] of Object.entries(result.mode)) {
|
||||
result.agent = mergeDeep(result.agent ?? {}, {
|
||||
@@ -192,6 +218,14 @@ export namespace Config {
|
||||
export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
|
||||
export type Permission = z.infer<typeof Permission>
|
||||
|
||||
export const Command = z.object({
|
||||
template: z.string(),
|
||||
description: z.string().optional(),
|
||||
agent: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
})
|
||||
export type Command = z.infer<typeof Command>
|
||||
|
||||
export const Agent = z
|
||||
.object({
|
||||
model: z.string().optional(),
|
||||
@@ -305,6 +339,10 @@ export namespace Config {
|
||||
theme: z.string().optional().describe("Theme name to use for the interface"),
|
||||
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
||||
tui: TUI.optional().describe("TUI specific settings"),
|
||||
command: z
|
||||
.record(z.string(), Command)
|
||||
.optional()
|
||||
.describe("Command configuration, see https://opencode.ai/docs/commands"),
|
||||
plugin: z.string().array().optional(),
|
||||
snapshot: z.boolean().optional(),
|
||||
share: z
|
||||
|
||||
@@ -5,6 +5,7 @@ export namespace Flag {
|
||||
export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE")
|
||||
export const OPENCODE_PERMISSION = process.env["OPENCODE_PERMISSION"]
|
||||
export const OPENCODE_DISABLE_DEFAULT_PLUGINS = truthy("OPENCODE_DISABLE_DEFAULT_PLUGINS")
|
||||
export const OPENCODE_DISABLE_LSP_DOWNLOAD = truthy("OPENCODE_DISABLE_LSP_DOWNLOAD")
|
||||
|
||||
function truthy(key: string) {
|
||||
const value = process.env[key]?.toLowerCase()
|
||||
|
||||
@@ -68,19 +68,29 @@ export namespace Format {
|
||||
|
||||
for (const item of await getFormatter(ext)) {
|
||||
log.info("running", { command: item.command })
|
||||
const proc = Bun.spawn({
|
||||
cmd: item.command.map((x) => x.replace("$FILE", file)),
|
||||
cwd: App.info().path.cwd,
|
||||
env: { ...process.env, ...item.environment },
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0)
|
||||
try {
|
||||
const proc = Bun.spawn({
|
||||
cmd: item.command.map((x) => x.replace("$FILE", file)),
|
||||
cwd: App.info().path.cwd,
|
||||
env: { ...process.env, ...item.environment },
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0)
|
||||
log.error("failed", {
|
||||
command: item.command,
|
||||
...item.environment,
|
||||
})
|
||||
} catch (error) {
|
||||
log.error("failed", {
|
||||
error,
|
||||
command: item.command,
|
||||
...item.environment,
|
||||
})
|
||||
// re-raising
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,13 +28,18 @@ await Promise.all([
|
||||
fs.mkdir(Global.Path.bin, { recursive: true }),
|
||||
])
|
||||
|
||||
const CACHE_VERSION = "8"
|
||||
const CACHE_VERSION = "9"
|
||||
|
||||
const version = await Bun.file(path.join(Global.Path.cache, "version"))
|
||||
.text()
|
||||
.catch(() => "0")
|
||||
|
||||
if (version !== CACHE_VERSION) {
|
||||
await fs.rm(Global.Path.cache, { recursive: true, force: true })
|
||||
try {
|
||||
const contents = await fs.readdir(Global.Path.cache)
|
||||
await Promise.all(
|
||||
contents.map((item) => fs.rm(path.join(Global.Path.cache, item), { recursive: true, force: true })),
|
||||
)
|
||||
} catch (e) {}
|
||||
await Bun.file(path.join(Global.Path.cache, "version")).write(CACHE_VERSION)
|
||||
}
|
||||
|
||||
@@ -126,7 +126,11 @@ export namespace LSP {
|
||||
result.push(match)
|
||||
continue
|
||||
}
|
||||
const handle = await server.spawn(App.info(), root)
|
||||
const handle = await server.spawn(App.info(), root).catch((err) => {
|
||||
s.broken.add(root + server.id)
|
||||
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
|
||||
return undefined
|
||||
})
|
||||
if (!handle) continue
|
||||
const client = await LSPClient.create({
|
||||
serverID: server.id,
|
||||
@@ -135,7 +139,8 @@ export namespace LSP {
|
||||
}).catch((err) => {
|
||||
s.broken.add(root + server.id)
|
||||
handle.process.kill()
|
||||
log.error("", { error: err })
|
||||
log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
|
||||
return undefined
|
||||
})
|
||||
if (!client) continue
|
||||
s.clients.push(client)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { BunProc } from "../bun"
|
||||
import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Flag } from "../flag/flag"
|
||||
|
||||
export namespace LSPServer {
|
||||
const log = Log.create({ service: "lsp.server" })
|
||||
@@ -95,6 +96,7 @@ export namespace LSPServer {
|
||||
"vue-language-server.js",
|
||||
)
|
||||
if (!(await Bun.file(js).exists())) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "@vue/language-server"], {
|
||||
cwd: Global.Path.bin,
|
||||
env: {
|
||||
@@ -146,8 +148,10 @@ export namespace LSPServer {
|
||||
async spawn(app, root) {
|
||||
const eslint = await Bun.resolve("eslint", app.path.cwd).catch(() => {})
|
||||
if (!eslint) return
|
||||
log.info("spawning eslint server")
|
||||
const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
|
||||
if (!(await Bun.file(serverPath).exists())) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("downloading and building VS Code ESLint server")
|
||||
const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
|
||||
if (!response.ok) return
|
||||
@@ -161,7 +165,9 @@ export namespace LSPServer {
|
||||
const extractedPath = path.join(Global.Path.bin, "vscode-eslint-main")
|
||||
const finalPath = path.join(Global.Path.bin, "vscode-eslint")
|
||||
|
||||
if (await Bun.file(finalPath).exists()) {
|
||||
const stats = await fs.stat(finalPath).catch(() => undefined)
|
||||
if (stats) {
|
||||
log.info("removing old eslint installation", { path: finalPath })
|
||||
await fs.rm(finalPath, { force: true, recursive: true })
|
||||
}
|
||||
await fs.rename(extractedPath, finalPath)
|
||||
@@ -200,6 +206,8 @@ export namespace LSPServer {
|
||||
})
|
||||
if (!bin) {
|
||||
if (!Bun.which("go")) return
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
|
||||
log.info("installing gopls")
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
|
||||
@@ -241,6 +249,7 @@ export namespace LSPServer {
|
||||
log.info("Ruby not found, please install Ruby first")
|
||||
return
|
||||
}
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("installing ruby-lsp")
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["gem", "install", "ruby-lsp", "--bindir", Global.Path.bin],
|
||||
@@ -276,6 +285,7 @@ export namespace LSPServer {
|
||||
if (!binary) {
|
||||
const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
|
||||
if (!(await Bun.file(js).exists())) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "pyright"], {
|
||||
cwd: Global.Path.bin,
|
||||
env: {
|
||||
@@ -323,6 +333,7 @@ export namespace LSPServer {
|
||||
return
|
||||
}
|
||||
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("downloading elixir-ls from GitHub releases")
|
||||
|
||||
const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
|
||||
@@ -372,6 +383,7 @@ export namespace LSPServer {
|
||||
return
|
||||
}
|
||||
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("downloading zls from GitHub releases")
|
||||
|
||||
const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
|
||||
@@ -475,6 +487,7 @@ export namespace LSPServer {
|
||||
return
|
||||
}
|
||||
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("installing csharp-ls via dotnet tool")
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin],
|
||||
@@ -527,6 +540,7 @@ export namespace LSPServer {
|
||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||
})
|
||||
if (!bin) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("downloading clangd from GitHub releases")
|
||||
|
||||
const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
|
||||
|
||||
@@ -25,7 +25,7 @@ export namespace Plugin {
|
||||
}
|
||||
const plugins = [...(config.plugin ?? [])]
|
||||
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
|
||||
plugins.push("opencode-copilot-auth")
|
||||
plugins.push("opencode-copilot-auth@0.0.2")
|
||||
plugins.push("opencode-anthropic-auth@0.0.2")
|
||||
}
|
||||
for (let plugin of plugins) {
|
||||
@@ -68,7 +68,12 @@ export namespace Plugin {
|
||||
return state().then((x) => x.hooks)
|
||||
}
|
||||
|
||||
export function init() {
|
||||
export async function init() {
|
||||
const hooks = await state().then((x) => x.hooks)
|
||||
const config = await Config.get()
|
||||
for (const hook of hooks) {
|
||||
await hook.config?.(config)
|
||||
}
|
||||
Bus.subscribeAll(async (input) => {
|
||||
const hooks = await state().then((x) => x.hooks)
|
||||
for (const hook of hooks) {
|
||||
|
||||
@@ -36,6 +36,12 @@ export namespace Provider {
|
||||
},
|
||||
}
|
||||
},
|
||||
async opencode(input) {
|
||||
return {
|
||||
autoload: Object.keys(input.models).length > 0,
|
||||
options: {},
|
||||
}
|
||||
},
|
||||
openai: async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
@@ -276,6 +282,15 @@ export namespace Provider {
|
||||
}
|
||||
|
||||
for (const [providerID, provider] of Object.entries(providers)) {
|
||||
// Filter out blacklisted models
|
||||
const filteredModels = Object.fromEntries(
|
||||
Object.entries(provider.info.models).filter(
|
||||
([modelID]) =>
|
||||
modelID !== "gpt-5-chat-latest" && !(providerID === "openrouter" && modelID === "openai/gpt-5-chat"),
|
||||
),
|
||||
)
|
||||
provider.info.models = filteredModels
|
||||
|
||||
if (Object.keys(provider.info.models).length === 0) {
|
||||
delete providers[providerID]
|
||||
continue
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Permission } from "../permission"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { Auth } from "../auth"
|
||||
import { Command } from "../command"
|
||||
|
||||
const ERRORS = {
|
||||
400: {
|
||||
@@ -102,7 +103,7 @@ export namespace Server {
|
||||
200: {
|
||||
description: "Event stream",
|
||||
content: {
|
||||
"application/json": {
|
||||
"text/event-stream": {
|
||||
schema: resolver(
|
||||
Bus.payloads().openapi({
|
||||
ref: "Event",
|
||||
@@ -611,7 +612,12 @@ export namespace Server {
|
||||
description: "Created message",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(MessageV2.Assistant),
|
||||
schema: resolver(
|
||||
z.object({
|
||||
info: MessageV2.Assistant,
|
||||
parts: MessageV2.Part.array(),
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -631,6 +637,41 @@ export namespace Server {
|
||||
return c.json(msg)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session/:id/command",
|
||||
describeRoute({
|
||||
description: "Send a new command to a session",
|
||||
operationId: "session.command",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Created message",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z.object({
|
||||
info: MessageV2.Assistant,
|
||||
parts: MessageV2.Part.array(),
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string().openapi({ description: "Session ID" }),
|
||||
}),
|
||||
),
|
||||
zValidator("json", Session.CommandInput.omit({ sessionID: true })),
|
||||
async (c) => {
|
||||
const sessionID = c.req.valid("param").id
|
||||
const body = c.req.valid("json")
|
||||
const msg = await Session.command({ ...body, sessionID })
|
||||
return c.json(msg)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session/:id/shell",
|
||||
describeRoute({
|
||||
@@ -653,7 +694,7 @@ export namespace Server {
|
||||
id: z.string().openapi({ description: "Session ID" }),
|
||||
}),
|
||||
),
|
||||
zValidator("json", Session.CommandInput.omit({ sessionID: true })),
|
||||
zValidator("json", Session.ShellInput.omit({ sessionID: true })),
|
||||
async (c) => {
|
||||
const sessionID = c.req.valid("param").id
|
||||
const body = c.req.valid("json")
|
||||
@@ -750,6 +791,27 @@ export namespace Server {
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/command",
|
||||
describeRoute({
|
||||
description: "List all commands",
|
||||
operationId: "command.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of commands",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Command.Info.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const commands = await Command.list()
|
||||
return c.json(commands)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/config/providers",
|
||||
describeRoute({
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
|
||||
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
|
||||
import PROMPT_PLAN from "../session/prompt/plan.txt"
|
||||
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
|
||||
|
||||
import { App } from "../app/app"
|
||||
import { Bus } from "../bus"
|
||||
@@ -46,6 +47,8 @@ import { Permission } from "../permission"
|
||||
import { Wildcard } from "../util/wildcard"
|
||||
import { ulid } from "ulid"
|
||||
import { defer } from "../util/defer"
|
||||
import { Command } from "../command"
|
||||
import { $ } from "bun"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -726,6 +729,18 @@ export namespace Session {
|
||||
synthetic: true,
|
||||
})
|
||||
}
|
||||
|
||||
const lastAssistantMsg = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
|
||||
if (lastAssistantMsg?.mode === "plan" && agent.name === "build") {
|
||||
msgs.at(-1)?.parts.push({
|
||||
id: Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
type: "text",
|
||||
text: BUILD_SWITCH,
|
||||
synthetic: true,
|
||||
})
|
||||
}
|
||||
let system = SystemPrompt.header(input.providerID)
|
||||
system.push(
|
||||
...(() => {
|
||||
@@ -945,6 +960,13 @@ export namespace Session {
|
||||
toolName: "invalid",
|
||||
}
|
||||
},
|
||||
headers:
|
||||
input.providerID === "opencode"
|
||||
? {
|
||||
"x-opencode-session": input.sessionID,
|
||||
"x-opencode-request": userMsg.id,
|
||||
}
|
||||
: undefined,
|
||||
maxRetries: 3,
|
||||
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
|
||||
maxOutputTokens: outputLimit,
|
||||
@@ -1005,13 +1027,13 @@ export namespace Session {
|
||||
return result
|
||||
}
|
||||
|
||||
export const CommandInput = z.object({
|
||||
export const ShellInput = z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
agent: z.string(),
|
||||
command: z.string(),
|
||||
})
|
||||
export type CommandInput = z.infer<typeof CommandInput>
|
||||
export async function shell(input: CommandInput) {
|
||||
export type ShellInput = z.infer<typeof ShellInput>
|
||||
export async function shell(input: ShellInput) {
|
||||
using abort = lock(input.sessionID)
|
||||
const msg: MessageV2.Assistant = {
|
||||
id: Identifier.ascending("message"),
|
||||
@@ -1135,6 +1157,76 @@ export namespace Session {
|
||||
return { info: msg, parts: [part] }
|
||||
}
|
||||
|
||||
export const CommandInput = z.object({
|
||||
messageID: Identifier.schema("message").optional(),
|
||||
sessionID: Identifier.schema("session"),
|
||||
agent: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
arguments: z.string(),
|
||||
command: z.string(),
|
||||
})
|
||||
export type CommandInput = z.infer<typeof CommandInput>
|
||||
const bashRegex = /!`([^`]+)`/g
|
||||
const fileRegex = /@([^\s]+)/g
|
||||
|
||||
export async function command(input: CommandInput) {
|
||||
const command = await Command.get(input.command)
|
||||
const agent = command.agent ?? input.agent ?? "build"
|
||||
const fmtModel = (model: { providerID: string; modelID: string }) => `${model.providerID}/${model.modelID}`
|
||||
|
||||
const model =
|
||||
command.model ??
|
||||
(command.agent && (await Agent.get(command.agent).then((x) => (x.model ? fmtModel(x.model) : undefined)))) ??
|
||||
input.model ??
|
||||
(input.agent && (await Agent.get(input.agent).then((x) => (x.model ? fmtModel(x.model) : undefined)))) ??
|
||||
fmtModel(await Provider.defaultModel())
|
||||
|
||||
let template = command.template.replace("$ARGUMENTS", input.arguments)
|
||||
|
||||
const bash = Array.from(template.matchAll(bashRegex))
|
||||
if (bash.length > 0) {
|
||||
const results = await Promise.all(
|
||||
bash.map(async ([, cmd]) => {
|
||||
try {
|
||||
return await $`${{ raw: cmd }}`.nothrow().text()
|
||||
} catch (error) {
|
||||
return `Error executing command: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
}),
|
||||
)
|
||||
let index = 0
|
||||
template = template.replace(bashRegex, () => results[index++])
|
||||
}
|
||||
|
||||
const parts = [
|
||||
{
|
||||
type: "text",
|
||||
text: template,
|
||||
},
|
||||
] as ChatInput["parts"]
|
||||
|
||||
const matches = template.matchAll(fileRegex)
|
||||
const app = App.info()
|
||||
|
||||
for (const match of matches) {
|
||||
const file = path.join(app.path.cwd, match[1])
|
||||
parts.push({
|
||||
type: "file",
|
||||
url: `file://${file}`,
|
||||
filename: match[1],
|
||||
mime: "text/plain",
|
||||
})
|
||||
}
|
||||
|
||||
return chat({
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
...Provider.parseModel(model!),
|
||||
agent,
|
||||
parts,
|
||||
})
|
||||
}
|
||||
|
||||
function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) {
|
||||
const toolcalls: Record<string, MessageV2.ToolPart> = {}
|
||||
let snapshot: string | undefined
|
||||
@@ -1592,7 +1684,7 @@ export namespace Session {
|
||||
const tokens = {
|
||||
input: usage.inputTokens ?? 0,
|
||||
output: usage.outputTokens ?? 0,
|
||||
reasoning: 0,
|
||||
reasoning: usage?.reasoningTokens ?? 0,
|
||||
cache: {
|
||||
write: (metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
|
||||
// @ts-expect-error
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Your operational mode has changed from plan to build. You are no longer in read-only mode. You are permitted to make file changes as necessary and utilize your arsenal of tools as needed.
|
||||
@@ -109,6 +109,9 @@ IMPORTANT: When the user asks you to create a pull request, follow these steps c
|
||||
gh pr create --title "the pr title" --body "$(cat <<'EOF'
|
||||
## Summary
|
||||
<1-3 bullet points>
|
||||
EOF
|
||||
)"
|
||||
</example>
|
||||
|
||||
Important:
|
||||
- NEVER update the git config
|
||||
|
||||
@@ -86,7 +86,7 @@ export namespace ToolRegistry {
|
||||
result["webfetch"] = false
|
||||
}
|
||||
|
||||
if (modelID.includes("qwen")) {
|
||||
if (modelID.toLowerCase().includes("qwen")) {
|
||||
result["todowrite"] = false
|
||||
result["todoread"] = false
|
||||
}
|
||||
|
||||
@@ -101,7 +101,12 @@ export namespace Log {
|
||||
...extra,
|
||||
})
|
||||
.filter(([_, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => `${key}=${typeof value === "object" ? JSON.stringify(value) : value}`)
|
||||
.map(([key, value]) => {
|
||||
const prefix = `${key}=`
|
||||
if (value instanceof Error) return prefix + value.message
|
||||
if (typeof value === "object") return prefix + JSON.stringify(value)
|
||||
return prefix + value
|
||||
})
|
||||
.join(" ")
|
||||
const next = new Date()
|
||||
const diff = next.getTime() - last
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.28",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
@@ -19,8 +19,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:",
|
||||
"@hey-api/openapi-ts": "0.80.1",
|
||||
"@tsconfig/node22": "catalog:"
|
||||
"@hey-api/openapi-ts": "0.81.0",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
UserMessage,
|
||||
Part,
|
||||
Auth,
|
||||
Config,
|
||||
} from "@opencode-ai/sdk"
|
||||
import type { BunShell } from "./shell"
|
||||
|
||||
@@ -20,6 +21,7 @@ export type Plugin = (input: PluginInput) => Promise<Hooks>
|
||||
|
||||
export interface Hooks {
|
||||
event?: (input: { event: Event }) => Promise<void>
|
||||
config?: (input: Config) => Promise<void>
|
||||
auth?: {
|
||||
provider: string
|
||||
loader?: (auth: () => Promise<Auth>, provider: Provider) => Promise<Record<string, any>>
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
"module": "preserve",
|
||||
"declaration": true,
|
||||
"moduleResolution": "bundler",
|
||||
"lib": [
|
||||
"es2022",
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
],
|
||||
"customConditions": [
|
||||
"development"
|
||||
]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configured_endpoints: 39
|
||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-e4b6496e5f2c68fa8b3ea1b88e40041eaf5ce2652001344df80bf130675d1766.yml
|
||||
openapi_spec_hash: df474311dc9e4a89cd483bd8b8d971d8
|
||||
config_hash: eab3723c4c2232a6ba1821151259d6da
|
||||
configured_endpoints: 41
|
||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-6d8e9dfd438cac63fc7d689ea29adfff81ff8880c2d8e1e10fc36f375a721594.yml
|
||||
openapi_spec_hash: 7ac6028dd5957c67a98d91e790863c80
|
||||
config_hash: fb625e876313a9f8f31532348fa91f59
|
||||
|
||||
+14
-1
@@ -70,6 +70,16 @@ Methods:
|
||||
|
||||
- <code title="get /config">client.Config.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Config">Config</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# Command
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Command">Command</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /command">client.Command.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#CommandService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Command">Command</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# Session
|
||||
|
||||
Params Types:
|
||||
@@ -105,6 +115,8 @@ Response Types:
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStatePending">ToolStatePending</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateRunning">ToolStateRunning</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessage">UserMessage</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatResponse">SessionChatResponse</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandResponse">SessionCommandResponse</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>
|
||||
|
||||
@@ -115,8 +127,9 @@ Methods:
|
||||
- <code title="get /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="delete /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatResponse">SessionChatResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /session/{id}/children">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Children">Children</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/command">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Command">Command</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandParams">SessionCommandParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandResponse">SessionCommandResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /session/{id}/message/{messageID}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Message">Message</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, messageID <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
@@ -21,6 +21,7 @@ type Client struct {
|
||||
Find *FindService
|
||||
File *FileService
|
||||
Config *ConfigService
|
||||
Command *CommandService
|
||||
Session *SessionService
|
||||
Tui *TuiService
|
||||
}
|
||||
@@ -49,6 +50,7 @@ func NewClient(opts ...option.RequestOption) (r *Client) {
|
||||
r.Find = NewFindService(opts...)
|
||||
r.File = NewFileService(opts...)
|
||||
r.Config = NewConfigService(opts...)
|
||||
r.Command = NewCommandService(opts...)
|
||||
r.Session = NewSessionService(opts...)
|
||||
r.Tui = NewTuiService(opts...)
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
package opencode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/sst/opencode-sdk-go/internal/apijson"
|
||||
"github.com/sst/opencode-sdk-go/internal/requestconfig"
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
)
|
||||
|
||||
// CommandService contains methods and other services that help with interacting
|
||||
// with the opencode API.
|
||||
//
|
||||
// Note, unlike clients, this service does not read variables from the environment
|
||||
// automatically. You should not instantiate this service directly, and instead use
|
||||
// the [NewCommandService] method instead.
|
||||
type CommandService struct {
|
||||
Options []option.RequestOption
|
||||
}
|
||||
|
||||
// NewCommandService generates a new service that applies the given options to each
|
||||
// request. These options are applied after the parent client's options (if there
|
||||
// is one), and before any request-specific options.
|
||||
func NewCommandService(opts ...option.RequestOption) (r *CommandService) {
|
||||
r = &CommandService{}
|
||||
r.Options = opts
|
||||
return
|
||||
}
|
||||
|
||||
// List all commands
|
||||
func (r *CommandService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Command, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
path := "command"
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
Name string `json:"name,required"`
|
||||
Template string `json:"template,required"`
|
||||
Agent string `json:"agent"`
|
||||
Description string `json:"description"`
|
||||
Model string `json:"model"`
|
||||
JSON commandJSON `json:"-"`
|
||||
}
|
||||
|
||||
// commandJSON contains the JSON metadata for the struct [Command]
|
||||
type commandJSON struct {
|
||||
Name apijson.Field
|
||||
Template apijson.Field
|
||||
Agent apijson.Field
|
||||
Description apijson.Field
|
||||
Model apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *Command) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r commandJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
package opencode_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode-sdk-go/internal/testutil"
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
)
|
||||
|
||||
func TestCommandList(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||
baseURL = envURL
|
||||
}
|
||||
if !testutil.CheckTestServer(t, baseURL) {
|
||||
return
|
||||
}
|
||||
client := opencode.NewClient(
|
||||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
_, err := client.Command.List(context.TODO())
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
t.Log(string(apierr.DumpRequest(true)))
|
||||
}
|
||||
t.Fatalf("err should be nil: %s", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,8 @@ type Config struct {
|
||||
// automatically
|
||||
Autoshare bool `json:"autoshare"`
|
||||
// Automatically update to the latest version
|
||||
Autoupdate bool `json:"autoupdate"`
|
||||
Autoupdate bool `json:"autoupdate"`
|
||||
Command map[string]ConfigCommand `json:"command"`
|
||||
// Disable providers that are loaded automatically
|
||||
DisabledProviders []string `json:"disabled_providers"`
|
||||
Experimental ConfigExperimental `json:"experimental"`
|
||||
@@ -79,7 +80,8 @@ type Config struct {
|
||||
SmallModel string `json:"small_model"`
|
||||
Snapshot bool `json:"snapshot"`
|
||||
// Theme name to use for the interface
|
||||
Theme string `json:"theme"`
|
||||
Theme string `json:"theme"`
|
||||
Tools map[string]bool `json:"tools"`
|
||||
// TUI specific settings
|
||||
Tui ConfigTui `json:"tui"`
|
||||
// Custom username to display in conversations instead of system username
|
||||
@@ -93,6 +95,7 @@ type configJSON struct {
|
||||
Agent apijson.Field
|
||||
Autoshare apijson.Field
|
||||
Autoupdate apijson.Field
|
||||
Command apijson.Field
|
||||
DisabledProviders apijson.Field
|
||||
Experimental apijson.Field
|
||||
Formatter apijson.Field
|
||||
@@ -110,6 +113,7 @@ type configJSON struct {
|
||||
SmallModel apijson.Field
|
||||
Snapshot apijson.Field
|
||||
Theme apijson.Field
|
||||
Tools apijson.Field
|
||||
Tui apijson.Field
|
||||
Username apijson.Field
|
||||
raw string
|
||||
@@ -662,6 +666,32 @@ func (r ConfigAgentPlanPermissionWebfetch) IsKnown() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type ConfigCommand struct {
|
||||
Template string `json:"template,required"`
|
||||
Agent string `json:"agent"`
|
||||
Description string `json:"description"`
|
||||
Model string `json:"model"`
|
||||
JSON configCommandJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configCommandJSON contains the JSON metadata for the struct [ConfigCommand]
|
||||
type configCommandJSON struct {
|
||||
Template apijson.Field
|
||||
Agent apijson.Field
|
||||
Description apijson.Field
|
||||
Model apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigCommand) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configCommandJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigExperimental struct {
|
||||
Hook ConfigExperimentalHook `json:"hook"`
|
||||
JSON configExperimentalJSON `json:"-"`
|
||||
@@ -1760,6 +1790,8 @@ type KeybindsConfig struct {
|
||||
SessionNew string `json:"session_new,required"`
|
||||
// Share current session
|
||||
SessionShare string `json:"session_share,required"`
|
||||
// Show session timeline
|
||||
SessionTimeline string `json:"session_timeline,required"`
|
||||
// Unshare current session
|
||||
SessionUnshare string `json:"session_unshare,required"`
|
||||
// @deprecated use agent_cycle. Next agent
|
||||
@@ -1821,6 +1853,7 @@ type keybindsConfigJSON struct {
|
||||
SessionList apijson.Field
|
||||
SessionNew apijson.Field
|
||||
SessionShare apijson.Field
|
||||
SessionTimeline apijson.Field
|
||||
SessionUnshare apijson.Field
|
||||
SwitchAgent apijson.Field
|
||||
SwitchAgentReverse apijson.Field
|
||||
|
||||
@@ -41,6 +41,7 @@ func (r *EventService) ListStreaming(ctx context.Context, opts ...option.Request
|
||||
err error
|
||||
)
|
||||
opts = append(r.Options[:], opts...)
|
||||
opts = append([]option.RequestOption{option.WithHeader("Accept", "text/event-stream")}, opts...)
|
||||
path := "event"
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &raw, opts...)
|
||||
return ssestream.NewStream[EventListResponse](ssestream.NewDecoder(raw), err)
|
||||
|
||||
@@ -91,7 +91,7 @@ func (r *SessionService) Abort(ctx context.Context, id string, opts ...option.Re
|
||||
}
|
||||
|
||||
// Create and send a new message to a session
|
||||
func (r *SessionService) Chat(ctx context.Context, id string, body SessionChatParams, opts ...option.RequestOption) (res *AssistantMessage, err error) {
|
||||
func (r *SessionService) Chat(ctx context.Context, id string, body SessionChatParams, opts ...option.RequestOption) (res *SessionChatResponse, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
if id == "" {
|
||||
err = errors.New("missing required id parameter")
|
||||
@@ -114,6 +114,18 @@ func (r *SessionService) Children(ctx context.Context, id string, opts ...option
|
||||
return
|
||||
}
|
||||
|
||||
// Send a new command to a session
|
||||
func (r *SessionService) Command(ctx context.Context, id string, body SessionCommandParams, opts ...option.RequestOption) (res *SessionCommandResponse, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
if id == "" {
|
||||
err = errors.New("missing required id parameter")
|
||||
return
|
||||
}
|
||||
path := fmt.Sprintf("session/%s/command", id)
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
// Get session
|
||||
func (r *SessionService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Session, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
@@ -2278,6 +2290,52 @@ func (r userMessageTimeJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type SessionChatResponse struct {
|
||||
Info AssistantMessage `json:"info,required"`
|
||||
Parts []Part `json:"parts,required"`
|
||||
JSON sessionChatResponseJSON `json:"-"`
|
||||
}
|
||||
|
||||
// sessionChatResponseJSON contains the JSON metadata for the struct
|
||||
// [SessionChatResponse]
|
||||
type sessionChatResponseJSON struct {
|
||||
Info apijson.Field
|
||||
Parts apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *SessionChatResponse) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r sessionChatResponseJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type SessionCommandResponse struct {
|
||||
Info AssistantMessage `json:"info,required"`
|
||||
Parts []Part `json:"parts,required"`
|
||||
JSON sessionCommandResponseJSON `json:"-"`
|
||||
}
|
||||
|
||||
// sessionCommandResponseJSON contains the JSON metadata for the struct
|
||||
// [SessionCommandResponse]
|
||||
type sessionCommandResponseJSON struct {
|
||||
Info apijson.Field
|
||||
Parts apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *SessionCommandResponse) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r sessionCommandResponseJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type SessionMessageResponse struct {
|
||||
Info Message `json:"info,required"`
|
||||
Parts []Part `json:"parts,required"`
|
||||
@@ -2396,6 +2454,18 @@ func (r SessionChatParamsPartsType) IsKnown() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type SessionCommandParams struct {
|
||||
Arguments param.Field[string] `json:"arguments,required"`
|
||||
Command param.Field[string] `json:"command,required"`
|
||||
Agent param.Field[string] `json:"agent"`
|
||||
MessageID param.Field[string] `json:"messageID"`
|
||||
Model param.Field[string] `json:"model"`
|
||||
}
|
||||
|
||||
func (r SessionCommandParams) MarshalJSON() (data []byte, err error) {
|
||||
return apijson.MarshalRoot(r)
|
||||
}
|
||||
|
||||
type SessionInitParams struct {
|
||||
MessageID param.Field[string] `json:"messageID,required"`
|
||||
ModelID param.Field[string] `json:"modelID,required"`
|
||||
|
||||
@@ -199,6 +199,38 @@ func TestSessionChildren(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionCommandWithOptionalParams(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||
baseURL = envURL
|
||||
}
|
||||
if !testutil.CheckTestServer(t, baseURL) {
|
||||
return
|
||||
}
|
||||
client := opencode.NewClient(
|
||||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
_, err := client.Session.Command(
|
||||
context.TODO(),
|
||||
"id",
|
||||
opencode.SessionCommandParams{
|
||||
Arguments: opencode.F("arguments"),
|
||||
Command: opencode.F("command"),
|
||||
Agent: opencode.F("agent"),
|
||||
MessageID: opencode.F("msg"),
|
||||
Model: opencode.F("model"),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
t.Log(string(apierr.DumpRequest(true)))
|
||||
}
|
||||
t.Fatalf("err should be nil: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionGet(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.28",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
@@ -10,6 +10,14 @@
|
||||
".": {
|
||||
"development": "./src/index.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./client": {
|
||||
"development": "./src/client.ts",
|
||||
"import": "./dist/client.js"
|
||||
},
|
||||
"./server": {
|
||||
"development": "./src/server.ts",
|
||||
"import": "./dist/server.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@@ -19,5 +27,8 @@
|
||||
"typescript": "catalog:",
|
||||
"@hey-api/openapi-ts": "0.80.1",
|
||||
"@tsconfig/node22": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hey-api/openapi-ts": "0.80.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export * from "./gen/types.gen.js"
|
||||
export { type Config as OpencodeClientConfig, OpencodeClient }
|
||||
|
||||
import { createClient } from "./gen/client/client.js"
|
||||
import { type Config } from "./gen/client/types.js"
|
||||
import { OpencodeClient } from "./gen/sdk.gen.js"
|
||||
|
||||
export function createOpencodeClient(config?: Config) {
|
||||
const client = createClient(config)
|
||||
return new OpencodeClient({ client })
|
||||
}
|
||||
@@ -39,6 +39,8 @@ import type {
|
||||
SessionChatResponses,
|
||||
SessionMessageData,
|
||||
SessionMessageResponses,
|
||||
SessionCommandData,
|
||||
SessionCommandResponses,
|
||||
SessionShellData,
|
||||
SessionShellResponses,
|
||||
SessionRevertData,
|
||||
@@ -47,6 +49,8 @@ import type {
|
||||
SessionUnrevertResponses,
|
||||
PostSessionByIdPermissionsByPermissionIdData,
|
||||
PostSessionByIdPermissionsByPermissionIdResponses,
|
||||
CommandListData,
|
||||
CommandListResponses,
|
||||
ConfigProvidersData,
|
||||
ConfigProvidersResponses,
|
||||
FindTextData,
|
||||
@@ -355,6 +359,20 @@ class Session extends _HeyApiClient {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a new command to a session
|
||||
*/
|
||||
public command<ThrowOnError extends boolean = false>(options: Options<SessionCommandData, ThrowOnError>) {
|
||||
return (options.client ?? this._client).post<SessionCommandResponses, unknown, ThrowOnError>({
|
||||
url: "/session/{id}/command",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a shell command
|
||||
*/
|
||||
@@ -394,6 +412,18 @@ class Session extends _HeyApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
class Command extends _HeyApiClient {
|
||||
/**
|
||||
* List all commands
|
||||
*/
|
||||
public list<ThrowOnError extends boolean = false>(options?: Options<CommandListData, ThrowOnError>) {
|
||||
return (options?.client ?? this._client).get<CommandListResponses, unknown, ThrowOnError>({
|
||||
url: "/command",
|
||||
...options,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Find extends _HeyApiClient {
|
||||
/**
|
||||
* Find text in files
|
||||
@@ -592,6 +622,7 @@ export class OpencodeClient extends _HeyApiClient {
|
||||
app = new App({ client: this._client })
|
||||
config = new Config({ client: this._client })
|
||||
session = new Session({ client: this._client })
|
||||
command = new Command({ client: this._client })
|
||||
find = new Find({ client: this._client })
|
||||
file = new File({ client: this._client })
|
||||
tui = new Tui({ client: this._client })
|
||||
|
||||
@@ -585,6 +585,17 @@ export type Config = {
|
||||
*/
|
||||
scroll_speed: number
|
||||
}
|
||||
/**
|
||||
* Command configuration, see https://opencode.ai/docs/commands
|
||||
*/
|
||||
command?: {
|
||||
[key: string]: {
|
||||
template: string
|
||||
description?: string
|
||||
agent?: string
|
||||
model?: string
|
||||
}
|
||||
}
|
||||
plugin?: Array<string>
|
||||
snapshot?: boolean
|
||||
/**
|
||||
@@ -1110,6 +1121,14 @@ export type AgentPartInput = {
|
||||
}
|
||||
}
|
||||
|
||||
export type Command = {
|
||||
name: string
|
||||
description?: string
|
||||
agent?: string
|
||||
model?: string
|
||||
template: string
|
||||
}
|
||||
|
||||
export type Symbol = {
|
||||
name: string
|
||||
kind: number
|
||||
@@ -1527,7 +1546,10 @@ export type SessionChatResponses = {
|
||||
/**
|
||||
* Created message
|
||||
*/
|
||||
200: AssistantMessage
|
||||
200: {
|
||||
info: AssistantMessage
|
||||
parts: Array<Part>
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionChatResponse = SessionChatResponses[keyof SessionChatResponses]
|
||||
@@ -1560,6 +1582,36 @@ export type SessionMessageResponses = {
|
||||
|
||||
export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses]
|
||||
|
||||
export type SessionCommandData = {
|
||||
body?: {
|
||||
messageID?: string
|
||||
agent?: string
|
||||
model?: string
|
||||
arguments: string
|
||||
command: string
|
||||
}
|
||||
path: {
|
||||
/**
|
||||
* Session ID
|
||||
*/
|
||||
id: string
|
||||
}
|
||||
query?: never
|
||||
url: "/session/{id}/command"
|
||||
}
|
||||
|
||||
export type SessionCommandResponses = {
|
||||
/**
|
||||
* Created message
|
||||
*/
|
||||
200: {
|
||||
info: AssistantMessage
|
||||
parts: Array<Part>
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionCommandResponse = SessionCommandResponses[keyof SessionCommandResponses]
|
||||
|
||||
export type SessionShellData = {
|
||||
body?: {
|
||||
agent: string
|
||||
@@ -1645,6 +1697,22 @@ export type PostSessionByIdPermissionsByPermissionIdResponses = {
|
||||
export type PostSessionByIdPermissionsByPermissionIdResponse =
|
||||
PostSessionByIdPermissionsByPermissionIdResponses[keyof PostSessionByIdPermissionsByPermissionIdResponses]
|
||||
|
||||
export type CommandListData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: never
|
||||
url: "/command"
|
||||
}
|
||||
|
||||
export type CommandListResponses = {
|
||||
/**
|
||||
* List of commands
|
||||
*/
|
||||
200: Array<Command>
|
||||
}
|
||||
|
||||
export type CommandListResponse = CommandListResponses[keyof CommandListResponses]
|
||||
|
||||
export type ConfigProvidersData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
||||
@@ -1,35 +1,2 @@
|
||||
import { createClient } from "./gen/client/client.js"
|
||||
import { type Config } from "./gen/client/types.js"
|
||||
import { OpencodeClient } from "./gen/sdk.gen.js"
|
||||
export * from "./gen/types.gen.js"
|
||||
import { spawn } from "child_process"
|
||||
|
||||
export function createOpencodeClient(config?: Config) {
|
||||
const client = createClient(config)
|
||||
return new OpencodeClient({ client })
|
||||
}
|
||||
|
||||
export type ServerConfig = {
|
||||
host?: string
|
||||
port?: number
|
||||
}
|
||||
|
||||
export async function createOpencodeServer(config?: ServerConfig) {
|
||||
config = Object.assign(
|
||||
{
|
||||
host: "127.0.0.1",
|
||||
port: 4096,
|
||||
},
|
||||
config ?? {},
|
||||
)
|
||||
|
||||
const proc = spawn(`opencode`, [`serve`, `--host=${config.host}`, `--port=${config.port}`])
|
||||
const url = `http://${config.host}:${config.port}`
|
||||
|
||||
return {
|
||||
url,
|
||||
close() {
|
||||
proc.kill()
|
||||
},
|
||||
}
|
||||
}
|
||||
export * from "./client.js"
|
||||
export * from "./server.js"
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { spawn } from "node:child_process"
|
||||
|
||||
export type ServerConfig = {
|
||||
hostname?: string
|
||||
port?: number
|
||||
signal?: AbortSignal
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export async function createOpencodeServer(config?: ServerConfig) {
|
||||
config = Object.assign(
|
||||
{
|
||||
hostname: "127.0.0.1",
|
||||
port: 4096,
|
||||
timeout: 5000,
|
||||
},
|
||||
config ?? {},
|
||||
)
|
||||
|
||||
const proc = spawn(`opencode`, [`serve`, `--hostname=${config.hostname}`, `--port=${config.port}`], {
|
||||
signal: config.signal,
|
||||
})
|
||||
|
||||
const url = await new Promise<string>((resolve, reject) => {
|
||||
const id = setTimeout(() => {
|
||||
reject(new Error(`Timeout waiting for server to start after ${config.timeout}ms`))
|
||||
}, config.timeout)
|
||||
let output = ""
|
||||
proc.stdout?.on("data", (chunk) => {
|
||||
output += chunk.toString()
|
||||
const lines = output.split("\n")
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("opencode server listening")) {
|
||||
const match = line.match(/on\s+(https?:\/\/[^\s]+)/)
|
||||
if (!match) {
|
||||
throw new Error(`Failed to parse server url from output: ${line}`)
|
||||
}
|
||||
clearTimeout(id)
|
||||
resolve(match[1])
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
proc.stderr?.on("data", (chunk) => {
|
||||
output += chunk.toString()
|
||||
})
|
||||
proc.on("exit", (code) => {
|
||||
clearTimeout(id)
|
||||
let msg = `Server exited with code ${code}`
|
||||
if (output.trim()) {
|
||||
msg += `\nServer output: ${output}`
|
||||
}
|
||||
reject(new Error(msg))
|
||||
})
|
||||
proc.on("error", (error) => {
|
||||
clearTimeout(id)
|
||||
reject(error)
|
||||
})
|
||||
if (config.signal) {
|
||||
config.signal.addEventListener("abort", () => {
|
||||
clearTimeout(id)
|
||||
reject(new Error("Aborted"))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
url,
|
||||
close() {
|
||||
proc.kill()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,12 @@ resources:
|
||||
methods:
|
||||
get: get /config
|
||||
|
||||
command:
|
||||
models:
|
||||
command: Command
|
||||
methods:
|
||||
list: get /command
|
||||
|
||||
session:
|
||||
models:
|
||||
session: Session
|
||||
@@ -126,6 +132,7 @@ resources:
|
||||
message: get /session/{id}/message/{messageID}
|
||||
messages: get /session/{id}/message
|
||||
chat: post /session/{id}/message
|
||||
command: post /session/{id}/command
|
||||
shell: post /session/{id}/shell
|
||||
update: patch /session/{id}
|
||||
revert: post /session/{id}/revert
|
||||
|
||||
@@ -84,6 +84,10 @@ type SendPrompt = Prompt
|
||||
type SendShell = struct {
|
||||
Command string
|
||||
}
|
||||
type SendCommand = struct {
|
||||
Command string
|
||||
Args string
|
||||
}
|
||||
type SetEditorContentMsg struct {
|
||||
Text string
|
||||
}
|
||||
@@ -183,6 +187,11 @@ func New(
|
||||
|
||||
slog.Debug("Loaded config", "config", configInfo)
|
||||
|
||||
customCommands, err := httpClient.Command.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
app := &App{
|
||||
Info: appInfo,
|
||||
Agents: agents,
|
||||
@@ -194,7 +203,7 @@ func New(
|
||||
AgentIndex: agentIndex,
|
||||
Session: &opencode.Session{},
|
||||
Messages: []Message{},
|
||||
Commands: commands.LoadFromConfig(configInfo),
|
||||
Commands: commands.LoadFromConfig(configInfo, *customCommands),
|
||||
InitialModel: initialModel,
|
||||
InitialPrompt: initialPrompt,
|
||||
InitialAgent: initialAgent,
|
||||
@@ -207,6 +216,9 @@ func New(
|
||||
|
||||
func (a *App) Keybind(commandName commands.CommandName) string {
|
||||
command := a.Commands[commandName]
|
||||
if len(command.Keybindings) == 0 {
|
||||
return ""
|
||||
}
|
||||
kb := command.Keybindings[0]
|
||||
key := kb.Key
|
||||
if kb.RequiresLeader {
|
||||
@@ -790,6 +802,40 @@ func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) {
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (a *App) SendCommand(ctx context.Context, command string, args string) (*App, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
if a.Session.ID == "" {
|
||||
session, err := a.CreateSession(ctx)
|
||||
if err != nil {
|
||||
return a, toast.NewErrorToast(err.Error())
|
||||
}
|
||||
a.Session = session
|
||||
cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
|
||||
}
|
||||
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
_, err := a.Client.Session.Command(
|
||||
context.Background(),
|
||||
a.Session.ID,
|
||||
opencode.SessionCommandParams{
|
||||
Command: opencode.F(command),
|
||||
Arguments: opencode.F(args),
|
||||
Agent: opencode.F(a.Agents[a.AgentIndex].Name),
|
||||
Model: opencode.F(a.State.Provider + "/" + a.State.Model),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to execute command", "error", err)
|
||||
return toast.NewErrorToast("Failed to execute command")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// The actual response will come through SSE
|
||||
// For now, just return success
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (a *App) SendShell(ctx context.Context, command string) (*App, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
if a.Session.ID == "" {
|
||||
|
||||
@@ -31,6 +31,7 @@ type Command struct {
|
||||
Description string
|
||||
Keybindings []Keybinding
|
||||
Trigger []string
|
||||
Custom bool
|
||||
}
|
||||
|
||||
func (c Command) Keys() []string {
|
||||
@@ -91,11 +92,18 @@ func (r CommandRegistry) Sorted() []Command {
|
||||
if b.Name == AppExitCommand {
|
||||
return -1
|
||||
}
|
||||
if a.Custom && !b.Custom {
|
||||
return 1
|
||||
}
|
||||
if !a.Custom && b.Custom {
|
||||
return -1
|
||||
}
|
||||
|
||||
return strings.Compare(string(a.Name), string(b.Name))
|
||||
})
|
||||
return commands
|
||||
}
|
||||
|
||||
func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command {
|
||||
var matched []Command
|
||||
for _, command := range r.Sorted() {
|
||||
@@ -182,7 +190,7 @@ func parseBindings(bindings ...string) []Keybinding {
|
||||
return parsedBindings
|
||||
}
|
||||
|
||||
func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
func LoadFromConfig(config *opencode.Config, customCommands []opencode.Command) CommandRegistry {
|
||||
defaults := []Command{
|
||||
{
|
||||
Name: AppHelpCommand,
|
||||
@@ -400,6 +408,16 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
}
|
||||
registry[command.Name] = command
|
||||
}
|
||||
for _, command := range customCommands {
|
||||
registry[CommandName(command.Name)] = Command{
|
||||
Name: CommandName(command.Name),
|
||||
Description: command.Description,
|
||||
Trigger: []string{command.Name},
|
||||
Keybindings: []Keybinding{},
|
||||
Custom: true,
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("Loaded commands", "commands", registry)
|
||||
return registry
|
||||
}
|
||||
|
||||
@@ -224,10 +224,17 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case dialog.CompletionSelectedMsg:
|
||||
switch msg.Item.ProviderID {
|
||||
case "commands":
|
||||
commandName := strings.TrimPrefix(msg.Item.Value, "/")
|
||||
command := msg.Item.RawData.(commands.Command)
|
||||
if command.Custom {
|
||||
m.SetValue("/" + command.PrimaryTrigger() + " ")
|
||||
return m, nil
|
||||
}
|
||||
|
||||
updated, cmd := m.Clear()
|
||||
m = updated.(*editorComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
commandName := strings.TrimPrefix(msg.Item.Value, "/")
|
||||
cmds = append(cmds, util.CmdHandler(commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)])))
|
||||
return m, tea.Batch(cmds...)
|
||||
case "files":
|
||||
@@ -382,11 +389,9 @@ func (m *editorComponent) Content() string {
|
||||
status = "waiting for permission"
|
||||
}
|
||||
if m.interruptKeyInDebounce && m.app.CurrentPermission.ID == "" {
|
||||
bright := t.Accent()
|
||||
if status == "waiting for permission" {
|
||||
bright = t.Warning()
|
||||
}
|
||||
hint = util.Shimmer(status, t.Background(), t.TextMuted(), bright) + m.spinner.View() + muted(
|
||||
hint = muted(
|
||||
status,
|
||||
) + m.spinner.View() + muted(
|
||||
" ",
|
||||
) + base(
|
||||
keyText+" again",
|
||||
@@ -394,11 +399,7 @@ func (m *editorComponent) Content() string {
|
||||
" interrupt",
|
||||
)
|
||||
} else {
|
||||
bright := t.Accent()
|
||||
if status == "waiting for permission" {
|
||||
bright = t.Warning()
|
||||
}
|
||||
hint = util.Shimmer(status, t.Background(), t.TextMuted(), bright) + m.spinner.View()
|
||||
hint = muted(status) + m.spinner.View()
|
||||
if m.app.CurrentPermission.ID == "" {
|
||||
hint += muted(" ") + base(keyText) + muted(" interrupt")
|
||||
}
|
||||
@@ -487,6 +488,25 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
var cmds []tea.Cmd
|
||||
if strings.HasPrefix(value, "/") {
|
||||
value = value[1:]
|
||||
commandName := strings.Split(value, " ")[0]
|
||||
command := m.app.Commands[commands.CommandName(commandName)]
|
||||
if command.Custom {
|
||||
args := strings.TrimPrefix(value, command.PrimaryTrigger()+" ")
|
||||
cmds = append(
|
||||
cmds,
|
||||
util.CmdHandler(app.SendCommand{Command: string(command.Name), Args: args}),
|
||||
)
|
||||
|
||||
updated, cmd := m.Clear()
|
||||
m = updated.(*editorComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
attachments := m.textarea.GetAttachments()
|
||||
|
||||
prompt := app.Prompt{Text: value, Attachments: attachments}
|
||||
|
||||
@@ -213,6 +213,7 @@ func renderText(
|
||||
extra string,
|
||||
isThinking bool,
|
||||
isQueued bool,
|
||||
shimmer bool,
|
||||
fileParts []opencode.FilePart,
|
||||
agentParts []opencode.AgentPart,
|
||||
toolCalls ...opencode.ToolPart,
|
||||
@@ -234,7 +235,12 @@ func renderText(
|
||||
}
|
||||
content = util.ToMarkdown(text, width, backgroundColor)
|
||||
if isThinking {
|
||||
label := util.Shimmer("Thinking...", backgroundColor, t.TextMuted(), t.Accent())
|
||||
var label string
|
||||
if shimmer {
|
||||
label = util.Shimmer("Thinking...", backgroundColor, t.TextMuted(), t.Accent())
|
||||
} else {
|
||||
label = styles.NewStyle().Background(backgroundColor).Foreground(t.TextMuted()).Render("Thinking...")
|
||||
}
|
||||
label = styles.NewStyle().Background(backgroundColor).Width(width - 6).Render(label)
|
||||
content = label + "\n\n" + content
|
||||
} else if strings.TrimSpace(text) == "Generating..." {
|
||||
@@ -665,10 +671,22 @@ func renderToolDetails(
|
||||
body = strings.Join(steps, "\n")
|
||||
|
||||
body += "\n\n"
|
||||
body += baseStyle(app.Keybind(commands.SessionChildCycleCommand)) +
|
||||
mutedStyle(", ") +
|
||||
baseStyle(app.Keybind(commands.SessionChildCycleReverseCommand)) +
|
||||
mutedStyle(" navigate child sessions")
|
||||
|
||||
// Build navigation hint with proper spacing
|
||||
cycleKeybind := app.Keybind(commands.SessionChildCycleCommand)
|
||||
cycleReverseKeybind := app.Keybind(commands.SessionChildCycleReverseCommand)
|
||||
|
||||
var navParts []string
|
||||
if cycleKeybind != "" {
|
||||
navParts = append(navParts, baseStyle(cycleKeybind))
|
||||
}
|
||||
if cycleReverseKeybind != "" {
|
||||
navParts = append(navParts, baseStyle(cycleReverseKeybind))
|
||||
}
|
||||
|
||||
if len(navParts) > 0 {
|
||||
body += strings.Join(navParts, mutedStyle(", ")) + mutedStyle(" navigate child sessions")
|
||||
}
|
||||
}
|
||||
body = defaultStyle(body)
|
||||
default:
|
||||
@@ -903,7 +921,9 @@ func renderArgs(args *map[string]any, titleKey string) string {
|
||||
continue
|
||||
}
|
||||
if key == "filePath" || key == "path" {
|
||||
value = util.Relative(value.(string))
|
||||
if strValue, ok := value.(string); ok {
|
||||
value = util.Relative(strValue)
|
||||
}
|
||||
}
|
||||
if key == titleKey {
|
||||
title = fmt.Sprintf("%s", value)
|
||||
|
||||
@@ -174,6 +174,10 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.viewport.GotoBottom()
|
||||
m.tail = true
|
||||
return m, nil
|
||||
case app.SendCommand:
|
||||
m.viewport.GotoBottom()
|
||||
m.tail = true
|
||||
return m, nil
|
||||
case dialog.ThemeSelectedMsg:
|
||||
m.cache.Clear()
|
||||
m.loading = true
|
||||
@@ -336,6 +340,25 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||
|
||||
width := m.width // always use full width
|
||||
|
||||
// Find the last streaming ReasoningPart to only shimmer that one
|
||||
lastStreamingReasoningID := ""
|
||||
if m.showThinkingBlocks {
|
||||
for mi := len(m.app.Messages) - 1; mi >= 0 && lastStreamingReasoningID == ""; mi-- {
|
||||
if _, ok := m.app.Messages[mi].Info.(opencode.AssistantMessage); !ok {
|
||||
continue
|
||||
}
|
||||
parts := m.app.Messages[mi].Parts
|
||||
for pi := len(parts) - 1; pi >= 0; pi-- {
|
||||
if rp, ok := parts[pi].(opencode.ReasoningPart); ok {
|
||||
if strings.TrimSpace(rp.Text) != "" && rp.Time.End == 0 {
|
||||
lastStreamingReasoningID = rp.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reverted := false
|
||||
revertedMessageCount := 0
|
||||
revertedToolCount := 0
|
||||
@@ -437,6 +460,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||
files,
|
||||
false,
|
||||
isQueued,
|
||||
false,
|
||||
fileParts,
|
||||
agentParts,
|
||||
)
|
||||
@@ -513,6 +537,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||
"",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
[]opencode.FilePart{},
|
||||
[]opencode.AgentPart{},
|
||||
toolCallParts...,
|
||||
@@ -530,6 +555,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||
"",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
[]opencode.FilePart{},
|
||||
[]opencode.AgentPart{},
|
||||
toolCallParts...,
|
||||
@@ -600,6 +626,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||
}
|
||||
if part.Text != "" {
|
||||
text := part.Text
|
||||
shimmer := part.Time.End == 0 && part.ID == lastStreamingReasoningID
|
||||
content = renderText(
|
||||
m.app,
|
||||
message.Info,
|
||||
@@ -610,6 +637,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||
"",
|
||||
true,
|
||||
false,
|
||||
shimmer,
|
||||
[]opencode.FilePart{},
|
||||
[]opencode.AgentPart{},
|
||||
)
|
||||
@@ -644,6 +672,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||
"",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
[]opencode.FilePart{},
|
||||
[]opencode.AgentPart{},
|
||||
)
|
||||
@@ -746,16 +775,18 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||
} else {
|
||||
for _, part := range response.Parts {
|
||||
if part.CallID == m.app.CurrentPermission.CallID {
|
||||
content := renderToolDetails(
|
||||
m.app,
|
||||
part.AsUnion().(opencode.ToolPart),
|
||||
m.app.CurrentPermission,
|
||||
width,
|
||||
)
|
||||
if content != "" {
|
||||
partCount++
|
||||
lineCount += lipgloss.Height(content) + 1
|
||||
blocks = append(blocks, content)
|
||||
if toolPart, ok := part.AsUnion().(opencode.ToolPart); ok {
|
||||
content := renderToolDetails(
|
||||
m.app,
|
||||
toolPart,
|
||||
m.app.CurrentPermission,
|
||||
width,
|
||||
)
|
||||
if content != "" {
|
||||
partCount++
|
||||
lineCount += lipgloss.Height(content) + 1
|
||||
blocks = append(blocks, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,6 +408,24 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
a.app, cmd = a.app.SendPrompt(context.Background(), msg)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
case app.SendCommand:
|
||||
// If we're in a child session, switch back to parent before sending prompt
|
||||
if a.app.Session.ParentID != "" {
|
||||
parentSession, err := a.app.Client.Session.Get(context.Background(), a.app.Session.ParentID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get parent session", "error", err)
|
||||
return a, toast.NewErrorToast("Failed to get parent session")
|
||||
}
|
||||
a.app.Session = parentSession
|
||||
a.app, cmd = a.app.SendCommand(context.Background(), msg.Command, msg.Args)
|
||||
cmds = append(cmds, tea.Sequence(
|
||||
util.CmdHandler(app.SessionSelectedMsg(parentSession)),
|
||||
cmd,
|
||||
))
|
||||
} else {
|
||||
a.app, cmd = a.app.SendCommand(context.Background(), msg.Command, msg.Args)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
case app.SendShell:
|
||||
// If we're in a child session, switch back to parent before sending prompt
|
||||
if a.app.Session.ParentID != "" {
|
||||
@@ -897,9 +915,10 @@ func (a Model) Cleanup() {
|
||||
func (a Model) home() (string, int, int) {
|
||||
t := theme.CurrentTheme()
|
||||
effectiveWidth := a.width - 4
|
||||
baseStyle := styles.NewStyle().Background(t.Background())
|
||||
baseStyle := styles.NewStyle().Foreground(t.Text()).Background(t.Background())
|
||||
base := baseStyle.Render
|
||||
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
|
||||
highlight := styles.NewStyle().Foreground(t.Accent()).Background(t.Background()).Render
|
||||
|
||||
open := `
|
||||
█▀▀█ █▀▀█ █▀▀ █▀▀▄
|
||||
@@ -934,9 +953,9 @@ func (a Model) home() (string, int, int) {
|
||||
)
|
||||
|
||||
// Use limit of 4 for vscode, 6 for others
|
||||
limit := 6
|
||||
limit := 4
|
||||
if util.IsVSCode() {
|
||||
limit = 4
|
||||
limit = 2
|
||||
}
|
||||
|
||||
showVscode := util.IsVSCode()
|
||||
@@ -953,15 +972,23 @@ func (a Model) home() (string, int, int) {
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
|
||||
grok := highlight("Grok Code is free for a limited time")
|
||||
grok = lipgloss.PlaceHorizontal(
|
||||
effectiveWidth,
|
||||
lipgloss.Center,
|
||||
grok,
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
|
||||
lines := []string{}
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, logoAndVersion)
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, cmds)
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, grok)
|
||||
lines = append(lines, "")
|
||||
|
||||
mainHeight := lipgloss.Height(strings.Join(lines, "\n"))
|
||||
|
||||
|
||||
@@ -2,12 +2,31 @@ package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
opencode "github.com/sst/opencode-sdk-go"
|
||||
)
|
||||
|
||||
func sanitizeValue(val any) any {
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err, ok := val.(error); ok {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(val)
|
||||
if v.Kind() == reflect.Interface && !v.IsNil() {
|
||||
return fmt.Sprintf("%T", val)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
type APILogHandler struct {
|
||||
client *opencode.Client
|
||||
service string
|
||||
@@ -67,21 +86,13 @@ func (h *APILogHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||
h.mu.Lock()
|
||||
for _, attr := range h.attrs {
|
||||
val := attr.Value.Any()
|
||||
if err, ok := val.(error); ok {
|
||||
extra[attr.Key] = err.Error()
|
||||
} else {
|
||||
extra[attr.Key] = val
|
||||
}
|
||||
extra[attr.Key] = sanitizeValue(val)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
r.Attrs(func(attr slog.Attr) bool {
|
||||
val := attr.Value.Any()
|
||||
if err, ok := val.(error); ok {
|
||||
extra[attr.Key] = err.Error()
|
||||
} else {
|
||||
extra[attr.Key] = val
|
||||
}
|
||||
extra[attr.Key] = sanitizeValue(val)
|
||||
return true
|
||||
})
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ import { rehypeHeadingIds } from "@astrojs/markdown-remark"
|
||||
import rehypeAutolinkHeadings from "rehype-autolink-headings"
|
||||
import { spawnSync } from "child_process"
|
||||
|
||||
const github = "https://github.com/sst/opencode"
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: config.url,
|
||||
@@ -49,7 +47,7 @@ export default defineConfig({
|
||||
},
|
||||
],
|
||||
editLink: {
|
||||
baseUrl: `${github}/edit/dev/packages/web/`,
|
||||
baseUrl: `${config.github}/edit/dev/packages/web/`,
|
||||
},
|
||||
markdown: {
|
||||
headingLinks: false,
|
||||
@@ -69,7 +67,7 @@ export default defineConfig({
|
||||
|
||||
{
|
||||
label: "Usage",
|
||||
items: ["docs/tui", "docs/cli", "docs/ide", "docs/share", "docs/github"],
|
||||
items: ["docs/tui", "docs/cli", "docs/ide", "docs/share", "docs/github", "docs/gitlab"],
|
||||
},
|
||||
|
||||
{
|
||||
@@ -79,14 +77,19 @@ export default defineConfig({
|
||||
"docs/agents",
|
||||
"docs/models",
|
||||
"docs/themes",
|
||||
"docs/plugins",
|
||||
"docs/keybinds",
|
||||
"docs/commands",
|
||||
"docs/formatters",
|
||||
"docs/permissions",
|
||||
"docs/lsp",
|
||||
"docs/mcp-servers",
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
label: "Develop",
|
||||
items: ["docs/sdk", "docs/server", "docs/plugins"],
|
||||
},
|
||||
],
|
||||
components: {
|
||||
Hero: "./src/components/Hero.astro",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode/web",
|
||||
"type": "module",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.28",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "sst shell --stage=dev --target=Web astro dev",
|
||||
@@ -31,7 +31,7 @@
|
||||
"sharp": "0.32.5",
|
||||
"shiki": "3.4.2",
|
||||
"solid-js": "1.9.7",
|
||||
"toolbeam-docs-theme": "0.4.3"
|
||||
"toolbeam-docs-theme": "0.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"opencode": "workspace:*",
|
||||
|
||||
@@ -58,12 +58,11 @@ Build is the **default** primary agent with all tools enabled. This is the stand
|
||||
|
||||
_Mode_: `primary`
|
||||
|
||||
A restricted agent designed for planning and analysis. In the plan agent, the following tools are disabled by default:
|
||||
A restricted agent designed for planning and analysis. We use a permission system to give you more control and prevent unintended changes.
|
||||
By default, all of the following are set to `ask`:
|
||||
|
||||
- `write` - Cannot create new files
|
||||
- `edit` - Cannot modify existing files
|
||||
- `patch` - Cannot apply patches
|
||||
- `bash` - Cannot execute shell commands
|
||||
- `file edits`: All writes, patches, and edits
|
||||
- `bash`: All bash commands
|
||||
|
||||
This agent is useful when you want the LLM to analyze code, suggest changes, or create plans without making any actual modifications to your codebase.
|
||||
|
||||
@@ -533,7 +532,7 @@ For example, with OpenAI's reasoning models, you can control the reasoning effor
|
||||
"agent": {
|
||||
"deep-thinker": {
|
||||
"description": "Agent that uses high reasoning effort for complex problems",
|
||||
"model": "openai/gpt-5-turbo",
|
||||
"model": "openai/gpt-5",
|
||||
"reasoningEffort": "high",
|
||||
"textVerbosity": "low"
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ opencode run Explain the use of context in Go
|
||||
|
||||
### serve
|
||||
|
||||
Start a headless opencode server for API access.
|
||||
Start a headless opencode server for API access. Check out the [server docs](/docs/server) for the full HTTP interface.
|
||||
|
||||
```bash
|
||||
opencode serve
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
---
|
||||
title: Commands
|
||||
description: Create custom commands for repetitive tasks.
|
||||
---
|
||||
|
||||
Custom commands let you specify a prompt you want to run when that command is executed in the TUI.
|
||||
|
||||
```bash frame="none"
|
||||
/my-command
|
||||
```
|
||||
|
||||
Custom commands are in addition to the built-in commands like `/init`, `/undo`, `/redo`, `/share`, `/help`. [Learn more](/docs/tui#commands).
|
||||
|
||||
---
|
||||
|
||||
## Create command files
|
||||
|
||||
Create markdown files in the `command/` directory to define custom commands.
|
||||
|
||||
Create `.opencode/command/test.md`:
|
||||
|
||||
```md title=".opencode/command/test.md"
|
||||
---
|
||||
description: Run tests with coverage
|
||||
agent: build
|
||||
model: anthropic/claude-3-5-sonnet-20241022
|
||||
---
|
||||
|
||||
Run the full test suite with coverage report and show any failures.
|
||||
Focus on the failing tests and suggest fixes.
|
||||
```
|
||||
|
||||
The frontmatter defines command properties. The content becomes the template.
|
||||
|
||||
Use the command by typing `/` followed by the command name.
|
||||
|
||||
```bash frame="none"
|
||||
"/test"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configure
|
||||
|
||||
You can add custom commands through the opencode config or by creating markdown files in the `command/` directory.
|
||||
|
||||
---
|
||||
|
||||
### JSON
|
||||
|
||||
Use the `command` option in your opencode [config](/docs/config):
|
||||
|
||||
```json title="opencode.jsonc" {4-12}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"command": {
|
||||
// This becomes the name of the command
|
||||
"test": {
|
||||
// This is the prompt that will be sent to the LLM
|
||||
"template": "Run the full test suite with coverage report and show any failures.\nFocus on the failing tests and suggest fixes.",
|
||||
// This is show as the description in the TUI
|
||||
"description": "Run tests with coverage",
|
||||
"agent": "build",
|
||||
"model": "anthropic/claude-3-5-sonnet-20241022"
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now you can run this command in the TUI:
|
||||
|
||||
```bash frame="none"
|
||||
/test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Markdown
|
||||
|
||||
You can also define commands using markdown files. Place them in:
|
||||
|
||||
- Global: `~/.config/opencode/command/`
|
||||
- Per-project: `.opencode/command/`
|
||||
|
||||
```markdown title="~/.config/opencode/command/test.md"
|
||||
---
|
||||
description: Run tests with coverage
|
||||
agent: build
|
||||
model: anthropic/claude-3-5-sonnet-20241022
|
||||
---
|
||||
|
||||
Run the full test suite with coverage report and show any failures.
|
||||
Focus on the failing tests and suggest fixes.
|
||||
```
|
||||
|
||||
The markdown file name becomes the command name. For example, `test.md` lets
|
||||
you run:
|
||||
|
||||
```bash frame="none"
|
||||
/test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prompt config
|
||||
|
||||
The prompts for the custom commands support several special placeholders and syntax.
|
||||
|
||||
---
|
||||
|
||||
### Arguments
|
||||
|
||||
Pass arguments to commands using the `$ARGUMENTS` placeholder.
|
||||
|
||||
```md title=".opencode/command/component.md"
|
||||
---
|
||||
description: Create a new component
|
||||
---
|
||||
|
||||
Create a new React component named $ARGUMENTS with TypeScript support.
|
||||
Include proper typing and basic structure.
|
||||
```
|
||||
|
||||
Run the command with arguments:
|
||||
|
||||
```bash frame="none"
|
||||
/component Button
|
||||
```
|
||||
|
||||
And `$ARGUMENTS` will be replaced with `Button`.
|
||||
|
||||
---
|
||||
|
||||
### Shell output
|
||||
|
||||
Use _!`command`_ to inject [bash command](/docs/tui#bash-commands) output into your prompt.
|
||||
|
||||
For example, to create a custom command that analyzes test coverage:
|
||||
|
||||
```md title=".opencode/command/analyze-coverage.md"
|
||||
---
|
||||
description: Analyze test coverage
|
||||
---
|
||||
|
||||
Here are the current test results:
|
||||
!`npm test`
|
||||
|
||||
Based on these results, suggest improvements to increase coverage.
|
||||
```
|
||||
|
||||
Or to review recent changes:
|
||||
|
||||
```md title=".opencode/command/review-changes.md"
|
||||
---
|
||||
description: Review recent changes
|
||||
---
|
||||
|
||||
Recent git commits:
|
||||
!`git log --oneline -10`
|
||||
|
||||
Review these changes and suggest any improvements.
|
||||
```
|
||||
|
||||
Commands run in your project's root directory and their output becomes part of the prompt.
|
||||
|
||||
---
|
||||
|
||||
### File references
|
||||
|
||||
Include files in your command using `@` followed by the filename.
|
||||
|
||||
```md title=".opencode/command/review-component.md"
|
||||
---
|
||||
description: Review component
|
||||
---
|
||||
|
||||
Review the component in @src/components/Button.tsx.
|
||||
Check for performance issues and suggest improvements.
|
||||
```
|
||||
|
||||
The file content gets included in the prompt automatically.
|
||||
|
||||
---
|
||||
|
||||
## Options
|
||||
|
||||
Let's look at the configuration options in detail.
|
||||
|
||||
---
|
||||
|
||||
### Template
|
||||
|
||||
The `template` option defines the prompt that will be sent to the LLM when the command is executed.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"command": {
|
||||
"test": {
|
||||
"template": "Run the full test suite with coverage report and show any failures.\nFocus on the failing tests and suggest fixes."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is a **required** config option.
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Use the `description` option to provide a brief description of what the command does.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"command": {
|
||||
"test": {
|
||||
"description": "Run tests with coverage"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is shown as the description in the TUI when you type in the command.
|
||||
|
||||
---
|
||||
|
||||
### Agent
|
||||
|
||||
Use the `agent` config to optionally specify which [agent](/docs/agents) should execute this command.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"command": {
|
||||
"review": {
|
||||
"agent": "plan"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is an **optional** config option. If not specified, defaults to "build".
|
||||
|
||||
---
|
||||
|
||||
### Model
|
||||
|
||||
Use the `model` config to override the default model for this command.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"command": {
|
||||
"analyze": {
|
||||
"model": "anthropic/claude-3-5-sonnet-20241022"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is an **optional** config option.
|
||||
|
||||
---
|
||||
|
||||
## Built-in
|
||||
|
||||
opencode includes several built-in commands like `/init`, `/undo`, `/redo`, `/share`, `/help`; [learn more](/docs/tui#commands).
|
||||
|
||||
:::note
|
||||
Custom commands can override built-in commands.
|
||||
:::
|
||||
|
||||
If you define a custom command with the same name, it will override the built-in command.
|
||||
@@ -152,6 +152,32 @@ By default, sharing is set to manual mode where you need to explicitly share con
|
||||
|
||||
---
|
||||
|
||||
### Commands
|
||||
|
||||
You can configure custom commands for repetitive tasks through the `command` option.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"command": {
|
||||
"test": {
|
||||
"template": "Run the full test suite with coverage report and show any failures.\nFocus on the failing tests and suggest fixes.",
|
||||
"description": "Run tests with coverage",
|
||||
"agent": "build",
|
||||
"model": "anthropic/claude-3-5-sonnet-20241022"
|
||||
},
|
||||
"component": {
|
||||
"template": "Create a new React component named $ARGUMENTS with TypeScript support.\nInclude proper typing and basic structure.",
|
||||
"description": "Create a new component"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also define commands using markdown files in `~/.config/opencode/command/` or `.opencode/command/`. [Learn more here](/docs/commands).
|
||||
|
||||
---
|
||||
|
||||
### Keybinds
|
||||
|
||||
You can customize your keybinds through the `keybinds` option.
|
||||
@@ -220,6 +246,11 @@ You can configure permissions to control what AI agents can do in your codebase
|
||||
}
|
||||
```
|
||||
|
||||
This allows you to configure explicit approval requirements for sensitive operations:
|
||||
|
||||
- `edit` - Controls whether file editing operations require user approval (`"ask"` or `"allow"`)
|
||||
- `bash` - Controls whether bash commands require user approval (can be `"ask"`/`"allow"` or a pattern map)
|
||||
|
||||
[Learn more about permissions here](/docs/permissions).
|
||||
|
||||
---
|
||||
@@ -259,13 +290,6 @@ about rules here](/docs/rules).
|
||||
|
||||
You can disable providers that are loaded automatically through the `disabled_providers` option. This is useful when you want to prevent certain providers from being loaded even if their credentials are available.
|
||||
|
||||
The `disabled_providers` option accepts an array of provider IDs. When a provider is disabled:
|
||||
|
||||
- It won't be loaded even if environment variables are set
|
||||
- It won't be loaded even if API keys are configured through `opencode auth login`
|
||||
- The provider's models won't appear in the model selection list
|
||||
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
@@ -273,12 +297,11 @@ The `disabled_providers` option accepts an array of provider IDs. When a provide
|
||||
}
|
||||
```
|
||||
|
||||
The permissions system allows you to configure explicit approval requirements for sensitive operations:
|
||||
The `disabled_providers` option accepts an array of provider IDs. When a provider is disabled:
|
||||
|
||||
- `edit` - Controls whether file editing operations require user approval (`"ask"` or `"allow"`)
|
||||
- `bash` - Controls whether bash commands require user approval (can be `"ask"`/`"allow"` or a pattern map)
|
||||
|
||||
[Learn more about permissions here](/docs/permissions).
|
||||
- It won't be loaded even if environment variables are set.
|
||||
- It won't be loaded even if API keys are configured through `opencode auth login`.
|
||||
- The provider's models won't appear in the model selection list.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ you to use opencode at your organization.
|
||||
To get started, we recommend:
|
||||
|
||||
1. Do a trial internally with your team.
|
||||
2. [**Contact us**](mailto:hello@sst.dev) to discuss pricing and implementation options.
|
||||
2. [**Contact us**](mailto:hello@anoma.ly) to discuss pricing and implementation options.
|
||||
|
||||
---
|
||||
|
||||
@@ -55,7 +55,7 @@ We recommend you disable this for your trial.
|
||||
## Deployment
|
||||
|
||||
Once you have completed your trial and you are ready to self-host opencode at
|
||||
your organization, you can [**contact us**](mailto:hello@sst.dev) to discuss
|
||||
your organization, you can [**contact us**](mailto:hello@anoma.ly) to discuss
|
||||
pricing and implementation options.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: GitHub
|
||||
description: Use opencode in GitHub issues and pull-requests
|
||||
description: Use opencode in GitHub issues and pull-requests.
|
||||
---
|
||||
|
||||
opencode integrates with your GitHub workflow. Mention `/opencode` or `/oc` in your comment, and opencode will execute tasks within your GitHub Actions runner.
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: GitLab
|
||||
description: Use opencode in GitLab issues and merge requests.
|
||||
---
|
||||
|
||||
opencode integrates with your GitLab workflow.
|
||||
Mention `@opencode` in a comment, and opencode will execute tasks within your GitLab CI pipeline.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Triage issues**: Ask opencode to look into an issue and explain it to you.
|
||||
- **Fix and implement**: Ask opencode to fix an issue or implement a feature.
|
||||
It will work create a new branch and raised a merge request with the changes.
|
||||
- **Secure**: opencode runs on your GitLab runners.
|
||||
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
opencode runs in your GitLab CI/CD pipeline, here's what you'll need to set it up:
|
||||
|
||||
:::tip
|
||||
Check out the [**GitLab docs**](https://docs.gitlab.com/user/duo_agent_platform/agent_assistant/) for up to date instructions.
|
||||
:::
|
||||
|
||||
1. Configure your GitLab environment
|
||||
2. Set up CI/CD
|
||||
3. Get an AI model provider API key
|
||||
4. Create a service account
|
||||
5. Configure CI/CD variables
|
||||
6. Create a flow config file, here's an example:
|
||||
|
||||
<details>
|
||||
<summary>Flow configuration</summary>
|
||||
|
||||
```yaml
|
||||
image: node:22-slim
|
||||
commands:
|
||||
- echo "Installing opencode"
|
||||
- npm install --global opencode-ai
|
||||
- echo "Installing glab"
|
||||
- export GITLAB_TOKEN=$GITLAB_TOKEN_OPENCODE
|
||||
- apt-get update --quiet && apt-get install --yes curl wget gpg git && rm --recursive --force /var/lib/apt/lists/*
|
||||
- curl --silent --show-error --location "https://raw.githubusercontent.com/upciti/wakemeops/main/assets/install_repository" | bash
|
||||
- apt-get install --yes glab
|
||||
- echo "Configuring glab"
|
||||
- echo $GITLAB_HOST
|
||||
- echo "Creating opencode auth configuration"
|
||||
- mkdir --parents ~/.local/share/opencode
|
||||
- |
|
||||
cat > ~/.local/share/opencode/auth.json << EOF
|
||||
{
|
||||
"anthropic": {
|
||||
"type": "api",
|
||||
"key": "$ANTHROPIC_API_KEY"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
- echo "Configuring git"
|
||||
- git config --global user.email "opencode@gitlab.com"
|
||||
- git config --global user.name "Opencode"
|
||||
- echo "Testing glab"
|
||||
- glab issue list
|
||||
- echo "Running Opencode"
|
||||
- |
|
||||
opencode run "
|
||||
You are an AI assistant helping with GitLab operations.
|
||||
|
||||
Context: $AI_FLOW_CONTEXT
|
||||
Task: $AI_FLOW_INPUT
|
||||
Event: $AI_FLOW_EVENT
|
||||
|
||||
Please execute the requested task using the available GitLab tools.
|
||||
Be thorough in your analysis and provide clear explanations.
|
||||
|
||||
<important>
|
||||
Please use the glab CLI to access data from GitLab. The glab CLI has already been authenticated. You can run the corresponding commands.
|
||||
|
||||
If you are asked to summarise an MR or issue or asked to provide more information then please post back a note to the MR/Issue so that the user can see it.
|
||||
You don't need to commit or push up changes, those will be done automatically based on the file changes you make.
|
||||
</important>
|
||||
"
|
||||
- git checkout --branch $CI_WORKLOAD_REF origin/$CI_WORKLOAD_REF
|
||||
- echo "Checking for git changes and pushing if any exist"
|
||||
- |
|
||||
if ! git diff --quiet || ! git diff --cached --quiet || [ --not --zero "$(git ls-files --others --exclude-standard)" ]; then
|
||||
echo "Git changes detected, adding and pushing..."
|
||||
git add .
|
||||
if git diff --cached --quiet; then
|
||||
echo "No staged changes to commit"
|
||||
else
|
||||
echo "Committing changes to branch: $CI_WORKLOAD_REF"
|
||||
git commit --message "Codex changes"
|
||||
echo "Pushing changes up to $CI_WORKLOAD_REF"
|
||||
git push https://gitlab-ci-token:$GITLAB_TOKEN@$GITLAB_HOST/gl-demo-ultimate-dev-ai-epic-17570/test-java-project.git $CI_WORKLOAD_REF
|
||||
echo "Changes successfully pushed"
|
||||
fi
|
||||
else
|
||||
echo "No git changes detected, skipping push"
|
||||
fi
|
||||
variables:
|
||||
- ANTHROPIC_API_KEY
|
||||
- GITLAB_TOKEN_OPENCODE
|
||||
- GITLAB_HOST
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
You can refer to the [GitLab CLI agents docs](https://docs.gitlab.com/user/duo_agent_platform/agent_assistant/) for detailed instructions.
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
Here are some examples of how you can use opencode in GitLab.
|
||||
|
||||
:::tip
|
||||
You can configure to use a different trigger phrase than `@opencode`.
|
||||
:::
|
||||
|
||||
- **Explain an issue**
|
||||
|
||||
Add this comment in a GitLab issue.
|
||||
|
||||
```
|
||||
@opencode explain this issue
|
||||
```
|
||||
|
||||
opencode will read the issue and reply with a clear explanation.
|
||||
|
||||
- **Fix an issue**
|
||||
|
||||
In a GitLab issue, say:
|
||||
|
||||
```
|
||||
@opencode fix this
|
||||
```
|
||||
|
||||
opencode will create a new branch, implement the changes, and open a merge request with the changes.
|
||||
|
||||
- **Review merge requests**
|
||||
|
||||
Leave the following comment on a GitLab merge request.
|
||||
|
||||
```
|
||||
@opencode review this merge request
|
||||
```
|
||||
|
||||
opencode will review the merge request and provide feedback.
|
||||
@@ -42,24 +42,24 @@ You can also install it with the following:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="npm">
|
||||
```bash
|
||||
npm install -g opencode-ai
|
||||
```
|
||||
```bash
|
||||
npm install -g opencode-ai
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Bun">
|
||||
```bash
|
||||
bun install -g opencode-ai
|
||||
```
|
||||
```bash
|
||||
bun install -g opencode-ai
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="pnpm">
|
||||
```bash
|
||||
pnpm install -g opencode-ai
|
||||
```
|
||||
```bash
|
||||
pnpm install -g opencode-ai
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Yarn">
|
||||
```bash
|
||||
yarn global add opencode-ai
|
||||
```
|
||||
```bash
|
||||
yarn global add opencode-ai
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
@@ -308,4 +308,4 @@ Here's an [example conversation](https://opencode.ai/s/4XP1fce5) with opencode.
|
||||
|
||||
And that's it! You are now a pro at using opencode.
|
||||
|
||||
To make it your own, we recommend [picking a theme](/docs/themes), [customizing the keybinds](/docs/keybinds), [configuring code formatters](/docs/formatters), or playing around with the [opencode config](/docs/config).
|
||||
To make it your own, we recommend [picking a theme](/docs/themes), [customizing the keybinds](/docs/keybinds), [configuring code formatters](/docs/formatters), [creating custom commands](/docs/commands), or playing around with the [opencode config](/docs/config).
|
||||
|
||||
@@ -20,7 +20,6 @@ opencode has a list of keybinds that you can customize through the opencode conf
|
||||
"session_export": "<leader>x",
|
||||
"session_new": "<leader>n",
|
||||
"session_list": "<leader>l",
|
||||
"session_navigation": "<leader>g",
|
||||
"session_share": "<leader>s",
|
||||
"session_unshare": "none",
|
||||
"session_interrupt": "esc",
|
||||
|
||||
@@ -3,7 +3,7 @@ title: LSP Servers
|
||||
description: opencode integrates with your LSP servers.
|
||||
---
|
||||
|
||||
opencode integrates with your Language Server Protocol (LSP) to help the LLM interacts with your codebase. It uses diagnostics to provide feebdack to the LLM. And _go-to-definition_ and _find-references_ to help navigate your codebase.
|
||||
opencode integrates with your Language Server Protocol (LSP) to help the LLM interacts with your codebase. It uses diagnostics to provide feedback to the LLM. And _go-to-definition_ and _find-references_ to help navigate your codebase.
|
||||
|
||||
---
|
||||
|
||||
@@ -11,19 +11,26 @@ opencode integrates with your Language Server Protocol (LSP) to help the LLM int
|
||||
|
||||
opencode comes with several built-in LSP servers for popular languages:
|
||||
|
||||
| LSP Server | Extensions | Requirements |
|
||||
| ---------- | -------------------------------------------- | ----------------------------------- |
|
||||
| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `typescript` dependency in project |
|
||||
| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `eslint` dependency in project |
|
||||
| gopls | .go | `go` command available |
|
||||
| ruby-lsp | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available |
|
||||
| pyright | .py, .pyi | `pyright` dependency installed |
|
||||
| elixir-ls | .ex, .exs | `elixir` command available |
|
||||
| zls | .zig, .zon | `zig` command available |
|
||||
| csharp | .cs | `.NET SDK` installed |
|
||||
| LSP Server | Extensions | Requirements |
|
||||
| ---------- | ---------------------------------------------------- | ----------------------------------- |
|
||||
| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `typescript` dependency in project |
|
||||
| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue | `eslint` dependency in project |
|
||||
| gopls | .go | `go` command available |
|
||||
| ruby-lsp | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available |
|
||||
| pyright | .py, .pyi | `pyright` dependency installed |
|
||||
| elixir-ls | .ex, .exs | `elixir` command available |
|
||||
| zls | .zig, .zon | `zig` command available |
|
||||
| csharp | .cs | `.NET SDK` installed |
|
||||
| vue | .vue | Auto-installs for Vue projects |
|
||||
| rust | .rs | `rust-analyzer` command available |
|
||||
| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects |
|
||||
|
||||
LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met.
|
||||
|
||||
:::note
|
||||
You can disable automatic LSP server downloads by setting the `OPENCODE_DISABLE_LSP_DOWNLOAD` environment variable to `true`.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -759,6 +759,23 @@ In this example:
|
||||
|
||||
---
|
||||
|
||||
### xAI
|
||||
|
||||
For a limited time, you can use xAI's Grok Code for free with opencode.
|
||||
|
||||
:::tip
|
||||
Grok Code is available for free for a limited time on opencode.
|
||||
:::
|
||||
|
||||
1. Make sure you are on the latest version of opencode.
|
||||
|
||||
2. Run the `/models` command and select **Grok Code Free**.
|
||||
|
||||
As a part of the trial period, the xAI team will be using the request logs to
|
||||
monitor and improve Grok Code.
|
||||
|
||||
---
|
||||
|
||||
### Z.AI
|
||||
|
||||
1. Head over to the [Z.AI API console](https://z.ai/manage-apikey/apikey-list), create an account, and click **Create a new API key**.
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
---
|
||||
title: SDK
|
||||
description: JS SDK for the opencode server.
|
||||
---
|
||||
|
||||
import config from "../../../../config.mjs"
|
||||
export const typesUrl = `${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts`
|
||||
|
||||
The opencode [JS/TS SDK](https://www.npmjs.com/package/@opencode-ai/sdk) provides a type-safe client for interacting with the opencode server. You can use it to build custom integrations and control opencode programmatically.
|
||||
|
||||
[Learn more](/docs/server) about how the opencode server works.
|
||||
|
||||
---
|
||||
|
||||
## Install
|
||||
|
||||
Install the SDK from npm:
|
||||
|
||||
```bash
|
||||
npm install @opencode-ai/sdk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Create client
|
||||
|
||||
Create a client instance to connect to your opencode server:
|
||||
|
||||
```javascript
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||
|
||||
const client = createOpencodeClient({
|
||||
baseUrl: "http://localhost:4096",
|
||||
})
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
| Option | Type | Description | Default |
|
||||
| --------------- | ---------- | --------------------------------- | ----------------------- |
|
||||
| `baseUrl` | `string` | URL of the opencode server | `http://localhost:4096` |
|
||||
| `fetch` | `function` | Custom fetch implementation | `globalThis.fetch` |
|
||||
| `parseAs` | `string` | Response parsing method | `auto` |
|
||||
| `responseStyle` | `string` | Return style: `data` or `fields` | `fields` |
|
||||
| `throwOnError` | `boolean` | Throw errors instead of returning | `false` |
|
||||
|
||||
---
|
||||
|
||||
## Start server
|
||||
|
||||
You can also programmatically start an opencode server:
|
||||
|
||||
````javascript
|
||||
import { createOpencodeServer } from "@opencode-ai/sdk"
|
||||
|
||||
const server = await createOpencodeServer({
|
||||
hostname: "127.0.0.1",
|
||||
port: 4096,
|
||||
})
|
||||
|
||||
console.log(`Server running at ${server.url}`)
|
||||
|
||||
server.close()
|
||||
}
|
||||
|
||||
#### Options
|
||||
|
||||
| Option | Type | Description | Default |
|
||||
| ---------- | -------------- | ------------------------------ | ------------- |
|
||||
| `hostname` | `string` | Server hostname | `127.0.0.1` |
|
||||
| `port` | `number` | Server port | `4096` |
|
||||
| `signal` | `AbortSignal` | Abort signal for cancellation | `undefined` |
|
||||
| `timeout` | `number` | Timeout in ms for server start | `5000` |
|
||||
|
||||
---
|
||||
|
||||
## Types
|
||||
|
||||
The SDK includes TypeScript definitions for all API types. Import them directly:
|
||||
|
||||
```typescript
|
||||
import type { Session, Message, Part } from "@opencode-ai/sdk"
|
||||
````
|
||||
|
||||
All types are generated from the server's OpenAPI specification and available in the <a href={typesUrl}>types file</a>.
|
||||
|
||||
---
|
||||
|
||||
## Errors
|
||||
|
||||
The SDK throws typed errors that you can catch and handle:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const session = await client.session.get({ id: "invalid-id" })
|
||||
} catch (error) {
|
||||
console.error("Failed to get session:", error.message)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## APIs
|
||||
|
||||
The SDK exposes all server APIs through a type-safe client interface.
|
||||
|
||||
---
|
||||
|
||||
### App
|
||||
|
||||
| Method | Description | Response |
|
||||
| ------------ | ------------------ | --------------------------------------- |
|
||||
| `app.get()` | Get app info | <a href={typesUrl}><code>App</code></a> |
|
||||
| `app.init()` | Initialize the app | `boolean` |
|
||||
|
||||
---
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
const app = await client.app.get()
|
||||
await client.app.init()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Config
|
||||
|
||||
| Method | Description | Response |
|
||||
| -------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| `config.get()` | Get config info | <a href={typesUrl}><code>Config</code></a> |
|
||||
| `config.providers()` | List providers and default models | `{ providers: `<a href={typesUrl}><code>Provider[]</code></a>`, default: { [key: string]: string } }` |
|
||||
|
||||
---
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
const config = await client.config.get()
|
||||
const { providers, default: defaults } = await client.config.providers()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Sessions
|
||||
|
||||
| Method | Description | Notes |
|
||||
| ------------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `session.list()` | List sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
|
||||
| `session.get({ id })` | Get session | Returns <a href={typesUrl}><code>Session</code></a> |
|
||||
| `session.children({ id })` | List child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
|
||||
| `session.create({ parentID?, title? })` | Create session | Returns <a href={typesUrl}><code>Session</code></a> |
|
||||
| `session.delete({ id })` | Delete session | Returns `boolean` |
|
||||
| `session.update({ id, title? })` | Update session properties | Returns <a href={typesUrl}><code>Session</code></a> |
|
||||
| `session.init({ id, messageID, providerID, modelID })` | Analyze app and create `AGENTS.md` | Returns `boolean` |
|
||||
| `session.abort({ id })` | Abort a running session | Returns `boolean` |
|
||||
| `session.share({ id })` | Share session | Returns <a href={typesUrl}><code>Session</code></a> |
|
||||
| `session.unshare({ id })` | Unshare session | Returns <a href={typesUrl}><code>Session</code></a> |
|
||||
| `session.summarize({ id, providerID, modelID })` | Summarize session | Returns `boolean` |
|
||||
| `session.messages({ id })` | List messages in a session | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}[]` |
|
||||
| `session.message({ id, messageID })` | Get message details | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
|
||||
| `session.chat({ id, ...chatInput })` | Send chat message | Returns <a href={typesUrl}><code>Message</code></a> |
|
||||
| `session.shell({ id, agent, command })` | Run a shell command | Returns <a href={typesUrl}><code>Message</code></a> |
|
||||
| `session.revert({ id, messageID, partID? })` | Revert a message | Returns <a href={typesUrl}><code>Session</code></a> |
|
||||
| `session.unrevert({ id })` | Restore reverted messages | Returns <a href={typesUrl}><code>Session</code></a> |
|
||||
| `session.permissions.respond({ id, permissionID, response })` | Respond to a permission request | Returns `boolean` |
|
||||
|
||||
---
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
// Create and manage sessions
|
||||
const session = await client.session.create({ title: "My session" })
|
||||
const sessions = await client.session.list()
|
||||
|
||||
// Send messages
|
||||
const message = await client.session.chat({
|
||||
id: session.id,
|
||||
providerID: "anthropic",
|
||||
modelID: "claude-3-5-sonnet-20241022",
|
||||
parts: [{ type: "text", text: "Hello!" }],
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Files
|
||||
|
||||
| Method | Description | Response |
|
||||
| ------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| `find.text({ pattern })` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
|
||||
| `find.files({ query })` | Find files by name | `string[]` (file paths) |
|
||||
| `find.symbols({ query })` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> |
|
||||
| `file.read({ path })` | Read a file | `{ type: "raw" \| "patch", content: string }` |
|
||||
| `file.status()` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> |
|
||||
|
||||
---
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
// Search and read files
|
||||
const textResults = await client.find.text({ pattern: "function.*opencode" })
|
||||
const files = await client.find.files({ query: "*.ts" })
|
||||
const content = await client.file.read({ path: "src/index.ts" })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Logging
|
||||
|
||||
| Method | Description | Response |
|
||||
| ------------------------------------------------ | --------------- | --------- |
|
||||
| `log.write({ service, level, message, extra? })` | Write log entry | `boolean` |
|
||||
|
||||
---
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
await client.log.write({
|
||||
service: "my-app",
|
||||
level: "info",
|
||||
message: "Operation completed",
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Agents
|
||||
|
||||
| Method | Description | Response |
|
||||
| -------------- | ------------------------- | ------------------------------------------- |
|
||||
| `agent.list()` | List all available agents | <a href={typesUrl}><code>Agent[]</code></a> |
|
||||
|
||||
---
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
const agents = await client.agent.list()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### TUI
|
||||
|
||||
| Method | Description | Response |
|
||||
| --------------------------------------------- | --------------------------------- | ---------------------- |
|
||||
| `tui.appendPrompt({ text })` | Append text to the prompt | `boolean` |
|
||||
| `tui.openHelp()` | Open the help dialog | `boolean` |
|
||||
| `tui.openSessions()` | Open the session selector | `boolean` |
|
||||
| `tui.openThemes()` | Open the theme selector | `boolean` |
|
||||
| `tui.openModels()` | Open the model selector | `boolean` |
|
||||
| `tui.submitPrompt()` | Submit the current prompt | `boolean` |
|
||||
| `tui.clearPrompt()` | Clear the prompt | `boolean` |
|
||||
| `tui.executeCommand({ command })` | Execute a command | `boolean` |
|
||||
| `tui.showToast({ title?, message, variant })` | Show toast notification | `boolean` |
|
||||
| `tui.control.next()` | Wait for the next control request | Control request object |
|
||||
| `tui.control.response({ body })` | Respond to a control request | `boolean` |
|
||||
|
||||
---
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
// Control TUI interface
|
||||
await client.tui.appendPrompt({ text: "Add this to prompt" })
|
||||
await client.tui.showToast({
|
||||
message: "Task completed",
|
||||
variant: "success",
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Auth
|
||||
|
||||
| Method | Description | Response |
|
||||
| ------------------------------- | ------------------------------ | --------- |
|
||||
| `auth.set({ id, ...authData })` | Set authentication credentials | `boolean` |
|
||||
|
||||
---
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
await client.auth.set({
|
||||
id: "anthropic",
|
||||
type: "api",
|
||||
key: "your-api-key",
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Events
|
||||
|
||||
| Method | Description | Response |
|
||||
| ------------------- | ------------------------- | ------------------------- |
|
||||
| `event.subscribe()` | Server-sent events stream | Server-sent events stream |
|
||||
|
||||
---
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
// Listen to real-time events
|
||||
const eventStream = await client.event.subscribe()
|
||||
for await (const event of eventStream) {
|
||||
console.log("Event:", event.type, event.properties)
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
title: Server
|
||||
description: Interact with opencode server over HTTP.
|
||||
---
|
||||
|
||||
import config from "../../../../config.mjs"
|
||||
export const typesUrl = `${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts`
|
||||
|
||||
The `opencode serve` command runs a headless HTTP server that exposes an OpenAPI endpoint that an opencode client can use.
|
||||
|
||||
---
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
opencode serve [--port <number>] [--hostname <string>]
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Short | Description | Default |
|
||||
| ------------ | ----- | --------------------- | ----------- |
|
||||
| `--port` | `-p` | Port to listen on | `4096` |
|
||||
| `--hostname` | `-h` | Hostname to listen on | `127.0.0.1` |
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
When you run `opencode` it starts a TUI and a server. Where the TUI is the
|
||||
client that talks to the server. The server exposes an OpenAPI 3.1 spec
|
||||
endpoint. This endpoint is also used to generate an [SDK](/docs/sdk).
|
||||
|
||||
:::tip
|
||||
Use the opencode server to interact with opencode programmatically.
|
||||
:::
|
||||
|
||||
This architecture lets opencode support multiple clients and allows you to interact with opencode programmatically.
|
||||
|
||||
You can run `opencode serve` to start a standalone server. If you have the
|
||||
opencode TUI running, `opencode serve` will start a new server.
|
||||
|
||||
---
|
||||
|
||||
#### Connect to an existing server
|
||||
|
||||
When you start the TUI it randomly assigns a port and hostname. You can instead pass in the `--hostname` and `--port` [flags](/docs/cli). Then use this to connect to its server.
|
||||
|
||||
The [`/tui`](#tui) endpoint can be used to drive the TUI through the server. For example, you can prefill or run a prompt. This setup is used by the opencode [IDE](/docs/ide) plugins.
|
||||
|
||||
---
|
||||
|
||||
## Spec
|
||||
|
||||
The server publishes an OpenAPI 3.1 spec that can be viewed at:
|
||||
|
||||
```
|
||||
http://<hostname>:<port>/doc
|
||||
```
|
||||
|
||||
For example, `http://localhost:4096/doc`. Use the spec to generate clients or inspect request and response types. Or view it in a Swagger explorer.
|
||||
|
||||
---
|
||||
|
||||
## APIs
|
||||
|
||||
The opencode server exposes the following APIs.
|
||||
|
||||
---
|
||||
|
||||
### App
|
||||
|
||||
| Method | Path | Description | Response |
|
||||
| ------ | ----------- | ------------------ | --------------------------------------- |
|
||||
| `GET` | `/app` | Get app info | <a href={typesUrl}><code>App</code></a> |
|
||||
| `POST` | `/app/init` | Initialize the app | `boolean` |
|
||||
|
||||
---
|
||||
|
||||
### Config
|
||||
|
||||
| Method | Path | Description | Response |
|
||||
| ------ | ------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| `GET` | `/config` | Get config info | <a href={typesUrl}><code>Config</code></a> |
|
||||
| `GET` | `/config/providers` | List providers and default models | `{ providers: `<a href={typesUrl}>Provider[]</a>`, default: { [key: string]: string } }` |
|
||||
|
||||
---
|
||||
|
||||
### Sessions
|
||||
|
||||
| Method | Path | Description | Notes |
|
||||
| -------- | ---------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `GET` | `/session` | List sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
|
||||
| `GET` | `/session/:id` | Get session | Returns <a href={typesUrl}><code>Session</code></a> |
|
||||
| `GET` | `/session/:id/children` | List child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
|
||||
| `POST` | `/session` | Create session | body: `{ parentID?, title? }`, returns <a href={typesUrl}><code>Session</code></a> |
|
||||
| `DELETE` | `/session/:id` | Delete session | |
|
||||
| `PATCH` | `/session/:id` | Update session properties | body: `{ title? }`, returns <a href={typesUrl}><code>Session</code></a> |
|
||||
| `POST` | `/session/:id/init` | Analyze app and create `AGENTS.md` | body: `{ messageID, providerID, modelID }` |
|
||||
| `POST` | `/session/:id/abort` | Abort a running session | |
|
||||
| `POST` | `/session/:id/share` | Share session | Returns <a href={typesUrl}><code>Session</code></a> |
|
||||
| `DELETE` | `/session/:id/share` | Unshare session | Returns <a href={typesUrl}><code>Session</code></a> |
|
||||
| `POST` | `/session/:id/summarize` | Summarize session | |
|
||||
| `GET` | `/session/:id/message` | List messages in a session | Returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}[]` |
|
||||
| `GET` | `/session/:id/message/:messageID` | Get message details | Returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
|
||||
| `POST` | `/session/:id/message` | Send chat message | body matches [`ChatInput`](https://github.com/sst/opencode/blob/main/packages/opencode/src/session/index.ts#L358), returns <a href={typesUrl}><code>Message</code></a> |
|
||||
| `POST` | `/session/:id/shell` | Run a shell command | body matches [`CommandInput`](https://github.com/sst/opencode/blob/main/packages/opencode/src/session/index.ts#L1007), returns <a href={typesUrl}><code>Message</code></a> |
|
||||
| `POST` | `/session/:id/revert` | Revert a message | body: `{ messageID }` |
|
||||
| `POST` | `/session/:id/unrevert` | Restore reverted messages | |
|
||||
| `POST` | `/session/:id/permissions/:permissionID` | Respond to a permission request | body: `{ response }` |
|
||||
|
||||
---
|
||||
|
||||
### Files
|
||||
|
||||
| Method | Path | Description | Response |
|
||||
| ------ | ------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| `GET` | `/find?pattern=<pat>` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
|
||||
| `GET` | `/find/file?query=<q>` | Find files by name | `string[]` (file paths) |
|
||||
| `GET` | `/find/symbol?query=<q>` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> |
|
||||
| `GET` | `/file?path=<path>` | Read a file | `{ type: "raw" \| "patch", content: string }` |
|
||||
| `GET` | `/file/status` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> |
|
||||
|
||||
---
|
||||
|
||||
### Logging
|
||||
|
||||
| Method | Path | Description | Response |
|
||||
| ------ | ------ | ------------------------------------------------------------ | --------- |
|
||||
| `POST` | `/log` | Write log entry. Body: `{ service, level, message, extra? }` | `boolean` |
|
||||
|
||||
---
|
||||
|
||||
### Agents
|
||||
|
||||
| Method | Path | Description | Response |
|
||||
| ------ | -------- | ------------------------- | ------------------------------------------- |
|
||||
| `GET` | `/agent` | List all available agents | <a href={typesUrl}><code>Agent[]</code></a> |
|
||||
|
||||
---
|
||||
|
||||
### TUI
|
||||
|
||||
| Method | Path | Description | Response |
|
||||
| ------ | ----------------------- | ------------------------------------------- | ---------------------- |
|
||||
| `POST` | `/tui/append-prompt` | Append text to the prompt | `boolean` |
|
||||
| `POST` | `/tui/open-help` | Open the help dialog | `boolean` |
|
||||
| `POST` | `/tui/open-sessions` | Open the session selector | `boolean` |
|
||||
| `POST` | `/tui/open-themes` | Open the theme selector | `boolean` |
|
||||
| `POST` | `/tui/open-models` | Open the model selector | `boolean` |
|
||||
| `POST` | `/tui/submit-prompt` | Submit the current prompt | `boolean` |
|
||||
| `POST` | `/tui/clear-prompt` | Clear the prompt | `boolean` |
|
||||
| `POST` | `/tui/execute-command` | Execute a command (`{ command }`) | `boolean` |
|
||||
| `POST` | `/tui/show-toast` | Show toast (`{ title?, message, variant }`) | `boolean` |
|
||||
| `GET` | `/tui/control/next` | Wait for the next control request | Control request object |
|
||||
| `POST` | `/tui/control/response` | Respond to a control request (`{ body }`) | `boolean` |
|
||||
|
||||
---
|
||||
|
||||
### Auth
|
||||
|
||||
| Method | Path | Description | Response |
|
||||
| ------ | ----------- | --------------------------------------------------------------- | --------- |
|
||||
| `PUT` | `/auth/:id` | Set authentication credentials. Body must match provider schema | `boolean` |
|
||||
|
||||
---
|
||||
|
||||
### Events
|
||||
|
||||
| Method | Path | Description | Response |
|
||||
| ------ | -------- | ----------------------------------------------------------------------------- | ------------------------- |
|
||||
| `GET` | `/event` | Server-sent events stream. First event is `server.connected`, then bus events | Server-sent events stream |
|
||||
|
||||
---
|
||||
|
||||
### Docs
|
||||
|
||||
| Method | Path | Description | Response |
|
||||
| ------ | ------ | -------------------------------------- | ------------------------------------------ |
|
||||
| `GET` | `/doc` | OpenAPI 3.1 specification | HTML page with OpenAPI spec |
|
||||
@@ -25,6 +25,12 @@ Once you're in the TUI, you can prompt it with a message.
|
||||
Give me a quick summary of the codebase.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File references
|
||||
|
||||
You can reference files in your messages using `@`. This does a fuzzy file search in the current working directory.
|
||||
|
||||
:::tip
|
||||
You can also use `@` to reference files in your messages.
|
||||
:::
|
||||
@@ -33,6 +39,20 @@ You can also use `@` to reference files in your messages.
|
||||
How is auth handled in @packages/functions/src/api/index.ts?
|
||||
```
|
||||
|
||||
The content of the file is added to the conversation automatically.
|
||||
|
||||
---
|
||||
|
||||
## Bash commands
|
||||
|
||||
Start a message with `!` to run a shell command.
|
||||
|
||||
```bash frame="none"
|
||||
!ls -la
|
||||
```
|
||||
|
||||
The output of the command is added to the conversation as a tool result.
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
@@ -235,18 +255,6 @@ Unshare current session. [Learn more](/docs/share#un-sharing).
|
||||
|
||||
---
|
||||
|
||||
## Bash commands
|
||||
|
||||
Start a message with `!` to run a shell command.
|
||||
|
||||
```bash frame="none"
|
||||
!ls -la
|
||||
```
|
||||
|
||||
The output of the command is added to the conversation as a tool result.
|
||||
|
||||
---
|
||||
|
||||
## Editor setup
|
||||
|
||||
Both the `/editor` and `/export` commands use the editor specified in your `EDITOR` environment variable.
|
||||
@@ -254,37 +262,54 @@ Both the `/editor` and `/export` commands use the editor specified in your `EDIT
|
||||
<Tabs>
|
||||
<TabItem label="Linux/macOS">
|
||||
```bash
|
||||
export EDITOR=nano # or vim, code, etc.
|
||||
# Example for nano or vim
|
||||
export EDITOR=nano
|
||||
export EDITOR=vim
|
||||
|
||||
# For GUI editors (VS Code, Cursor, VSCodium, Windsurf, Zed, etc.) include --wait
|
||||
export EDITOR="code --wait"
|
||||
```
|
||||
|
||||
To make it permanent, add this to your shell profile;
|
||||
`~/.bashrc`, `~/.zshrc`, etc.
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Windows (CMD)">
|
||||
```bash
|
||||
set EDITOR=notepad # or code, vim, etc.
|
||||
set EDITOR=notepad
|
||||
|
||||
# For GUI editors (VS Code, Cursor, VSCodium, Windsurf, Zed, etc.) include --wait
|
||||
set EDITOR=code --wait
|
||||
```
|
||||
|
||||
To make it permanent, use **System Properties** > **Environment
|
||||
Variables**.
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Windows (PowerShell)">
|
||||
```bash
|
||||
$env:EDITOR = "notepad" # or "code", "vim", etc.
|
||||
```powershell
|
||||
$env:EDITOR = "notepad"
|
||||
|
||||
# For GUI editors (VS Code, Cursor, VSCodium, Windsurf, Zed, etc.) include --wait
|
||||
$env:EDITOR = "code --wait"
|
||||
```
|
||||
|
||||
To make it permanent, add this to your PowerShell
|
||||
profile.
|
||||
|
||||
To make it permanent, add this to your PowerShell profile.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Popular editor options include:
|
||||
|
||||
- `code` - Visual Studio Code
|
||||
- `cursor` - Cursor
|
||||
- `windsurf` - Windsurf
|
||||
- `vim` - Vim editor
|
||||
- `nano` - Nano editor
|
||||
- `notepad` - Windows Notepad
|
||||
- `subl` - Sublime Text
|
||||
|
||||
:::note
|
||||
Some editors like VS Code need to be started with the `--wait` flag.
|
||||
:::
|
||||
|
||||
Some editors need command-line arguments to run in blocking mode. The `--wait` flag makes the editor process block until closed.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.28",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Vendored
-4
@@ -27,10 +27,6 @@ declare module "sst" {
|
||||
"Bucket": {
|
||||
"type": "sst.cloudflare.Bucket"
|
||||
}
|
||||
"Console": {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"DATABASE_PASSWORD": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user