mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 08:46:15 -04:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13d4a802ac | |||
| c4ae3e429c | |||
| e9cb360cb7 | |||
| 596d4e4490 | |||
| 45451095f7 | |||
| aae354c951 | |||
| 4cddda3e16 | |||
| 834aa036a4 | |||
| 6d1b6a6fb1 | |||
| e78fe8dcba | |||
| f62d826037 | |||
| 342f4239e4 | |||
| 5141fe8d7d | |||
| 3a9dd306db | |||
| e32b6e2cc2 | |||
| fab0e5de04 | |||
| 61105b487f | |||
| 6f584ec641 | |||
| 3a2b2f13f2 | |||
| be13e71fb9 | |||
| dd0c049119 | |||
| ee2b57958d | |||
| 4178b3c6ae | |||
| 2c2752ee02 | |||
| 5a17f44da4 | |||
| 354e55ecef | |||
| 10735f93ca | |||
| ccaebdcd16 | |||
| 2bbd7a167a | |||
| f12d470b33 | |||
| 0835170224 | |||
| 3530885f48 | |||
| a071a2b7f4 | |||
| b2f2c9ac37 | |||
| 631722213b | |||
| 80b25c79bb | |||
| 3d9c5b4adf | |||
| a3064e2c32 | |||
| 275bc4d2c8 | |||
| 39106718ed | |||
| 02cfdfbf5b | |||
| 1ec71e419b | |||
| 5fbbdcaf64 | |||
| 2b6afe90d0 | |||
| 5f34dcc792 | |||
| 681abcbf2d | |||
| 603e81ef5a | |||
| fb0a200ecf |
@@ -0,0 +1,50 @@
|
||||
name: Duplicate Issue Detection
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
check-duplicates:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Check for duplicate issues
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "*": "deny" } }'
|
||||
run: |
|
||||
opencode run -m anthropic/claude-sonnet-4-20250514 "A new issue has been created: '${{ github.event.issue.title }}'
|
||||
|
||||
Issue body:
|
||||
${{ github.event.issue.body }}
|
||||
|
||||
Please search through existing issues in this repository to find any potential duplicates of this new issue. Consider:
|
||||
1. Similar titles or descriptions
|
||||
2. Same error messages or symptoms
|
||||
3. Related functionality or components
|
||||
4. Similar feature requests
|
||||
|
||||
If you find any potential duplicates, please comment on the new issue with:
|
||||
- A brief explanation of why it might be a duplicate
|
||||
- Links to the potentially duplicate issues
|
||||
- A suggestion to check those issues first
|
||||
|
||||
Use this format for the comment:
|
||||
'👋 This issue might be a duplicate of existing issues. Please check:
|
||||
- #[issue_number]: [brief description of similarity]
|
||||
|
||||
If none of these address your specific case, please let us know how this issue differs.'
|
||||
|
||||
If no clear duplicates are found, do not comment."
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Guidelines Check
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
check-guidelines:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Check PR guidelines compliance
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh review*": "deny", "*": "deny" } }'
|
||||
run: |
|
||||
opencode run -m anthropic/claude-sonnet-4-20250514 "A new pull request has been created: '${{ github.event.pull_request.title }}'
|
||||
|
||||
<pr-number>
|
||||
${{ github.event.pull_request.number }}
|
||||
</pr-number>
|
||||
|
||||
<pr-description>
|
||||
${{ github.event.pull_request.body }}
|
||||
</pr-description>
|
||||
|
||||
Please check all the code changes in this pull request against the guidelines in AGENTS.md file in this repository. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
|
||||
|
||||
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
|
||||
|
||||
Command MUST be like this.
|
||||
```
|
||||
gh api \
|
||||
--method POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments \
|
||||
-f 'body=[summary of issue]' -f 'commit_id=${{ github.event.pull_request.head.sha }}' -f 'path=[path-to-file]' -F "line=[line]" -f 'side=RIGHT'
|
||||
```
|
||||
|
||||
Only create comments for actual violations. If the code follows all guidelines, don't run any gh commands."
|
||||
@@ -48,20 +48,20 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Install makepkg
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pacman-package-manager
|
||||
# Temporarily disabled AUR steps
|
||||
# - name: Install makepkg
|
||||
# run: |
|
||||
# sudo apt-get update
|
||||
# sudo apt-get install -y pacman-package-manager
|
||||
|
||||
- name: Setup SSH for AUR
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
|
||||
# ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
description: Use this agent when you are saked to commit and push code changes to a git repository.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You commit and push to git
|
||||
|
||||
Commit messages should be brief since they are used to generate release notes.
|
||||
|
||||
Messages should say WHY the change was made and not WHAT was changed.
|
||||
@@ -1,47 +1,48 @@
|
||||
# Download Stats
|
||||
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | ---------------- | ---------------- | ---------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | ---------------- | ---------------- | ----------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/cloud-core",
|
||||
"version": "0.4.24",
|
||||
"version": "0.4.36",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/cloud-function",
|
||||
"version": "0.4.24",
|
||||
"version": "0.4.36",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/cloud-web",
|
||||
"version": "0.4.24",
|
||||
"version": "0.4.36",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/function",
|
||||
"version": "0.4.24",
|
||||
"version": "0.4.36",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "0.4.24",
|
||||
"version": "0.4.36",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -97,6 +97,7 @@ 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
|
||||
const pkgbuild = [
|
||||
"# Maintainer: dax",
|
||||
@@ -136,6 +137,7 @@ if (!snapshot) {
|
||||
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 { Provider } from "../provider/provider"
|
||||
import { generateObject, type ModelMessage } from "ai"
|
||||
import PROMPT_GENERATE from "./generate.txt"
|
||||
import { SystemPrompt } from "../session/system"
|
||||
import { mergeDeep } from "remeda"
|
||||
|
||||
export namespace Agent {
|
||||
export const Info = z
|
||||
@@ -14,6 +15,11 @@ export namespace Agent {
|
||||
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]),
|
||||
topP: z.number().optional(),
|
||||
temperature: z.number().optional(),
|
||||
permission: z.object({
|
||||
edit: Config.Permission,
|
||||
bash: z.record(z.string(), Config.Permission),
|
||||
webfetch: Config.Permission.optional(),
|
||||
}),
|
||||
model: z
|
||||
.object({
|
||||
modelID: z.string(),
|
||||
@@ -31,6 +37,13 @@ export namespace Agent {
|
||||
|
||||
const state = App.state("agent", async () => {
|
||||
const cfg = await Config.get()
|
||||
const defaultPermission: Info["permission"] = {
|
||||
edit: "allow",
|
||||
bash: {
|
||||
"*": "allow",
|
||||
},
|
||||
webfetch: "allow",
|
||||
}
|
||||
const result: Record<string, Info> = {
|
||||
general: {
|
||||
name: "general",
|
||||
@@ -41,17 +54,20 @@ export namespace Agent {
|
||||
todowrite: false,
|
||||
},
|
||||
options: {},
|
||||
permission: defaultPermission,
|
||||
mode: "subagent",
|
||||
},
|
||||
build: {
|
||||
name: "build",
|
||||
tools: {},
|
||||
options: {},
|
||||
permission: defaultPermission,
|
||||
mode: "primary",
|
||||
},
|
||||
plan: {
|
||||
name: "plan",
|
||||
options: {},
|
||||
permission: defaultPermission,
|
||||
tools: {
|
||||
write: false,
|
||||
edit: false,
|
||||
@@ -70,25 +86,48 @@ export namespace Agent {
|
||||
item = result[key] = {
|
||||
name: key,
|
||||
mode: "all",
|
||||
permission: defaultPermission,
|
||||
options: {},
|
||||
tools: {},
|
||||
}
|
||||
const { model, prompt, tools, description, temperature, top_p, mode, ...extra } = value
|
||||
const { model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
|
||||
item.options = {
|
||||
...item.options,
|
||||
...extra,
|
||||
}
|
||||
if (value.model) item.model = Provider.parseModel(value.model)
|
||||
if (value.prompt) item.prompt = value.prompt
|
||||
if (value.tools)
|
||||
if (model) item.model = Provider.parseModel(model)
|
||||
if (prompt) item.prompt = prompt
|
||||
if (tools)
|
||||
item.tools = {
|
||||
...item.tools,
|
||||
...value.tools,
|
||||
...tools,
|
||||
}
|
||||
if (value.description) item.description = value.description
|
||||
if (value.temperature != undefined) item.temperature = value.temperature
|
||||
if (value.top_p != undefined) item.topP = value.top_p
|
||||
if (value.mode) item.mode = value.mode
|
||||
if (description) item.description = description
|
||||
if (temperature != undefined) item.temperature = temperature
|
||||
if (top_p != undefined) item.topP = top_p
|
||||
if (mode) item.mode = mode
|
||||
|
||||
if (permission ?? cfg.permission) {
|
||||
const merged = mergeDeep(cfg.permission ?? {}, permission ?? {})
|
||||
if (merged.edit) item.permission.edit = merged.edit
|
||||
if (merged.webfetch) item.permission.webfetch = merged.webfetch
|
||||
if (merged.bash) {
|
||||
if (typeof merged.bash === "string") {
|
||||
item.permission.bash = {
|
||||
"*": merged.bash,
|
||||
}
|
||||
}
|
||||
// if granular permissions are provided, default to "ask"
|
||||
if (typeof merged.bash === "object") {
|
||||
item.permission.bash = mergeDeep(
|
||||
{
|
||||
"*": "ask",
|
||||
},
|
||||
merged.bash,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Bus } from "../../bus"
|
||||
import { Log } from "../../util/log"
|
||||
import { FileWatcher } from "../../file/watch"
|
||||
import { Ide } from "../../ide"
|
||||
import { Agent } from "../../agent/agent"
|
||||
|
||||
import { Flag } from "../../flag/flag"
|
||||
import { Session } from "../../session"
|
||||
|
||||
@@ -141,7 +141,6 @@ export const TuiCommand = cmd({
|
||||
CGO_ENABLED: "0",
|
||||
OPENCODE_SERVER: server.url.toString(),
|
||||
OPENCODE_APP_INFO: JSON.stringify(app),
|
||||
OPENCODE_AGENTS: JSON.stringify(await Agent.list()),
|
||||
},
|
||||
onExit: () => {
|
||||
server.stop()
|
||||
|
||||
@@ -110,6 +110,10 @@ export namespace Config {
|
||||
].map((x) => "file://" + x),
|
||||
)
|
||||
|
||||
if (Flag.OPENCODE_PERMISSION) {
|
||||
result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION))
|
||||
}
|
||||
|
||||
// Handle migration from autoshare to share field
|
||||
if (result.autoshare === true && !result.share) {
|
||||
result.share = "auto"
|
||||
@@ -164,6 +168,9 @@ export namespace Config {
|
||||
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
||||
export type Mcp = z.infer<typeof Mcp>
|
||||
|
||||
export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
|
||||
export type Permission = z.infer<typeof Permission>
|
||||
|
||||
export const Agent = z
|
||||
.object({
|
||||
model: z.string().optional(),
|
||||
@@ -174,6 +181,13 @@ export namespace Config {
|
||||
disable: z.boolean().optional(),
|
||||
description: z.string().optional().describe("Description of when to use the agent"),
|
||||
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(),
|
||||
permission: z
|
||||
.object({
|
||||
edit: Permission.optional(),
|
||||
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
|
||||
webfetch: Permission.optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.catchall(z.any())
|
||||
.openapi({
|
||||
@@ -243,9 +257,6 @@ export namespace Config {
|
||||
})
|
||||
export type Layout = z.infer<typeof Layout>
|
||||
|
||||
export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
|
||||
export type Permission = z.infer<typeof Permission>
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
||||
|
||||
@@ -3,6 +3,7 @@ export namespace Flag {
|
||||
export const OPENCODE_DISABLE_WATCHER = truthy("OPENCODE_DISABLE_WATCHER")
|
||||
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
|
||||
export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE")
|
||||
export const OPENCODE_PERMISSION = process.env["OPENCODE_PERMISSION"]
|
||||
|
||||
function truthy(key: string) {
|
||||
const value = process.env[key]?.toLowerCase()
|
||||
|
||||
@@ -71,7 +71,7 @@ export namespace Format {
|
||||
const proc = Bun.spawn({
|
||||
cmd: item.command.map((x) => x.replace("$FILE", file)),
|
||||
cwd: App.info().path.cwd,
|
||||
env: item.environment,
|
||||
env: { ...process.env, ...item.environment },
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
|
||||
@@ -25,9 +25,10 @@ await Promise.all([
|
||||
fs.mkdir(Global.Path.config, { recursive: true }),
|
||||
fs.mkdir(Global.Path.state, { recursive: true }),
|
||||
fs.mkdir(Global.Path.log, { recursive: true }),
|
||||
fs.mkdir(Global.Path.bin, { recursive: true }),
|
||||
])
|
||||
|
||||
const CACHE_VERSION = "6"
|
||||
const CACHE_VERSION = "7"
|
||||
|
||||
const version = await Bun.file(path.join(Global.Path.cache, "version"))
|
||||
.text()
|
||||
|
||||
@@ -21,6 +21,9 @@ import { GithubCommand } from "./cli/cmd/github"
|
||||
|
||||
const cancel = new AbortController()
|
||||
|
||||
try {
|
||||
} catch (e) {}
|
||||
|
||||
process.on("unhandledRejection", (e) => {
|
||||
Log.Default.error("rejection", {
|
||||
e: e instanceof Error ? e.message : e,
|
||||
|
||||
@@ -155,7 +155,7 @@ export namespace Permission {
|
||||
public readonly permissionID: string,
|
||||
public readonly toolCallID?: string,
|
||||
) {
|
||||
super(`The user rejected permission to use this functionality`)
|
||||
super(`The user rejected permission to use this specific tool call. You may try again with different parameters.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +87,7 @@ export namespace ProviderTransform {
|
||||
if (modelID.includes("gpt-5")) {
|
||||
if (providerID === "azure") {
|
||||
return {
|
||||
reasoning_effort: "minimal",
|
||||
text_verbosity: "verbose",
|
||||
reasoningEffort: "minimal",
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -42,6 +42,7 @@ import { ToolRegistry } from "../tool/registry"
|
||||
import { Plugin } from "../plugin"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { Permission } from "../permission"
|
||||
import { Wildcard } from "../util/wildcard"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -522,6 +523,7 @@ export namespace Session {
|
||||
t.execute(args, {
|
||||
sessionID: input.sessionID,
|
||||
abort: new AbortController().signal,
|
||||
agent: input.agent!,
|
||||
messageID: userMsg.id,
|
||||
metadata: async () => {},
|
||||
}),
|
||||
@@ -764,11 +766,11 @@ export namespace Session {
|
||||
|
||||
const enabledTools = pipe(
|
||||
agent.tools,
|
||||
mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID)),
|
||||
mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID, agent)),
|
||||
mergeDeep(input.tools ?? {}),
|
||||
)
|
||||
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
|
||||
if (enabledTools[item.id] === false) continue
|
||||
if (Wildcard.all(item.id, enabledTools) === false) continue
|
||||
tools[item.id] = tool({
|
||||
id: item.id as any,
|
||||
description: item.description,
|
||||
@@ -790,6 +792,7 @@ export namespace Session {
|
||||
abort: options.abortSignal!,
|
||||
messageID: assistantMsg.id,
|
||||
callID: options.toolCallId,
|
||||
agent: agent.name,
|
||||
metadata: async (val) => {
|
||||
const match = processor.partFromToolCall(options.toolCallId)
|
||||
if (match && match.state.status === "running") {
|
||||
@@ -829,7 +832,7 @@ export namespace Session {
|
||||
}
|
||||
|
||||
for (const [key, item] of Object.entries(await MCP.tools())) {
|
||||
if (enabledTools[key] === false) continue
|
||||
if (Wildcard.all(key, enabledTools) === false) continue
|
||||
const execute = item.execute
|
||||
if (!execute) continue
|
||||
item.execute = async (args, opts) => {
|
||||
|
||||
@@ -5,12 +5,12 @@ import { Tool } from "./tool"
|
||||
import DESCRIPTION from "./bash.txt"
|
||||
import { App } from "../app/app"
|
||||
import { Permission } from "../permission"
|
||||
import { Config } from "../config/config"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { Log } from "../util/log"
|
||||
import { Wildcard } from "../util/wildcard"
|
||||
import { $ } from "bun"
|
||||
import { Agent } from "../agent/agent"
|
||||
|
||||
const MAX_OUTPUT_LENGTH = 30000
|
||||
const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
||||
@@ -40,20 +40,8 @@ export const BashTool = Tool.define("bash", {
|
||||
async execute(params, ctx) {
|
||||
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
|
||||
const app = App.info()
|
||||
const cfg = await Config.get()
|
||||
const tree = await parser().then((p) => p.parse(params.command))
|
||||
const permissions = (() => {
|
||||
const value = cfg.permission?.bash
|
||||
if (!value)
|
||||
return {
|
||||
"*": "allow",
|
||||
}
|
||||
if (typeof value === "string")
|
||||
return {
|
||||
"*": value,
|
||||
}
|
||||
return value
|
||||
})()
|
||||
const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash)
|
||||
|
||||
let needsAsk = false
|
||||
for (const node of tree.rootNode.descendantsOfType("command")) {
|
||||
@@ -93,17 +81,10 @@ export const BashTool = Tool.define("bash", {
|
||||
|
||||
// always allow cd if it passes above check
|
||||
if (!needsAsk && command[0] !== "cd") {
|
||||
const action = (() => {
|
||||
for (const [pattern, value] of Object.entries(permissions)) {
|
||||
const match = Wildcard.match(node.text, pattern)
|
||||
log.info("checking", { text: node.text.trim(), pattern, match })
|
||||
if (match) return value
|
||||
}
|
||||
return "ask"
|
||||
})()
|
||||
const action = Wildcard.all(node.text, permissions)
|
||||
if (action === "deny") {
|
||||
throw new Error(
|
||||
"The user has specifically restricted access to this command, you are not allowed to execute it.",
|
||||
`The user has specifically restricted access to this command, you are not allowed to execute it. Here is the configuration: ${JSON.stringify(permissions)}`,
|
||||
)
|
||||
}
|
||||
if (action === "ask") needsAsk = true
|
||||
|
||||
@@ -14,8 +14,8 @@ import { App } from "../app/app"
|
||||
import { File } from "../file"
|
||||
import { Bus } from "../bus"
|
||||
import { FileTime } from "../file/time"
|
||||
import { Config } from "../config/config"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Agent } from "../agent/agent"
|
||||
|
||||
export const EditTool = Tool.define("edit", {
|
||||
description: DESCRIPTION,
|
||||
@@ -40,7 +40,7 @@ export const EditTool = Tool.define("edit", {
|
||||
throw new Error(`File ${filePath} is not in the current working directory`)
|
||||
}
|
||||
|
||||
const cfg = await Config.get()
|
||||
const agent = await Agent.get(ctx.agent)
|
||||
let diff = ""
|
||||
let contentOld = ""
|
||||
let contentNew = ""
|
||||
@@ -48,7 +48,7 @@ export const EditTool = Tool.define("edit", {
|
||||
if (params.oldString === "") {
|
||||
contentNew = params.newString
|
||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||
if (cfg.permission?.edit === "ask") {
|
||||
if (agent.permission.edit === "ask") {
|
||||
await Permission.ask({
|
||||
type: "edit",
|
||||
sessionID: ctx.sessionID,
|
||||
@@ -77,7 +77,7 @@ export const EditTool = Tool.define("edit", {
|
||||
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
|
||||
|
||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||
if (cfg.permission?.edit === "ask") {
|
||||
if (agent.permission.edit === "ask") {
|
||||
await Permission.ask({
|
||||
type: "edit",
|
||||
sessionID: ctx.sessionID,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { TodoWriteTool, TodoReadTool } from "./todo"
|
||||
import { WebFetchTool } from "./webfetch"
|
||||
import { WriteTool } from "./write"
|
||||
import { InvalidTool } from "./invalid"
|
||||
import { Config } from "../config/config"
|
||||
import type { Agent } from "../agent/agent"
|
||||
|
||||
export namespace ToolRegistry {
|
||||
const ALL = [
|
||||
@@ -66,20 +66,23 @@ export namespace ToolRegistry {
|
||||
return result
|
||||
}
|
||||
|
||||
export async function enabled(_providerID: string, _modelID: string): Promise<Record<string, boolean>> {
|
||||
const cfg = await Config.get()
|
||||
export async function enabled(
|
||||
_providerID: string,
|
||||
_modelID: string,
|
||||
agent: Agent.Info,
|
||||
): Promise<Record<string, boolean>> {
|
||||
const result: Record<string, boolean> = {}
|
||||
result["patch"] = false
|
||||
|
||||
if (cfg.permission?.edit === "deny") {
|
||||
if (agent.permission.edit === "deny") {
|
||||
result["edit"] = false
|
||||
result["patch"] = false
|
||||
result["write"] = false
|
||||
}
|
||||
if (cfg?.permission?.bash === "deny") {
|
||||
if (agent.permission.bash["*"] === "deny" && Object.keys(agent.permission.bash).length === 1) {
|
||||
result["bash"] = false
|
||||
}
|
||||
if (cfg?.permission?.webfetch === "deny") {
|
||||
if (agent.permission.webfetch === "deny") {
|
||||
result["webfetch"] = false
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export namespace Tool {
|
||||
export type Context<M extends Metadata = Metadata> = {
|
||||
sessionID: string
|
||||
messageID: string
|
||||
agent: string
|
||||
callID?: string
|
||||
abort: AbortSignal
|
||||
metadata(input: { title?: string; metadata?: M }): void
|
||||
|
||||
@@ -8,8 +8,8 @@ import { App } from "../app/app"
|
||||
import { Bus } from "../bus"
|
||||
import { File } from "../file"
|
||||
import { FileTime } from "../file/time"
|
||||
import { Config } from "../config/config"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Agent } from "../agent/agent"
|
||||
|
||||
export const WriteTool = Tool.define("write", {
|
||||
description: DESCRIPTION,
|
||||
@@ -28,8 +28,8 @@ export const WriteTool = Tool.define("write", {
|
||||
const exists = await file.exists()
|
||||
if (exists) await FileTime.assert(ctx.sessionID, filepath)
|
||||
|
||||
const cfg = await Config.get()
|
||||
if (cfg.permission?.edit === "ask")
|
||||
const agent = await Agent.get(ctx.agent)
|
||||
if (agent.permission.edit === "ask")
|
||||
await Permission.ask({
|
||||
type: "write",
|
||||
sessionID: ctx.sessionID,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { sortBy, pipe } from "remeda"
|
||||
|
||||
export namespace Wildcard {
|
||||
export function match(str: string, pattern: string) {
|
||||
const regex = new RegExp(
|
||||
@@ -11,4 +13,16 @@ export namespace Wildcard {
|
||||
)
|
||||
return regex.test(str)
|
||||
}
|
||||
|
||||
export function all(input: string, patterns: Record<string, any>) {
|
||||
const sorted = pipe(patterns, Object.entries, sortBy([([key]) => key.length, "asc"], [([key]) => key, "asc"]))
|
||||
let result = undefined
|
||||
for (const [pattern, value] of sorted) {
|
||||
if (match(input, pattern)) {
|
||||
result = value
|
||||
continue
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ const ctx = {
|
||||
sessionID: "test",
|
||||
messageID: "",
|
||||
toolCallID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
metadata: () => {},
|
||||
}
|
||||
@@ -33,7 +34,7 @@ describe("tool.bash", () => {
|
||||
|
||||
test("cd ../ should fail outside of project root", async () => {
|
||||
await App.provide({ cwd: projectRoot }, async () => {
|
||||
await expect(
|
||||
expect(
|
||||
bash.execute(
|
||||
{
|
||||
command: "cd ../",
|
||||
|
||||
@@ -8,6 +8,7 @@ const ctx = {
|
||||
sessionID: "test",
|
||||
messageID: "",
|
||||
toolCallID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
metadata: () => {},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "0.4.24",
|
||||
"version": "0.4.36",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "0.4.24",
|
||||
"version": "0.4.36",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
|
||||
@@ -912,6 +912,15 @@ export type AgentConfig = {
|
||||
*/
|
||||
description?: string
|
||||
mode?: string
|
||||
permission?: {
|
||||
edit?: string
|
||||
bash?:
|
||||
| string
|
||||
| {
|
||||
[key: string]: string
|
||||
}
|
||||
webfetch?: string
|
||||
}
|
||||
[key: string]:
|
||||
| unknown
|
||||
| string
|
||||
@@ -921,6 +930,15 @@ export type AgentConfig = {
|
||||
}
|
||||
| boolean
|
||||
| string
|
||||
| {
|
||||
edit?: string
|
||||
bash?:
|
||||
| string
|
||||
| {
|
||||
[key: string]: string
|
||||
}
|
||||
webfetch?: string
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
|
||||
@@ -1061,6 +1079,13 @@ export type Agent = {
|
||||
mode: string
|
||||
topP?: number
|
||||
temperature?: number
|
||||
permission: {
|
||||
edit: string
|
||||
bash: {
|
||||
[key: string]: string
|
||||
}
|
||||
webfetch?: string
|
||||
}
|
||||
model?: {
|
||||
modelID: string
|
||||
providerID: string
|
||||
|
||||
@@ -45,14 +45,6 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
agentsStr := os.Getenv("OPENCODE_AGENTS")
|
||||
var agents []opencode.Agent
|
||||
err = json.Unmarshal([]byte(agentsStr), &agents)
|
||||
if err != nil {
|
||||
slog.Error("Failed to unmarshal modes", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
stat, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
slog.Error("Failed to stat stdin", "error", err)
|
||||
@@ -81,13 +73,25 @@ func main() {
|
||||
option.WithBaseURL(url),
|
||||
)
|
||||
|
||||
// Fetch agents from the /agent endpoint
|
||||
agentsPtr, err := httpClient.App.Agents(context.Background())
|
||||
if err != nil {
|
||||
slog.Error("Failed to fetch agents", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if agentsPtr == nil {
|
||||
slog.Error("No agents returned from server")
|
||||
os.Exit(1)
|
||||
}
|
||||
agents := *agentsPtr
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
apiHandler := util.NewAPILogHandler(ctx, httpClient, "tui", slog.LevelDebug)
|
||||
logger := slog.New(apiHandler)
|
||||
slog.SetDefault(logger)
|
||||
|
||||
slog.Debug("TUI launched", "app", appInfoStr, "modes", agentsStr, "url", url)
|
||||
slog.Debug("TUI launched", "app", appInfoStr, "agents_count", len(agents), "url", url)
|
||||
|
||||
go func() {
|
||||
err = clipboard.Init()
|
||||
|
||||
@@ -31,6 +31,7 @@ type EditorComponent interface {
|
||||
tea.Model
|
||||
tea.ViewModel
|
||||
Content() string
|
||||
Cursor() *tea.Cursor
|
||||
Lines() int
|
||||
Value() string
|
||||
Length() int
|
||||
@@ -407,6 +408,10 @@ func (m *editorComponent) Content() string {
|
||||
return content
|
||||
}
|
||||
|
||||
func (m *editorComponent) Cursor() *tea.Cursor {
|
||||
return m.textarea.Cursor()
|
||||
}
|
||||
|
||||
func (m *editorComponent) View() string {
|
||||
width := m.width
|
||||
if m.app.Session.ID == "" {
|
||||
@@ -694,6 +699,7 @@ func NewEditorComponent(app *app.App) EditorComponent {
|
||||
ta.Prompt = " "
|
||||
ta.ShowLineNumbers = false
|
||||
ta.CharLimit = -1
|
||||
ta.VirtualCursor = false
|
||||
ta = updateTextareaStyles(ta)
|
||||
|
||||
m := &editorComponent{
|
||||
|
||||
@@ -325,7 +325,9 @@ func renderText(
|
||||
|
||||
// wrap styled text
|
||||
styledText := result.String()
|
||||
wrappedText := ansi.WordwrapWc(styledText, width-6, " -")
|
||||
styledText = strings.ReplaceAll(styledText, "-", "\u2011")
|
||||
wrappedText := ansi.WordwrapWc(styledText, width-6, " ")
|
||||
wrappedText = strings.ReplaceAll(wrappedText, "\u2011", "-")
|
||||
content = base.Width(width - 6).Render(wrappedText)
|
||||
}
|
||||
|
||||
|
||||
@@ -83,10 +83,10 @@ func (c *commandsComponent) View() string {
|
||||
}
|
||||
commandsToShow = append(commandsToShow,
|
||||
// empty line
|
||||
commands.Command{
|
||||
Name: "",
|
||||
Description: "",
|
||||
},
|
||||
// commands.Command{
|
||||
// Name: "",
|
||||
// Description: "",
|
||||
// },
|
||||
commands.Command{
|
||||
Name: commands.CommandName(util.Ide()),
|
||||
Description: "open opencode",
|
||||
|
||||
@@ -2045,6 +2045,109 @@ func itemWidth(item any) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// forceWrapAttachment splits an attachment's display text across multiple lines
|
||||
func forceWrapAttachment(att *attachment.Attachment, width int) [][]any {
|
||||
if width <= 0 {
|
||||
return [][]any{{att}}
|
||||
}
|
||||
|
||||
display := att.Display
|
||||
displayRunes := []rune(display)
|
||||
|
||||
if len(displayRunes) <= width {
|
||||
return [][]any{{att}}
|
||||
}
|
||||
|
||||
var lines [][]any
|
||||
start := 0
|
||||
|
||||
for start < len(displayRunes) {
|
||||
// Calculate how many runes fit in this line
|
||||
end := start + width
|
||||
if end > len(displayRunes) {
|
||||
end = len(displayRunes)
|
||||
}
|
||||
|
||||
// Create a wrapped attachment for this segment
|
||||
wrappedAtt := &attachment.Attachment{
|
||||
ID: att.ID,
|
||||
Type: att.Type,
|
||||
Display: string(displayRunes[start:end]),
|
||||
URL: att.URL,
|
||||
Filename: att.Filename,
|
||||
MediaType: att.MediaType,
|
||||
Source: att.Source,
|
||||
}
|
||||
|
||||
lines = append(lines, []any{wrappedAtt})
|
||||
start = end
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// forceWrapWord splits a word that's too long to fit within the given width
|
||||
func forceWrapWord(word []any, width int) [][]any {
|
||||
if width <= 0 || len(word) == 0 {
|
||||
return [][]any{word}
|
||||
}
|
||||
|
||||
var lines [][]any
|
||||
currentLine := []any{}
|
||||
currentWidth := 0
|
||||
|
||||
for _, item := range word {
|
||||
if att, ok := item.(*attachment.Attachment); ok {
|
||||
// Handle attachment that might be too wide
|
||||
attWidth := uniseg.StringWidth(att.Display)
|
||||
|
||||
// If the attachment display is too wide, split it
|
||||
if attWidth > width {
|
||||
// Finish current line if it has content
|
||||
if len(currentLine) > 0 {
|
||||
lines = append(lines, currentLine)
|
||||
currentLine = []any{}
|
||||
currentWidth = 0
|
||||
}
|
||||
|
||||
// Split the attachment display across multiple lines
|
||||
wrappedAttachment := forceWrapAttachment(att, width)
|
||||
lines = append(lines, wrappedAttachment...)
|
||||
continue
|
||||
}
|
||||
|
||||
// If adding this attachment would exceed the width, start a new line
|
||||
if currentWidth+attWidth > width && len(currentLine) > 0 {
|
||||
lines = append(lines, currentLine)
|
||||
currentLine = []any{}
|
||||
currentWidth = 0
|
||||
}
|
||||
|
||||
currentLine = append(currentLine, item)
|
||||
currentWidth += attWidth
|
||||
} else if r, ok := item.(rune); ok {
|
||||
itemWidth := rw.RuneWidth(r)
|
||||
|
||||
// If adding this rune would exceed the width, start a new line
|
||||
if currentWidth+itemWidth > width && len(currentLine) > 0 {
|
||||
lines = append(lines, currentLine)
|
||||
currentLine = []any{}
|
||||
currentWidth = 0
|
||||
}
|
||||
|
||||
currentLine = append(currentLine, item)
|
||||
currentWidth += itemWidth
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last line if it has content
|
||||
if len(currentLine) > 0 {
|
||||
lines = append(lines, currentLine)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func wrapInterfaces(content []any, width int) [][]any {
|
||||
if width <= 0 {
|
||||
return [][]any{content}
|
||||
@@ -2076,11 +2179,49 @@ func wrapInterfaces(content []any, width int) [][]any {
|
||||
if !inSpaces {
|
||||
// End of a word
|
||||
if lineW > 0 && lineW+wordW > width {
|
||||
lines = append(lines, word)
|
||||
lineW = wordW
|
||||
// If the word itself is too long to fit on a line, force-wrap it
|
||||
if wordW > width {
|
||||
wrappedLines := forceWrapWord(word, width)
|
||||
lines = append(lines, wrappedLines...)
|
||||
// Calculate width of the last wrapped line
|
||||
lastLine := wrappedLines[len(wrappedLines)-1]
|
||||
lineW = 0
|
||||
for _, item := range lastLine {
|
||||
if r, ok := item.(rune); ok {
|
||||
lineW += rw.RuneWidth(r)
|
||||
} else if att, ok := item.(*attachment.Attachment); ok {
|
||||
lineW += uniseg.StringWidth(att.Display)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lines = append(lines, word)
|
||||
lineW = wordW
|
||||
}
|
||||
} else {
|
||||
lines[len(lines)-1] = append(lines[len(lines)-1], word...)
|
||||
lineW += wordW
|
||||
// Check if the word needs to be force-wrapped even when it fits on the current line
|
||||
if wordW > width {
|
||||
currentLine := lines[len(lines)-1]
|
||||
wrappedWord := forceWrapWord(word, width-lineW)
|
||||
if len(wrappedWord) > 0 {
|
||||
lines[len(lines)-1] = append(currentLine, wrappedWord[0]...)
|
||||
for i := 1; i < len(wrappedWord); i++ {
|
||||
lines = append(lines, wrappedWord[i])
|
||||
}
|
||||
// Calculate width of the last wrapped line
|
||||
lastLine := wrappedWord[len(wrappedWord)-1]
|
||||
lineW = 0
|
||||
for _, item := range lastLine {
|
||||
if r, ok := item.(rune); ok {
|
||||
lineW += rw.RuneWidth(r)
|
||||
} else if att, ok := item.(*attachment.Attachment); ok {
|
||||
lineW += uniseg.StringWidth(att.Display)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lines[len(lines)-1] = append(lines[len(lines)-1], word...)
|
||||
lineW += wordW
|
||||
}
|
||||
}
|
||||
word = nil
|
||||
wordW = 0
|
||||
@@ -2110,11 +2251,49 @@ func wrapInterfaces(content []any, width int) [][]any {
|
||||
// Handle any remaining word/spaces at the end of the content.
|
||||
if wordW > 0 {
|
||||
if lineW > 0 && lineW+wordW > width {
|
||||
lines = append(lines, word)
|
||||
lineW = wordW
|
||||
// If the word itself is too long to fit on a line, force-wrap it
|
||||
if wordW > width {
|
||||
wrappedLines := forceWrapWord(word, width)
|
||||
lines = append(lines, wrappedLines...)
|
||||
// Calculate width of the last wrapped line
|
||||
lastLine := wrappedLines[len(wrappedLines)-1]
|
||||
lineW = 0
|
||||
for _, item := range lastLine {
|
||||
if r, ok := item.(rune); ok {
|
||||
lineW += rw.RuneWidth(r)
|
||||
} else if att, ok := item.(*attachment.Attachment); ok {
|
||||
lineW += uniseg.StringWidth(att.Display)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lines = append(lines, word)
|
||||
lineW = wordW
|
||||
}
|
||||
} else {
|
||||
lines[len(lines)-1] = append(lines[len(lines)-1], word...)
|
||||
lineW += wordW
|
||||
// Check if the word needs to be force-wrapped even when it fits on the current line
|
||||
if wordW > width {
|
||||
currentLine := lines[len(lines)-1]
|
||||
wrappedWord := forceWrapWord(word, width-lineW)
|
||||
if len(wrappedWord) > 0 {
|
||||
lines[len(lines)-1] = append(currentLine, wrappedWord[0]...)
|
||||
for i := 1; i < len(wrappedWord); i++ {
|
||||
lines = append(lines, wrappedWord[i])
|
||||
}
|
||||
// Calculate width of the last wrapped line
|
||||
lastLine := wrappedWord[len(wrappedWord)-1]
|
||||
lineW = 0
|
||||
for _, item := range lastLine {
|
||||
if r, ok := item.(rune); ok {
|
||||
lineW += rw.RuneWidth(r)
|
||||
} else if att, ok := item.(*attachment.Attachment); ok {
|
||||
lineW += uniseg.StringWidth(att.Display)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lines[len(lines)-1] = append(lines[len(lines)-1], word...)
|
||||
lineW += wordW
|
||||
}
|
||||
}
|
||||
}
|
||||
if spaceW > 0 {
|
||||
|
||||
@@ -59,6 +59,8 @@ const interruptDebounceTimeout = 1 * time.Second
|
||||
const exitDebounceTimeout = 1 * time.Second
|
||||
|
||||
type Model struct {
|
||||
tea.Model
|
||||
tea.CursorModel
|
||||
width, height int
|
||||
app *app.App
|
||||
modal layout.Modal
|
||||
@@ -748,15 +750,17 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (a Model) View() string {
|
||||
func (a Model) View() (string, *tea.Cursor) {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
var mainLayout string
|
||||
|
||||
var editorX int
|
||||
var editorY int
|
||||
if a.app.Session.ID == "" {
|
||||
mainLayout = a.home()
|
||||
mainLayout, editorX, editorY = a.home()
|
||||
} else {
|
||||
mainLayout = a.chat()
|
||||
mainLayout, editorX, editorY = a.chat()
|
||||
}
|
||||
mainLayout = styles.NewStyle().
|
||||
Background(t.Background()).
|
||||
@@ -780,7 +784,12 @@ func (a Model) View() string {
|
||||
if theme.CurrentThemeUsesAnsiColors() {
|
||||
mainLayout = util.ConvertRGBToAnsi16Colors(mainLayout)
|
||||
}
|
||||
return mainLayout + "\n" + a.status.View()
|
||||
|
||||
cursor := a.editor.Cursor()
|
||||
cursor.Position.X += editorX
|
||||
cursor.Position.Y += editorY
|
||||
|
||||
return mainLayout + "\n" + a.status.View(), cursor
|
||||
}
|
||||
|
||||
func (a Model) Cleanup() {
|
||||
@@ -807,7 +816,7 @@ func (a Model) openFile(filepath string) (tea.Model, tea.Cmd) {
|
||||
return a, cmd
|
||||
}
|
||||
|
||||
func (a Model) home() string {
|
||||
func (a Model) home() (string, int, int) {
|
||||
t := theme.CurrentTheme()
|
||||
effectiveWidth := a.width - 4
|
||||
baseStyle := styles.NewStyle().Background(t.Background())
|
||||
@@ -899,14 +908,21 @@ func (a Model) home() string {
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
|
||||
editorX := (effectiveWidth - editorWidth) / 2
|
||||
editorX := max(0, (effectiveWidth-editorWidth)/2)
|
||||
editorY := (a.height / 2) + (mainHeight / 2) - 2
|
||||
|
||||
if editorLines > 1 {
|
||||
content := a.editor.Content()
|
||||
editorHeight := lipgloss.Height(content)
|
||||
|
||||
if editorY+editorHeight > a.height {
|
||||
difference := (editorY + editorHeight) - a.height
|
||||
editorY -= difference
|
||||
}
|
||||
mainLayout = layout.PlaceOverlay(
|
||||
editorX,
|
||||
editorY,
|
||||
a.editor.Content(),
|
||||
content,
|
||||
mainLayout,
|
||||
)
|
||||
}
|
||||
@@ -924,10 +940,10 @@ func (a Model) home() string {
|
||||
)
|
||||
}
|
||||
|
||||
return mainLayout
|
||||
return mainLayout, editorX + 5, editorY + 2
|
||||
}
|
||||
|
||||
func (a Model) chat() string {
|
||||
func (a Model) chat() (string, int, int) {
|
||||
effectiveWidth := a.width - 4
|
||||
t := theme.CurrentTheme()
|
||||
editorView := a.editor.View()
|
||||
@@ -944,14 +960,20 @@ func (a Model) chat() string {
|
||||
)
|
||||
|
||||
mainLayout := messagesView + "\n" + editorView
|
||||
editorX := (effectiveWidth - editorWidth) / 2
|
||||
editorX := max(0, (effectiveWidth-editorWidth)/2)
|
||||
editorY := a.height - editorHeight
|
||||
|
||||
if lines > 1 {
|
||||
editorY := a.height - editorHeight
|
||||
content := a.editor.Content()
|
||||
editorHeight := lipgloss.Height(content)
|
||||
if editorY+editorHeight > a.height {
|
||||
difference := (editorY + editorHeight) - a.height
|
||||
editorY -= difference
|
||||
}
|
||||
mainLayout = layout.PlaceOverlay(
|
||||
editorX,
|
||||
editorY,
|
||||
a.editor.Content(),
|
||||
content,
|
||||
mainLayout,
|
||||
)
|
||||
}
|
||||
@@ -970,7 +992,7 @@ func (a Model) chat() string {
|
||||
)
|
||||
}
|
||||
|
||||
return mainLayout
|
||||
return mainLayout, editorX + 5, editorY + 2
|
||||
}
|
||||
|
||||
func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package util
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
@@ -85,6 +86,8 @@ func Extension(path string) string {
|
||||
func ToMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string {
|
||||
r := styles.GetMarkdownRenderer(width-6, backgroundColor)
|
||||
content = strings.ReplaceAll(content, RootPath+"/", "")
|
||||
hyphenRegex := regexp.MustCompile(`-([^ ]|$)`)
|
||||
content = hyphenRegex.ReplaceAllString(content, "\u2011$1")
|
||||
rendered, _ := r.Render(content)
|
||||
lines := strings.Split(rendered, "\n")
|
||||
|
||||
@@ -105,5 +108,6 @@ func ToMarkdown(content string, width int, backgroundColor compat.AdaptiveColor)
|
||||
}
|
||||
}
|
||||
content = strings.Join(lines, "\n")
|
||||
content = strings.ReplaceAll(content, "\u2011", "-")
|
||||
return strings.TrimSuffix(content, "\n")
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
)
|
||||
|
||||
var SUPPORTED_IDES = []struct {
|
||||
Search string
|
||||
Search string
|
||||
ShortName string
|
||||
}{
|
||||
{"Windsurf", "Windsurf"},
|
||||
{"Visual Studio Code", "VS Code"},
|
||||
{"Visual Studio Code", "vscode"},
|
||||
{"Cursor", "Cursor"},
|
||||
{"VSCodium", "VSCodium"},
|
||||
}
|
||||
@@ -27,4 +27,5 @@ func Ide() string {
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode/web",
|
||||
"type": "module",
|
||||
"version": "0.4.24",
|
||||
"version": "0.4.36",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "sst shell --stage=dev --target=Web astro dev",
|
||||
|
||||
@@ -318,6 +318,22 @@ Control which tools are available in this agent with the `tools` config. You can
|
||||
}
|
||||
```
|
||||
|
||||
You can also use wildcards to control multiple tools at once. For example, to disable all tools from an MCP server:
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"agent": {
|
||||
"readonly": {
|
||||
"tools": {
|
||||
"mymcp_*": false,
|
||||
"write": false,
|
||||
"edit": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If no tools are specified, all tools are enabled by default.
|
||||
|
||||
---
|
||||
@@ -342,6 +358,147 @@ Here are all the tools can be controlled through the agent config.
|
||||
|
||||
---
|
||||
|
||||
### Permissions
|
||||
|
||||
Permissions control what actions an agent can take.
|
||||
|
||||
- edit, bash, webfetch
|
||||
|
||||
Each permission can be set to allow, ask, or deny.
|
||||
|
||||
- allow, ask, deny
|
||||
|
||||
Configure permissions globally in opencode.json.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"permission": {
|
||||
"edit": "ask",
|
||||
"bash": "allow",
|
||||
"webfetch": "deny"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can override permissions per agent in JSON.
|
||||
|
||||
```json title="opencode.json" {7-18}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"agent": {
|
||||
"build": {
|
||||
"permission": {
|
||||
"edit": "allow",
|
||||
"bash": {
|
||||
"*": "allow",
|
||||
"git push": "ask",
|
||||
"terraform *": "deny"
|
||||
},
|
||||
"webfetch": "ask"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also set permissions in Markdown agents.
|
||||
|
||||
```markdown title="~/.config/opencode/agent/review.md"
|
||||
---
|
||||
description: Code review without edits
|
||||
mode: subagent
|
||||
permission:
|
||||
edit: deny
|
||||
bash: ask
|
||||
webfetch: deny
|
||||
---
|
||||
|
||||
Only analyze code and suggest changes.
|
||||
```
|
||||
|
||||
Bash permissions support granular patterns for fine-grained control.
|
||||
|
||||
```json title="Allow most, ask for risky, deny terraform"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"permission": {
|
||||
"bash": {
|
||||
"*": "allow",
|
||||
"git push": "ask",
|
||||
"terraform *": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you provide a granular bash map, the default becomes ask unless you set \* explicitly.
|
||||
|
||||
```json title="Granular defaults to ask"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"permission": {
|
||||
"bash": {
|
||||
"git status": "allow"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Agent-level permissions merge over global settings.
|
||||
|
||||
- Global sets defaults; agent overrides when specified
|
||||
|
||||
Specific bash rules can override a global default.
|
||||
|
||||
```json title="Global ask, agent allows safe commands"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"permission": { "bash": "ask" },
|
||||
"agent": {
|
||||
"build": {
|
||||
"permission": {
|
||||
"bash": { "git status": "allow", "*": "ask" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Permissions affect tool availability and prompts differently.
|
||||
|
||||
- deny hides tools (edit also hides write/patch); ask prompts; allow runs
|
||||
|
||||
For quick reference, here are common setups.
|
||||
|
||||
```json title="Read-only reviewer"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"agent": {
|
||||
"review": {
|
||||
"permission": { "edit": "deny", "bash": "deny", "webfetch": "allow" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json title="Planning agent that can browse but cannot change code"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"agent": {
|
||||
"plan": {
|
||||
"permission": { "edit": "deny", "bash": "deny", "webfetch": "ask" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See the full permissions guide for more patterns.
|
||||
|
||||
- /docs/permissions
|
||||
|
||||
---
|
||||
|
||||
### Mode
|
||||
|
||||
Control the agent's mode with the `mode` config. The `mode` option is used to determine how the agent can be used.
|
||||
@@ -371,7 +528,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-turbo",
|
||||
"reasoningEffort": "high",
|
||||
"textVerbosity": "low"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ Permissions are configured in your `opencode.json` file under the `permission` k
|
||||
| `bash` | Control bash command execution |
|
||||
| `webfetch` | Control web content fetching |
|
||||
|
||||
They can also be configured per agent, see [Agent Configuration](/docs/agents#agent-configuration) for more details.
|
||||
|
||||
---
|
||||
|
||||
### edit
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "0.4.24",
|
||||
"version": "0.4.36",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user