Compare commits
142
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d57a83a75 | ||
|
|
6a5e329427 | ||
|
|
4b65b1e053 | ||
|
|
d431a0e4b4 | ||
|
|
5720883d5d | ||
|
|
007b57f078 | ||
|
|
fb07c2070c | ||
|
|
25dc6f09bc | ||
|
|
b70e2700ef | ||
|
|
1aed6b1d8b | ||
|
|
c1f607d206 | ||
|
|
2c819f290f | ||
|
|
6e9f10ad3f | ||
|
|
1251a870cb | ||
|
|
67047fa766 | ||
|
|
a366128a93 | ||
|
|
9f708e748a | ||
|
|
7bc26dafae | ||
|
|
ce89bcb8e2 | ||
|
|
c2b1974ddd | ||
|
|
ca6150d6f0 | ||
|
|
825ab2e38d | ||
|
|
755cd561ec | ||
|
|
6312c55d55 | ||
|
|
a9dc0fae3d | ||
|
|
7749d8e85f | ||
|
|
28112fbd12 | ||
|
|
387220f368 | ||
|
|
adb7cb1037 | ||
|
|
c06af70ab0 | ||
|
|
40dc2fa3c1 | ||
|
|
df7dd06a0f | ||
|
|
57d5c095d8 | ||
|
|
13ac849db5 | ||
|
|
8694c5b68f | ||
|
|
0a7d02c87c | ||
|
|
e77867ef05 | ||
|
|
fb224d8974 | ||
|
|
101566131d | ||
|
|
8433e8b433 | ||
|
|
379600b5ab | ||
|
|
7a503de606 | ||
|
|
2ad1eb56d3 | ||
|
|
a43f767abb | ||
|
|
0ee3b87289 | ||
|
|
3c9f3c5786 | ||
|
|
ca75ac6681 | ||
|
|
d1f597b5b5 | ||
|
|
8299fb3e2b | ||
|
|
4f7f90133d | ||
|
|
b205e104f6 | ||
|
|
252e2f98e6 | ||
|
|
e2afdc1202 | ||
|
|
a08e4c9651 | ||
|
|
7ccab8d272 | ||
|
|
fc57eb3b8e | ||
|
|
9179bafd54 | ||
|
|
2df8eda8a3 | ||
|
|
bd32252a7e | ||
|
|
1717d636a2 | ||
|
|
8e016b4703 | ||
|
|
b89d48a2a4 | ||
|
|
33312bfd1b | ||
|
|
3f1ce36418 | ||
|
|
0e13279545 | ||
|
|
5f03d892c0 | ||
|
|
bdabb102fe | ||
|
|
a79a6594b0 | ||
|
|
a3d282a4c2 | ||
|
|
db24f89313 | ||
|
|
31cb0bfa4f | ||
|
|
af9fdf0a1c | ||
|
|
be88cd5cb9 | ||
|
|
b4cc7d13b6 | ||
|
|
0ba013f8de | ||
|
|
0956b15c52 | ||
|
|
61150f6391 | ||
|
|
7409dcc6bd | ||
|
|
2829943ad1 | ||
|
|
c4311dda31 | ||
|
|
ad05a46d74 | ||
|
|
a6cadba814 | ||
|
|
a3bc5d35b0 | ||
|
|
1409a0715c | ||
|
|
e98c291866 | ||
|
|
e709dc34fb | ||
|
|
9293cddb3a | ||
|
|
68b3448b09 | ||
|
|
80f2b13a55 | ||
|
|
7d91d3b1ed | ||
|
|
a6464062b7 | ||
|
|
fd01dc9c89 | ||
|
|
d10fb88b66 | ||
|
|
6b68b1020e | ||
|
|
85bb9007ba | ||
|
|
9bef88e3b0 | ||
|
|
f98053c34e | ||
|
|
36007aecf4 | ||
|
|
4de44bbbef | ||
|
|
9d03d4419e | ||
|
|
7ab1c1c74a | ||
|
|
3f459819ba | ||
|
|
1986a6e817 | ||
|
|
dfe1325fca | ||
|
|
c1686c6ddc | ||
|
|
79b6ce5db4 | ||
|
|
0c816eb4b1 | ||
|
|
e318e173d8 | ||
|
|
b314781a1a | ||
|
|
8396d6b016 | ||
|
|
43e20874f4 | ||
|
|
c444e971b0 | ||
|
|
430bde9e9b | ||
|
|
05b82a6a30 | ||
|
|
6cd02c05c2 | ||
|
|
b3a7513765 | ||
|
|
f8738c9002 | ||
|
|
b460db15d7 | ||
|
|
ff4779ca11 | ||
|
|
146ff8ad85 | ||
|
|
0d0ec7dc46 | ||
|
|
1ea6e6cd4b | ||
|
|
96061222d2 | ||
|
|
3b9155714d | ||
|
|
7371db5cc6 | ||
|
|
b09b7d28b8 | ||
|
|
31ed4602e1 | ||
|
|
6a76346734 | ||
|
|
78b3000031 | ||
|
|
4c4860fb24 | ||
|
|
5242a1c6b4 | ||
|
|
075f876e6f | ||
|
|
a849812e9f | ||
|
|
d99dde6306 | ||
|
|
becf57ee6a | ||
|
|
f33aec1139 | ||
|
|
1571933096 | ||
|
|
160928a9a9 | ||
|
|
d297c29f22 | ||
|
|
0b498dd448 | ||
|
|
cec9c6122a | ||
|
|
51e310c9ce |
@@ -1,6 +1,5 @@
|
|||||||
name: Bug report
|
name: Bug report
|
||||||
description: Report an issue that should be fixed
|
description: Report an issue that should be fixed
|
||||||
labels: ["bug"]
|
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
name: 🚀 Feature Request
|
name: 🚀 Feature Request
|
||||||
description: Suggest an idea, feature, or enhancement
|
description: Suggest an idea, feature, or enhancement
|
||||||
labels: [discussion]
|
|
||||||
title: "[FEATURE]:"
|
title: "[FEATURE]:"
|
||||||
|
|
||||||
body:
|
body:
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
name: Question
|
name: Question
|
||||||
description: Ask a question
|
description: Ask a question
|
||||||
labels: ["question"]
|
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: question
|
id: question
|
||||||
|
|||||||
@@ -11,6 +11,5 @@ MrMushrooooom
|
|||||||
nexxeln
|
nexxeln
|
||||||
R44VC0RP
|
R44VC0RP
|
||||||
rekram1-node
|
rekram1-node
|
||||||
RhysSullivan
|
|
||||||
thdxr
|
thdxr
|
||||||
simonklee
|
simonklee
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
# Vouched contributors for this project.
|
|
||||||
#
|
|
||||||
# See https://github.com/mitchellh/vouch for details.
|
|
||||||
#
|
|
||||||
# Syntax:
|
|
||||||
# - One handle per line (without @), sorted alphabetically.
|
|
||||||
# - Optional platform prefix: platform:username (e.g., github:user).
|
|
||||||
# - Denounce with minus prefix: -username or -platform:username.
|
|
||||||
# - Optional details after a space following the handle.
|
|
||||||
adamdotdevin
|
|
||||||
-agusbasari29 AI PR slop
|
|
||||||
ariane-emory
|
|
||||||
-atharvau AI review spamming literally every PR
|
|
||||||
-borealbytes
|
|
||||||
-carycooper777
|
|
||||||
-danieljoshuanazareth
|
|
||||||
-danieljoshuanazareth
|
|
||||||
-davidbernat looks to be a clawdbot that spams team and sends super weird emails, doesnt appear to be a real person
|
|
||||||
dmtrkovalenko
|
|
||||||
edemaine
|
|
||||||
fahreddinozcan
|
|
||||||
-florianleibert
|
|
||||||
fwang
|
|
||||||
iamdavidhill
|
|
||||||
jayair
|
|
||||||
kitlangton
|
|
||||||
kommander
|
|
||||||
-opencode2026
|
|
||||||
-opencodeengineer bot that spams issues
|
|
||||||
r44vc0rp
|
|
||||||
rekram1-node
|
|
||||||
-ricardo-m-l
|
|
||||||
-robinmordasiewicz
|
|
||||||
rubdos
|
|
||||||
-saisharan0103 spamming ai prs
|
|
||||||
shantur
|
|
||||||
simonklee
|
|
||||||
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
|
||||||
-terisuke
|
|
||||||
thdxr
|
|
||||||
-toastythebot
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
name: daily-issues-recap
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Run at 6 PM EST (23:00 UTC, or 22:00 UTC during daylight saving)
|
|
||||||
- cron: "0 23 * * *"
|
|
||||||
workflow_dispatch: # Allow manual trigger for testing
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
daily-recap:
|
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
|
||||||
|
|
||||||
- name: Install opencode
|
|
||||||
run: curl -fsSL https://opencode.ai/install | bash
|
|
||||||
|
|
||||||
- name: Generate daily issues recap
|
|
||||||
id: recap
|
|
||||||
env:
|
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
OPENCODE_PERMISSION: |
|
|
||||||
{
|
|
||||||
"bash": {
|
|
||||||
"*": "deny",
|
|
||||||
"gh issue*": "allow",
|
|
||||||
"gh search*": "allow"
|
|
||||||
},
|
|
||||||
"webfetch": "deny",
|
|
||||||
"edit": "deny",
|
|
||||||
"write": "deny"
|
|
||||||
}
|
|
||||||
run: |
|
|
||||||
# Get today's date range
|
|
||||||
TODAY=$(date -u +%Y-%m-%d)
|
|
||||||
|
|
||||||
opencode run -m opencode/claude-sonnet-4-5 "Generate a daily issues recap for the OpenCode repository.
|
|
||||||
|
|
||||||
TODAY'S DATE: ${TODAY}
|
|
||||||
|
|
||||||
STEP 1: Gather today's issues
|
|
||||||
Search for all OPEN issues created today (${TODAY}) using:
|
|
||||||
gh issue list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500
|
|
||||||
|
|
||||||
IMPORTANT: EXCLUDE all issues authored by Anomaly team members. Filter out issues where the author login matches ANY of these:
|
|
||||||
adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr
|
|
||||||
This recap is specifically for COMMUNITY (external) issues only.
|
|
||||||
|
|
||||||
STEP 2: Analyze and categorize
|
|
||||||
For each issue created today, categorize it:
|
|
||||||
|
|
||||||
**Severity Assessment:**
|
|
||||||
- CRITICAL: Crashes, data loss, security issues, blocks major functionality
|
|
||||||
- HIGH: Significant bugs affecting many users, important features broken
|
|
||||||
- MEDIUM: Bugs with workarounds, minor features broken
|
|
||||||
- LOW: Minor issues, cosmetic, nice-to-haves
|
|
||||||
|
|
||||||
**Activity Assessment:**
|
|
||||||
- Note issues with high comment counts or engagement
|
|
||||||
- Note issues from repeat reporters (check if author has filed before)
|
|
||||||
|
|
||||||
STEP 3: Cross-reference with existing issues
|
|
||||||
For issues that seem like feature requests or recurring bugs:
|
|
||||||
- Search for similar older issues to identify patterns
|
|
||||||
- Note if this is a frequently requested feature
|
|
||||||
- Identify any issues that are duplicates of long-standing requests
|
|
||||||
|
|
||||||
STEP 4: Generate the recap
|
|
||||||
Create a structured recap with these sections:
|
|
||||||
|
|
||||||
===DISCORD_START===
|
|
||||||
**Daily Issues Recap - ${TODAY}**
|
|
||||||
|
|
||||||
**Summary Stats**
|
|
||||||
- Total issues opened today: [count]
|
|
||||||
- By category: [bugs/features/questions]
|
|
||||||
|
|
||||||
**Critical/High Priority Issues**
|
|
||||||
[List any CRITICAL or HIGH severity issues with brief descriptions and issue numbers]
|
|
||||||
|
|
||||||
**Most Active/Discussed**
|
|
||||||
[Issues with significant engagement or from active community members]
|
|
||||||
|
|
||||||
**Trending Topics**
|
|
||||||
[Patterns noticed - e.g., 'Multiple reports about X', 'Continued interest in Y feature']
|
|
||||||
|
|
||||||
**Duplicates & Related**
|
|
||||||
[Issues that relate to existing open issues]
|
|
||||||
===DISCORD_END===
|
|
||||||
|
|
||||||
STEP 5: Format for Discord
|
|
||||||
Format the recap as a Discord-compatible message:
|
|
||||||
- Use Discord markdown (**, __, etc.)
|
|
||||||
- BE EXTREMELY CONCISE - this is an EOD summary, not a detailed report
|
|
||||||
- Use hyperlinked issue numbers with suppressed embeds: [#1234](<https://github.com/${{ github.repository }}/issues/1234>)
|
|
||||||
- Group related issues on single lines where possible
|
|
||||||
- Add emoji sparingly for critical items only
|
|
||||||
- HARD LIMIT: Keep under 1800 characters total
|
|
||||||
- Skip sections that have nothing notable (e.g., if no critical issues, omit that section)
|
|
||||||
- Prioritize signal over completeness - only surface what matters
|
|
||||||
|
|
||||||
OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/recap_raw.txt
|
|
||||||
|
|
||||||
# Extract only the Discord message between markers
|
|
||||||
sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/recap_raw.txt | grep -v '===DISCORD' > /tmp/recap.txt
|
|
||||||
|
|
||||||
echo "recap_file=/tmp/recap.txt" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Post to Discord
|
|
||||||
env:
|
|
||||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }}
|
|
||||||
run: |
|
|
||||||
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
|
|
||||||
echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post"
|
|
||||||
cat /tmp/recap.txt
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Read the recap
|
|
||||||
RECAP_RAW=$(cat /tmp/recap.txt)
|
|
||||||
RECAP_LENGTH=${#RECAP_RAW}
|
|
||||||
|
|
||||||
echo "Recap length: ${RECAP_LENGTH} chars"
|
|
||||||
|
|
||||||
# Function to post a message to Discord
|
|
||||||
post_to_discord() {
|
|
||||||
local msg="$1"
|
|
||||||
local content=$(echo "$msg" | jq -Rs '.')
|
|
||||||
curl -s -H "Content-Type: application/json" \
|
|
||||||
-X POST \
|
|
||||||
-d "{\"content\": ${content}}" \
|
|
||||||
"$DISCORD_WEBHOOK_URL"
|
|
||||||
sleep 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# If under limit, send as single message
|
|
||||||
if [ "$RECAP_LENGTH" -le 1950 ]; then
|
|
||||||
post_to_discord "$RECAP_RAW"
|
|
||||||
else
|
|
||||||
echo "Splitting into multiple messages..."
|
|
||||||
remaining="$RECAP_RAW"
|
|
||||||
while [ ${#remaining} -gt 0 ]; do
|
|
||||||
if [ ${#remaining} -le 1950 ]; then
|
|
||||||
post_to_discord "$remaining"
|
|
||||||
break
|
|
||||||
else
|
|
||||||
chunk="${remaining:0:1900}"
|
|
||||||
last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1)
|
|
||||||
if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then
|
|
||||||
chunk="${remaining:0:$last_newline}"
|
|
||||||
remaining="${remaining:$((last_newline+1))}"
|
|
||||||
else
|
|
||||||
chunk="${remaining:0:1900}"
|
|
||||||
remaining="${remaining:1900}"
|
|
||||||
fi
|
|
||||||
post_to_discord "$chunk"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Posted daily recap to Discord"
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
name: daily-pr-recap
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Run at 5pm EST (22:00 UTC, or 21:00 UTC during daylight saving)
|
|
||||||
- cron: "0 22 * * *"
|
|
||||||
workflow_dispatch: # Allow manual trigger for testing
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pr-recap:
|
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
|
||||||
|
|
||||||
- name: Install opencode
|
|
||||||
run: curl -fsSL https://opencode.ai/install | bash
|
|
||||||
|
|
||||||
- name: Generate daily PR recap
|
|
||||||
id: recap
|
|
||||||
env:
|
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
OPENCODE_PERMISSION: |
|
|
||||||
{
|
|
||||||
"bash": {
|
|
||||||
"*": "deny",
|
|
||||||
"gh pr*": "allow",
|
|
||||||
"gh search*": "allow"
|
|
||||||
},
|
|
||||||
"webfetch": "deny",
|
|
||||||
"edit": "deny",
|
|
||||||
"write": "deny"
|
|
||||||
}
|
|
||||||
run: |
|
|
||||||
TODAY=$(date -u +%Y-%m-%d)
|
|
||||||
|
|
||||||
opencode run -m opencode/claude-sonnet-4-5 "Generate a daily PR activity recap for the OpenCode repository.
|
|
||||||
|
|
||||||
TODAY'S DATE: ${TODAY}
|
|
||||||
|
|
||||||
STEP 1: Gather PR data
|
|
||||||
Run these commands to gather PR information. ONLY include OPEN PRs created or updated TODAY (${TODAY}):
|
|
||||||
|
|
||||||
# Open PRs created today
|
|
||||||
gh pr list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
|
||||||
|
|
||||||
# Open PRs with activity today (updated today)
|
|
||||||
gh pr list --repo ${{ github.repository }} --state open --search \"updated:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
|
||||||
|
|
||||||
IMPORTANT: EXCLUDE all PRs authored by Anomaly team members. Filter out PRs where the author login matches ANY of these:
|
|
||||||
adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr
|
|
||||||
This recap is specifically for COMMUNITY (external) contributions only.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
STEP 2: For high-activity PRs, check comment counts
|
|
||||||
For promising PRs, run:
|
|
||||||
gh pr view [NUMBER] --repo ${{ github.repository }} --json comments --jq '[.comments[] | select(.author.login != \"copilot-pull-request-reviewer\" and .author.login != \"github-actions\")] | length'
|
|
||||||
|
|
||||||
IMPORTANT: When counting comments/activity, EXCLUDE these bot accounts:
|
|
||||||
- copilot-pull-request-reviewer
|
|
||||||
- github-actions
|
|
||||||
|
|
||||||
STEP 3: Identify what matters (ONLY from today's PRs)
|
|
||||||
|
|
||||||
**Bug Fixes From Today:**
|
|
||||||
- PRs with 'fix' or 'bug' in title created/updated today
|
|
||||||
- Small bug fixes (< 100 lines changed) that are easy to review
|
|
||||||
- Bug fixes from community contributors
|
|
||||||
|
|
||||||
**High Activity Today:**
|
|
||||||
- PRs with significant human comments today (excluding bots listed above)
|
|
||||||
- PRs with back-and-forth discussion today
|
|
||||||
|
|
||||||
**Quick Wins:**
|
|
||||||
- Small PRs (< 50 lines) that are approved or nearly approved
|
|
||||||
- PRs that just need a final review
|
|
||||||
|
|
||||||
STEP 4: Generate the recap
|
|
||||||
Create a structured recap:
|
|
||||||
|
|
||||||
===DISCORD_START===
|
|
||||||
**Daily PR Recap - ${TODAY}**
|
|
||||||
|
|
||||||
**New PRs Today**
|
|
||||||
[PRs opened today - group by type: bug fixes, features, etc.]
|
|
||||||
|
|
||||||
**Active PRs Today**
|
|
||||||
[PRs with activity/updates today - significant discussion]
|
|
||||||
|
|
||||||
**Quick Wins**
|
|
||||||
[Small PRs ready to merge]
|
|
||||||
===DISCORD_END===
|
|
||||||
|
|
||||||
STEP 5: Format for Discord
|
|
||||||
- Use Discord markdown (**, __, etc.)
|
|
||||||
- BE EXTREMELY CONCISE - surface what we might miss
|
|
||||||
- Use hyperlinked PR numbers with suppressed embeds: [#1234](<https://github.com/${{ github.repository }}/pull/1234>)
|
|
||||||
- Include PR author: [#1234](<url>) (@author)
|
|
||||||
- For bug fixes, add brief description of what it fixes
|
|
||||||
- Show line count for quick wins: \"(+15/-3 lines)\"
|
|
||||||
- HARD LIMIT: Keep under 1800 characters total
|
|
||||||
- Skip empty sections
|
|
||||||
- Focus on PRs that need human eyes
|
|
||||||
|
|
||||||
OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/pr_recap_raw.txt
|
|
||||||
|
|
||||||
# Extract only the Discord message between markers
|
|
||||||
sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/pr_recap_raw.txt | grep -v '===DISCORD' > /tmp/pr_recap.txt
|
|
||||||
|
|
||||||
echo "recap_file=/tmp/pr_recap.txt" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Post to Discord
|
|
||||||
env:
|
|
||||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }}
|
|
||||||
run: |
|
|
||||||
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
|
|
||||||
echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post"
|
|
||||||
cat /tmp/pr_recap.txt
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Read the recap
|
|
||||||
RECAP_RAW=$(cat /tmp/pr_recap.txt)
|
|
||||||
RECAP_LENGTH=${#RECAP_RAW}
|
|
||||||
|
|
||||||
echo "Recap length: ${RECAP_LENGTH} chars"
|
|
||||||
|
|
||||||
# Function to post a message to Discord
|
|
||||||
post_to_discord() {
|
|
||||||
local msg="$1"
|
|
||||||
local content=$(echo "$msg" | jq -Rs '.')
|
|
||||||
curl -s -H "Content-Type: application/json" \
|
|
||||||
-X POST \
|
|
||||||
-d "{\"content\": ${content}}" \
|
|
||||||
"$DISCORD_WEBHOOK_URL"
|
|
||||||
sleep 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# If under limit, send as single message
|
|
||||||
if [ "$RECAP_LENGTH" -le 1950 ]; then
|
|
||||||
post_to_discord "$RECAP_RAW"
|
|
||||||
else
|
|
||||||
echo "Splitting into multiple messages..."
|
|
||||||
remaining="$RECAP_RAW"
|
|
||||||
while [ ${#remaining} -gt 0 ]; do
|
|
||||||
if [ ${#remaining} -le 1950 ]; then
|
|
||||||
post_to_discord "$remaining"
|
|
||||||
break
|
|
||||||
else
|
|
||||||
chunk="${remaining:0:1900}"
|
|
||||||
last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1)
|
|
||||||
if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then
|
|
||||||
chunk="${remaining:0:$last_newline}"
|
|
||||||
remaining="${remaining:$((last_newline+1))}"
|
|
||||||
else
|
|
||||||
chunk="${remaining:0:1900}"
|
|
||||||
remaining="${remaining:1900}"
|
|
||||||
fi
|
|
||||||
post_to_discord "$chunk"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Posted daily PR recap to Discord"
|
|
||||||
+34
-185
@@ -209,182 +209,6 @@ jobs:
|
|||||||
packages/opencode/dist/opencode-windows-x64
|
packages/opencode/dist/opencode-windows-x64
|
||||||
packages/opencode/dist/opencode-windows-x64-baseline
|
packages/opencode/dist/opencode-windows-x64-baseline
|
||||||
|
|
||||||
build-tauri:
|
|
||||||
needs:
|
|
||||||
- build-cli
|
|
||||||
- version
|
|
||||||
continue-on-error: false
|
|
||||||
env:
|
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
|
||||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
|
||||||
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
|
||||||
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
|
||||||
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
settings:
|
|
||||||
- host: macos-latest
|
|
||||||
target: x86_64-apple-darwin
|
|
||||||
- host: macos-latest
|
|
||||||
target: aarch64-apple-darwin
|
|
||||||
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
|
|
||||||
- host: windows-2025
|
|
||||||
target: aarch64-pc-windows-msvc
|
|
||||||
- host: blacksmith-4vcpu-windows-2025
|
|
||||||
target: x86_64-pc-windows-msvc
|
|
||||||
- host: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
target: x86_64-unknown-linux-gnu
|
|
||||||
- host: blacksmith-8vcpu-ubuntu-2404-arm
|
|
||||||
target: aarch64-unknown-linux-gnu
|
|
||||||
runs-on: ${{ matrix.settings.host }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-tags: true
|
|
||||||
|
|
||||||
- uses: apple-actions/import-codesign-certs@v2
|
|
||||||
if: ${{ runner.os == 'macOS' }}
|
|
||||||
with:
|
|
||||||
keychain: build
|
|
||||||
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
|
|
||||||
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Verify Certificate
|
|
||||||
if: ${{ runner.os == 'macOS' }}
|
|
||||||
run: |
|
|
||||||
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
|
|
||||||
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
|
||||||
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
|
||||||
echo "Certificate imported."
|
|
||||||
|
|
||||||
- name: Setup Apple API Key
|
|
||||||
if: ${{ runner.os == 'macOS' }}
|
|
||||||
run: |
|
|
||||||
echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
|
||||||
|
|
||||||
- name: Azure login
|
|
||||||
if: runner.os == 'Windows'
|
|
||||||
uses: azure/login@v2
|
|
||||||
with:
|
|
||||||
client-id: ${{ env.AZURE_CLIENT_ID }}
|
|
||||||
tenant-id: ${{ env.AZURE_TENANT_ID }}
|
|
||||||
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "24"
|
|
||||||
|
|
||||||
- name: Cache apt packages
|
|
||||||
if: contains(matrix.settings.host, 'ubuntu')
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/apt-cache
|
|
||||||
key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-${{ hashFiles('.github/workflows/publish.yml') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-${{ matrix.settings.target }}-apt-
|
|
||||||
|
|
||||||
- name: install dependencies (ubuntu only)
|
|
||||||
if: contains(matrix.settings.host, 'ubuntu')
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/apt-cache && chmod -R a+rw ~/apt-cache
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y --no-install-recommends -o dir::cache::archives="$HOME/apt-cache" libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
|
||||||
sudo chmod -R a+rw ~/apt-cache
|
|
||||||
|
|
||||||
- name: install Rust stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
targets: ${{ matrix.settings.target }}
|
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: packages/desktop/src-tauri
|
|
||||||
shared-key: ${{ matrix.settings.target }}
|
|
||||||
|
|
||||||
- name: Prepare
|
|
||||||
run: |
|
|
||||||
cd packages/desktop
|
|
||||||
bun ./scripts/prepare.ts
|
|
||||||
env:
|
|
||||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
|
||||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
|
||||||
OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }}
|
|
||||||
RUST_TARGET: ${{ matrix.settings.target }}
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
|
||||||
|
|
||||||
- name: Resolve tauri portable SHA
|
|
||||||
if: contains(matrix.settings.host, 'ubuntu')
|
|
||||||
run: echo "TAURI_PORTABLE_SHA=$(git ls-remote https://github.com/tauri-apps/tauri.git refs/heads/feat/truly-portable-appimage | cut -f1)" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
|
||||||
- name: Install tauri-cli from portable appimage branch
|
|
||||||
uses: taiki-e/cache-cargo-install-action@v3
|
|
||||||
if: contains(matrix.settings.host, 'ubuntu')
|
|
||||||
with:
|
|
||||||
tool: tauri-cli
|
|
||||||
git: https://github.com/tauri-apps/tauri
|
|
||||||
# branch: feat/truly-portable-appimage
|
|
||||||
rev: ${{ env.TAURI_PORTABLE_SHA }}
|
|
||||||
|
|
||||||
- name: Show tauri-cli version
|
|
||||||
if: contains(matrix.settings.host, 'ubuntu')
|
|
||||||
run: cargo tauri --version
|
|
||||||
|
|
||||||
- name: Setup git committer
|
|
||||||
id: committer
|
|
||||||
uses: ./.github/actions/setup-git-committer
|
|
||||||
with:
|
|
||||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
|
||||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
|
||||||
|
|
||||||
- name: Build and upload artifacts
|
|
||||||
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
|
||||||
timeout-minutes: 60
|
|
||||||
with:
|
|
||||||
projectPath: packages/desktop
|
|
||||||
uploadWorkflowArtifacts: true
|
|
||||||
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
|
||||||
args: --target ${{ matrix.settings.target }} --config ${{ (github.ref_name == 'beta' && './src-tauri/tauri.beta.conf.json') || './src-tauri/tauri.prod.conf.json' }} --verbose
|
|
||||||
updaterJsonPreferNsis: true
|
|
||||||
releaseId: ${{ needs.version.outputs.release }}
|
|
||||||
tagName: ${{ needs.version.outputs.tag }}
|
|
||||||
releaseDraft: true
|
|
||||||
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
|
|
||||||
repo: ${{ (github.ref_name == 'beta' && 'opencode-beta') || '' }}
|
|
||||||
releaseCommitish: ${{ github.sha }}
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
|
||||||
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
||||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
||||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
||||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
|
||||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
||||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
|
||||||
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
|
||||||
|
|
||||||
- name: Verify signed Windows desktop artifacts
|
|
||||||
if: runner.os == 'Windows'
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$files = @(
|
|
||||||
"${{ github.workspace }}\packages\desktop\src-tauri\sidecars\opencode-cli-${{ matrix.settings.target }}.exe"
|
|
||||||
)
|
|
||||||
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\src-tauri\target\${{ matrix.settings.target }}\release\bundle\nsis\*.exe" | Select-Object -ExpandProperty FullName
|
|
||||||
|
|
||||||
foreach ($file in $files) {
|
|
||||||
$sig = Get-AuthenticodeSignature $file
|
|
||||||
if ($sig.Status -ne "Valid") {
|
|
||||||
throw "Invalid signature for ${file}: $($sig.Status)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
build-electron:
|
build-electron:
|
||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
@@ -524,6 +348,30 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
|
|
||||||
|
- name: Create and upload macOS .app.tar.gz
|
||||||
|
if: runner.os == 'macOS' && needs.version.outputs.release
|
||||||
|
working-directory: packages/desktop-electron/dist
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
|
run: |
|
||||||
|
if [[ "${{ matrix.settings.target }}" == "x86_64-apple-darwin" ]]; then
|
||||||
|
APP_DIR="mac"
|
||||||
|
OUT_NAME="opencode-desktop-mac-x64.app.tar.gz"
|
||||||
|
elif [[ "${{ matrix.settings.target }}" == "aarch64-apple-darwin" ]]; then
|
||||||
|
APP_DIR="mac-arm64"
|
||||||
|
OUT_NAME="opencode-desktop-mac-arm64.app.tar.gz"
|
||||||
|
else
|
||||||
|
echo "Unknown macOS target: ${{ matrix.settings.target }}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
APP_PATH=$(find "$APP_DIR" -maxdepth 1 -name "*.app" -type d | head -1)
|
||||||
|
if [ -z "$APP_PATH" ]; then
|
||||||
|
echo "No .app bundle found in $APP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
tar -czf "$OUT_NAME" -C "$(dirname "$APP_PATH")" "$(basename "$APP_PATH")"
|
||||||
|
gh release upload "v${{ needs.version.outputs.version }}" "$OUT_NAME" --clobber --repo "${{ needs.version.outputs.repo }}"
|
||||||
|
|
||||||
- name: Verify signed Windows Electron artifacts
|
- name: Verify signed Windows Electron artifacts
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
@@ -542,7 +390,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: opencode-electron-${{ matrix.settings.target }}
|
name: opencode-desktop-${{ matrix.settings.target }}
|
||||||
path: packages/desktop-electron/dist/*
|
path: packages/desktop-electron/dist/*
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
@@ -556,7 +404,6 @@ jobs:
|
|||||||
- version
|
- version
|
||||||
- build-cli
|
- build-cli
|
||||||
- sign-cli-windows
|
- sign-cli-windows
|
||||||
- build-tauri
|
|
||||||
- build-electron
|
- build-electron
|
||||||
if: always() && !failure() && !cancelled()
|
if: always() && !failure() && !cancelled()
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
@@ -583,13 +430,6 @@ jobs:
|
|||||||
node-version: "24"
|
node-version: "24"
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
- name: Setup git committer
|
|
||||||
id: committer
|
|
||||||
uses: ./.github/actions/setup-git-committer
|
|
||||||
with:
|
|
||||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
|
||||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: opencode-cli
|
name: opencode-cli
|
||||||
@@ -611,6 +451,13 @@ jobs:
|
|||||||
pattern: latest-yml-*
|
pattern: latest-yml-*
|
||||||
path: /tmp/latest-yml
|
path: /tmp/latest-yml
|
||||||
|
|
||||||
|
- name: Setup git committer
|
||||||
|
id: committer
|
||||||
|
uses: ./.github/actions/setup-git-committer
|
||||||
|
with:
|
||||||
|
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||||
|
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||||
|
|
||||||
- name: Cache apt packages (AUR)
|
- name: Cache apt packages (AUR)
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -639,3 +486,5 @@ jobs:
|
|||||||
GH_REPO: ${{ needs.version.outputs.repo }}
|
GH_REPO: ${{ needs.version.outputs.repo }}
|
||||||
NPM_CONFIG_PROVENANCE: false
|
NPM_CONFIG_PROVENANCE: false
|
||||||
LATEST_YML_DIR: /tmp/latest-yml
|
LATEST_YML_DIR: /tmp/latest-yml
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
name: vouch-check-issue
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check if issue author is denounced
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const author = context.payload.issue.user.login;
|
|
||||||
const issueNumber = context.payload.issue.number;
|
|
||||||
|
|
||||||
// Skip bots
|
|
||||||
if (author.endsWith('[bot]')) {
|
|
||||||
core.info(`Skipping bot: ${author}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the VOUCHED.td file via API (no checkout needed)
|
|
||||||
let content;
|
|
||||||
try {
|
|
||||||
const response = await github.rest.repos.getContent({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
path: '.github/VOUCHED.td',
|
|
||||||
});
|
|
||||||
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
|
|
||||||
} catch (error) {
|
|
||||||
if (error.status === 404) {
|
|
||||||
core.info('No .github/VOUCHED.td file found, skipping check.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the .td file for vouched and denounced users
|
|
||||||
const vouched = new Set();
|
|
||||||
const denounced = new Map();
|
|
||||||
for (const line of content.split('\n')) {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
||||||
|
|
||||||
const isDenounced = trimmed.startsWith('-');
|
|
||||||
const rest = isDenounced ? trimmed.slice(1).trim() : trimmed;
|
|
||||||
if (!rest) continue;
|
|
||||||
|
|
||||||
const spaceIdx = rest.indexOf(' ');
|
|
||||||
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
|
|
||||||
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
|
|
||||||
|
|
||||||
// Handle platform:username or bare username
|
|
||||||
// Only match bare usernames or github: prefix (skip other platforms)
|
|
||||||
const colonIdx = handle.indexOf(':');
|
|
||||||
if (colonIdx !== -1) {
|
|
||||||
const platform = handle.slice(0, colonIdx).toLowerCase();
|
|
||||||
if (platform !== 'github') continue;
|
|
||||||
}
|
|
||||||
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
|
|
||||||
if (!username) continue;
|
|
||||||
|
|
||||||
if (isDenounced) {
|
|
||||||
denounced.set(username.toLowerCase(), reason);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
vouched.add(username.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the author is denounced
|
|
||||||
const reason = denounced.get(author.toLowerCase());
|
|
||||||
if (reason !== undefined) {
|
|
||||||
// Author is denounced — close the issue
|
|
||||||
const body = 'This issue has been automatically closed.';
|
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issueNumber,
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
|
|
||||||
await github.rest.issues.update({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issueNumber,
|
|
||||||
state: 'closed',
|
|
||||||
state_reason: 'not_planned',
|
|
||||||
});
|
|
||||||
|
|
||||||
core.info(`Closed issue #${issueNumber} from denounced user ${author}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Author is positively vouched — add label
|
|
||||||
if (!vouched.has(author.toLowerCase())) {
|
|
||||||
core.info(`User ${author} is not denounced or vouched. Allowing issue.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issueNumber,
|
|
||||||
labels: ['Vouched'],
|
|
||||||
});
|
|
||||||
|
|
||||||
core.info(`Added vouched label to issue #${issueNumber} from ${author}`);
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
name: vouch-check-pr
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check if PR author is denounced
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const author = context.payload.pull_request.user.login;
|
|
||||||
const prNumber = context.payload.pull_request.number;
|
|
||||||
|
|
||||||
// Skip bots
|
|
||||||
if (author.endsWith('[bot]')) {
|
|
||||||
core.info(`Skipping bot: ${author}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the VOUCHED.td file via API (no checkout needed)
|
|
||||||
let content;
|
|
||||||
try {
|
|
||||||
const response = await github.rest.repos.getContent({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
path: '.github/VOUCHED.td',
|
|
||||||
});
|
|
||||||
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
|
|
||||||
} catch (error) {
|
|
||||||
if (error.status === 404) {
|
|
||||||
core.info('No .github/VOUCHED.td file found, skipping check.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the .td file for vouched and denounced users
|
|
||||||
const vouched = new Set();
|
|
||||||
const denounced = new Map();
|
|
||||||
for (const line of content.split('\n')) {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
||||||
|
|
||||||
const isDenounced = trimmed.startsWith('-');
|
|
||||||
const rest = isDenounced ? trimmed.slice(1).trim() : trimmed;
|
|
||||||
if (!rest) continue;
|
|
||||||
|
|
||||||
const spaceIdx = rest.indexOf(' ');
|
|
||||||
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
|
|
||||||
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
|
|
||||||
|
|
||||||
// Handle platform:username or bare username
|
|
||||||
// Only match bare usernames or github: prefix (skip other platforms)
|
|
||||||
const colonIdx = handle.indexOf(':');
|
|
||||||
if (colonIdx !== -1) {
|
|
||||||
const platform = handle.slice(0, colonIdx).toLowerCase();
|
|
||||||
if (platform !== 'github') continue;
|
|
||||||
}
|
|
||||||
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
|
|
||||||
if (!username) continue;
|
|
||||||
|
|
||||||
if (isDenounced) {
|
|
||||||
denounced.set(username.toLowerCase(), reason);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
vouched.add(username.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the author is denounced
|
|
||||||
const reason = denounced.get(author.toLowerCase());
|
|
||||||
if (reason !== undefined) {
|
|
||||||
// Author is denounced — close the PR
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: prNumber,
|
|
||||||
body: 'This pull request has been automatically closed.',
|
|
||||||
});
|
|
||||||
|
|
||||||
await github.rest.pulls.update({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
pull_number: prNumber,
|
|
||||||
state: 'closed',
|
|
||||||
});
|
|
||||||
|
|
||||||
core.info(`Closed PR #${prNumber} from denounced user ${author}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Author is positively vouched — add label
|
|
||||||
if (!vouched.has(author.toLowerCase())) {
|
|
||||||
core.info(`User ${author} is not denounced or vouched. Allowing PR.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: prNumber,
|
|
||||||
labels: ['Vouched'],
|
|
||||||
});
|
|
||||||
|
|
||||||
core.info(`Added vouched label to PR #${prNumber} from ${author}`);
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
name: vouch-manage-by-issue
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: vouch-manage
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
manage:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup git committer
|
|
||||||
id: committer
|
|
||||||
uses: ./.github/actions/setup-git-committer
|
|
||||||
with:
|
|
||||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
|
||||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
|
||||||
|
|
||||||
- uses: mitchellh/vouch/action/manage-by-issue@main
|
|
||||||
with:
|
|
||||||
issue-id: ${{ github.event.issue.number }}
|
|
||||||
comment-id: ${{ github.event.comment.id }}
|
|
||||||
roles: admin,maintain,write
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
|
||||||
+15
-112
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
mode: primary
|
mode: primary
|
||||||
hidden: true
|
hidden: true
|
||||||
model: opencode/minimax-m2.5
|
model: opencode/gpt-5.4-nano
|
||||||
color: "#44BA81"
|
color: "#44BA81"
|
||||||
tools:
|
tools:
|
||||||
"*": false
|
"*": false
|
||||||
@@ -14,127 +14,30 @@ Use your github-triage tool to triage issues.
|
|||||||
|
|
||||||
This file is the source of truth for ownership/routing rules.
|
This file is the source of truth for ownership/routing rules.
|
||||||
|
|
||||||
## Labels
|
Assign issues by choosing the team with the strongest overlap. The github-triage tool will assign a random member from that team.
|
||||||
|
|
||||||
### windows
|
Do not add labels to issues. Only assign an owner.
|
||||||
|
|
||||||
Use for any issue that mentions Windows (the OS). Be sure they are saying that they are on Windows.
|
When calling github-triage, pass one of these team values: tui, desktop_web, core, inference, windows.
|
||||||
|
|
||||||
- Use if they mention WSL too
|
## Teams
|
||||||
|
|
||||||
#### perf
|
### TUI
|
||||||
|
|
||||||
Performance-related issues:
|
Terminal UI issues, including rendering, keybindings, scrolling, terminal compatibility, SSH behavior, crashes in the TUI, and low-level TUI performance.
|
||||||
|
|
||||||
- Slow performance
|
### Desktop / Web
|
||||||
- High RAM usage
|
|
||||||
- High CPU usage
|
|
||||||
|
|
||||||
**Only** add if it's likely a RAM or CPU issue. **Do not** add for LLM slowness.
|
Desktop application and browser-based app issues, including `opencode web`, desktop-specific UI behavior, packaging, and web view problems.
|
||||||
|
|
||||||
#### desktop
|
### Core
|
||||||
|
|
||||||
Desktop app issues:
|
Core opencode server and harness issues, including sqlite, snapshots, memory, API behavior, agent context construction, tool execution, provider integrations, model behavior, documentation, and larger architectural features.
|
||||||
|
|
||||||
- `opencode web` command
|
### Inference
|
||||||
- The desktop app itself
|
|
||||||
|
|
||||||
**Only** add if it's specifically about the Desktop application or `opencode web` view. **Do not** add for terminal, TUI, or general opencode issues.
|
OpenCode Zen, OpenCode Go, and billing issues.
|
||||||
|
|
||||||
#### nix
|
### Windows
|
||||||
|
|
||||||
**Only** add if the issue explicitly mentions nix.
|
Windows-specific issues, including native Windows behavior, WSL interactions, path handling, shell compatibility, and installation or runtime problems that only happen on Windows.
|
||||||
|
|
||||||
If the issue does not mention nix, do not add nix.
|
|
||||||
|
|
||||||
If the issue mentions nix, assign to `rekram1-node`.
|
|
||||||
|
|
||||||
#### zen
|
|
||||||
|
|
||||||
**Only** add if the issue mentions "zen" or "opencode zen" or "opencode black".
|
|
||||||
|
|
||||||
If the issue doesn't have "zen" or "opencode black" in it then don't add zen label
|
|
||||||
|
|
||||||
#### core
|
|
||||||
|
|
||||||
Use for core server issues in `packages/opencode/`, excluding `packages/opencode/src/cli/cmd/tui/`.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- LSP server behavior
|
|
||||||
- Harness behavior (agent + tools)
|
|
||||||
- Feature requests for server behavior
|
|
||||||
- Agent context construction
|
|
||||||
- API endpoints
|
|
||||||
- Provider integration issues
|
|
||||||
- New, broken, or poor-quality models
|
|
||||||
|
|
||||||
#### acp
|
|
||||||
|
|
||||||
If the issue mentions acp support, assign acp label.
|
|
||||||
|
|
||||||
#### docs
|
|
||||||
|
|
||||||
Add if the issue requests better documentation or docs updates.
|
|
||||||
|
|
||||||
#### opentui
|
|
||||||
|
|
||||||
TUI issues potentially caused by our underlying TUI library:
|
|
||||||
|
|
||||||
- Keybindings not working
|
|
||||||
- Scroll speed issues (too fast/slow/laggy)
|
|
||||||
- Screen flickering
|
|
||||||
- Crashes with opentui in the log
|
|
||||||
|
|
||||||
**Do not** add for general TUI bugs.
|
|
||||||
|
|
||||||
When assigning to people here are the following rules:
|
|
||||||
|
|
||||||
Desktop / Web:
|
|
||||||
Use for desktop-labeled issues only.
|
|
||||||
|
|
||||||
- adamdotdevin
|
|
||||||
- iamdavidhill
|
|
||||||
- Brendonovich
|
|
||||||
- nexxeln
|
|
||||||
|
|
||||||
Zen:
|
|
||||||
ONLY assign if the issue will have the "zen" label.
|
|
||||||
|
|
||||||
- fwang
|
|
||||||
- MrMushrooooom
|
|
||||||
|
|
||||||
TUI (`packages/opencode/src/cli/cmd/tui/...`):
|
|
||||||
|
|
||||||
- thdxr for TUI UX/UI product decisions and interaction flow
|
|
||||||
- kommander for OpenTUI engine issues: rendering artifacts, keybind handling, terminal compatibility, SSH behavior, and low-level perf bottlenecks
|
|
||||||
- rekram1-node for TUI bugs that are not clearly OpenTUI engine issues
|
|
||||||
|
|
||||||
Core (`packages/opencode/...`, excluding TUI subtree):
|
|
||||||
|
|
||||||
- thdxr for sqlite/snapshot/memory bugs and larger architectural core features
|
|
||||||
- jlongster for opencode server + API feature work (tool currently remaps jlongster -> thdxr until assignable)
|
|
||||||
- rekram1-node for harness issues, provider issues, and other bug-squashing
|
|
||||||
|
|
||||||
For core bugs that do not clearly map, either thdxr or rekram1-node is acceptable.
|
|
||||||
|
|
||||||
Docs:
|
|
||||||
|
|
||||||
- R44VC0RP
|
|
||||||
|
|
||||||
Windows:
|
|
||||||
|
|
||||||
- Hona (assign any issue that mentions Windows or is likely Windows-specific)
|
|
||||||
|
|
||||||
Determinism rules:
|
|
||||||
|
|
||||||
- If title + body does not contain "zen", do not add the "zen" label
|
|
||||||
- If "nix" label is added but title + body does not mention nix/nixos, the tool will drop "nix"
|
|
||||||
- If title + body mentions nix/nixos, assign to `rekram1-node`
|
|
||||||
- If "desktop" label is added, the tool will override assignee and randomly pick one Desktop / Web owner
|
|
||||||
|
|
||||||
In all other cases, choose the team/section with the most overlap with the issue and assign a member from that team at random.
|
|
||||||
|
|
||||||
ACP:
|
|
||||||
|
|
||||||
- rekram1-node (assign any acp issues to rekram1-node)
|
|
||||||
|
|||||||
@@ -18,9 +18,12 @@ Do not use `git log` or author metadata when deciding attribution.
|
|||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- Write the final file with sections in this order:
|
- Write the final file with release sections in this order:
|
||||||
`## Core`, `## TUI`, `## Desktop`, `## SDK`, `## Extensions`
|
`## Core`, `## TUI`, `## Desktop`, `## SDK`, `## Extensions`
|
||||||
- Only include sections that have at least one notable entry
|
- Only include sections that have at least one notable entry
|
||||||
|
- Within each release section, keep bug fixes grouped under `### Bugfixes`
|
||||||
|
- Keep other notable entries under `### Improvements` when a section has bug fixes too
|
||||||
|
- Omit empty subsections
|
||||||
- Keep one bullet per commit you keep
|
- Keep one bullet per commit you keep
|
||||||
- Skip commits that are entirely internal, CI, tests, refactors, or otherwise not user-facing
|
- Skip commits that are entirely internal, CI, tests, refactors, or otherwise not user-facing
|
||||||
- Start each bullet with a capital letter
|
- Start each bullet with a capital letter
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
/// <reference path="../env.d.ts" />
|
/// <reference path="../env.d.ts" />
|
||||||
import { tool } from "@opencode-ai/plugin"
|
import { tool } from "@opencode-ai/plugin"
|
||||||
|
|
||||||
const TEAM = {
|
const TEAM = {
|
||||||
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
|
tui: ["kommander", "simonklee"],
|
||||||
zen: ["fwang", "MrMushrooooom"],
|
desktop_web: ["Hona", "Brendonovich"],
|
||||||
tui: ["kommander", "rekram1-node", "simonklee"],
|
core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton"],
|
||||||
core: ["kitlangton", "rekram1-node", "jlongster"],
|
inference: ["fwang", "MrMushrooooom"],
|
||||||
docs: ["R44VC0RP"],
|
|
||||||
windows: ["Hona"],
|
windows: ["Hona"],
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const ASSIGNEES = [...new Set(Object.values(TEAM).flat())]
|
|
||||||
|
|
||||||
function pick<T>(items: readonly T[]) {
|
function pick<T>(items: readonly T[]) {
|
||||||
return items[Math.floor(Math.random() * items.length)]!
|
return items[Math.floor(Math.random() * items.length)]!
|
||||||
}
|
}
|
||||||
@@ -38,79 +36,25 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default tool({
|
export default tool({
|
||||||
description: `Use this tool to assign and/or label a GitHub issue.
|
description: `Use this tool to assign a GitHub issue.
|
||||||
|
|
||||||
Choose labels and assignee using the current triage policy and ownership rules.
|
Provide the team that should own the issue. This tool picks a random assignee from that team and does not apply labels.`,
|
||||||
Pick the most fitting labels for the issue and assign one owner.
|
|
||||||
|
|
||||||
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.`,
|
|
||||||
args: {
|
args: {
|
||||||
assignee: tool.schema
|
team: tool.schema
|
||||||
.enum(ASSIGNEES as [string, ...string[]])
|
.enum(Object.keys(TEAM) as [keyof typeof TEAM, ...(keyof typeof TEAM)[]])
|
||||||
.describe("The username of the assignee")
|
.describe("The owning team"),
|
||||||
.default("rekram1-node"),
|
|
||||||
labels: tool.schema
|
|
||||||
.array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"]))
|
|
||||||
.describe("The labels(s) to add to the issue")
|
|
||||||
.default([]),
|
|
||||||
},
|
},
|
||||||
async execute(args) {
|
async execute(args) {
|
||||||
const issue = getIssueNumber()
|
const issue = getIssueNumber()
|
||||||
const owner = "anomalyco"
|
const owner = "anomalyco"
|
||||||
const repo = "opencode"
|
const repo = "opencode"
|
||||||
|
const assignee = pick(TEAM[args.team])
|
||||||
const results: string[] = []
|
|
||||||
let labels = [...new Set(args.labels.map((x) => (x === "desktop" ? "web" : x)))]
|
|
||||||
const web = labels.includes("web")
|
|
||||||
const text = `${process.env.ISSUE_TITLE ?? ""}\n${process.env.ISSUE_BODY ?? ""}`.toLowerCase()
|
|
||||||
const zen = /\bzen\b/.test(text) || text.includes("opencode black")
|
|
||||||
const nix = /\bnix(os)?\b/.test(text)
|
|
||||||
|
|
||||||
if (labels.includes("nix") && !nix) {
|
|
||||||
labels = labels.filter((x) => x !== "nix")
|
|
||||||
results.push("Dropped label: nix (issue does not mention nix)")
|
|
||||||
}
|
|
||||||
|
|
||||||
const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
|
|
||||||
|
|
||||||
if (labels.includes("zen") && !zen) {
|
|
||||||
throw new Error("Only add the zen label when issue title/body contains 'zen'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (web && !nix && !(TEAM.desktop as readonly string[]).includes(assignee)) {
|
|
||||||
throw new Error("Web issues must be assigned to adamdotdevin, iamdavidhill, Brendonovich, or nexxeln")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((TEAM.zen as readonly string[]).includes(assignee) && !labels.includes("zen")) {
|
|
||||||
throw new Error("Only zen issues should be assigned to fwang or MrMushrooooom")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (assignee === "Hona" && !labels.includes("windows")) {
|
|
||||||
throw new Error("Only windows issues should be assigned to Hona")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (assignee === "R44VC0RP" && !labels.includes("docs")) {
|
|
||||||
throw new Error("Only docs issues should be assigned to R44VC0RP")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (assignee === "kommander" && !labels.includes("opentui")) {
|
|
||||||
throw new Error("Only opentui issues should be assigned to kommander")
|
|
||||||
}
|
|
||||||
|
|
||||||
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, {
|
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ assignees: [assignee] }),
|
body: JSON.stringify({ assignees: [assignee] }),
|
||||||
})
|
})
|
||||||
results.push(`Assigned @${assignee} to issue #${issue}`)
|
|
||||||
|
|
||||||
if (labels.length > 0) {
|
return `Assigned @${assignee} from ${args.team} to issue #${issue}`
|
||||||
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/labels`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ labels }),
|
|
||||||
})
|
|
||||||
results.push(`Added labels: ${labels.join(", ")}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.join("\n")
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ It's very similar to Claude Code in terms of capability. Here are the key differ
|
|||||||
|
|
||||||
- 100% open source
|
- 100% open source
|
||||||
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen), OpenCode can be used with Claude, OpenAI, Google, or even local models. As models evolve, the gaps between them will close and pricing will drop, so being provider-agnostic is important.
|
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen), OpenCode can be used with Claude, OpenAI, Google, or even local models. As models evolve, the gaps between them will close and pricing will drop, so being provider-agnostic is important.
|
||||||
- Out-of-the-box LSP support
|
- Built-in opt-in LSP support
|
||||||
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
||||||
- A client/server architecture. This, for example, can allow OpenCode to run on your computer while you drive it remotely from a mobile app, meaning that the TUI frontend is just one of the possible clients.
|
- A client/server architecture. This, for example, can allow OpenCode to run on your computer while you drive it remotely from a mobile app, meaning that the TUI frontend is just one of the possible clients.
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.64",
|
"@ai-sdk/anthropic": "3.0.64",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -263,7 +263,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop-electron": {
|
"packages/desktop-electron": {
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -309,7 +309,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -338,7 +338,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -354,7 +354,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -496,7 +496,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -511,8 +511,8 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentui/core": ">=0.2.0",
|
"@opentui/core": ">=0.2.2",
|
||||||
"@opentui/solid": ">=0.2.0",
|
"@opentui/solid": ">=0.2.2",
|
||||||
},
|
},
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"@opentui/core",
|
"@opentui/core",
|
||||||
@@ -531,7 +531,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -546,7 +546,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -581,7 +581,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -630,7 +630,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -690,8 +690,8 @@
|
|||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@opentui/core": "0.2.0",
|
"@opentui/core": "0.2.2",
|
||||||
"@opentui/solid": "0.2.0",
|
"@opentui/solid": "0.2.2",
|
||||||
"@pierre/diffs": "1.1.0-beta.18",
|
"@pierre/diffs": "1.1.0-beta.18",
|
||||||
"@playwright/test": "1.59.1",
|
"@playwright/test": "1.59.1",
|
||||||
"@sentry/solid": "10.36.0",
|
"@sentry/solid": "10.36.0",
|
||||||
@@ -715,7 +715,7 @@
|
|||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
"effect": "4.0.0-beta.57",
|
"effect": "4.0.0-beta.59",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
@@ -1618,21 +1618,21 @@
|
|||||||
|
|
||||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
||||||
|
|
||||||
"@opentui/core": ["@opentui/core@0.2.0", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.2.0", "@opentui/core-darwin-x64": "0.2.0", "@opentui/core-linux-arm64": "0.2.0", "@opentui/core-linux-x64": "0.2.0", "@opentui/core-win32-arm64": "0.2.0", "@opentui/core-win32-x64": "0.2.0", "bun-webgpu": "0.1.7", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-7YOEqPUQmsgrOb9nmLEBlX8RVHPFy4HquK1C489DwfvvPTiws8nTbZ+webNQDWha7shgnYQK4Zo1EcOlpQ5+1Q=="],
|
"@opentui/core": ["@opentui/core@0.2.2", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.2", "@opentui/core-darwin-x64": "0.2.2", "@opentui/core-linux-arm64": "0.2.2", "@opentui/core-linux-x64": "0.2.2", "@opentui/core-win32-arm64": "0.2.2", "@opentui/core-win32-x64": "0.2.2" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-wxg1CD58SVrowu+WgbhZNi3UP/wWxPio2Kj2IeTjomoIE+6EXLxR8eCCxHYVuQUd9E4fknrKkY5HmiSsp6oPow=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VVmKwth3hzsQPjAZ7WGJxmzuzx0uCtynd79JJDg26D7QRM9V5beVGbKwwU5SKsDlK74EyQoY85Mv9xFY5E4jrA=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tY5n3ZRQx+b0kyhQJJLsyJMeZ+0w4FV37YZc/Qqv3qvOqE9kZPw/7adR77FYwWDm/7fax94mLMrR8Y5bKUkDmw=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-eX+WNdbSNr7Bozdq/MH6p1vXIALGt0SqBHR4YtWyTh6X7KDz9FTtJT3ylxMPqiVRUGBNAiWOxoqKGXW7JLQ0TA=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-W/R7OnqY30FXcTG0tiP2JkQFmgtYbIte5afQ5PC12TliRoee1RqG3iCG6kY1jxW+3Vg6jge88uiSjUEDpeV2gA=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ARZa+ywbN/OV7esT5ZdJMlQW3a4Pr56qLlEI/X65ik88C2sgmDze4Kf2FmqtvJ1hbv1YsMfLHH9MfhLl5twyHQ=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-1pzTYFEZauYuw6AGycw2TYGtAlZVGjuUtSdxH1fP51kBPS3oVWduUY2j7GKREz3SU5NulvO2Wc6HWsm3feMqwQ=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZjNxrD45P51cdbABoivVQLBakVYwDqAridJbHhkK6T/+EU7YsTrmAu9ae19N9ZGnrlKzLViQF8GOavNUNjAbhw=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ucVwUtUYeOYGVFPBLbPoxzbrPdhD0PDyKNQ2X4n1AJ9jlQX4gqBZRcXMEF8hiXDjFxsikZwef7De0ciCcWvAMg=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ImMjFPOWE8wcZQ2lUz1D418xonS/5EwnItUF1g5dbp1q9+A0vv2P3bxTenLwMqcYvG4wjO6gKT3n2QLnRd6qKg=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-MPhYdJNdxmC5Bqsq6sis/+VkjRgkEjm+bQ1Tl++NSKLuiTU32Re0ImcZlgHbe+LZtZoGMZHVSgZlkGd3oYXO2g=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6yfYHTtJ4yzbl8kXCW3Pc4eWbZDYVw21GumwdNgkjJJ2JqQAQ861em0riEoucYAa5qPYYTiMUEw7X4Fv8lGwuQ=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-19BroLfn2h0RDYfJS5o96Fc8kYCDhRBcseIXtHIkoKIsKMxx62KiDLo/byVye6rp+yQRRB7Xkd2uWqsbdiWo9w=="],
|
||||||
|
|
||||||
"@opentui/solid": ["@opentui/solid@0.2.0", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.0", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-kZR9i0FPAcVtomrPsKuSb+D9smooplo9zggFfU2vnnguNuQjGNbEmuJtxhCacy7ig9g3GomdNtQAzD4LiAY+3w=="],
|
"@opentui/solid": ["@opentui/solid@0.2.2", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.2", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-ZBVfCoVAhcUGQWPAWOTdzuVldMaRkuPpCu4U1VZCqmIw9DtbCuiVr0WnDocDxKhJLbTu8bl3qEWtVCf6lTSi3w=="],
|
||||||
|
|
||||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||||
|
|
||||||
@@ -2768,21 +2768,21 @@
|
|||||||
|
|
||||||
"builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="],
|
"builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="],
|
||||||
|
|
||||||
"bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
"bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="],
|
||||||
|
|
||||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
||||||
|
|
||||||
"bun-webgpu": ["bun-webgpu@0.1.7", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.7", "bun-webgpu-darwin-x64": "^0.1.7", "bun-webgpu-linux-x64": "^0.1.7", "bun-webgpu-win32-x64": "^0.1.7" } }, "sha512-KUxUp+oQIf7pPBMD4Hv1TUu7DWaOZ4ciKulTk9to9+Uc8yHoYrMW7L2SJCJ4FHHkywgf/7aLRgRx0b7i6DvGIQ=="],
|
"bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="],
|
||||||
|
|
||||||
"bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mRrFFyHzPWjsTRidAZBRcu808CPQBOUL0P6b4nxLhp+XHcV/mbUHERZMgW9s58tsojQfSdzschiQa8q+JCgRWA=="],
|
"bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lIsDkPzJzPl6yrB5CUOINJFPnTRv6fF/Q8J1mAr43ogSp86WZEg9XZKaT6f3EUJ+9ETogGoMnoj1q0AwHUTbAQ=="],
|
||||||
|
|
||||||
"bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-g0NXGNgvaVCSH/jCWWlfdiquOHkbUN6vP4zqzSkIxWKQeLnqm3oADcok7SO3yIgI7v5mKpRc/ks7NDEKNH+jNQ=="],
|
"bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-uEddf5U7GvKIkM/BV18rUKtYHL6d0KeqBjNHwfqDH9QgEo9KVSKvJXS5I/sMefk5V5pIYE+8tQhtrREevhocng=="],
|
||||||
|
|
||||||
"bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-UEP7UZdEhx9otvkZczjsszL8ZVlrODANQvgl+C88/bNVmxDoFi7w1fWzGi1sZyakiETjmtFDq2/xCLhbSZxjqw=="],
|
"bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-Y/f15j9r8ba0xUz+3lATtS74OE+PPzQXO7Do/1eCluJcuOlfa77kMjvBK/ShWnem3Y9xqi59pebTPOGRB+CaJA=="],
|
||||||
|
|
||||||
"bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.7", "", { "os": "win32", "cpu": "x64" }, "sha512-KZktiFkBz6sN7PEm1NVdeaLP5Q5X/PlSHZqefY4nNuWtf0LNvh54NhZe7yVv/Plz/nGbv92b0KHMBY3ki/pp6g=="],
|
"bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-MHSFAKqizISb+C5NfDrFe3g0Al5Njnu0j/A+oO2Q+bIWX+fUYjBSowiYE1ZXJx65KuryuB+tiM7Qh6cQbVvkEg=="],
|
||||||
|
|
||||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||||
|
|
||||||
@@ -3078,7 +3078,7 @@
|
|||||||
|
|
||||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
"effect": ["effect@4.0.0-beta.57", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-rg32VgXnLKaPRs9tbRDaZ5jxmzNY7ojXt85gSHGUTwdlbWH5Ik+OCUY2q14TXliygPGoHwCAvNWS4bQJOqf00g=="],
|
"effect": ["effect@4.0.0-beta.59", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-xyUDLeHSe8d6lWGOvR6Fgn2HL6gYeTZ/S4Jzk9uc4ZUxMPPsNZlNXrvk0C7/utQFzeX7uAWcVnG2BjbA0SRoAA=="],
|
||||||
|
|
||||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||||
|
|
||||||
@@ -4204,7 +4204,7 @@
|
|||||||
|
|
||||||
"pagefind": ["pagefind@1.5.2", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.5.2", "@pagefind/darwin-x64": "1.5.2", "@pagefind/freebsd-x64": "1.5.2", "@pagefind/linux-arm64": "1.5.2", "@pagefind/linux-x64": "1.5.2", "@pagefind/windows-arm64": "1.5.2", "@pagefind/windows-x64": "1.5.2" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q=="],
|
"pagefind": ["pagefind@1.5.2", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.5.2", "@pagefind/darwin-x64": "1.5.2", "@pagefind/freebsd-x64": "1.5.2", "@pagefind/linux-arm64": "1.5.2", "@pagefind/linux-x64": "1.5.2", "@pagefind/windows-arm64": "1.5.2", "@pagefind/windows-x64": "1.5.2" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q=="],
|
||||||
|
|
||||||
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
"pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
|
||||||
|
|
||||||
"param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="],
|
"param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="],
|
||||||
|
|
||||||
@@ -5640,6 +5640,8 @@
|
|||||||
|
|
||||||
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
||||||
|
|
||||||
|
"@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||||
|
|
||||||
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||||
|
|
||||||
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
||||||
@@ -6122,8 +6124,6 @@
|
|||||||
|
|
||||||
"type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
|
|
||||||
|
|
||||||
"unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
"unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||||
|
|
||||||
"unplugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
"unplugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||||
@@ -6132,6 +6132,8 @@
|
|||||||
|
|
||||||
"uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
|
"utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||||
|
|
||||||
"venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
"venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
|
|
||||||
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||||
@@ -6798,7 +6800,7 @@
|
|||||||
|
|
||||||
"opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.105", "", { "os": "win32", "cpu": "x64" }, "sha512-f9FqqUmxehwhF+cgyazm0YT0v0BYTTCPzd6eztqhl74N3x/kC+jOOz2rdJDC/tTBo1JVsF64KupOnhIs6/Cogg=="],
|
"opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.105", "", { "os": "win32", "cpu": "x64" }, "sha512-f9FqqUmxehwhF+cgyazm0YT0v0BYTTCPzd6eztqhl74N3x/kC+jOOz2rdJDC/tTBo1JVsF64KupOnhIs6/Cogg=="],
|
||||||
|
|
||||||
"opentui-spinner/@opentui/core/bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="],
|
"opentui-spinner/@opentui/core/bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
||||||
|
|
||||||
"opentui-spinner/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
"opentui-spinner/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||||
|
|
||||||
@@ -7158,16 +7160,6 @@
|
|||||||
|
|
||||||
"opencontrol/@modelcontextprotocol/sdk/express/type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
"opencontrol/@modelcontextprotocol/sdk/express/type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||||
|
|
||||||
"opentui-spinner/@opentui/core/bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="],
|
|
||||||
|
|
||||||
"opentui-spinner/@opentui/core/bun-webgpu/bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lIsDkPzJzPl6yrB5CUOINJFPnTRv6fF/Q8J1mAr43ogSp86WZEg9XZKaT6f3EUJ+9ETogGoMnoj1q0AwHUTbAQ=="],
|
|
||||||
|
|
||||||
"opentui-spinner/@opentui/core/bun-webgpu/bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-uEddf5U7GvKIkM/BV18rUKtYHL6d0KeqBjNHwfqDH9QgEo9KVSKvJXS5I/sMefk5V5pIYE+8tQhtrREevhocng=="],
|
|
||||||
|
|
||||||
"opentui-spinner/@opentui/core/bun-webgpu/bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-Y/f15j9r8ba0xUz+3lATtS74OE+PPzQXO7Do/1eCluJcuOlfa77kMjvBK/ShWnem3Y9xqi59pebTPOGRB+CaJA=="],
|
|
||||||
|
|
||||||
"opentui-spinner/@opentui/core/bun-webgpu/bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-MHSFAKqizISb+C5NfDrFe3g0Al5Njnu0j/A+oO2Q+bIWX+fUYjBSowiYE1ZXJx65KuryuB+tiM7Qh6cQbVvkEg=="],
|
|
||||||
|
|
||||||
"opentui-spinner/@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"opentui-spinner/@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"ora/bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
"ora/bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-OtyfKTBEHsJpjzAjN9vCR0PzGzdK6CDHdyU7eZ6Gl1s=",
|
"x86_64-linux": "sha256-9wTDLZsuGjkWyVOb6AG2VRYPiaSj/lnXwVkSwNeDcns=",
|
||||||
"aarch64-linux": "sha256-3eHJs3S/+uDUPAouWPsdBOlEvAOhOYx5bJzahL0tAJk=",
|
"aarch64-linux": "sha256-gmKlL2fQxY8bo+//8m9e1TNYJK3RXa4i8xsgtd046bc=",
|
||||||
"aarch64-darwin": "sha256-rFXzrkhPVb3yM20J8R8m7GqroNNk1vAEz+o/Ks+iAI4=",
|
"aarch64-darwin": "sha256-ENSJK+7rZi3m342mjtGg9N0P6zWEypXMpI7QdFMydbc=",
|
||||||
"x86_64-darwin": "sha256-lb1IGgbpxg723Qxj2WVPkxKUUmyOIsFOAhA5LoZ8GwY="
|
"x86_64-darwin": "sha256-gkxCxGh5dlwj03vZdz20pbiAwFEDpAlu/5iU8cwZOGI="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ stdenvNoCC.mkDerivation {
|
|||||||
--filter './packages/opencode' \
|
--filter './packages/opencode' \
|
||||||
--filter './packages/desktop' \
|
--filter './packages/desktop' \
|
||||||
--filter './packages/app' \
|
--filter './packages/app' \
|
||||||
--filter './packages/shared' \
|
|
||||||
--frozen-lockfile \
|
--frozen-lockfile \
|
||||||
--ignore-scripts \
|
--ignore-scripts \
|
||||||
--no-progress
|
--no-progress
|
||||||
|
|||||||
+3
-3
@@ -34,8 +34,8 @@
|
|||||||
"@types/cross-spawn": "6.0.6",
|
"@types/cross-spawn": "6.0.6",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@opentui/core": "0.2.0",
|
"@opentui/core": "0.2.2",
|
||||||
"@opentui/solid": "0.2.0",
|
"@opentui/solid": "0.2.2",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "3.7.1",
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
"effect": "4.0.0-beta.57",
|
"effect": "4.0.0-beta.59",
|
||||||
"ai": "6.0.168",
|
"ai": "6.0.168",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { terminalFontFamily, useSettings } from "@/context/settings"
|
|||||||
import type { LocalPTY } from "@/context/terminal"
|
import type { LocalPTY } from "@/context/terminal"
|
||||||
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
|
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
|
||||||
import { terminalWriter } from "@/utils/terminal-writer"
|
import { terminalWriter } from "@/utils/terminal-writer"
|
||||||
|
import { terminalWebSocketURL } from "@/utils/terminal-websocket-url"
|
||||||
|
|
||||||
const TOGGLE_TERMINAL_ID = "terminal.toggle"
|
const TOGGLE_TERMINAL_ID = "terminal.toggle"
|
||||||
const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`"
|
const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`"
|
||||||
@@ -67,13 +68,6 @@ const debugTerminal = (...values: unknown[]) => {
|
|||||||
console.debug("[terminal]", ...values)
|
console.debug("[terminal]", ...values)
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorName = (err: unknown) => {
|
|
||||||
if (!err || typeof err !== "object") return
|
|
||||||
if (!("name" in err)) return
|
|
||||||
const errorName = err.name
|
|
||||||
return typeof errorName === "string" ? errorName : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const useTerminalUiBindings = (input: {
|
const useTerminalUiBindings = (input: {
|
||||||
container: HTMLDivElement
|
container: HTMLDivElement
|
||||||
term: Term
|
term: Term
|
||||||
@@ -478,14 +472,34 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
|
|
||||||
const gone = () =>
|
const gone = () =>
|
||||||
client.pty
|
client.pty
|
||||||
.get({ ptyID: id })
|
.get({ ptyID: id }, { throwOnError: false })
|
||||||
.then(() => false)
|
.then((result) => result.response.status === 404)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (errorName(err) === "NotFoundError") return true
|
|
||||||
debugTerminal("failed to inspect terminal session", err)
|
debugTerminal("failed to inspect terminal session", err)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const connectToken = async () => {
|
||||||
|
const result = await client.pty
|
||||||
|
.connectToken(
|
||||||
|
{ ptyID: id, directory },
|
||||||
|
{
|
||||||
|
throwOnError: false,
|
||||||
|
headers: { "x-opencode-ticket": "1" },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (err instanceof Error && err.message.includes("Request is not supported")) return
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
if (!result) return
|
||||||
|
if (result.response.status === 200 && result.data?.ticket) return result.data.ticket
|
||||||
|
if (result.response.status === 404 || result.response.status === 405) return
|
||||||
|
if (result.response.status === 403)
|
||||||
|
throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.")
|
||||||
|
throw new Error(`PTY connect ticket failed with ${result.response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
const retry = (err: unknown) => {
|
const retry = (err: unknown) => {
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
if (reconn !== undefined) return
|
if (reconn !== undefined) return
|
||||||
@@ -505,22 +519,30 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
}, ms)
|
}, ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
const open = () => {
|
const open = async () => {
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
drop?.()
|
drop?.()
|
||||||
|
|
||||||
const next = new URL(url + `/pty/${id}/connect`)
|
const ticket = await connectToken().catch((err) => {
|
||||||
next.searchParams.set("directory", directory)
|
fail(err)
|
||||||
next.searchParams.set("cursor", String(seek))
|
return undefined
|
||||||
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
|
})
|
||||||
if (!sameOrigin && password) {
|
if (once.value) return
|
||||||
next.searchParams.set("auth_token", btoa(`${username}:${password}`))
|
if (disposed) return
|
||||||
// For same-origin requests, let the browser reuse the page's existing auth.
|
|
||||||
next.username = username
|
|
||||||
next.password = password
|
|
||||||
}
|
|
||||||
|
|
||||||
const socket = new WebSocket(next)
|
const socket = new WebSocket(
|
||||||
|
terminalWebSocketURL({
|
||||||
|
url,
|
||||||
|
id,
|
||||||
|
directory,
|
||||||
|
cursor: seek,
|
||||||
|
ticket,
|
||||||
|
sameOrigin,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
authToken: server.current?.type === "http" ? server.current.authToken : false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
socket.binaryType = "arraybuffer"
|
socket.binaryType = "arraybuffer"
|
||||||
ws = socket
|
ws = socket
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { resolveServerList, ServerConnection } from "./server"
|
||||||
|
|
||||||
|
describe("resolveServerList", () => {
|
||||||
|
test("lets startup auth_token credentials override a persisted same-url server", () => {
|
||||||
|
const list = resolveServerList({
|
||||||
|
stored: [{ url: "https://server.example.test" }],
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
type: "http",
|
||||||
|
authToken: true,
|
||||||
|
http: {
|
||||||
|
url: "https://server.example.test",
|
||||||
|
username: "opencode",
|
||||||
|
password: "secret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(list).toHaveLength(1)
|
||||||
|
expect(list[0]?.type).toBe("http")
|
||||||
|
expect(list[0]?.http).toEqual({
|
||||||
|
url: "https://server.example.test",
|
||||||
|
username: "opencode",
|
||||||
|
password: "secret",
|
||||||
|
})
|
||||||
|
expect(list[0]?.type === "http" ? list[0].authToken : false).toBe(true)
|
||||||
|
expect(ServerConnection.key(list[0]!) as string).toBe("https://server.example.test")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps persisted credentials when startup has no auth_token", () => {
|
||||||
|
const list = resolveServerList({
|
||||||
|
stored: [
|
||||||
|
{
|
||||||
|
url: "https://server.example.test",
|
||||||
|
username: "opencode",
|
||||||
|
password: "saved",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
props: [{ type: "http", http: { url: "https://server.example.test" } }],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(list).toHaveLength(1)
|
||||||
|
expect(list[0]?.type).toBe("http")
|
||||||
|
expect(list[0]?.http).toEqual({
|
||||||
|
url: "https://server.example.test",
|
||||||
|
username: "opencode",
|
||||||
|
password: "saved",
|
||||||
|
})
|
||||||
|
expect(list[0]?.type === "http" ? list[0].authToken : true).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -33,6 +33,33 @@ function isLocalHost(url: string) {
|
|||||||
if (host === "localhost" || host === "127.0.0.1") return "local"
|
if (host === "localhost" || host === "127.0.0.1") return "local"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveServerList(input: {
|
||||||
|
props?: Array<ServerConnection.Any>
|
||||||
|
stored: StoredServer[]
|
||||||
|
}): Array<ServerConnection.Any> {
|
||||||
|
const servers = [
|
||||||
|
...input.stored.map((value) =>
|
||||||
|
typeof value === "string"
|
||||||
|
? {
|
||||||
|
type: "http" as const,
|
||||||
|
http: { url: value },
|
||||||
|
}
|
||||||
|
: value,
|
||||||
|
),
|
||||||
|
...(input.props ?? []),
|
||||||
|
]
|
||||||
|
|
||||||
|
const deduped = new Map<ServerConnection.Key, ServerConnection.Any>()
|
||||||
|
for (const value of servers) {
|
||||||
|
const conn: ServerConnection.Any = "type" in value ? value : { type: "http", http: value }
|
||||||
|
const key = ServerConnection.key(conn)
|
||||||
|
if (deduped.has(key) && conn.type === "http" && !conn.authToken) continue
|
||||||
|
deduped.set(key, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...deduped.values()]
|
||||||
|
}
|
||||||
|
|
||||||
export namespace ServerConnection {
|
export namespace ServerConnection {
|
||||||
type Base = { displayName?: string }
|
type Base = { displayName?: string }
|
||||||
|
|
||||||
@@ -46,6 +73,7 @@ export namespace ServerConnection {
|
|||||||
export type Http = {
|
export type Http = {
|
||||||
type: "http"
|
type: "http"
|
||||||
http: HttpBase
|
http: HttpBase
|
||||||
|
authToken?: boolean
|
||||||
} & Base
|
} & Base
|
||||||
|
|
||||||
export type Sidecar = {
|
export type Sidecar = {
|
||||||
@@ -113,26 +141,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
const url = (x: StoredServer) => (typeof x === "string" ? x : "type" in x ? x.http.url : x.url)
|
const url = (x: StoredServer) => (typeof x === "string" ? x : "type" in x ? x.http.url : x.url)
|
||||||
|
|
||||||
const allServers = createMemo((): Array<ServerConnection.Any> => {
|
const allServers = createMemo((): Array<ServerConnection.Any> => {
|
||||||
const servers = [
|
return resolveServerList({ stored: store.list, props: props.servers })
|
||||||
...(props.servers ?? []),
|
|
||||||
...store.list.map((value) =>
|
|
||||||
typeof value === "string"
|
|
||||||
? {
|
|
||||||
type: "http" as const,
|
|
||||||
http: { url: value },
|
|
||||||
}
|
|
||||||
: value,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
const deduped = new Map(
|
|
||||||
servers.map((value) => {
|
|
||||||
const conn: ServerConnection.Any = "type" in value ? value : { type: "http", http: value }
|
|
||||||
return [ServerConnection.key(conn), conn]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return [...deduped.values()]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
@@ -174,7 +183,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
function add(input: ServerConnection.Http) {
|
function add(input: ServerConnection.Http) {
|
||||||
const url_ = normalizeServerUrl(input.http.url)
|
const url_ = normalizeServerUrl(input.http.url)
|
||||||
if (!url_) return
|
if (!url_) return
|
||||||
const conn = { ...input, http: { ...input.http, url: url_ } }
|
const conn: ServerConnection.Http = { ...input, authToken: undefined, http: { ...input.http, url: url_ } }
|
||||||
return batch(() => {
|
return batch(() => {
|
||||||
const existing = store.list.findIndex((x) => url(x) === url_)
|
const existing = store.list.findIndex((x) => url(x) === url_)
|
||||||
if (existing !== -1) {
|
if (existing !== -1) {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { beforeAll, describe, expect, mock, test } from "bun:test"
|
import { beforeAll, describe, expect, mock, test } from "bun:test"
|
||||||
|
|
||||||
let getWorkspaceTerminalCacheKey: (dir: string) => string
|
type ServerKey = Parameters<typeof import("./terminal").getTerminalServerScope>[1]
|
||||||
|
|
||||||
|
let getWorkspaceTerminalCacheKey: (dir: string, scope?: string) => string
|
||||||
|
let getTerminalServerScope: typeof import("./terminal").getTerminalServerScope
|
||||||
let getLegacyTerminalStorageKeys: (dir: string, legacySessionID?: string) => string[]
|
let getLegacyTerminalStorageKeys: (dir: string, legacySessionID?: string) => string[]
|
||||||
let migrateTerminalState: (value: unknown) => unknown
|
let migrateTerminalState: (value: unknown) => unknown
|
||||||
|
|
||||||
@@ -17,6 +20,7 @@ beforeAll(async () => {
|
|||||||
}))
|
}))
|
||||||
const mod = await import("./terminal")
|
const mod = await import("./terminal")
|
||||||
getWorkspaceTerminalCacheKey = mod.getWorkspaceTerminalCacheKey
|
getWorkspaceTerminalCacheKey = mod.getWorkspaceTerminalCacheKey
|
||||||
|
getTerminalServerScope = mod.getTerminalServerScope
|
||||||
getLegacyTerminalStorageKeys = mod.getLegacyTerminalStorageKeys
|
getLegacyTerminalStorageKeys = mod.getLegacyTerminalStorageKeys
|
||||||
migrateTerminalState = mod.migrateTerminalState
|
migrateTerminalState = mod.migrateTerminalState
|
||||||
})
|
})
|
||||||
@@ -25,6 +29,45 @@ describe("getWorkspaceTerminalCacheKey", () => {
|
|||||||
test("uses workspace-only directory cache key", () => {
|
test("uses workspace-only directory cache key", () => {
|
||||||
expect(getWorkspaceTerminalCacheKey("/repo")).toBe("/repo:__workspace__")
|
expect(getWorkspaceTerminalCacheKey("/repo")).toBe("/repo:__workspace__")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("can include a server scope", () => {
|
||||||
|
expect(getWorkspaceTerminalCacheKey("/repo", "wsl:Debian")).toBe("wsl:Debian:/repo:__workspace__")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getTerminalServerScope", () => {
|
||||||
|
test("preserves local server keys", () => {
|
||||||
|
expect(
|
||||||
|
getTerminalServerScope(
|
||||||
|
{ type: "sidecar", variant: "base", http: { url: "http://127.0.0.1:4096" } },
|
||||||
|
"sidecar" as ServerKey,
|
||||||
|
),
|
||||||
|
).toBeUndefined()
|
||||||
|
expect(
|
||||||
|
getTerminalServerScope(
|
||||||
|
{ type: "http", http: { url: "http://localhost:4096" } },
|
||||||
|
"http://localhost:4096" as ServerKey,
|
||||||
|
),
|
||||||
|
).toBeUndefined()
|
||||||
|
expect(
|
||||||
|
getTerminalServerScope({ type: "http", http: { url: "http://[::1]:4096" } }, "http://[::1]:4096" as ServerKey),
|
||||||
|
).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("scopes non-local server keys", () => {
|
||||||
|
expect(
|
||||||
|
getTerminalServerScope(
|
||||||
|
{ type: "sidecar", variant: "wsl", distro: "Debian", http: { url: "http://127.0.0.1:4096" } },
|
||||||
|
"wsl:Debian" as ServerKey,
|
||||||
|
),
|
||||||
|
).toBe("wsl:Debian" as ServerKey)
|
||||||
|
expect(
|
||||||
|
getTerminalServerScope(
|
||||||
|
{ type: "http", http: { url: "https://example.com" } },
|
||||||
|
"https://example.com" as ServerKey,
|
||||||
|
),
|
||||||
|
).toBe("https://example.com" as ServerKey)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("getLegacyTerminalStorageKeys", () => {
|
describe("getLegacyTerminalStorageKeys", () => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "soli
|
|||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import type { Platform } from "./platform"
|
import type { Platform } from "./platform"
|
||||||
|
import { ServerConnection, useServer } from "./server"
|
||||||
import { defaultTitle, titleNumber } from "./terminal-title"
|
import { defaultTitle, titleNumber } from "./terminal-title"
|
||||||
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
||||||
|
|
||||||
@@ -82,10 +83,31 @@ export function migrateTerminalState(value: unknown) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWorkspaceTerminalCacheKey(dir: string) {
|
export function getWorkspaceTerminalCacheKey(dir: string, scope?: string) {
|
||||||
|
if (scope) return `${scope}:${dir}:${WORKSPACE_KEY}`
|
||||||
return `${dir}:${WORKSPACE_KEY}`
|
return `${dir}:${WORKSPACE_KEY}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTerminalServerScope(conn: ServerConnection.Any | undefined, key: ServerConnection.Key) {
|
||||||
|
if (!conn) return
|
||||||
|
if (conn.type === "sidecar" && conn.variant === "base") return
|
||||||
|
if (conn.type === "http") {
|
||||||
|
try {
|
||||||
|
const url = new URL(conn.http.url)
|
||||||
|
if (
|
||||||
|
url.hostname === "localhost" ||
|
||||||
|
url.hostname === "127.0.0.1" ||
|
||||||
|
url.hostname === "::1" ||
|
||||||
|
url.hostname === "[::1]"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
} catch {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
export function getLegacyTerminalStorageKeys(dir: string, legacySessionID?: string) {
|
export function getLegacyTerminalStorageKeys(dir: string, legacySessionID?: string) {
|
||||||
if (!legacySessionID) return [`${dir}/terminal.v1`]
|
if (!legacySessionID) return [`${dir}/terminal.v1`]
|
||||||
return [`${dir}/terminal/${legacySessionID}.v1`, `${dir}/terminal.v1`]
|
return [`${dir}/terminal/${legacySessionID}.v1`, `${dir}/terminal.v1`]
|
||||||
@@ -110,15 +132,16 @@ const trimTerminal = (pty: LocalPTY) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform) {
|
export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform, scope?: string) {
|
||||||
const key = getWorkspaceTerminalCacheKey(dir)
|
const key = getWorkspaceTerminalCacheKey(dir, scope)
|
||||||
for (const cache of caches) {
|
for (const cache of caches) {
|
||||||
const entry = cache.get(key)
|
const entry = cache.get(key)
|
||||||
entry?.value.clear()
|
entry?.value.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
void removePersisted(Persist.workspace(dir, "terminal"), platform)
|
void removePersisted(Persist.workspace(dir, scope ? `terminal:${scope}` : "terminal"), platform)
|
||||||
|
|
||||||
|
if (scope) return
|
||||||
const legacy = new Set(getLegacyTerminalStorageKeys(dir))
|
const legacy = new Set(getLegacyTerminalStorageKeys(dir))
|
||||||
for (const id of sessionIDs ?? []) {
|
for (const id of sessionIDs ?? []) {
|
||||||
for (const key of getLegacyTerminalStorageKeys(dir, id)) {
|
for (const key of getLegacyTerminalStorageKeys(dir, id)) {
|
||||||
@@ -130,12 +153,17 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, legacySessionID?: string) {
|
function createWorkspaceTerminalSession(
|
||||||
const legacy = getLegacyTerminalStorageKeys(dir, legacySessionID)
|
sdk: ReturnType<typeof useSDK>,
|
||||||
|
dir: string,
|
||||||
|
legacySessionID?: string,
|
||||||
|
scope?: string,
|
||||||
|
) {
|
||||||
|
const legacy = scope ? [] : getLegacyTerminalStorageKeys(dir, legacySessionID)
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
{
|
{
|
||||||
...Persist.workspace(dir, "terminal", legacy),
|
...Persist.workspace(dir, scope ? `terminal:${scope}` : "terminal", legacy),
|
||||||
migrate: migrateTerminalState,
|
migrate: migrateTerminalState,
|
||||||
},
|
},
|
||||||
createStore<{
|
createStore<{
|
||||||
@@ -357,8 +385,12 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
|||||||
gate: false,
|
gate: false,
|
||||||
init: () => {
|
init: () => {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
const server = useServer()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const cache = new Map<string, TerminalCacheEntry>()
|
const cache = new Map<string, TerminalCacheEntry>()
|
||||||
|
const scope = createMemo(() => {
|
||||||
|
return getTerminalServerScope(server.current, server.key)
|
||||||
|
})
|
||||||
|
|
||||||
caches.add(cache)
|
caches.add(cache)
|
||||||
onCleanup(() => caches.delete(cache))
|
onCleanup(() => caches.delete(cache))
|
||||||
@@ -382,9 +414,9 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadWorkspace = (dir: string, legacySessionID?: string) => {
|
const loadWorkspace = (dir: string, legacySessionID: string | undefined, serverScope: string | undefined) => {
|
||||||
// Terminals are workspace-scoped so tabs persist while switching sessions in the same directory.
|
// Terminals are workspace-scoped so tabs persist while switching sessions in the same directory.
|
||||||
const key = getWorkspaceTerminalCacheKey(dir)
|
const key = getWorkspaceTerminalCacheKey(dir, serverScope)
|
||||||
const existing = cache.get(key)
|
const existing = cache.get(key)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
cache.delete(key)
|
cache.delete(key)
|
||||||
@@ -393,7 +425,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
const entry = createRoot((dispose) => ({
|
const entry = createRoot((dispose) => ({
|
||||||
value: createWorkspaceTerminalSession(sdk, dir, legacySessionID),
|
value: createWorkspaceTerminalSession(sdk, dir, legacySessionID, serverScope),
|
||||||
dispose,
|
dispose,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -402,16 +434,16 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
|||||||
return entry.value
|
return entry.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspace = createMemo(() => loadWorkspace(params.dir!, params.id))
|
const workspace = createMemo(() => loadWorkspace(params.dir!, params.id, scope()))
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => ({ dir: params.dir, id: params.id }),
|
() => ({ dir: params.dir, id: params.id, scope: scope() }),
|
||||||
(next, prev) => {
|
(next, prev) => {
|
||||||
if (!prev?.dir) return
|
if (!prev?.dir) return
|
||||||
if (next.dir === prev.dir && next.id === prev.id) return
|
if (next.dir === prev.dir && next.id === prev.id && next.scope === prev.scope) return
|
||||||
if (next.dir === prev.dir && next.id) return
|
if (next.dir === prev.dir && next.id && next.scope === prev.scope) return
|
||||||
loadWorkspace(prev.dir, prev.id).trimAll()
|
loadWorkspace(prev.dir, prev.id, prev.scope).trimAll()
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { type Platform, PlatformProvider } from "@/context/platform"
|
|||||||
import { dict as en } from "@/i18n/en"
|
import { dict as en } from "@/i18n/en"
|
||||||
import { dict as zh } from "@/i18n/zh"
|
import { dict as zh } from "@/i18n/zh"
|
||||||
import { handleNotificationClick } from "@/utils/notification-click"
|
import { handleNotificationClick } from "@/utils/notification-click"
|
||||||
|
import { authFromToken } from "@/utils/server"
|
||||||
import pkg from "../package.json"
|
import pkg from "../package.json"
|
||||||
import { ServerConnection } from "./context/server"
|
import { ServerConnection } from "./context/server"
|
||||||
|
|
||||||
@@ -111,6 +112,13 @@ const getDefaultUrl = () => {
|
|||||||
return getCurrentUrl()
|
return getCurrentUrl()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clearAuthToken = () => {
|
||||||
|
const params = new URLSearchParams(location.search)
|
||||||
|
if (!params.has("auth_token")) return
|
||||||
|
params.delete("auth_token")
|
||||||
|
history.replaceState(null, "", location.pathname + (params.size ? `?${params}` : "") + location.hash)
|
||||||
|
}
|
||||||
|
|
||||||
const platform: Platform = {
|
const platform: Platform = {
|
||||||
platform: "web",
|
platform: "web",
|
||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
@@ -146,7 +154,16 @@ if (import.meta.env.VITE_SENTRY_DSN) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (root instanceof HTMLElement) {
|
if (root instanceof HTMLElement) {
|
||||||
const server: ServerConnection.Http = { type: "http", http: { url: getCurrentUrl() } }
|
const auth = authFromToken(new URLSearchParams(location.search).get("auth_token"))
|
||||||
|
clearAuthToken()
|
||||||
|
const server: ServerConnection.Http = {
|
||||||
|
type: "http",
|
||||||
|
authToken: !!auth,
|
||||||
|
http: {
|
||||||
|
url: getCurrentUrl(),
|
||||||
|
...auth,
|
||||||
|
},
|
||||||
|
}
|
||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
<PlatformProvider value={platform}>
|
<PlatformProvider value={platform}>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import type { DragEvent } from "@thisbeyond/solid-dnd"
|
|||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
|
import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { clearWorkspaceTerminals } from "@/context/terminal"
|
import { clearWorkspaceTerminals, getTerminalServerScope } from "@/context/terminal"
|
||||||
import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache"
|
import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache"
|
||||||
import {
|
import {
|
||||||
clearSessionPrefetchInflight,
|
clearSessionPrefetchInflight,
|
||||||
@@ -1557,6 +1557,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
directory,
|
directory,
|
||||||
sessions.map((s) => s.id),
|
sessions.map((s) => s.id),
|
||||||
platform,
|
platform,
|
||||||
|
getTerminalServerScope(server.current, server.key),
|
||||||
)
|
)
|
||||||
await globalSDK.client.instance.dispose({ directory }).catch(() => undefined)
|
await globalSDK.client.instance.dispose({ directory }).catch(() => undefined)
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export function TerminalPanel() {
|
|||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
autoCreated: false,
|
autoCreated: false,
|
||||||
activeDraggable: undefined as string | undefined,
|
activeDraggable: undefined as string | undefined,
|
||||||
|
recovered: {} as Record<string, boolean>,
|
||||||
view: typeof window === "undefined" ? 1000 : (window.visualViewport?.height ?? window.innerHeight),
|
view: typeof window === "undefined" ? 1000 : (window.visualViewport?.height ?? window.innerHeight),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -145,6 +146,21 @@ export function TerminalPanel() {
|
|||||||
const all = terminal.all
|
const all = terminal.all
|
||||||
const ids = createMemo(() => all().map((pty) => pty.id))
|
const ids = createMemo(() => all().map((pty) => pty.id))
|
||||||
|
|
||||||
|
const recoverTerminal = (key: string, id: string, clone: (id: string) => Promise<void>) => {
|
||||||
|
if (store.recovered[key]) return
|
||||||
|
setStore("recovered", key, true)
|
||||||
|
void clone(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const terminalRecoveryKey = (pty: { id: string; title: string; titleNumber: number }) => {
|
||||||
|
return String(pty.titleNumber || pty.title || pty.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const markTerminalConnected = (key: string, id: string, trim: (id: string) => void) => {
|
||||||
|
setStore("recovered", key, false)
|
||||||
|
trim(id)
|
||||||
|
}
|
||||||
|
|
||||||
const handleTerminalDragStart = (event: unknown) => {
|
const handleTerminalDragStart = (event: unknown) => {
|
||||||
const id = getDraggableId(event)
|
const id = getDraggableId(event)
|
||||||
if (!id) return
|
if (!id) return
|
||||||
@@ -280,9 +296,9 @@ export function TerminalPanel() {
|
|||||||
<Terminal
|
<Terminal
|
||||||
pty={pty()}
|
pty={pty()}
|
||||||
autoFocus={opened()}
|
autoFocus={opened()}
|
||||||
onConnect={() => ops.trim(id)}
|
onConnect={() => markTerminalConnected(terminalRecoveryKey(pty()), id, ops.trim)}
|
||||||
onCleanup={ops.update}
|
onCleanup={ops.update}
|
||||||
onConnectError={() => ops.clone(id)}
|
onConnectError={() => recoverTerminal(terminalRecoveryKey(pty()), id, ops.clone)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { authFromToken, authTokenFromCredentials } from "./server"
|
||||||
|
|
||||||
|
describe("authFromToken", () => {
|
||||||
|
test("decodes basic auth credentials from auth_token", () => {
|
||||||
|
expect(authFromToken(btoa("kit:secret"))).toEqual({ username: "kit", password: "secret" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("defaults blank username to opencode", () => {
|
||||||
|
expect(authFromToken(btoa(":secret"))).toEqual({ username: "opencode", password: "secret" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("ignores malformed tokens", () => {
|
||||||
|
expect(authFromToken("not base64")).toBeUndefined()
|
||||||
|
expect(authFromToken(btoa("missing-separator"))).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("authTokenFromCredentials", () => {
|
||||||
|
test("encodes credentials with the default username", () => {
|
||||||
|
expect(authTokenFromCredentials({ password: "secret" })).toBe(btoa("opencode:secret"))
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
import type { ServerConnection } from "@/context/server"
|
import type { ServerConnection } from "@/context/server"
|
||||||
|
import { decode64 } from "@/utils/base64"
|
||||||
|
|
||||||
|
export function authTokenFromCredentials(input: { username?: string; password: string }) {
|
||||||
|
return btoa(`${input.username ?? "opencode"}:${input.password}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function authFromToken(token: string | null) {
|
||||||
|
const decoded = decode64(token ?? undefined)
|
||||||
|
if (!decoded) return
|
||||||
|
const separator = decoded.indexOf(":")
|
||||||
|
if (separator === -1) return
|
||||||
|
return {
|
||||||
|
username: decoded.slice(0, separator) || "opencode",
|
||||||
|
password: decoded.slice(separator + 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createSdkForServer({
|
export function createSdkForServer({
|
||||||
server,
|
server,
|
||||||
@@ -10,7 +26,7 @@ export function createSdkForServer({
|
|||||||
const auth = (() => {
|
const auth = (() => {
|
||||||
if (!server.password) return
|
if (!server.password) return
|
||||||
return {
|
return {
|
||||||
Authorization: `Basic ${btoa(`${server.username ?? "opencode"}:${server.password}`)}`,
|
Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`,
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { terminalWebSocketURL } from "./terminal-websocket-url"
|
||||||
|
|
||||||
|
describe("terminalWebSocketURL", () => {
|
||||||
|
test("uses query auth without embedding credentials in websocket URL", () => {
|
||||||
|
const url = terminalWebSocketURL({
|
||||||
|
url: "http://127.0.0.1:49365",
|
||||||
|
id: "pty_test",
|
||||||
|
directory: "/tmp/project",
|
||||||
|
cursor: 0,
|
||||||
|
sameOrigin: false,
|
||||||
|
username: "opencode",
|
||||||
|
password: "secret",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(url.protocol).toBe("ws:")
|
||||||
|
expect(url.username).toBe("")
|
||||||
|
expect(url.password).toBe("")
|
||||||
|
expect(url.searchParams.get("auth_token")).toBe(btoa("opencode:secret"))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("omits query auth for same-origin saved credentials", () => {
|
||||||
|
const url = terminalWebSocketURL({
|
||||||
|
url: "https://app.example.test",
|
||||||
|
id: "pty_test",
|
||||||
|
directory: "/tmp/project",
|
||||||
|
cursor: 10,
|
||||||
|
sameOrigin: true,
|
||||||
|
username: "opencode",
|
||||||
|
password: "secret",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(url.protocol).toBe("wss:")
|
||||||
|
expect(url.searchParams.has("auth_token")).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses query auth for same-origin credentials from auth_token", () => {
|
||||||
|
const url = terminalWebSocketURL({
|
||||||
|
url: "https://app.example.test",
|
||||||
|
id: "pty_test",
|
||||||
|
directory: "/tmp/project",
|
||||||
|
cursor: 10,
|
||||||
|
sameOrigin: true,
|
||||||
|
username: "opencode",
|
||||||
|
password: "secret",
|
||||||
|
authToken: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(url.protocol).toBe("wss:")
|
||||||
|
expect(url.searchParams.get("auth_token")).toBe(btoa("opencode:secret"))
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { authTokenFromCredentials } from "@/utils/server"
|
||||||
|
|
||||||
|
export function terminalWebSocketURL(input: {
|
||||||
|
url: string
|
||||||
|
id: string
|
||||||
|
directory: string
|
||||||
|
cursor: number
|
||||||
|
ticket?: string
|
||||||
|
sameOrigin?: boolean
|
||||||
|
username?: string
|
||||||
|
password?: string
|
||||||
|
authToken?: boolean
|
||||||
|
}) {
|
||||||
|
const next = new URL(`${input.url}/pty/${input.id}/connect`)
|
||||||
|
next.searchParams.set("directory", input.directory)
|
||||||
|
next.searchParams.set("cursor", String(input.cursor))
|
||||||
|
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
|
||||||
|
if (input.ticket) {
|
||||||
|
next.searchParams.set("ticket", input.ticket)
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
if (input.password && (!input.sameOrigin || input.authToken))
|
||||||
|
next.searchParams.set(
|
||||||
|
"auth_token",
|
||||||
|
authTokenFromCredentials({ username: input.username, password: input.password }),
|
||||||
|
)
|
||||||
|
return next
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -158,12 +158,14 @@ export async function handler(
|
|||||||
Object.entries(obj).flatMap(([k, v]) => {
|
Object.entries(obj).flatMap(([k, v]) => {
|
||||||
if (Array.isArray(v)) return [[k, v]]
|
if (Array.isArray(v)) return [[k, v]]
|
||||||
if (typeof v === "object") return [[k, replacer(v)]]
|
if (typeof v === "object") return [[k, replacer(v)]]
|
||||||
|
if (typeof v === "string") {
|
||||||
if (v === "$ip") return [[k, ip]]
|
if (v === "$ip") return [[k, ip]]
|
||||||
if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo?.workspaceID]] : []
|
if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo?.workspaceID]] : []
|
||||||
if (v.startsWith("$header.")) {
|
if (v.startsWith("$header.")) {
|
||||||
const headerValue = input.request.headers.get(v.slice(8))
|
const headerValue = input.request.headers.get(v.slice(8))
|
||||||
return headerValue ? [[k, headerValue]] : []
|
return headerValue ? [[k, headerValue]] : []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return [[k, v]]
|
return [[k, v]]
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -917,6 +919,13 @@ export async function handler(
|
|||||||
"tokens.cache_read": cacheReadTokens,
|
"tokens.cache_read": cacheReadTokens,
|
||||||
"tokens.cache_write_5m": cacheWrite5mTokens,
|
"tokens.cache_write_5m": cacheWrite5mTokens,
|
||||||
"tokens.cache_write_1h": cacheWrite1hTokens,
|
"tokens.cache_write_1h": cacheWrite1hTokens,
|
||||||
|
"cost.input.microcents": centsToMicroCents(inputCost),
|
||||||
|
"cost.output.microcents": centsToMicroCents(outputCost),
|
||||||
|
"cost.reasoning.microcents": reasoningCost ? centsToMicroCents(reasoningCost) : undefined,
|
||||||
|
"cost.cache_read.microcents": cacheReadCost ? centsToMicroCents(cacheReadCost) : undefined,
|
||||||
|
"cost.cache_write.microcents": cacheWrite5mCost ? centsToMicroCents(cacheWrite5mCost) : undefined,
|
||||||
|
"cost.total.microcents": centsToMicroCents(totalCostInCent),
|
||||||
|
// deprecated - remove after May 20, 2026
|
||||||
"cost.input": Math.round(inputCost),
|
"cost.input": Math.round(inputCost),
|
||||||
"cost.output": Math.round(outputCost),
|
"cost.output": Math.round(outputCost),
|
||||||
"cost.reasoning": reasoningCost ? Math.round(reasoningCost) : undefined,
|
"cost.reasoning": reasoningCost ? Math.round(reasoningCost) : undefined,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export namespace AppFileSystem {
|
|||||||
readonly isDir: (path: string) => Effect.Effect<boolean>
|
readonly isDir: (path: string) => Effect.Effect<boolean>
|
||||||
readonly isFile: (path: string) => Effect.Effect<boolean>
|
readonly isFile: (path: string) => Effect.Effect<boolean>
|
||||||
readonly existsSafe: (path: string) => Effect.Effect<boolean>
|
readonly existsSafe: (path: string) => Effect.Effect<boolean>
|
||||||
|
readonly readFileStringSafe: (path: string) => Effect.Effect<string | undefined, Error>
|
||||||
readonly readJson: (path: string) => Effect.Effect<unknown, Error>
|
readonly readJson: (path: string) => Effect.Effect<unknown, Error>
|
||||||
readonly writeJson: (path: string, data: unknown, mode?: number) => Effect.Effect<void, Error>
|
readonly writeJson: (path: string, data: unknown, mode?: number) => Effect.Effect<void, Error>
|
||||||
readonly ensureDir: (path: string) => Effect.Effect<void, Error>
|
readonly ensureDir: (path: string) => Effect.Effect<void, Error>
|
||||||
@@ -47,6 +48,12 @@ export namespace AppFileSystem {
|
|||||||
return yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false))
|
return yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const readFileStringSafe = Effect.fn("FileSystem.readFileStringSafe")(function* (path: string) {
|
||||||
|
return yield* fs
|
||||||
|
.readFileString(path)
|
||||||
|
.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)))
|
||||||
|
})
|
||||||
|
|
||||||
const isDir = Effect.fn("FileSystem.isDir")(function* (path: string) {
|
const isDir = Effect.fn("FileSystem.isDir")(function* (path: string) {
|
||||||
const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void))
|
const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void))
|
||||||
return info?.type === "Directory"
|
return info?.type === "Directory"
|
||||||
@@ -163,6 +170,7 @@ export namespace AppFileSystem {
|
|||||||
return Service.of({
|
return Service.of({
|
||||||
...fs,
|
...fs,
|
||||||
existsSafe,
|
existsSafe,
|
||||||
|
readFileStringSafe,
|
||||||
isDir,
|
isDir,
|
||||||
isFile,
|
isFile,
|
||||||
readDirectoryEntries,
|
readDirectoryEntries,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Config } from "effect"
|
import { Config } from "effect"
|
||||||
|
import { InstallationChannel } from "../installation/version"
|
||||||
|
|
||||||
function truthy(key: string) {
|
function truthy(key: string) {
|
||||||
const value = process.env[key]?.toLowerCase()
|
const value = process.env[key]?.toLowerCase()
|
||||||
@@ -10,6 +11,10 @@ function falsy(key: string) {
|
|||||||
return value === "false" || value === "0"
|
return value === "false" || value === "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Channels that default to the new effect-httpapi server backend. The legacy
|
||||||
|
// hono backend remains the default for stable (`prod`/`latest`) installs.
|
||||||
|
const HTTPAPI_DEFAULT_ON_CHANNELS = new Set(["dev", "beta", "local"])
|
||||||
|
|
||||||
function number(key: string) {
|
function number(key: string) {
|
||||||
const value = process.env[key]
|
const value = process.env[key]
|
||||||
if (!value) return undefined
|
if (!value) return undefined
|
||||||
@@ -81,8 +86,16 @@ export const Flag = {
|
|||||||
OPENCODE_STRICT_CONFIG_DEPS: truthy("OPENCODE_STRICT_CONFIG_DEPS"),
|
OPENCODE_STRICT_CONFIG_DEPS: truthy("OPENCODE_STRICT_CONFIG_DEPS"),
|
||||||
|
|
||||||
OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"],
|
OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"],
|
||||||
OPENCODE_EXPERIMENTAL_HTTPAPI: truthy("OPENCODE_EXPERIMENTAL_HTTPAPI"),
|
// Defaults to true on dev/beta/local channels so internal users exercise the
|
||||||
|
// new effect-httpapi server backend. Stable (`prod`/`latest`) installs stay
|
||||||
|
// on the legacy hono backend until the rollout is complete. An explicit env
|
||||||
|
// var ("true"/"1" or "false"/"0") always wins, providing an opt-in for
|
||||||
|
// stable users and an escape hatch for dev/beta users.
|
||||||
|
OPENCODE_EXPERIMENTAL_HTTPAPI:
|
||||||
|
truthy("OPENCODE_EXPERIMENTAL_HTTPAPI") ||
|
||||||
|
(!falsy("OPENCODE_EXPERIMENTAL_HTTPAPI") && HTTPAPI_DEFAULT_ON_CHANNELS.has(InstallationChannel)),
|
||||||
OPENCODE_EXPERIMENTAL_WORKSPACES: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WORKSPACES"),
|
OPENCODE_EXPERIMENTAL_WORKSPACES: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WORKSPACES"),
|
||||||
|
OPENCODE_EXPERIMENTAL_EVENT_SYSTEM: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"),
|
||||||
|
|
||||||
// Evaluated at access time (not module load) because tests, the CLI, and
|
// Evaluated at access time (not module load) because tests, the CLI, and
|
||||||
// external tooling set these env vars at runtime.
|
// external tooling set these env vars at runtime.
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export * as Log from "./log"
|
||||||
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { createWriteStream } from "fs"
|
import { createWriteStream } from "fs"
|
||||||
|
|||||||
@@ -65,6 +65,34 @@ describe("AppFileSystem", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("readFileStringSafe", () => {
|
||||||
|
it(
|
||||||
|
"returns file contents when file exists",
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const fs = yield* AppFileSystem.Service
|
||||||
|
const filesys = yield* FileSystem.FileSystem
|
||||||
|
const tmp = yield* filesys.makeTempDirectoryScoped()
|
||||||
|
const file = path.join(tmp, "exists.txt")
|
||||||
|
yield* filesys.writeFileString(file, "hello")
|
||||||
|
|
||||||
|
const result = yield* fs.readFileStringSafe(file)
|
||||||
|
expect(result).toBe("hello")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it(
|
||||||
|
"returns undefined for missing file (NotFound)",
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const fs = yield* AppFileSystem.Service
|
||||||
|
const filesys = yield* FileSystem.FileSystem
|
||||||
|
const tmp = yield* filesys.makeTempDirectoryScoped()
|
||||||
|
|
||||||
|
const result = yield* fs.readFileStringSafe(path.join(tmp, "does-not-exist.txt"))
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
describe("readJson / writeJson", () => {
|
describe("readJson / writeJson", () => {
|
||||||
it(
|
it(
|
||||||
"round-trips JSON data",
|
"round-trips JSON data",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const channel = (() => {
|
|||||||
})()
|
})()
|
||||||
|
|
||||||
const getBase = (): Configuration => ({
|
const getBase = (): Configuration => ({
|
||||||
artifactName: "opencode-electron-${os}-${arch}.${ext}",
|
artifactName: "opencode-desktop-${os}-${arch}.${ext}",
|
||||||
directories: {
|
directories: {
|
||||||
output: "dist",
|
output: "dist",
|
||||||
buildResources: "resources",
|
buildResources: "resources",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://opencode.ai",
|
"homepage": "https://opencode.ai",
|
||||||
|
|||||||
@@ -26,13 +26,20 @@ const applyZoom = (next: number) => {
|
|||||||
window.addEventListener("keydown", (event) => {
|
window.addEventListener("keydown", (event) => {
|
||||||
if (!(OS_NAME === "macos" ? event.metaKey : event.ctrlKey)) return
|
if (!(OS_NAME === "macos" ? event.metaKey : event.ctrlKey)) return
|
||||||
|
|
||||||
let newZoom = webviewZoom()
|
if (event.key === "-") {
|
||||||
|
event.preventDefault()
|
||||||
if (event.key === "-") newZoom -= 0.2
|
applyZoom(clamp(webviewZoom() - 0.2))
|
||||||
if (event.key === "=" || event.key === "+") newZoom += 0.2
|
return
|
||||||
if (event.key === "0") newZoom = 1
|
}
|
||||||
|
if (event.key === "=" || event.key === "+") {
|
||||||
applyZoom(clamp(newZoom))
|
event.preventDefault()
|
||||||
|
applyZoom(clamp(webviewZoom() + 0.2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === "0") {
|
||||||
|
event.preventDefault()
|
||||||
|
applyZoom(1)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export { webviewZoom }
|
export { webviewZoom }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
import { Buffer } from "node:buffer"
|
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
|
import path from "node:path"
|
||||||
|
import { parseArgs } from "node:util"
|
||||||
|
|
||||||
const { values } = parseArgs({
|
const { values } = parseArgs({
|
||||||
args: Bun.argv.slice(2),
|
args: Bun.argv.slice(2),
|
||||||
@@ -12,8 +13,6 @@ const { values } = parseArgs({
|
|||||||
|
|
||||||
const dryRun = values["dry-run"]
|
const dryRun = values["dry-run"]
|
||||||
|
|
||||||
import { parseArgs } from "node:util"
|
|
||||||
|
|
||||||
const repo = process.env.GH_REPO
|
const repo = process.env.GH_REPO
|
||||||
if (!repo) throw new Error("GH_REPO is required")
|
if (!repo) throw new Error("GH_REPO is required")
|
||||||
|
|
||||||
@@ -23,20 +22,22 @@ if (!releaseId) throw new Error("OPENCODE_RELEASE is required")
|
|||||||
const version = process.env.OPENCODE_VERSION
|
const version = process.env.OPENCODE_VERSION
|
||||||
if (!version) throw new Error("OPENCODE_VERSION is required")
|
if (!version) throw new Error("OPENCODE_VERSION is required")
|
||||||
|
|
||||||
|
const dir = process.env.LATEST_YML_DIR
|
||||||
|
if (!dir) throw new Error("LATEST_YML_DIR is required")
|
||||||
|
const root = dir
|
||||||
|
|
||||||
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN
|
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN
|
||||||
if (!token) throw new Error("GH_TOKEN or GITHUB_TOKEN is required")
|
if (!token) throw new Error("GH_TOKEN or GITHUB_TOKEN is required")
|
||||||
|
|
||||||
const apiHeaders = {
|
const rel = await fetch(`https://api.github.com/repos/${repo}/releases/${releaseId}`, {
|
||||||
|
headers: {
|
||||||
Authorization: `token ${token}`,
|
Authorization: `token ${token}`,
|
||||||
Accept: "application/vnd.github+json",
|
Accept: "application/vnd.github+json",
|
||||||
}
|
},
|
||||||
|
|
||||||
const releaseRes = await fetch(`https://api.github.com/repos/${repo}/releases/${releaseId}`, {
|
|
||||||
headers: apiHeaders,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!releaseRes.ok) {
|
if (!rel.ok) {
|
||||||
throw new Error(`Failed to fetch release: ${releaseRes.status} ${releaseRes.statusText}`)
|
throw new Error(`Failed to fetch release: ${rel.status} ${rel.statusText}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Asset = {
|
type Asset = {
|
||||||
@@ -45,115 +46,169 @@ type Asset = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Release = {
|
type Release = {
|
||||||
tag_name?: string
|
|
||||||
assets?: Asset[]
|
assets?: Asset[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const release = (await releaseRes.json()) as Release
|
const assets = ((await rel.json()) as Release).assets ?? []
|
||||||
const assets = release.assets ?? []
|
const amap = new Map(assets.map((item) => [item.name, item]))
|
||||||
const assetByName = new Map(assets.map((asset) => [asset.name, asset]))
|
|
||||||
|
|
||||||
const latestAsset = assetByName.get("latest.json")
|
type Item = {
|
||||||
if (!latestAsset) {
|
url: string
|
||||||
console.log("latest.json not found, skipping tauri finalization")
|
|
||||||
process.exit(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestRes = await fetch(latestAsset.url, {
|
type Yml = {
|
||||||
headers: {
|
version: string
|
||||||
Authorization: `token ${token}`,
|
files: Item[]
|
||||||
Accept: "application/octet-stream",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!latestRes.ok) {
|
|
||||||
throw new Error(`Failed to fetch latest.json: ${latestRes.status} ${latestRes.statusText}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestText = new TextDecoder().decode(await latestRes.arrayBuffer())
|
function parse(text: string): Yml {
|
||||||
const latest = JSON.parse(latestText)
|
const lines = text.split("\n")
|
||||||
const base = { ...latest }
|
let version = ""
|
||||||
delete base.platforms
|
const files: Item[] = []
|
||||||
|
let url = ""
|
||||||
|
|
||||||
const fetchSignature = async (asset: Asset) => {
|
const flush = () => {
|
||||||
const res = await fetch(asset.url, {
|
if (!url) return
|
||||||
|
files.push({ url })
|
||||||
|
url = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trim = line.trim()
|
||||||
|
if (line.startsWith("version:")) {
|
||||||
|
version = line.slice("version:".length).trim()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (trim.startsWith("- url:")) {
|
||||||
|
flush()
|
||||||
|
url = trim.slice("- url:".length).trim()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const indented = line.startsWith(" ") || line.startsWith("\t")
|
||||||
|
if (!indented) flush()
|
||||||
|
}
|
||||||
|
flush()
|
||||||
|
|
||||||
|
return { version, files }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function read(sub: string, file: string) {
|
||||||
|
const item = Bun.file(path.join(root, sub, file))
|
||||||
|
if (!(await item.exists())) return undefined
|
||||||
|
return parse(await item.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
function pick(list: Item[], exts: string[]) {
|
||||||
|
for (const ext of exts) {
|
||||||
|
const found = list.find((item) => item.url.split("?")[0]?.toLowerCase().endsWith(ext))
|
||||||
|
if (found) return found.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function link(raw: string) {
|
||||||
|
if (raw.startsWith("https://") || raw.startsWith("http://")) return raw
|
||||||
|
return `https://github.com/${repo}/releases/download/v${version}/${raw}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sign(url: string, key: string) {
|
||||||
|
const name = decodeURIComponent(new URL(url).pathname.split("/").pop() ?? key)
|
||||||
|
const asset = amap.get(name)
|
||||||
|
const res = await fetch(asset?.url ?? url, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `token ${token}`,
|
Authorization: `token ${token}`,
|
||||||
Accept: "application/octet-stream",
|
...(asset ? { Accept: "application/octet-stream" } : {}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`Failed to fetch signature: ${res.status} ${res.statusText}`)
|
throw new Error(`Failed to fetch file ${name}: ${res.status} ${res.statusText} (${asset?.url ?? url})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Buffer.from(await res.arrayBuffer()).toString()
|
const tmp = process.env.RUNNER_TEMP ?? "/tmp"
|
||||||
|
const file = path.join(tmp, name)
|
||||||
|
await Bun.write(file, await res.arrayBuffer())
|
||||||
|
await $`bunx @tauri-apps/cli signer sign ${file}`
|
||||||
|
const sigFile = Bun.file(`${file}.sig`)
|
||||||
|
if (!(await sigFile.exists())) throw new Error(`Signature file not found for ${name}`)
|
||||||
|
return (await sigFile.text()).trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries: Record<string, { url: string; signature: string }> = {}
|
const add = async (data: Record<string, { url: string; signature: string }>, key: string, raw: string | undefined) => {
|
||||||
const add = (key: string, asset: Asset, signature: string) => {
|
if (!raw) return
|
||||||
if (entries[key]) return
|
if (data[key]) return
|
||||||
entries[key] = {
|
const url = link(raw)
|
||||||
url: `https://github.com/${repo}/releases/download/v${version}/${asset.name}`,
|
data[key] = { url, signature: await sign(url, key) }
|
||||||
signature,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const targets = [
|
const alias = (data: Record<string, { url: string; signature: string }>, key: string, src: string) => {
|
||||||
{ key: "linux-x86_64-deb", asset: "opencode-desktop-linux-amd64.deb" },
|
if (data[key]) return
|
||||||
{ key: "linux-x86_64-rpm", asset: "opencode-desktop-linux-x86_64.rpm" },
|
if (!data[src]) return
|
||||||
{ key: "linux-aarch64-deb", asset: "opencode-desktop-linux-arm64.deb" },
|
data[key] = data[src]
|
||||||
{ key: "linux-aarch64-rpm", asset: "opencode-desktop-linux-aarch64.rpm" },
|
|
||||||
{ key: "windows-aarch64-nsis", asset: "opencode-desktop-windows-arm64.exe" },
|
|
||||||
{ key: "windows-x86_64-nsis", asset: "opencode-desktop-windows-x64.exe" },
|
|
||||||
{ key: "darwin-x86_64-app", asset: "opencode-desktop-darwin-x64.app.tar.gz" },
|
|
||||||
{
|
|
||||||
key: "darwin-aarch64-app",
|
|
||||||
asset: "opencode-desktop-darwin-aarch64.app.tar.gz",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const target of targets) {
|
|
||||||
const asset = assetByName.get(target.asset)
|
|
||||||
if (!asset) continue
|
|
||||||
|
|
||||||
const sig = assetByName.get(`${target.asset}.sig`)
|
|
||||||
if (!sig) continue
|
|
||||||
|
|
||||||
const signature = await fetchSignature(sig)
|
|
||||||
add(target.key, asset, signature)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const alias = (key: string, source: string) => {
|
const winx = await read("latest-yml-x86_64-pc-windows-msvc", "latest.yml")
|
||||||
if (entries[key]) return
|
const wina = await read("latest-yml-aarch64-pc-windows-msvc", "latest.yml")
|
||||||
const entry = entries[source]
|
const macx = await read("latest-yml-x86_64-apple-darwin", "latest-mac.yml")
|
||||||
if (!entry) return
|
const maca = await read("latest-yml-aarch64-apple-darwin", "latest-mac.yml")
|
||||||
entries[key] = entry
|
const linx = await read("latest-yml-x86_64-unknown-linux-gnu", "latest-linux.yml")
|
||||||
}
|
const lina = await read("latest-yml-aarch64-unknown-linux-gnu", "latest-linux-arm64.yml")
|
||||||
|
|
||||||
alias("linux-x86_64", "linux-x86_64-deb")
|
const yver = winx?.version ?? wina?.version ?? macx?.version ?? maca?.version ?? linx?.version ?? lina?.version
|
||||||
alias("linux-aarch64", "linux-aarch64-deb")
|
if (yver && yver !== version) throw new Error(`latest.yml version mismatch: expected ${version}, got ${yver}`)
|
||||||
alias("windows-aarch64", "windows-aarch64-nsis")
|
|
||||||
alias("windows-x86_64", "windows-x86_64-nsis")
|
const out: Record<string, { url: string; signature: string }> = {}
|
||||||
alias("darwin-x86_64", "darwin-x86_64-app")
|
|
||||||
alias("darwin-aarch64", "darwin-aarch64-app")
|
const winxexe = pick(winx?.files ?? [], [".exe"])
|
||||||
|
const winaexe = pick(wina?.files ?? [], [".exe"])
|
||||||
|
|
||||||
|
const macxTarGz = "opencode-desktop-mac-x64.app.tar.gz"
|
||||||
|
const macaTarGz = "opencode-desktop-mac-arm64.app.tar.gz"
|
||||||
|
|
||||||
|
const linxDeb = pick(linx?.files ?? [], [".deb"])
|
||||||
|
const linxRpm = pick(linx?.files ?? [], [".rpm"])
|
||||||
|
const linxAppImage = pick(linx?.files ?? [], [".appimage"])
|
||||||
|
const linaDeb = pick(lina?.files ?? [], [".deb"])
|
||||||
|
const linaRpm = pick(lina?.files ?? [], [".rpm"])
|
||||||
|
const linaAppImage = pick(lina?.files ?? [], [".appimage"])
|
||||||
|
|
||||||
|
await add(out, "windows-x86_64-nsis", winxexe)
|
||||||
|
await add(out, "windows-aarch64-nsis", winaexe)
|
||||||
|
await add(out, "darwin-x86_64-app", macxTarGz)
|
||||||
|
await add(out, "darwin-aarch64-app", macaTarGz)
|
||||||
|
|
||||||
|
await add(out, "linux-x86_64-deb", linxDeb)
|
||||||
|
await add(out, "linux-x86_64-rpm", linxRpm)
|
||||||
|
await add(out, "linux-x86_64-appimage", linxAppImage)
|
||||||
|
await add(out, "linux-aarch64-deb", linaDeb)
|
||||||
|
await add(out, "linux-aarch64-rpm", linaRpm)
|
||||||
|
await add(out, "linux-aarch64-appimage", linaAppImage)
|
||||||
|
|
||||||
|
alias(out, "windows-x86_64", "windows-x86_64-nsis")
|
||||||
|
alias(out, "windows-aarch64", "windows-aarch64-nsis")
|
||||||
|
alias(out, "darwin-x86_64", "darwin-x86_64-app")
|
||||||
|
alias(out, "darwin-aarch64", "darwin-aarch64-app")
|
||||||
|
alias(out, "linux-x86_64", "linux-x86_64-deb")
|
||||||
|
alias(out, "linux-aarch64", "linux-aarch64-deb")
|
||||||
|
|
||||||
const platforms = Object.fromEntries(
|
const platforms = Object.fromEntries(
|
||||||
Object.keys(entries)
|
Object.keys(out)
|
||||||
.sort()
|
.sort()
|
||||||
.map((key) => [key, entries[key]]),
|
.map((key) => [key, out[key]]),
|
||||||
)
|
)
|
||||||
const output = {
|
|
||||||
...base,
|
if (!Object.keys(platforms).length) throw new Error("No updater files found in latest.yml artifacts")
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
version,
|
||||||
|
notes: "",
|
||||||
|
pub_date: new Date().toISOString(),
|
||||||
platforms,
|
platforms,
|
||||||
}
|
}
|
||||||
|
|
||||||
const dir = process.env.RUNNER_TEMP ?? "/tmp"
|
const tmp = process.env.RUNNER_TEMP ?? "/tmp"
|
||||||
const file = `${dir}/latest.json`
|
const file = path.join(tmp, "latest.json")
|
||||||
await Bun.write(file, JSON.stringify(output, null, 2))
|
await Bun.write(file, JSON.stringify(data, null, 2))
|
||||||
|
|
||||||
const tag = release.tag_name
|
const tag = `v${version}`
|
||||||
if (!tag) throw new Error("Release tag not found")
|
|
||||||
|
|
||||||
if (dryRun) {
|
if (dryRun) {
|
||||||
console.log(`dry-run: wrote latest.json for ${tag} to ${file}`)
|
console.log(`dry-run: wrote latest.json for ${tag} to ${file}`)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "1.14.31"
|
version = "1.14.35"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/anomalyco/opencode"
|
repository = "https://github.com/anomalyco/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-darwin-arm64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.35/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-darwin-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.35/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.35/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.35/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-windows-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.35/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
CREATE TABLE `session_message` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`session_id` text NOT NULL,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`time_created` integer NOT NULL,
|
||||||
|
`time_updated` integer NOT NULL,
|
||||||
|
`data` text NOT NULL,
|
||||||
|
CONSTRAINT `fk_session_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
DROP INDEX IF EXISTS `session_entry_session_idx`;--> statement-breakpoint
|
||||||
|
DROP INDEX IF EXISTS `session_entry_session_type_idx`;--> statement-breakpoint
|
||||||
|
DROP INDEX IF EXISTS `session_entry_time_created_idx`;--> statement-breakpoint
|
||||||
|
CREATE INDEX `session_message_session_idx` ON `session_message` (`session_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `session_message_session_type_idx` ON `session_message` (`session_id`,`type`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `session_message_time_created_idx` ON `session_message` (`time_created`);--> statement-breakpoint
|
||||||
|
DROP TABLE `session_entry`;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
|||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "aaa2ebeb-caa4-478d-8365-4fc595d16856",
|
"id": "aaa2ebeb-caa4-478d-8365-4fc595d16856",
|
||||||
"prevIds": ["66cbe0d7-def0-451b-b88a-7608513a9b44"],
|
"prevIds": ["61f807f9-6398-4067-be05-804acc2561bc"],
|
||||||
"ddl": [
|
"ddl": [
|
||||||
{
|
{
|
||||||
"name": "account_state",
|
"name": "account_state",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"entityType": "tables"
|
"entityType": "tables"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "session_entry",
|
"name": "session_message",
|
||||||
"entityType": "tables"
|
"entityType": "tables"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -598,7 +598,7 @@
|
|||||||
"generated": null,
|
"generated": null,
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"entityType": "columns",
|
"entityType": "columns",
|
||||||
"table": "session_entry"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -608,7 +608,7 @@
|
|||||||
"generated": null,
|
"generated": null,
|
||||||
"name": "session_id",
|
"name": "session_id",
|
||||||
"entityType": "columns",
|
"entityType": "columns",
|
||||||
"table": "session_entry"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -618,7 +618,7 @@
|
|||||||
"generated": null,
|
"generated": null,
|
||||||
"name": "type",
|
"name": "type",
|
||||||
"entityType": "columns",
|
"entityType": "columns",
|
||||||
"table": "session_entry"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -628,7 +628,7 @@
|
|||||||
"generated": null,
|
"generated": null,
|
||||||
"name": "time_created",
|
"name": "time_created",
|
||||||
"entityType": "columns",
|
"entityType": "columns",
|
||||||
"table": "session_entry"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -638,7 +638,7 @@
|
|||||||
"generated": null,
|
"generated": null,
|
||||||
"name": "time_updated",
|
"name": "time_updated",
|
||||||
"entityType": "columns",
|
"entityType": "columns",
|
||||||
"table": "session_entry"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -648,7 +648,7 @@
|
|||||||
"generated": null,
|
"generated": null,
|
||||||
"name": "data",
|
"name": "data",
|
||||||
"entityType": "columns",
|
"entityType": "columns",
|
||||||
"table": "session_entry"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -1112,9 +1112,9 @@
|
|||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "fk_session_entry_session_id_session_id_fk",
|
"name": "fk_session_message_session_id_session_id_fk",
|
||||||
"entityType": "fks",
|
"entityType": "fks",
|
||||||
"table": "session_entry"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id"],
|
"columns": ["project_id"],
|
||||||
@@ -1226,8 +1226,8 @@
|
|||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": ["id"],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_entry_pk",
|
"name": "session_message_pk",
|
||||||
"table": "session_entry",
|
"table": "session_message",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1322,9 +1322,9 @@
|
|||||||
"isUnique": false,
|
"isUnique": false,
|
||||||
"where": null,
|
"where": null,
|
||||||
"origin": "manual",
|
"origin": "manual",
|
||||||
"name": "session_entry_session_idx",
|
"name": "session_message_session_idx",
|
||||||
"entityType": "indexes",
|
"entityType": "indexes",
|
||||||
"table": "session_entry"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": [
|
||||||
@@ -1340,9 +1340,9 @@
|
|||||||
"isUnique": false,
|
"isUnique": false,
|
||||||
"where": null,
|
"where": null,
|
||||||
"origin": "manual",
|
"origin": "manual",
|
||||||
"name": "session_entry_session_type_idx",
|
"name": "session_message_session_type_idx",
|
||||||
"entityType": "indexes",
|
"entityType": "indexes",
|
||||||
"table": "session_entry"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": [
|
||||||
@@ -1354,9 +1354,9 @@
|
|||||||
"isUnique": false,
|
"isUnique": false,
|
||||||
"where": null,
|
"where": null,
|
||||||
"origin": "manual",
|
"origin": "manual",
|
||||||
"name": "session_entry_time_created_idx",
|
"name": "session_message_time_created_idx",
|
||||||
"entityType": "indexes",
|
"entityType": "indexes",
|
||||||
"table": "session_entry"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": [
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `session` ADD `agent` text;--> statement-breakpoint
|
||||||
|
ALTER TABLE `session` ADD `model` text;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.14.31",
|
"version": "1.14.35",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -37,6 +37,11 @@
|
|||||||
"bun": "./src/server/adapter.bun.ts",
|
"bun": "./src/server/adapter.bun.ts",
|
||||||
"node": "./src/server/adapter.node.ts",
|
"node": "./src/server/adapter.node.ts",
|
||||||
"default": "./src/server/adapter.bun.ts"
|
"default": "./src/server/adapter.bun.ts"
|
||||||
|
},
|
||||||
|
"#httpapi-server": {
|
||||||
|
"bun": "./src/server/httpapi-server.node.ts",
|
||||||
|
"node": "./src/server/httpapi-server.node.ts",
|
||||||
|
"default": "./src/server/httpapi-server.node.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ const createEmbeddedWebUIBundle = async () => {
|
|||||||
await $`bun run --cwd ${appDir} build`
|
await $`bun run --cwd ${appDir} build`
|
||||||
const files = (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: dist })))
|
const files = (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: dist })))
|
||||||
.map((file) => file.replaceAll("\\", "/"))
|
.map((file) => file.replaceAll("\\", "/"))
|
||||||
|
.filter((file) => !file.endsWith(".map"))
|
||||||
.sort()
|
.sort()
|
||||||
const imports = files.map((file, i) => {
|
const imports = files.map((file, i) => {
|
||||||
const spec = path.relative(dir, path.join(dist, file)).replaceAll("\\", "/")
|
const spec = path.relative(dir, path.join(dist, file)).replaceAll("\\", "/")
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,7 @@ import { LoadAPIKeyError } from "ai"
|
|||||||
import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
|
import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
|
||||||
import { applyPatch } from "diff"
|
import { applyPatch } from "diff"
|
||||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
|
import { ShellID } from "@/tool/shell/id"
|
||||||
|
|
||||||
type ModeOption = { id: string; name: string; description?: string }
|
type ModeOption = { id: string; name: string; description?: string }
|
||||||
type ModelOption = { modelId: string; name: string }
|
type ModelOption = { modelId: string; name: string }
|
||||||
@@ -129,7 +130,7 @@ async function sendUsageUpdate(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
|
export function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
|
||||||
return {
|
return {
|
||||||
create: (connection: AgentSideConnection, fullConfig: ACPConfig) => {
|
create: (connection: AgentSideConnection, fullConfig: ACPConfig) => {
|
||||||
return new Agent(connection, fullConfig)
|
return new Agent(connection, fullConfig)
|
||||||
@@ -144,7 +145,7 @@ export class Agent implements ACPAgent {
|
|||||||
private sessionManager: ACPSessionManager
|
private sessionManager: ACPSessionManager
|
||||||
private eventAbort = new AbortController()
|
private eventAbort = new AbortController()
|
||||||
private eventStarted = false
|
private eventStarted = false
|
||||||
private bashSnapshots = new Map<string, string>()
|
private shellSnapshots = new Map<string, string>()
|
||||||
private toolStarts = new Set<string>()
|
private toolStarts = new Set<string>()
|
||||||
private permissionQueues = new Map<string, Promise<void>>()
|
private permissionQueues = new Map<string, Promise<void>>()
|
||||||
private permissionOptions: PermissionOption[] = [
|
private permissionOptions: PermissionOption[] = [
|
||||||
@@ -283,16 +284,16 @@ export class Agent implements ACPAgent {
|
|||||||
|
|
||||||
switch (part.state.status) {
|
switch (part.state.status) {
|
||||||
case "pending":
|
case "pending":
|
||||||
this.bashSnapshots.delete(part.callID)
|
this.shellSnapshots.delete(part.callID)
|
||||||
return
|
return
|
||||||
|
|
||||||
case "running":
|
case "running":
|
||||||
const output = this.bashOutput(part)
|
const output = this.shellOutput(part)
|
||||||
const content: ToolCallContent[] = []
|
const content: ToolCallContent[] = []
|
||||||
if (output) {
|
if (output) {
|
||||||
const hash = Hash.fast(output)
|
const hash = Hash.fast(output)
|
||||||
if (part.tool === "bash") {
|
if (part.tool === ShellID.ToolID) {
|
||||||
if (this.bashSnapshots.get(part.callID) === hash) {
|
if (this.shellSnapshots.get(part.callID) === hash) {
|
||||||
await this.connection
|
await this.connection
|
||||||
.sessionUpdate({
|
.sessionUpdate({
|
||||||
sessionId,
|
sessionId,
|
||||||
@@ -311,7 +312,7 @@ export class Agent implements ACPAgent {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.bashSnapshots.set(part.callID, hash)
|
this.shellSnapshots.set(part.callID, hash)
|
||||||
}
|
}
|
||||||
content.push({
|
content.push({
|
||||||
type: "content",
|
type: "content",
|
||||||
@@ -342,7 +343,7 @@ export class Agent implements ACPAgent {
|
|||||||
|
|
||||||
case "completed": {
|
case "completed": {
|
||||||
this.toolStarts.delete(part.callID)
|
this.toolStarts.delete(part.callID)
|
||||||
this.bashSnapshots.delete(part.callID)
|
this.shellSnapshots.delete(part.callID)
|
||||||
const kind = toToolKind(part.tool)
|
const kind = toToolKind(part.tool)
|
||||||
const content: ToolCallContent[] = [
|
const content: ToolCallContent[] = [
|
||||||
{
|
{
|
||||||
@@ -423,7 +424,7 @@ export class Agent implements ACPAgent {
|
|||||||
}
|
}
|
||||||
case "error":
|
case "error":
|
||||||
this.toolStarts.delete(part.callID)
|
this.toolStarts.delete(part.callID)
|
||||||
this.bashSnapshots.delete(part.callID)
|
this.shellSnapshots.delete(part.callID)
|
||||||
await this.connection
|
await this.connection
|
||||||
.sessionUpdate({
|
.sessionUpdate({
|
||||||
sessionId,
|
sessionId,
|
||||||
@@ -837,10 +838,10 @@ export class Agent implements ACPAgent {
|
|||||||
await this.toolStart(sessionId, part)
|
await this.toolStart(sessionId, part)
|
||||||
switch (part.state.status) {
|
switch (part.state.status) {
|
||||||
case "pending":
|
case "pending":
|
||||||
this.bashSnapshots.delete(part.callID)
|
this.shellSnapshots.delete(part.callID)
|
||||||
break
|
break
|
||||||
case "running":
|
case "running":
|
||||||
const output = this.bashOutput(part)
|
const output = this.shellOutput(part)
|
||||||
const runningContent: ToolCallContent[] = []
|
const runningContent: ToolCallContent[] = []
|
||||||
if (output) {
|
if (output) {
|
||||||
runningContent.push({
|
runningContent.push({
|
||||||
@@ -871,7 +872,7 @@ export class Agent implements ACPAgent {
|
|||||||
break
|
break
|
||||||
case "completed":
|
case "completed":
|
||||||
this.toolStarts.delete(part.callID)
|
this.toolStarts.delete(part.callID)
|
||||||
this.bashSnapshots.delete(part.callID)
|
this.shellSnapshots.delete(part.callID)
|
||||||
const kind = toToolKind(part.tool)
|
const kind = toToolKind(part.tool)
|
||||||
const content: ToolCallContent[] = [
|
const content: ToolCallContent[] = [
|
||||||
{
|
{
|
||||||
@@ -951,7 +952,7 @@ export class Agent implements ACPAgent {
|
|||||||
break
|
break
|
||||||
case "error":
|
case "error":
|
||||||
this.toolStarts.delete(part.callID)
|
this.toolStarts.delete(part.callID)
|
||||||
this.bashSnapshots.delete(part.callID)
|
this.shellSnapshots.delete(part.callID)
|
||||||
await this.connection
|
await this.connection
|
||||||
.sessionUpdate({
|
.sessionUpdate({
|
||||||
sessionId,
|
sessionId,
|
||||||
@@ -1105,8 +1106,8 @@ export class Agent implements ACPAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bashOutput(part: ToolPart) {
|
private shellOutput(part: ToolPart) {
|
||||||
if (part.tool !== "bash") return
|
if (part.tool !== ShellID.ToolID) return
|
||||||
if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return
|
if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return
|
||||||
const output = part.state.metadata["output"]
|
const output = part.state.metadata["output"]
|
||||||
if (typeof output !== "string") return
|
if (typeof output !== "string") return
|
||||||
@@ -1549,9 +1550,11 @@ export class Agent implements ACPAgent {
|
|||||||
|
|
||||||
function toToolKind(toolName: string): ToolKind {
|
function toToolKind(toolName: string): ToolKind {
|
||||||
const tool = toolName.toLocaleLowerCase()
|
const tool = toolName.toLocaleLowerCase()
|
||||||
|
|
||||||
switch (tool) {
|
switch (tool) {
|
||||||
case "bash":
|
case ShellID.ToolID:
|
||||||
return "execute"
|
return "execute"
|
||||||
|
|
||||||
case "webfetch":
|
case "webfetch":
|
||||||
return "fetch"
|
return "fetch"
|
||||||
|
|
||||||
@@ -1576,6 +1579,7 @@ function toToolKind(toolName: string): ToolKind {
|
|||||||
|
|
||||||
function toLocations(toolName: string, input: Record<string, any>): { path: string }[] {
|
function toLocations(toolName: string, input: Record<string, any>): { path: string }[] {
|
||||||
const tool = toolName.toLocaleLowerCase()
|
const tool = toolName.toLocaleLowerCase()
|
||||||
|
|
||||||
switch (tool) {
|
switch (tool) {
|
||||||
case "read":
|
case "read":
|
||||||
case "edit":
|
case "edit":
|
||||||
@@ -1584,7 +1588,7 @@ function toLocations(toolName: string, input: Record<string, any>): { path: stri
|
|||||||
case "glob":
|
case "glob":
|
||||||
case "grep":
|
case "grep":
|
||||||
return input["path"] ? [{ path: input["path"] }] : []
|
return input["path"] ? [{ path: input["path"] }] : []
|
||||||
case "bash":
|
case ShellID.ToolID:
|
||||||
return []
|
return []
|
||||||
default:
|
default:
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export function payloads() {
|
|||||||
.map(([type, def]) => {
|
.map(([type, def]) => {
|
||||||
return z
|
return z
|
||||||
.object({
|
.object({
|
||||||
|
id: z.string(),
|
||||||
type: z.literal(type),
|
type: z.literal(type),
|
||||||
properties: zodObject(def.properties),
|
properties: zodObject(def.properties),
|
||||||
})
|
})
|
||||||
@@ -39,6 +40,7 @@ export function effectPayloads() {
|
|||||||
.entries()
|
.entries()
|
||||||
.map(([type, def]) =>
|
.map(([type, def]) =>
|
||||||
Schema.Struct({
|
Schema.Struct({
|
||||||
|
id: Schema.String,
|
||||||
type: Schema.Literal(type),
|
type: Schema.Literal(type),
|
||||||
properties: def.properties,
|
properties: def.properties,
|
||||||
}).annotate({ identifier: `Event.${type}` }),
|
}).annotate({ identifier: `Event.${type}` }),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { EventEmitter } from "events"
|
import { EventEmitter } from "events"
|
||||||
|
import { Identifier } from "@/id/id"
|
||||||
|
|
||||||
export type GlobalEvent = {
|
export type GlobalEvent = {
|
||||||
directory?: string
|
directory?: string
|
||||||
@@ -7,6 +8,15 @@ export type GlobalEvent = {
|
|||||||
payload: any
|
payload: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GlobalBus = new EventEmitter<{
|
class GlobalBusEmitter extends EventEmitter<{
|
||||||
event: [GlobalEvent]
|
event: [GlobalEvent]
|
||||||
}>()
|
}> {
|
||||||
|
override emit(eventName: "event", event: GlobalEvent): boolean {
|
||||||
|
if (event.payload && typeof event.payload === "object" && !("id" in event.payload)) {
|
||||||
|
event.payload.id = event.payload.syncEvent?.id ?? Identifier.create("evt", "ascending")
|
||||||
|
}
|
||||||
|
return super.emit(eventName, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlobalBus = new GlobalBusEmitter()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { BusEvent } from "./bus-event"
|
|||||||
import { GlobalBus } from "./global"
|
import { GlobalBus } from "./global"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
|
import { Identifier } from "@/id/id"
|
||||||
|
|
||||||
const log = Log.create({ service: "bus" })
|
const log = Log.create({ service: "bus" })
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ export const InstanceDisposed = BusEvent.define(
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Payload<D extends BusEvent.Definition = BusEvent.Definition> = {
|
type Payload<D extends BusEvent.Definition = BusEvent.Definition> = {
|
||||||
|
id: string
|
||||||
type: D["type"]
|
type: D["type"]
|
||||||
properties: BusProperties<D>
|
properties: BusProperties<D>
|
||||||
}
|
}
|
||||||
@@ -28,7 +30,11 @@ type State = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly publish: <D extends BusEvent.Definition>(def: D, properties: BusProperties<D>) => Effect.Effect<void>
|
readonly publish: <D extends BusEvent.Definition>(
|
||||||
|
def: D,
|
||||||
|
properties: BusProperties<D>,
|
||||||
|
options?: { id?: string },
|
||||||
|
) => Effect.Effect<void>
|
||||||
readonly subscribe: <D extends BusEvent.Definition>(def: D) => Stream.Stream<Payload<D>>
|
readonly subscribe: <D extends BusEvent.Definition>(def: D) => Stream.Stream<Payload<D>>
|
||||||
readonly subscribeAll: () => Stream.Stream<Payload>
|
readonly subscribeAll: () => Stream.Stream<Payload>
|
||||||
readonly subscribeCallback: <D extends BusEvent.Definition>(
|
readonly subscribeCallback: <D extends BusEvent.Definition>(
|
||||||
@@ -53,6 +59,7 @@ export const layer = Layer.effect(
|
|||||||
// Publish InstanceDisposed before shutting down so subscribers see it
|
// Publish InstanceDisposed before shutting down so subscribers see it
|
||||||
yield* PubSub.publish(wildcard, {
|
yield* PubSub.publish(wildcard, {
|
||||||
type: InstanceDisposed.type,
|
type: InstanceDisposed.type,
|
||||||
|
id: createID(),
|
||||||
properties: { directory: ctx.directory },
|
properties: { directory: ctx.directory },
|
||||||
})
|
})
|
||||||
yield* PubSub.shutdown(wildcard)
|
yield* PubSub.shutdown(wildcard)
|
||||||
@@ -77,10 +84,10 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function publish<D extends BusEvent.Definition>(def: D, properties: BusProperties<D>) {
|
function publish<D extends BusEvent.Definition>(def: D, properties: BusProperties<D>, options?: { id?: string }) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const s = yield* InstanceState.get(state)
|
const s = yield* InstanceState.get(state)
|
||||||
const payload: Payload = { type: def.type, properties }
|
const payload: Payload = { id: options?.id ?? createID(), type: def.type, properties }
|
||||||
log.info("publishing", { type: def.type })
|
log.info("publishing", { type: def.type })
|
||||||
|
|
||||||
const ps = s.typed.get(def.type)
|
const ps = s.typed.get(def.type)
|
||||||
@@ -173,8 +180,16 @@ const { runPromise, runSync } = makeRuntime(Service, layer)
|
|||||||
|
|
||||||
// runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe,
|
// runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe,
|
||||||
// Scope.make, Effect.forkScoped) is entirely synchronous. If any step becomes async, this will throw.
|
// Scope.make, Effect.forkScoped) is entirely synchronous. If any step becomes async, this will throw.
|
||||||
export async function publish<D extends BusEvent.Definition>(def: D, properties: BusProperties<D>) {
|
export function createID() {
|
||||||
return runPromise((svc) => svc.publish(def, properties))
|
return Identifier.create("evt", "ascending")
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function publish<D extends BusEvent.Definition>(
|
||||||
|
def: D,
|
||||||
|
properties: BusProperties<D>,
|
||||||
|
options?: { id?: string },
|
||||||
|
) {
|
||||||
|
return runPromise((svc) => svc.publish(def, properties, options))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subscribe<D extends BusEvent.Definition>(def: D, callback: (event: Payload<D>) => unknown) {
|
export function subscribe<D extends BusEvent.Definition>(def: D, callback: (event: Payload<D>) => unknown) {
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { InstanceBootstrap } from "../project/bootstrap"
|
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
|
import { InstanceRuntime } from "../project/instance-runtime"
|
||||||
|
import { WithInstance } from "../project/with-instance"
|
||||||
|
|
||||||
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
|
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
|
||||||
return Instance.provide({
|
return WithInstance.provide({
|
||||||
directory,
|
directory,
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
try {
|
try {
|
||||||
const result = await cb()
|
const result = await cb()
|
||||||
return result
|
return result
|
||||||
} finally {
|
} finally {
|
||||||
await Instance.dispose()
|
await InstanceRuntime.disposeInstance(Instance.current)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Duration, Effect, Match, Option } from "effect"
|
|||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { Account } from "@/account/account"
|
import { Account } from "@/account/account"
|
||||||
import { AccountID, OrgID, PollExpired, type PollResult, type AccountError } from "@/account/schema"
|
import { AccountID, OrgID, PollExpired, type PollResult, type AccountError } from "@/account/schema"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { effectCmd } from "../effect-cmd"
|
||||||
import * as Prompt from "../effect/prompt"
|
import * as Prompt from "../effect/prompt"
|
||||||
import open from "open"
|
import open from "open"
|
||||||
|
|
||||||
@@ -172,60 +172,65 @@ const openEffect = Effect.fn("open")(function* () {
|
|||||||
yield* Prompt.outro("Opened " + url)
|
yield* Prompt.outro("Opened " + url)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const LoginCommand = cmd({
|
export const LoginCommand = effectCmd({
|
||||||
command: "login <url>",
|
command: "login <url>",
|
||||||
describe: false,
|
describe: false,
|
||||||
|
instance: false,
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs.positional("url", {
|
yargs.positional("url", {
|
||||||
describe: "server URL",
|
describe: "server URL",
|
||||||
type: "string",
|
type: "string",
|
||||||
demandOption: true,
|
demandOption: true,
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.account.login")(function* (args) {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
await AppRuntime.runPromise(loginEffect(args.url))
|
yield* Effect.orDie(loginEffect(args.url))
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const LogoutCommand = cmd({
|
export const LogoutCommand = effectCmd({
|
||||||
command: "logout [email]",
|
command: "logout [email]",
|
||||||
describe: false,
|
describe: false,
|
||||||
|
instance: false,
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs.positional("email", {
|
yargs.positional("email", {
|
||||||
describe: "account email to log out from",
|
describe: "account email to log out from",
|
||||||
type: "string",
|
type: "string",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.account.logout")(function* (args) {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
await AppRuntime.runPromise(logoutEffect(args.email))
|
yield* Effect.orDie(logoutEffect(args.email))
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const SwitchCommand = cmd({
|
export const SwitchCommand = effectCmd({
|
||||||
command: "switch",
|
command: "switch",
|
||||||
describe: false,
|
describe: false,
|
||||||
async handler() {
|
instance: false,
|
||||||
|
handler: Effect.fn("Cli.account.switch")(function* () {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
await AppRuntime.runPromise(switchEffect())
|
yield* Effect.orDie(switchEffect())
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const OrgsCommand = cmd({
|
export const OrgsCommand = effectCmd({
|
||||||
command: "orgs",
|
command: "orgs",
|
||||||
describe: false,
|
describe: false,
|
||||||
async handler() {
|
instance: false,
|
||||||
|
handler: Effect.fn("Cli.account.orgs")(function* () {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
await AppRuntime.runPromise(orgsEffect())
|
yield* Effect.orDie(orgsEffect())
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const OpenCommand = cmd({
|
export const OpenCommand = effectCmd({
|
||||||
command: "open",
|
command: "open",
|
||||||
describe: false,
|
describe: false,
|
||||||
async handler() {
|
instance: false,
|
||||||
|
handler: Effect.fn("Cli.account.open")(function* () {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
await AppRuntime.runPromise(openEffect())
|
yield* Effect.orDie(openEffect())
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ConsoleCommand = cmd({
|
export const ConsoleCommand = cmd({
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { bootstrap } from "../bootstrap"
|
import { Effect } from "effect"
|
||||||
import { cmd } from "./cmd"
|
import { effectCmd } from "../effect-cmd"
|
||||||
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
|
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
|
||||||
import { ACP } from "@/acp/agent"
|
import { ACP } from "@/acp/agent"
|
||||||
import { Server } from "@/server/server"
|
import { Server } from "@/server/server"
|
||||||
|
import { ServerAuth } from "@/server/auth"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||||
|
|
||||||
const log = Log.create({ service: "acp-command" })
|
const log = Log.create({ service: "acp-command" })
|
||||||
|
|
||||||
export const AcpCommand = cmd({
|
export const AcpCommand = effectCmd({
|
||||||
command: "acp",
|
command: "acp",
|
||||||
describe: "start ACP (Agent Client Protocol) server",
|
describe: "start ACP (Agent Client Protocol) server",
|
||||||
builder: (yargs) => {
|
builder: (yargs) => {
|
||||||
@@ -19,14 +20,14 @@ export const AcpCommand = cmd({
|
|||||||
default: process.cwd(),
|
default: process.cwd(),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
handler: async (args) => {
|
handler: Effect.fn("Cli.acp")(function* (args) {
|
||||||
process.env.OPENCODE_CLIENT = "acp"
|
process.env.OPENCODE_CLIENT = "acp"
|
||||||
await bootstrap(process.cwd(), async () => {
|
const opts = yield* Effect.promise(() => resolveNetworkOptions(args))
|
||||||
const opts = await resolveNetworkOptions(args)
|
const server = yield* Effect.promise(() => Server.listen(opts))
|
||||||
const server = await Server.listen(opts)
|
|
||||||
|
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createOpencodeClient({
|
||||||
baseUrl: `http://${server.hostname}:${server.port}`,
|
baseUrl: `http://${server.hostname}:${server.port}`,
|
||||||
|
headers: ServerAuth.headers(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const input = new WritableStream<Uint8Array>({
|
const input = new WritableStream<Uint8Array>({
|
||||||
@@ -53,7 +54,7 @@ export const AcpCommand = cmd({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const stream = ndJsonStream(input, output)
|
const stream = ndJsonStream(input, output)
|
||||||
const agent = await ACP.init({ sdk })
|
const agent = ACP.init({ sdk })
|
||||||
|
|
||||||
new AgentSideConnection((conn) => {
|
new AgentSideConnection((conn) => {
|
||||||
return agent.create(conn, { sdk })
|
return agent.create(conn, { sdk })
|
||||||
@@ -61,10 +62,12 @@ export const AcpCommand = cmd({
|
|||||||
|
|
||||||
log.info("setup connection")
|
log.info("setup connection")
|
||||||
process.stdin.resume()
|
process.stdin.resume()
|
||||||
await new Promise((resolve, reject) => {
|
yield* Effect.promise(
|
||||||
process.stdin.on("end", resolve)
|
() =>
|
||||||
|
new Promise<void>((resolve, reject) => {
|
||||||
|
process.stdin.on("end", () => resolve())
|
||||||
process.stdin.on("error", reject)
|
process.stdin.on("error", reject)
|
||||||
})
|
}),
|
||||||
})
|
)
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import * as prompts from "@clack/prompts"
|
import * as prompts from "@clack/prompts"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Agent } from "../../agent/agent"
|
import { Agent } from "../../agent/agent"
|
||||||
@@ -9,9 +8,11 @@ import path from "path"
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import matter from "gray-matter"
|
import matter from "gray-matter"
|
||||||
import { Instance } from "../../project/instance"
|
import { InstanceRef } from "@/effect/instance-ref"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import type { Argv } from "yargs"
|
import type { Argv } from "yargs"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { effectCmd } from "../effect-cmd"
|
||||||
|
|
||||||
type AgentMode = "all" | "primary" | "subagent"
|
type AgentMode = "all" | "primary" | "subagent"
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ const AVAILABLE_PERMISSIONS = [
|
|||||||
"skill",
|
"skill",
|
||||||
]
|
]
|
||||||
|
|
||||||
const AgentCreateCommand = cmd({
|
const AgentCreateCommand = effectCmd({
|
||||||
command: "create",
|
command: "create",
|
||||||
describe: "create a new agent",
|
describe: "create a new agent",
|
||||||
builder: (yargs: Argv) =>
|
builder: (yargs: Argv) =>
|
||||||
@@ -60,10 +61,12 @@ const AgentCreateCommand = cmd({
|
|||||||
alias: ["m"],
|
alias: ["m"],
|
||||||
describe: "model to use in the format of provider/model",
|
describe: "model to use in the format of provider/model",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.agent.create")(function* (args) {
|
||||||
await Instance.provide({
|
const maybeCtx = yield* InstanceRef
|
||||||
directory: process.cwd(),
|
if (!maybeCtx) return yield* Effect.die("InstanceRef not provided")
|
||||||
async fn() {
|
const ctx = maybeCtx
|
||||||
|
const agentSvc = yield* Agent.Service
|
||||||
|
yield* Effect.promise(async () => {
|
||||||
const cliPath = args.path
|
const cliPath = args.path
|
||||||
const cliDescription = args.description
|
const cliDescription = args.description
|
||||||
const cliMode = args.mode as AgentMode | undefined
|
const cliMode = args.mode as AgentMode | undefined
|
||||||
@@ -76,7 +79,7 @@ const AgentCreateCommand = cmd({
|
|||||||
prompts.intro("Create agent")
|
prompts.intro("Create agent")
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = Instance.project
|
const project = ctx.project
|
||||||
|
|
||||||
// Determine scope/path
|
// Determine scope/path
|
||||||
let targetPath: string
|
let targetPath: string
|
||||||
@@ -91,7 +94,7 @@ const AgentCreateCommand = cmd({
|
|||||||
{
|
{
|
||||||
label: "Current project",
|
label: "Current project",
|
||||||
value: "project" as const,
|
value: "project" as const,
|
||||||
hint: Instance.worktree,
|
hint: ctx.worktree,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Global",
|
label: "Global",
|
||||||
@@ -103,10 +106,7 @@ const AgentCreateCommand = cmd({
|
|||||||
if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
|
if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
|
||||||
scope = scopeResult
|
scope = scopeResult
|
||||||
}
|
}
|
||||||
targetPath = path.join(
|
targetPath = path.join(scope === "global" ? Global.Path.config : path.join(ctx.worktree, ".opencode"), "agent")
|
||||||
scope === "global" ? Global.Path.config : path.join(Instance.worktree, ".opencode"),
|
|
||||||
"agent",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get description
|
// Get description
|
||||||
@@ -127,9 +127,7 @@ const AgentCreateCommand = cmd({
|
|||||||
const spinner = prompts.spinner()
|
const spinner = prompts.spinner()
|
||||||
spinner.start("Generating agent configuration...")
|
spinner.start("Generating agent configuration...")
|
||||||
const model = args.model ? Provider.parseModel(args.model) : undefined
|
const model = args.model ? Provider.parseModel(args.model) : undefined
|
||||||
const generated = await AppRuntime.runPromise(
|
const generated = await Effect.runPromise(agentSvc.generate({ description, model })).catch((error) => {
|
||||||
Agent.Service.use((svc) => svc.generate({ description, model })),
|
|
||||||
).catch((error) => {
|
|
||||||
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
|
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
|
||||||
if (isFullyNonInteractive) process.exit(1)
|
if (isFullyNonInteractive) process.exit(1)
|
||||||
throw new UI.CancelledError()
|
throw new UI.CancelledError()
|
||||||
@@ -227,19 +225,15 @@ const AgentCreateCommand = cmd({
|
|||||||
prompts.log.success(`Agent created: ${filePath}`)
|
prompts.log.success(`Agent created: ${filePath}`)
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
}
|
}
|
||||||
},
|
|
||||||
})
|
})
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const AgentListCommand = cmd({
|
const AgentListCommand = effectCmd({
|
||||||
command: "list",
|
command: "list",
|
||||||
describe: "list all available agents",
|
describe: "list all available agents",
|
||||||
async handler() {
|
handler: Effect.fn("Cli.agent.list")(function* () {
|
||||||
await Instance.provide({
|
const agents = yield* Agent.Service.use((svc) => svc.list())
|
||||||
directory: process.cwd(),
|
|
||||||
async fn() {
|
|
||||||
const agents = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.list()))
|
|
||||||
const sortedAgents = agents.sort((a, b) => {
|
const sortedAgents = agents.sort((a, b) => {
|
||||||
if (a.native !== b.native) {
|
if (a.native !== b.native) {
|
||||||
return a.native ? -1 : 1
|
return a.native ? -1 : 1
|
||||||
@@ -251,9 +245,7 @@ const AgentListCommand = cmd({
|
|||||||
process.stdout.write(`${agent.name} (${agent.mode})` + EOL)
|
process.stdout.write(`${agent.name} (${agent.mode})` + EOL)
|
||||||
process.stdout.write(` ${JSON.stringify(agent.permission, null, 2)}` + EOL)
|
process.stdout.write(` ${JSON.stringify(agent.permission, null, 2)}` + EOL)
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const AgentCommand = cmd({
|
export const AgentCommand = cmd({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { CommandModule } from "yargs"
|
import type { CommandModule } from "yargs"
|
||||||
|
|
||||||
type WithDoubleDash<T> = T & { "--"?: string[] }
|
export type WithDoubleDash<T> = T & { "--"?: string[] }
|
||||||
|
|
||||||
export function cmd<T, U>(input: CommandModule<T, WithDoubleDash<U>>) {
|
export function cmd<T, U>(input: CommandModule<T, WithDoubleDash<U>>) {
|
||||||
return input
|
return input
|
||||||
|
|||||||
@@ -7,14 +7,13 @@ import { Session } from "@/session/session"
|
|||||||
import type { MessageV2 } from "../../../session/message-v2"
|
import type { MessageV2 } from "../../../session/message-v2"
|
||||||
import { MessageID, PartID } from "../../../session/schema"
|
import { MessageID, PartID } from "../../../session/schema"
|
||||||
import { ToolRegistry } from "@/tool/registry"
|
import { ToolRegistry } from "@/tool/registry"
|
||||||
import { Instance } from "../../../project/instance"
|
|
||||||
import { Permission } from "../../../permission"
|
import { Permission } from "../../../permission"
|
||||||
import { iife } from "../../../util/iife"
|
import { iife } from "../../../util/iife"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { effectCmd, fail } from "../../effect-cmd"
|
||||||
import { cmd } from "../cmd"
|
import { InstanceRef } from "@/effect/instance-ref"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import type { InstanceContext } from "@/project/instance"
|
||||||
|
|
||||||
export const AgentCommand = cmd({
|
export const AgentCommand = effectCmd({
|
||||||
command: "agent <name>",
|
command: "agent <name>",
|
||||||
describe: "show agent configuration details",
|
describe: "show agent configuration details",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
@@ -32,32 +31,41 @@ export const AgentCommand = cmd({
|
|||||||
type: "string",
|
type: "string",
|
||||||
description: "Tool params as JSON or a JS object literal",
|
description: "Tool params as JSON or a JS object literal",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.debug.agent")(function* (args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const ctx = yield* InstanceRef
|
||||||
const agentName = args.name as string
|
if (!ctx) return
|
||||||
const agent = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.get(agentName)))
|
return yield* run(args, ctx)
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const run = Effect.fn("Cli.debug.agent.body")(function* (
|
||||||
|
args: { name: string; tool?: string; params?: string },
|
||||||
|
ctx: InstanceContext,
|
||||||
|
) {
|
||||||
|
const agentName = args.name
|
||||||
|
const agent = yield* Agent.Service.use((svc) => svc.get(agentName))
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
process.stderr.write(
|
process.stderr.write(
|
||||||
`Agent ${agentName} not found, run '${basename(process.execPath)} agent list' to get an agent list` + EOL,
|
`Agent ${agentName} not found, run '${basename(process.execPath)} agent list' to get an agent list` + EOL,
|
||||||
)
|
)
|
||||||
process.exit(1)
|
return yield* fail("", 1)
|
||||||
}
|
}
|
||||||
const availableTools = await getAvailableTools(agent)
|
const availableTools = yield* getAvailableTools(agent)
|
||||||
const resolvedTools = await resolveTools(agent, availableTools)
|
const resolvedTools = resolveTools(agent, availableTools)
|
||||||
const toolID = args.tool as string | undefined
|
const toolID = args.tool
|
||||||
if (toolID) {
|
if (toolID) {
|
||||||
const tool = availableTools.find((item) => item.id === toolID)
|
const tool = availableTools.find((item) => item.id === toolID)
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
process.stderr.write(`Tool ${toolID} not found for agent ${agentName}` + EOL)
|
process.stderr.write(`Tool ${toolID} not found for agent ${agentName}` + EOL)
|
||||||
process.exit(1)
|
return yield* fail("", 1)
|
||||||
}
|
}
|
||||||
if (resolvedTools[toolID] === false) {
|
if (resolvedTools[toolID] === false) {
|
||||||
process.stderr.write(`Tool ${toolID} is disabled for agent ${agentName}` + EOL)
|
process.stderr.write(`Tool ${toolID} is disabled for agent ${agentName}` + EOL)
|
||||||
process.exit(1)
|
return yield* fail("", 1)
|
||||||
}
|
}
|
||||||
const params = parseToolParams(args.params as string | undefined)
|
const params = parseToolParams(args.params)
|
||||||
const ctx = await createToolContext(agent)
|
const toolCtx = yield* createToolContext(agent, ctx)
|
||||||
const result = await tool.execute(params, ctx)
|
const result = yield* tool.execute(params, toolCtx)
|
||||||
process.stdout.write(JSON.stringify({ tool: toolID, input: params, result }, null, 2) + EOL)
|
process.stdout.write(JSON.stringify({ tool: toolID, input: params, result }, null, 2) + EOL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -68,24 +76,15 @@ export const AgentCommand = cmd({
|
|||||||
}
|
}
|
||||||
process.stdout.write(JSON.stringify(output, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(output, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
async function getAvailableTools(agent: Agent.Info) {
|
const getAvailableTools = Effect.fn("Cli.debug.agent.getAvailableTools")(function* (agent: Agent.Info) {
|
||||||
return AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const provider = yield* Provider.Service
|
const provider = yield* Provider.Service
|
||||||
const registry = yield* ToolRegistry.Service
|
const registry = yield* ToolRegistry.Service
|
||||||
const model = agent.model ?? (yield* provider.defaultModel())
|
const model = agent.model ?? (yield* provider.defaultModel())
|
||||||
return yield* registry.tools({
|
return yield* registry.tools({ ...model, agent })
|
||||||
...model,
|
|
||||||
agent,
|
|
||||||
})
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {
|
function resolveTools(agent: Agent.Info, availableTools: { id: string }[]) {
|
||||||
const disabled = Permission.disabled(
|
const disabled = Permission.disabled(
|
||||||
availableTools.map((tool) => tool.id),
|
availableTools.map((tool) => tool.id),
|
||||||
agent.permission,
|
agent.permission,
|
||||||
@@ -123,11 +122,12 @@ function parseToolParams(input?: string) {
|
|||||||
return parsed as Record<string, unknown>
|
return parsed as Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createToolContext(agent: Agent.Info) {
|
const createToolContext = Effect.fn("Cli.debug.agent.createToolContext")(function* (
|
||||||
const { session, messageID } = await AppRuntime.runPromise(
|
agent: Agent.Info,
|
||||||
Effect.gen(function* () {
|
ctx: InstanceContext,
|
||||||
const session = yield* Session.Service
|
) {
|
||||||
const result = yield* session.create({ title: `Debug tool run (${agent.name})` })
|
const sessionSvc = yield* Session.Service
|
||||||
|
const session = yield* sessionSvc.create({ title: `Debug tool run (${agent.name})` })
|
||||||
const messageID = MessageID.ascending()
|
const messageID = MessageID.ascending()
|
||||||
const model = agent.model
|
const model = agent.model
|
||||||
? agent.model
|
? agent.model
|
||||||
@@ -138,35 +138,22 @@ async function createToolContext(agent: Agent.Info) {
|
|||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const message: MessageV2.Assistant = {
|
const message: MessageV2.Assistant = {
|
||||||
id: messageID,
|
id: messageID,
|
||||||
sessionID: result.id,
|
sessionID: session.id,
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
time: {
|
time: { created: now },
|
||||||
created: now,
|
|
||||||
},
|
|
||||||
parentID: messageID,
|
parentID: messageID,
|
||||||
modelID: model.modelID,
|
modelID: model.modelID,
|
||||||
providerID: model.providerID,
|
providerID: model.providerID,
|
||||||
mode: "debug",
|
mode: "debug",
|
||||||
agent: agent.name,
|
agent: agent.name,
|
||||||
path: {
|
path: {
|
||||||
cwd: Instance.directory,
|
cwd: ctx.directory,
|
||||||
root: Instance.worktree,
|
root: ctx.worktree,
|
||||||
},
|
},
|
||||||
cost: 0,
|
cost: 0,
|
||||||
tokens: {
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||||
input: 0,
|
|
||||||
output: 0,
|
|
||||||
reasoning: 0,
|
|
||||||
cache: {
|
|
||||||
read: 0,
|
|
||||||
write: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
yield* session.updateMessage(message)
|
yield* sessionSvc.updateMessage(message)
|
||||||
return { session: result, messageID }
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const ruleset = Permission.merge(agent.permission, session.permission ?? [])
|
const ruleset = Permission.merge(agent.permission, session.permission ?? [])
|
||||||
|
|
||||||
@@ -189,4 +176,4 @@ async function createToolContext(agent: Agent.Info) {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
|
import { Effect } from "effect"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { effectCmd } from "../../effect-cmd"
|
||||||
import { bootstrap } from "../../bootstrap"
|
|
||||||
import { cmd } from "../cmd"
|
|
||||||
|
|
||||||
export const ConfigCommand = cmd({
|
export const ConfigCommand = effectCmd({
|
||||||
command: "config",
|
command: "config",
|
||||||
describe: "show resolved configuration",
|
describe: "show resolved configuration",
|
||||||
builder: (yargs) => yargs,
|
builder: (yargs) => yargs,
|
||||||
async handler() {
|
handler: Effect.fn("Cli.debug.config")(function* () {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const config = yield* Config.Service.use((cfg) => cfg.get())
|
||||||
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
|
|
||||||
process.stdout.write(JSON.stringify(config, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(config, null, 2) + EOL)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { Effect } from "effect"
|
||||||
import { File } from "../../../file"
|
import { File } from "../../../file"
|
||||||
import { Ripgrep } from "@/file/ripgrep"
|
import { Ripgrep } from "@/file/ripgrep"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { effectCmd } from "../../effect-cmd"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
|
|
||||||
const FileSearchCommand = cmd({
|
const FileSearchCommand = effectCmd({
|
||||||
command: "search <query>",
|
command: "search <query>",
|
||||||
describe: "search files by query",
|
describe: "search files by query",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
@@ -14,15 +14,13 @@ const FileSearchCommand = cmd({
|
|||||||
demandOption: true,
|
demandOption: true,
|
||||||
description: "Search query",
|
description: "Search query",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.debug.file.search")(function* (args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const results = yield* File.Service.use((svc) => svc.search({ query: args.query }))
|
||||||
const results = await AppRuntime.runPromise(File.Service.use((svc) => svc.search({ query: args.query })))
|
|
||||||
process.stdout.write(results.join(EOL) + EOL)
|
process.stdout.write(results.join(EOL) + EOL)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const FileReadCommand = cmd({
|
const FileReadCommand = effectCmd({
|
||||||
command: "read <path>",
|
command: "read <path>",
|
||||||
describe: "read file contents as JSON",
|
describe: "read file contents as JSON",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
@@ -31,27 +29,23 @@ const FileReadCommand = cmd({
|
|||||||
demandOption: true,
|
demandOption: true,
|
||||||
description: "File path to read",
|
description: "File path to read",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.debug.file.read")(function* (args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const content = yield* File.Service.use((svc) => svc.read(args.path))
|
||||||
const content = await AppRuntime.runPromise(File.Service.use((svc) => svc.read(args.path)))
|
|
||||||
process.stdout.write(JSON.stringify(content, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(content, null, 2) + EOL)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const FileStatusCommand = cmd({
|
const FileStatusCommand = effectCmd({
|
||||||
command: "status",
|
command: "status",
|
||||||
describe: "show file status information",
|
describe: "show file status information",
|
||||||
builder: (yargs) => yargs,
|
builder: (yargs) => yargs,
|
||||||
async handler() {
|
handler: Effect.fn("Cli.debug.file.status")(function* () {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const status = yield* File.Service.use((svc) => svc.status())
|
||||||
const status = await AppRuntime.runPromise(File.Service.use((svc) => svc.status()))
|
|
||||||
process.stdout.write(JSON.stringify(status, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(status, null, 2) + EOL)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const FileListCommand = cmd({
|
const FileListCommand = effectCmd({
|
||||||
command: "list <path>",
|
command: "list <path>",
|
||||||
describe: "list files in a directory",
|
describe: "list files in a directory",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
@@ -60,15 +54,13 @@ const FileListCommand = cmd({
|
|||||||
demandOption: true,
|
demandOption: true,
|
||||||
description: "File path to list",
|
description: "File path to list",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.debug.file.list")(function* (args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const files = yield* File.Service.use((svc) => svc.list(args.path))
|
||||||
const files = await AppRuntime.runPromise(File.Service.use((svc) => svc.list(args.path)))
|
|
||||||
process.stdout.write(JSON.stringify(files, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(files, null, 2) + EOL)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const FileTreeCommand = cmd({
|
const FileTreeCommand = effectCmd({
|
||||||
command: "tree [dir]",
|
command: "tree [dir]",
|
||||||
describe: "show directory tree",
|
describe: "show directory tree",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
@@ -77,12 +69,10 @@ const FileTreeCommand = cmd({
|
|||||||
description: "Directory to tree",
|
description: "Directory to tree",
|
||||||
default: process.cwd(),
|
default: process.cwd(),
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.debug.file.tree")(function* (args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const tree = yield* Effect.orDie(Ripgrep.Service.use((svc) => svc.tree({ cwd: args.dir, limit: 200 })))
|
||||||
const tree = await AppRuntime.runPromise(Ripgrep.Service.use((svc) => svc.tree({ cwd: args.dir, limit: 200 })))
|
|
||||||
console.log(JSON.stringify(tree, null, 2))
|
console.log(JSON.stringify(tree, null, 2))
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const FileCommand = cmd({
|
export const FileCommand = cmd({
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
|
import os from "os"
|
||||||
|
import { Duration, Effect } from "effect"
|
||||||
|
import { Config } from "@/config/config"
|
||||||
|
import { ConfigPlugin } from "@/config/plugin"
|
||||||
|
import { effectCmd } from "../../effect-cmd"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
import { ConfigCommand } from "./config"
|
import { ConfigCommand } from "./config"
|
||||||
import { FileCommand } from "./file"
|
import { FileCommand } from "./file"
|
||||||
@@ -25,20 +31,49 @@ export const DebugCommand = cmd({
|
|||||||
.command(SnapshotCommand)
|
.command(SnapshotCommand)
|
||||||
.command(StartupCommand)
|
.command(StartupCommand)
|
||||||
.command(AgentCommand)
|
.command(AgentCommand)
|
||||||
|
.command(InfoCommand)
|
||||||
.command(PathsCommand)
|
.command(PathsCommand)
|
||||||
.command({
|
.command(WaitCommand)
|
||||||
command: "wait",
|
|
||||||
describe: "wait indefinitely (for debugging)",
|
|
||||||
async handler() {
|
|
||||||
await bootstrap(process.cwd(), async () => {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1_000 * 60 * 60 * 24))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.demandCommand(),
|
.demandCommand(),
|
||||||
async handler() {},
|
async handler() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const WaitCommand = effectCmd({
|
||||||
|
command: "wait",
|
||||||
|
describe: "wait indefinitely (for debugging)",
|
||||||
|
handler: Effect.fn("Cli.debug.wait")(function* () {
|
||||||
|
yield* Effect.sleep(Duration.days(1))
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const InfoCommand = effectCmd({
|
||||||
|
command: "info",
|
||||||
|
describe: "show debug information",
|
||||||
|
handler: Effect.fn("Cli.debug.info")(function* () {
|
||||||
|
const config = yield* Config.Service.use((cfg) => cfg.get())
|
||||||
|
const termProgram = process.env.TERM_PROGRAM
|
||||||
|
? `${process.env.TERM_PROGRAM}${process.env.TERM_PROGRAM_VERSION ? ` ${process.env.TERM_PROGRAM_VERSION}` : ""}`
|
||||||
|
: undefined
|
||||||
|
const terminal = [termProgram, process.env.TERM].filter((item): item is string => Boolean(item)).join(" / ")
|
||||||
|
|
||||||
|
console.log(`opencode version: ${InstallationVersion}`)
|
||||||
|
console.log(`os: ${os.type()} ${os.release()} ${os.arch()}`)
|
||||||
|
console.log(`terminal: ${terminal || "unknown"}`)
|
||||||
|
console.log("plugins:")
|
||||||
|
if (Flag.OPENCODE_PURE) {
|
||||||
|
console.log("external plugins disabled (--pure)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!config.plugin_origins?.length) {
|
||||||
|
console.log("none")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const plugin of config.plugin_origins) {
|
||||||
|
console.log(`- ${ConfigPlugin.pluginSpecifier(plugin.spec)}`)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
const PathsCommand = cmd({
|
const PathsCommand = cmd({
|
||||||
command: "paths",
|
command: "paths",
|
||||||
describe: "show global paths (data, config, cache, state)",
|
describe: "show global paths (data, config, cache, state)",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { LSP } from "@/lsp/lsp"
|
import { LSP } from "@/lsp/lsp"
|
||||||
import { AppRuntime } from "../../../effect/app-runtime"
|
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { effectCmd } from "../../effect-cmd"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
@@ -14,47 +13,39 @@ export const LSPCommand = cmd({
|
|||||||
async handler() {},
|
async handler() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
const DiagnosticsCommand = cmd({
|
const DiagnosticsCommand = effectCmd({
|
||||||
command: "diagnostics <file>",
|
command: "diagnostics <file>",
|
||||||
describe: "get diagnostics for a file",
|
describe: "get diagnostics for a file",
|
||||||
builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
|
builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.debug.lsp.diagnostics")(function* (args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const out = yield* LSP.Service.use((lsp) =>
|
||||||
const out = await AppRuntime.runPromise(
|
|
||||||
LSP.Service.use((lsp) =>
|
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* lsp.touchFile(args.file, "full")
|
yield* lsp.touchFile(args.file, "full")
|
||||||
return yield* lsp.diagnostics()
|
return yield* lsp.diagnostics()
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
process.stdout.write(JSON.stringify(out, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(out, null, 2) + EOL)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const SymbolsCommand = cmd({
|
export const SymbolsCommand = effectCmd({
|
||||||
command: "symbols <query>",
|
command: "symbols <query>",
|
||||||
describe: "search workspace symbols",
|
describe: "search workspace symbols",
|
||||||
builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }),
|
builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.debug.lsp.symbols")(function* (args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
|
||||||
using _ = Log.Default.time("symbols")
|
using _ = Log.Default.time("symbols")
|
||||||
const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.workspaceSymbol(args.query)))
|
const results = yield* LSP.Service.use((lsp) => lsp.workspaceSymbol(args.query))
|
||||||
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const DocumentSymbolsCommand = cmd({
|
export const DocumentSymbolsCommand = effectCmd({
|
||||||
command: "document-symbols <uri>",
|
command: "document-symbols <uri>",
|
||||||
describe: "get symbols from a document",
|
describe: "get symbols from a document",
|
||||||
builder: (yargs) => yargs.positional("uri", { type: "string", demandOption: true }),
|
builder: (yargs) => yargs.positional("uri", { type: "string", demandOption: true }),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.debug.lsp.documentSymbols")(function* (args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
|
||||||
using _ = Log.Default.time("document-symbols")
|
using _ = Log.Default.time("document-symbols")
|
||||||
const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.documentSymbol(args.uri)))
|
const results = yield* LSP.Service.use((lsp) => lsp.documentSymbol(args.uri))
|
||||||
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Effect, Stream } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
import { AppRuntime } from "../../../effect/app-runtime"
|
|
||||||
import { Ripgrep } from "../../../file/ripgrep"
|
import { Ripgrep } from "../../../file/ripgrep"
|
||||||
import { Instance } from "../../../project/instance"
|
import { effectCmd } from "../../effect-cmd"
|
||||||
import { bootstrap } from "../../bootstrap"
|
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
|
import { InstanceRef } from "@/effect/instance-ref"
|
||||||
|
|
||||||
export const RipgrepCommand = cmd({
|
export const RipgrepCommand = cmd({
|
||||||
command: "rg",
|
command: "rg",
|
||||||
@@ -13,24 +12,22 @@ export const RipgrepCommand = cmd({
|
|||||||
async handler() {},
|
async handler() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
const TreeCommand = cmd({
|
const TreeCommand = effectCmd({
|
||||||
command: "tree",
|
command: "tree",
|
||||||
describe: "show file tree using ripgrep",
|
describe: "show file tree using ripgrep",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs.option("limit", {
|
yargs.option("limit", {
|
||||||
type: "number",
|
type: "number",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.debug.rg.tree")(function* (args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const ctx = yield* InstanceRef
|
||||||
const tree = await AppRuntime.runPromise(
|
if (!ctx) return
|
||||||
Ripgrep.Service.use((svc) => svc.tree({ cwd: Instance.directory, limit: args.limit })),
|
const tree = yield* Effect.orDie(Ripgrep.Service.use((svc) => svc.tree({ cwd: ctx.directory, limit: args.limit })))
|
||||||
)
|
|
||||||
process.stdout.write(tree + EOL)
|
process.stdout.write(tree + EOL)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const FilesCommand = cmd({
|
const FilesCommand = effectCmd({
|
||||||
command: "files",
|
command: "files",
|
||||||
describe: "list files using ripgrep",
|
describe: "list files using ripgrep",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
@@ -47,29 +44,26 @@ const FilesCommand = cmd({
|
|||||||
type: "number",
|
type: "number",
|
||||||
description: "Limit number of results",
|
description: "Limit number of results",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.debug.rg.files")(function* (args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const ctx = yield* InstanceRef
|
||||||
const files = await AppRuntime.runPromise(
|
if (!ctx) return
|
||||||
Effect.gen(function* () {
|
|
||||||
const rg = yield* Ripgrep.Service
|
const rg = yield* Ripgrep.Service
|
||||||
return yield* rg
|
const files = yield* rg
|
||||||
.files({
|
.files({
|
||||||
cwd: Instance.directory,
|
cwd: ctx.directory,
|
||||||
glob: args.glob ? [args.glob] : undefined,
|
glob: args.glob ? [args.glob] : undefined,
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
Stream.take(args.limit ?? Infinity),
|
Stream.take(args.limit ?? Infinity),
|
||||||
Stream.runCollect,
|
Stream.runCollect,
|
||||||
Effect.map((c) => [...c]),
|
Effect.map((c) => [...c]),
|
||||||
)
|
Effect.orDie,
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
process.stdout.write(files.join(EOL) + EOL)
|
process.stdout.write(files.join(EOL) + EOL)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const SearchCommand = cmd({
|
const SearchCommand = effectCmd({
|
||||||
command: "search <pattern>",
|
command: "search <pattern>",
|
||||||
describe: "search file contents using ripgrep",
|
describe: "search file contents using ripgrep",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
@@ -87,12 +81,13 @@ const SearchCommand = cmd({
|
|||||||
type: "number",
|
type: "number",
|
||||||
description: "Limit number of results",
|
description: "Limit number of results",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.debug.rg.search")(function* (args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const ctx = yield* InstanceRef
|
||||||
const results = await AppRuntime.runPromise(
|
if (!ctx) return
|
||||||
|
const results = yield* Effect.orDie(
|
||||||
Ripgrep.Service.use((svc) =>
|
Ripgrep.Service.use((svc) =>
|
||||||
svc.search({
|
svc.search({
|
||||||
cwd: Instance.directory,
|
cwd: ctx.directory,
|
||||||
pattern: args.pattern,
|
pattern: args.pattern,
|
||||||
glob: args.glob as string[] | undefined,
|
glob: args.glob as string[] | undefined,
|
||||||
limit: args.limit,
|
limit: args.limit,
|
||||||
@@ -100,6 +95,5 @@ const SearchCommand = cmd({
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
process.stdout.write(JSON.stringify(results.items, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(results.items, null, 2) + EOL)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { Skill } from "../../../skill"
|
import { Skill } from "../../../skill"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { effectCmd } from "../../effect-cmd"
|
||||||
import { cmd } from "../cmd"
|
|
||||||
|
|
||||||
export const SkillCommand = cmd({
|
export const SkillCommand = effectCmd({
|
||||||
command: "skill",
|
command: "skill",
|
||||||
describe: "list all available skills",
|
describe: "list all available skills",
|
||||||
builder: (yargs) => yargs,
|
builder: (yargs) => yargs,
|
||||||
async handler() {
|
handler: Effect.fn("Cli.debug.skill")(function* () {
|
||||||
await bootstrap(process.cwd(), async () => {
|
|
||||||
const skills = await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const skill = yield* Skill.Service
|
const skill = yield* Skill.Service
|
||||||
return yield* skill.all()
|
const skills = yield* skill.all()
|
||||||
}),
|
|
||||||
)
|
|
||||||
process.stdout.write(JSON.stringify(skills, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(skills, null, 2) + EOL)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { Effect } from "effect"
|
||||||
import { Snapshot } from "../../../snapshot"
|
import { Snapshot } from "../../../snapshot"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { effectCmd } from "../../effect-cmd"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
|
|
||||||
export const SnapshotCommand = cmd({
|
export const SnapshotCommand = cmd({
|
||||||
@@ -10,17 +10,16 @@ export const SnapshotCommand = cmd({
|
|||||||
async handler() {},
|
async handler() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
const TrackCommand = cmd({
|
const TrackCommand = effectCmd({
|
||||||
command: "track",
|
command: "track",
|
||||||
describe: "track current snapshot state",
|
describe: "track current snapshot state",
|
||||||
async handler() {
|
handler: Effect.fn("Cli.debug.snapshot.track")(function* () {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const out = yield* Snapshot.Service.use((svc) => svc.track())
|
||||||
console.log(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track())))
|
console.log(out)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const PatchCommand = cmd({
|
const PatchCommand = effectCmd({
|
||||||
command: "patch <hash>",
|
command: "patch <hash>",
|
||||||
describe: "show patch for a snapshot hash",
|
describe: "show patch for a snapshot hash",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
@@ -29,14 +28,13 @@ const PatchCommand = cmd({
|
|||||||
description: "hash",
|
description: "hash",
|
||||||
demandOption: true,
|
demandOption: true,
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.debug.snapshot.patch")(function* (args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const out = yield* Snapshot.Service.use((svc) => svc.patch(args.hash))
|
||||||
console.log(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(args.hash))))
|
console.log(out)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const DiffCommand = cmd({
|
const DiffCommand = effectCmd({
|
||||||
command: "diff <hash>",
|
command: "diff <hash>",
|
||||||
describe: "show diff for a snapshot hash",
|
describe: "show diff for a snapshot hash",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
@@ -45,9 +43,8 @@ const DiffCommand = cmd({
|
|||||||
description: "hash",
|
description: "hash",
|
||||||
demandOption: true,
|
demandOption: true,
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.debug.snapshot.diff")(function* (args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const out = yield* Snapshot.Service.use((svc) => svc.diff(args.hash))
|
||||||
console.log(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diff(args.hash))))
|
console.log(out)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import type { Argv } from "yargs"
|
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
import { MessageV2 } from "../../session/message-v2"
|
import { MessageV2 } from "../../session/message-v2"
|
||||||
import { SessionID } from "../../session/schema"
|
import { SessionID } from "../../session/schema"
|
||||||
import { cmd } from "./cmd"
|
import { effectCmd, fail } from "../effect-cmd"
|
||||||
import { bootstrap } from "../bootstrap"
|
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import * as prompts from "@clack/prompts"
|
import * as prompts from "@clack/prompts"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { Effect } from "effect"
|
||||||
|
|
||||||
function redact(kind: string, id: string, value: string) {
|
function redact(kind: string, id: string, value: string) {
|
||||||
return value.trim() ? `[redacted:${kind}:${id}]` : value
|
return value.trim() ? `[redacted:${kind}:${id}]` : value
|
||||||
@@ -220,11 +218,11 @@ function sanitize(data: { info: Session.Info; messages: MessageV2.WithParts[] })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExportCommand = cmd({
|
export const ExportCommand = effectCmd({
|
||||||
command: "export [sessionID]",
|
command: "export [sessionID]",
|
||||||
describe: "export session data as JSON",
|
describe: "export session data as JSON",
|
||||||
builder: (yargs: Argv) => {
|
builder: (yargs) =>
|
||||||
return yargs
|
yargs
|
||||||
.positional("sessionID", {
|
.positional("sessionID", {
|
||||||
describe: "session id to export",
|
describe: "session id to export",
|
||||||
type: "string",
|
type: "string",
|
||||||
@@ -232,34 +230,33 @@ export const ExportCommand = cmd({
|
|||||||
.option("sanitize", {
|
.option("sanitize", {
|
||||||
describe: "redact sensitive transcript and file data",
|
describe: "redact sensitive transcript and file data",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
|
}),
|
||||||
|
handler: Effect.fn("Cli.export")(function* (args) {
|
||||||
|
return yield* run(args)
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
},
|
|
||||||
handler: async (args) => {
|
const run = Effect.fn("Cli.export.body")(function* (args: { sessionID?: string; sanitize?: boolean }) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const svc = yield* Session.Service
|
||||||
let sessionID = args.sessionID ? SessionID.make(args.sessionID) : undefined
|
let sessionID = args.sessionID ? SessionID.make(args.sessionID) : undefined
|
||||||
process.stderr.write(`Exporting session: ${sessionID ?? "latest"}\n`)
|
process.stderr.write(`Exporting session: ${sessionID ?? "latest"}\n`)
|
||||||
|
|
||||||
if (!sessionID) {
|
if (!sessionID) {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("Export session", {
|
prompts.intro("Export session", { output: process.stderr })
|
||||||
output: process.stderr,
|
|
||||||
})
|
|
||||||
|
|
||||||
const sessions = await AppRuntime.runPromise(Session.Service.use((svc) => svc.list()))
|
const sessions = yield* svc.list()
|
||||||
|
|
||||||
if (sessions.length === 0) {
|
if (sessions.length === 0) {
|
||||||
prompts.log.error("No sessions found", {
|
prompts.log.error("No sessions found", { output: process.stderr })
|
||||||
output: process.stderr,
|
prompts.outro("Done", { output: process.stderr })
|
||||||
})
|
|
||||||
prompts.outro("Done", {
|
|
||||||
output: process.stderr,
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sessions.sort((a, b) => b.time.updated - a.time.updated)
|
sessions.sort((a, b) => b.time.updated - a.time.updated)
|
||||||
|
|
||||||
const selectedSession = await prompts.autocomplete({
|
const selectedSession = yield* Effect.promise(() =>
|
||||||
|
prompts.autocomplete({
|
||||||
message: "Select session to export",
|
message: "Select session to export",
|
||||||
maxItems: 10,
|
maxItems: 10,
|
||||||
options: sessions.map((session) => ({
|
options: sessions.map((session) => ({
|
||||||
@@ -268,36 +265,27 @@ export const ExportCommand = cmd({
|
|||||||
hint: `${new Date(session.time.updated).toLocaleString()} • ${session.id.slice(-8)}`,
|
hint: `${new Date(session.time.updated).toLocaleString()} • ${session.id.slice(-8)}`,
|
||||||
})),
|
})),
|
||||||
output: process.stderr,
|
output: process.stderr,
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
if (prompts.isCancel(selectedSession)) {
|
if (prompts.isCancel(selectedSession)) {
|
||||||
throw new UI.CancelledError()
|
return yield* Effect.die(new UI.CancelledError())
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID = selectedSession
|
sessionID = selectedSession
|
||||||
|
|
||||||
prompts.outro("Exporting session...", {
|
prompts.outro("Exporting session...", { output: process.stderr })
|
||||||
output: process.stderr,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Match legacy try/catch — catches both typed failures and defects
|
||||||
const sessionInfo = await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID!)))
|
// (Session.Service.get throws NotFoundError as a defect, not a typed E).
|
||||||
const messages = await AppRuntime.runPromise(
|
return yield* Effect.gen(function* () {
|
||||||
Session.Service.use((svc) => svc.messages({ sessionID: sessionInfo.id })),
|
const sessionInfo = yield* svc.get(sessionID!)
|
||||||
)
|
const messages = yield* svc.messages({ sessionID: sessionInfo.id })
|
||||||
|
|
||||||
const exportData = {
|
const exportData = { info: sessionInfo, messages }
|
||||||
info: sessionInfo,
|
|
||||||
messages,
|
|
||||||
}
|
|
||||||
|
|
||||||
process.stdout.write(JSON.stringify(args.sanitize ? sanitize(exportData) : exportData, null, 2))
|
process.stdout.write(JSON.stringify(args.sanitize ? sanitize(exportData) : exportData, null, 2))
|
||||||
process.stdout.write(EOL)
|
process.stdout.write(EOL)
|
||||||
} catch {
|
}).pipe(Effect.catchCause(() => fail(`Session not found: ${sessionID!}`)))
|
||||||
UI.error(`Session not found: ${sessionID!}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
import { Server } from "../../server/server"
|
import { Server } from "../../server/server"
|
||||||
import { PublicApi } from "../../server/routes/instance/httpapi/public"
|
|
||||||
import type { CommandModule } from "yargs"
|
import type { CommandModule } from "yargs"
|
||||||
import { OpenApi } from "effect/unstable/httpapi"
|
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
httpapi: boolean
|
httpapi: boolean
|
||||||
|
hono: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GenerateCommand = {
|
export const GenerateCommand = {
|
||||||
command: "generate",
|
command: "generate",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs.option("httpapi", {
|
yargs
|
||||||
|
.option("httpapi", {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
default: false,
|
default: false,
|
||||||
description: "Generate OpenAPI from the experimental Effect HttpApi contract",
|
description:
|
||||||
|
"Generate OpenAPI from the Effect HttpApi contract (default; flag retained for backwards compatibility)",
|
||||||
|
})
|
||||||
|
.option("hono", {
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
description: "Generate OpenAPI from the legacy Hono backend (parity-diff only; will be removed)",
|
||||||
}),
|
}),
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
const specs = args.httpapi ? OpenApi.fromApi(PublicApi) : await Server.openapi()
|
const specs = args.hono ? await Server.openapiHono() : await Server.openapi()
|
||||||
for (const item of Object.values(specs.paths)) {
|
for (const item of Object.values(specs.paths)) {
|
||||||
for (const method of ["get", "post", "put", "delete", "patch"] as const) {
|
for (const method of ["get", "post", "put", "delete", "patch"] as const) {
|
||||||
const operation = item[method]
|
const operation = item[method]
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ import type {
|
|||||||
} from "@octokit/webhooks-types"
|
} from "@octokit/webhooks-types"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
|
import { effectCmd } from "../effect-cmd"
|
||||||
import { ModelsDev } from "@/provider/models"
|
import { ModelsDev } from "@/provider/models"
|
||||||
import { Instance } from "@/project/instance"
|
import { InstanceRef } from "@/effect/instance-ref"
|
||||||
import { bootstrap } from "../bootstrap"
|
|
||||||
import { SessionShare } from "@/share/session"
|
import { SessionShare } from "@/share/session"
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
import type { SessionID } from "../../session/schema"
|
import type { SessionID } from "../../session/schema"
|
||||||
@@ -29,7 +29,6 @@ import { Provider } from "@/provider/provider"
|
|||||||
import { Bus } from "../../bus"
|
import { Bus } from "../../bus"
|
||||||
import { MessageV2 } from "../../session/message-v2"
|
import { MessageV2 } from "../../session/message-v2"
|
||||||
import { SessionPrompt } from "@/session/prompt"
|
import { SessionPrompt } from "@/session/prompt"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { Git } from "@/git"
|
import { Git } from "@/git"
|
||||||
import { setTimeout as sleep } from "node:timers/promises"
|
import { setTimeout as sleep } from "node:timers/promises"
|
||||||
import { Process } from "@/util/process"
|
import { Process } from "@/util/process"
|
||||||
@@ -199,20 +198,23 @@ export const GithubCommand = cmd({
|
|||||||
async handler() {},
|
async handler() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const GithubInstallCommand = cmd({
|
export const GithubInstallCommand = effectCmd({
|
||||||
command: "install",
|
command: "install",
|
||||||
describe: "install the GitHub agent",
|
describe: "install the GitHub agent",
|
||||||
async handler() {
|
handler: Effect.fn("Cli.github.install")(function* () {
|
||||||
await Instance.provide({
|
const maybeCtx = yield* InstanceRef
|
||||||
directory: process.cwd(),
|
if (!maybeCtx) return yield* Effect.die("InstanceRef not provided")
|
||||||
async fn() {
|
const ctx = maybeCtx
|
||||||
|
const modelsDev = yield* ModelsDev.Service
|
||||||
|
const gitSvc = yield* Git.Service
|
||||||
|
yield* Effect.promise(async () => {
|
||||||
{
|
{
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("Install GitHub agent")
|
prompts.intro("Install GitHub agent")
|
||||||
const app = await getAppInfo()
|
const app = await getAppInfo()
|
||||||
await installGitHubApp()
|
await installGitHubApp()
|
||||||
|
|
||||||
const providers = await ModelsDev.get().then((p) => {
|
const providers = await Effect.runPromise(modelsDev.get()).then((p) => {
|
||||||
// TODO: add guide for copilot, for now just hide it
|
// TODO: add guide for copilot, for now just hide it
|
||||||
delete p["github-copilot"]
|
delete p["github-copilot"]
|
||||||
return p
|
return p
|
||||||
@@ -253,22 +255,22 @@ export const GithubInstallCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getAppInfo() {
|
async function getAppInfo() {
|
||||||
const project = Instance.project
|
const project = ctx.project
|
||||||
if (project.vcs !== "git") {
|
if (project.vcs !== "git") {
|
||||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||||
throw new UI.CancelledError()
|
throw new UI.CancelledError()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get repo info
|
// Get repo info
|
||||||
const info = await AppRuntime.runPromise(
|
const info = await Effect.runPromise(gitSvc.run(["remote", "get-url", "origin"], { cwd: ctx.worktree })).then(
|
||||||
Git.Service.use((git) => git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })),
|
(x) => x.text().trim(),
|
||||||
).then((x) => x.text().trim())
|
)
|
||||||
const parsed = parseGitHubRemote(info)
|
const parsed = parseGitHubRemote(info)
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||||
throw new UI.CancelledError()
|
throw new UI.CancelledError()
|
||||||
}
|
}
|
||||||
return { owner: parsed.owner, repo: parsed.repo, root: Instance.worktree }
|
return { owner: parsed.owner, repo: parsed.repo, root: ctx.worktree }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function promptProvider() {
|
async function promptProvider() {
|
||||||
@@ -419,12 +421,11 @@ jobs:
|
|||||||
prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
|
prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
})
|
})
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const GithubRunCommand = cmd({
|
export const GithubRunCommand = effectCmd({
|
||||||
command: "run",
|
command: "run",
|
||||||
describe: "run the GitHub agent",
|
describe: "run the GitHub agent",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
@@ -437,8 +438,14 @@ export const GithubRunCommand = cmd({
|
|||||||
type: "string",
|
type: "string",
|
||||||
describe: "GitHub personal access token (github_pat_********)",
|
describe: "GitHub personal access token (github_pat_********)",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.github.run")(function* (args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const ctx = yield* InstanceRef
|
||||||
|
if (!ctx) return yield* Effect.die("InstanceRef not provided")
|
||||||
|
const gitSvc = yield* Git.Service
|
||||||
|
const sessionSvc = yield* Session.Service
|
||||||
|
const sessionShare = yield* SessionShare.Service
|
||||||
|
const sessionPrompt = yield* SessionPrompt.Service
|
||||||
|
yield* Effect.promise(async () => {
|
||||||
const isMock = args.token || args.event
|
const isMock = args.token || args.event
|
||||||
|
|
||||||
const context = isMock ? (JSON.parse(args.event!) as Context) : github.context
|
const context = isMock ? (JSON.parse(args.event!) as Context) : github.context
|
||||||
@@ -501,21 +508,20 @@ export const GithubRunCommand = cmd({
|
|||||||
: "issue"
|
: "issue"
|
||||||
: undefined
|
: undefined
|
||||||
const gitText = async (args: string[]) => {
|
const gitText = async (args: string[]) => {
|
||||||
const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
|
const result = await Effect.runPromise(gitSvc.run(args, { cwd: ctx.worktree }))
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
||||||
}
|
}
|
||||||
return result.text().trim()
|
return result.text().trim()
|
||||||
}
|
}
|
||||||
const gitRun = async (args: string[]) => {
|
const gitRun = async (args: string[]) => {
|
||||||
const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
|
const result = await Effect.runPromise(gitSvc.run(args, { cwd: ctx.worktree }))
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
const gitStatus = (args: string[]) =>
|
const gitStatus = (args: string[]) => Effect.runPromise(gitSvc.run(args, { cwd: ctx.worktree }))
|
||||||
AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
|
|
||||||
const commitChanges = async (summary: string, actor?: string) => {
|
const commitChanges = async (summary: string, actor?: string) => {
|
||||||
const args = ["commit", "-m", summary]
|
const args = ["commit", "-m", summary]
|
||||||
if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)
|
if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)
|
||||||
@@ -552,9 +558,8 @@ export const GithubRunCommand = cmd({
|
|||||||
|
|
||||||
// Setup opencode session
|
// Setup opencode session
|
||||||
const repoData = await fetchRepo()
|
const repoData = await fetchRepo()
|
||||||
session = await AppRuntime.runPromise(
|
session = await Effect.runPromise(
|
||||||
Session.Service.use((svc) =>
|
sessionSvc.create({
|
||||||
svc.create({
|
|
||||||
permission: [
|
permission: [
|
||||||
{
|
{
|
||||||
permission: "question",
|
permission: "question",
|
||||||
@@ -563,13 +568,12 @@ export const GithubRunCommand = cmd({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
subscribeSessionEvents()
|
subscribeSessionEvents()
|
||||||
shareId = await (async () => {
|
shareId = await (async () => {
|
||||||
if (share === false) return
|
if (share === false) return
|
||||||
if (!share && repoData.data.private) return
|
if (!share && repoData.data.private) return
|
||||||
await AppRuntime.runPromise(SessionShare.Service.use((svc) => svc.share(session.id)))
|
await Effect.runPromise(sessionShare.share(session.id))
|
||||||
return session.id.slice(-8)
|
return session.id.slice(-8)
|
||||||
})()
|
})()
|
||||||
console.log("opencode session", session.id)
|
console.log("opencode session", session.id)
|
||||||
@@ -879,7 +883,7 @@ export const GithubRunCommand = cmd({
|
|||||||
function subscribeSessionEvents() {
|
function subscribeSessionEvents() {
|
||||||
const TOOL: Record<string, [string, string]> = {
|
const TOOL: Record<string, [string, string]> = {
|
||||||
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||||
bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
|
bash: ["Shell", UI.Style.TEXT_DANGER_BOLD],
|
||||||
edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
|
edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
|
||||||
glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
|
glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
|
||||||
grep: ["Grep", UI.Style.TEXT_INFO_BOLD],
|
grep: ["Grep", UI.Style.TEXT_INFO_BOLD],
|
||||||
@@ -942,9 +946,9 @@ export const GithubRunCommand = cmd({
|
|||||||
async function chat(message: string, files: PromptFiles = []) {
|
async function chat(message: string, files: PromptFiles = []) {
|
||||||
console.log("Sending message to opencode...")
|
console.log("Sending message to opencode...")
|
||||||
|
|
||||||
return AppRuntime.runPromise(
|
return Effect.runPromise(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const prompt = yield* SessionPrompt.Service
|
const prompt = sessionPrompt
|
||||||
const result = yield* prompt.prompt({
|
const result = yield* prompt.prompt({
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
messageID: MessageID.ascending(),
|
messageID: MessageID.ascending(),
|
||||||
@@ -1645,5 +1649,5 @@ query($owner: String!, $repo: String!, $number: Int!) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import type { Argv } from "yargs"
|
|
||||||
import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2"
|
import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2"
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
import { MessageV2 } from "../../session/message-v2"
|
import { MessageV2 } from "../../session/message-v2"
|
||||||
import { cmd } from "./cmd"
|
import { CliError, effectCmd } from "../effect-cmd"
|
||||||
import { bootstrap } from "../bootstrap"
|
|
||||||
import { Database } from "@/storage/db"
|
import { Database } from "@/storage/db"
|
||||||
import { SessionTable, MessageTable, PartTable } from "../../session/session.sql"
|
import { SessionTable, MessageTable, PartTable } from "../../session/session.sql"
|
||||||
import { Instance } from "../../project/instance"
|
import { InstanceRef } from "@/effect/instance-ref"
|
||||||
import { ShareNext } from "@/share/share-next"
|
import { ShareNext } from "@/share/share-next"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { Effect, Schema } from "effect"
|
||||||
import { Schema } from "effect"
|
|
||||||
|
|
||||||
const decodeMessageInfo = Schema.decodeUnknownSync(MessageV2.Info)
|
const decodeMessageInfo = Schema.decodeUnknownSync(MessageV2.Info)
|
||||||
const decodePart = Schema.decodeUnknownSync(MessageV2.Part)
|
const decodePart = Schema.decodeUnknownSync(MessageV2.Part)
|
||||||
@@ -78,53 +75,58 @@ export function transformShareData(shareData: ShareData[]): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImportCommand = cmd({
|
type ExportData = { info: SDKSession; messages: Array<{ info: Message; parts: Part[] }> }
|
||||||
|
|
||||||
|
export const ImportCommand = effectCmd({
|
||||||
command: "import <file>",
|
command: "import <file>",
|
||||||
describe: "import session data from JSON file or URL",
|
describe: "import session data from JSON file or URL",
|
||||||
builder: (yargs: Argv) => {
|
builder: (yargs) =>
|
||||||
return yargs.positional("file", {
|
yargs.positional("file", {
|
||||||
describe: "path to JSON file or share URL",
|
describe: "path to JSON file or share URL",
|
||||||
type: "string",
|
type: "string",
|
||||||
demandOption: true,
|
demandOption: true,
|
||||||
|
}),
|
||||||
|
handler: Effect.fn("Cli.import")(function* (args) {
|
||||||
|
const ctx = yield* InstanceRef
|
||||||
|
if (!ctx) return yield* Effect.die("InstanceRef not provided")
|
||||||
|
return yield* runImport(args.file, ctx.project.id)
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
},
|
|
||||||
handler: async (args) => {
|
|
||||||
await bootstrap(process.cwd(), async () => {
|
|
||||||
let exportData:
|
|
||||||
| {
|
|
||||||
info: SDKSession
|
|
||||||
messages: Array<{
|
|
||||||
info: Message
|
|
||||||
parts: Part[]
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
|
|
||||||
const isUrl = args.file.startsWith("http://") || args.file.startsWith("https://")
|
const runImport = Effect.fn("Cli.import.body")(function* (file: string, projectID: string) {
|
||||||
|
const share = yield* ShareNext.Service
|
||||||
|
|
||||||
|
let exportData: ExportData | undefined
|
||||||
|
|
||||||
|
const isUrl = file.startsWith("http://") || file.startsWith("https://")
|
||||||
|
|
||||||
if (isUrl) {
|
if (isUrl) {
|
||||||
const slug = parseShareUrl(args.file)
|
const slug = parseShareUrl(file)
|
||||||
if (!slug) {
|
if (!slug) {
|
||||||
const baseUrl = await AppRuntime.runPromise(ShareNext.Service.use((svc) => svc.url()))
|
const baseUrl = yield* Effect.orDie(share.url())
|
||||||
process.stdout.write(`Invalid URL format. Expected: ${baseUrl}/share/<slug>`)
|
process.stdout.write(`Invalid URL format. Expected: ${baseUrl}/share/<slug>`)
|
||||||
process.stdout.write(EOL)
|
process.stdout.write(EOL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = new URL(args.file)
|
const baseUrl = new URL(file).origin
|
||||||
const baseUrl = parsed.origin
|
const req = yield* Effect.orDie(share.request())
|
||||||
const req = await AppRuntime.runPromise(ShareNext.Service.use((svc) => svc.request()))
|
const headers = shouldAttachShareAuthHeaders(file, req.baseUrl) ? req.headers : {}
|
||||||
const headers = shouldAttachShareAuthHeaders(args.file, req.baseUrl) ? req.headers : {}
|
|
||||||
|
const tryFetch = (url: string) =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: () => fetch(url, { headers }),
|
||||||
|
catch: (e) =>
|
||||||
|
new CliError({
|
||||||
|
message: `Failed to fetch share data: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
const dataPath = req.api.data(slug)
|
const dataPath = req.api.data(slug)
|
||||||
let response = await fetch(`${baseUrl}${dataPath}`, {
|
let response = yield* tryFetch(`${baseUrl}${dataPath}`)
|
||||||
headers,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok && dataPath !== `/api/share/${slug}/data`) {
|
if (!response.ok && dataPath !== `/api/share/${slug}/data`) {
|
||||||
response = await fetch(`${baseUrl}/api/share/${slug}/data`, {
|
response = yield* tryFetch(`${baseUrl}/api/share/${slug}/data`)
|
||||||
headers,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -133,7 +135,10 @@ export const ImportCommand = cmd({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareData: ShareData[] = await response.json()
|
const shareData = yield* Effect.tryPromise({
|
||||||
|
try: () => response.json() as Promise<ShareData[]>,
|
||||||
|
catch: () => new CliError({ message: "Share data was not valid JSON" }),
|
||||||
|
})
|
||||||
const transformed = transformShareData(shareData)
|
const transformed = transformShareData(shareData)
|
||||||
|
|
||||||
if (!transformed) {
|
if (!transformed) {
|
||||||
@@ -144,9 +149,11 @@ export const ImportCommand = cmd({
|
|||||||
|
|
||||||
exportData = transformed
|
exportData = transformed
|
||||||
} else {
|
} else {
|
||||||
exportData = await Filesystem.readJson<NonNullable<typeof exportData>>(args.file).catch(() => undefined)
|
exportData = yield* Effect.promise(() =>
|
||||||
|
Filesystem.readJson<NonNullable<typeof exportData>>(file).catch(() => undefined),
|
||||||
|
)
|
||||||
if (!exportData) {
|
if (!exportData) {
|
||||||
process.stdout.write(`File not found: ${args.file}`)
|
process.stdout.write(`File not found: ${file}`)
|
||||||
process.stdout.write(EOL)
|
process.stdout.write(EOL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -160,7 +167,7 @@ export const ImportCommand = cmd({
|
|||||||
|
|
||||||
const info = Schema.decodeUnknownSync(Session.Info)({
|
const info = Schema.decodeUnknownSync(Session.Info)({
|
||||||
...exportData.info,
|
...exportData.info,
|
||||||
projectID: Instance.project.id,
|
projectID,
|
||||||
}) as Session.Info
|
}) as Session.Info
|
||||||
const row = Session.toRow(info)
|
const row = Session.toRow(info)
|
||||||
Database.use((db) =>
|
Database.use((db) =>
|
||||||
@@ -208,5 +215,3 @@ export const ImportCommand = cmd({
|
|||||||
process.stdout.write(`Imported session: ${exportData.info.id}`)
|
process.stdout.write(`Imported session: ${exportData.info.id}`)
|
||||||
process.stdout.write(EOL)
|
process.stdout.write(EOL)
|
||||||
})
|
})
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
|
import { effectCmd } from "../effect-cmd"
|
||||||
|
import { Cause } from "effect"
|
||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
||||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
||||||
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
|
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
|
||||||
@@ -9,7 +11,7 @@ import { McpAuth } from "../../mcp/auth"
|
|||||||
import { McpOAuthProvider } from "../../mcp/oauth-provider"
|
import { McpOAuthProvider } from "../../mcp/oauth-provider"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { ConfigMCP } from "../../config/mcp"
|
import { ConfigMCP } from "../../config/mcp"
|
||||||
import { Instance } from "../../project/instance"
|
import { InstanceRef } from "@/effect/instance-ref"
|
||||||
import { Installation } from "../../installation"
|
import { Installation } from "../../installation"
|
||||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
@@ -17,7 +19,6 @@ import { Global } from "@opencode-ai/core/global"
|
|||||||
import { modify, applyEdits } from "jsonc-parser"
|
import { modify, applyEdits } from "jsonc-parser"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { Bus } from "../../bus"
|
import { Bus } from "../../bus"
|
||||||
import { AppRuntime } from "../../effect/app-runtime"
|
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
|
|
||||||
function getAuthStatusIcon(status: MCP.AuthStatus): string {
|
function getAuthStatusIcon(status: MCP.AuthStatus): string {
|
||||||
@@ -64,9 +65,8 @@ function oauthServers(config: Config.Info) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listState() {
|
function listState() {
|
||||||
return AppRuntime.runPromise(
|
return Effect.gen(function* () {
|
||||||
Effect.gen(function* () {
|
|
||||||
const cfg = yield* Config.Service
|
const cfg = yield* Config.Service
|
||||||
const mcp = yield* MCP.Service
|
const mcp = yield* MCP.Service
|
||||||
const config = yield* cfg.get()
|
const config = yield* cfg.get()
|
||||||
@@ -76,13 +76,11 @@ async function listState() {
|
|||||||
{ concurrency: "unbounded" },
|
{ concurrency: "unbounded" },
|
||||||
)
|
)
|
||||||
return { config, statuses, stored }
|
return { config, statuses, stored }
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function authState() {
|
function authState() {
|
||||||
return AppRuntime.runPromise(
|
return Effect.gen(function* () {
|
||||||
Effect.gen(function* () {
|
|
||||||
const cfg = yield* Config.Service
|
const cfg = yield* Config.Service
|
||||||
const mcp = yield* MCP.Service
|
const mcp = yield* MCP.Service
|
||||||
const config = yield* cfg.get()
|
const config = yield* cfg.get()
|
||||||
@@ -91,8 +89,7 @@ async function authState() {
|
|||||||
{ concurrency: "unbounded" },
|
{ concurrency: "unbounded" },
|
||||||
)
|
)
|
||||||
return { config, auth }
|
return { config, auth }
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const McpCommand = cmd({
|
export const McpCommand = cmd({
|
||||||
@@ -109,18 +106,15 @@ export const McpCommand = cmd({
|
|||||||
async handler() {},
|
async handler() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const McpListCommand = cmd({
|
export const McpListCommand = effectCmd({
|
||||||
command: "list",
|
command: "list",
|
||||||
aliases: ["ls"],
|
aliases: ["ls"],
|
||||||
describe: "list MCP servers and their status",
|
describe: "list MCP servers and their status",
|
||||||
async handler() {
|
handler: Effect.fn("Cli.mcp.list")(function* () {
|
||||||
await Instance.provide({
|
|
||||||
directory: process.cwd(),
|
|
||||||
async fn() {
|
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP Servers")
|
prompts.intro("MCP Servers")
|
||||||
|
|
||||||
const { config, statuses, stored } = await listState()
|
const { config, statuses, stored } = yield* listState()
|
||||||
const servers = configuredServers(config)
|
const servers = configuredServers(config)
|
||||||
|
|
||||||
if (servers.length === 0) {
|
if (servers.length === 0) {
|
||||||
@@ -170,12 +164,10 @@ export const McpListCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
prompts.outro(`${servers.length} server(s)`)
|
prompts.outro(`${servers.length} server(s)`)
|
||||||
},
|
}),
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const McpAuthCommand = cmd({
|
export const McpAuthCommand = effectCmd({
|
||||||
command: "auth [name]",
|
command: "auth [name]",
|
||||||
describe: "authenticate with an OAuth-enabled MCP server",
|
describe: "authenticate with an OAuth-enabled MCP server",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
@@ -185,14 +177,11 @@ export const McpAuthCommand = cmd({
|
|||||||
type: "string",
|
type: "string",
|
||||||
})
|
})
|
||||||
.command(McpAuthListCommand),
|
.command(McpAuthListCommand),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.mcp.auth")(function* (args) {
|
||||||
await Instance.provide({
|
|
||||||
directory: process.cwd(),
|
|
||||||
async fn() {
|
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Authentication")
|
prompts.intro("MCP OAuth Authentication")
|
||||||
|
|
||||||
const { config, auth } = await authState()
|
const { config, auth } = yield* authState()
|
||||||
const mcpServers = config.mcp ?? {}
|
const mcpServers = config.mcp ?? {}
|
||||||
const servers = oauthServers(config)
|
const servers = oauthServers(config)
|
||||||
|
|
||||||
@@ -225,10 +214,12 @@ export const McpAuthCommand = cmd({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const selected = await prompts.select({
|
const selected = yield* Effect.promise(() =>
|
||||||
|
prompts.select({
|
||||||
message: "Select MCP server to authenticate",
|
message: "Select MCP server to authenticate",
|
||||||
options,
|
options,
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
||||||
serverName = selected
|
serverName = selected
|
||||||
}
|
}
|
||||||
@@ -247,12 +238,13 @@ export const McpAuthCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if already authenticated
|
// Check if already authenticated
|
||||||
const authStatus =
|
const authStatus = auth[serverName] ?? (yield* MCP.Service.use((mcp) => mcp.getAuthStatus(serverName)))
|
||||||
auth[serverName] ?? (await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.getAuthStatus(serverName))))
|
|
||||||
if (authStatus === "authenticated") {
|
if (authStatus === "authenticated") {
|
||||||
const confirm = await prompts.confirm({
|
const confirm = yield* Effect.promise(() =>
|
||||||
|
prompts.confirm({
|
||||||
message: `${serverName} already has valid credentials. Re-authenticate?`,
|
message: `${serverName} already has valid credentials. Re-authenticate?`,
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
if (prompts.isCancel(confirm) || !confirm) {
|
if (prompts.isCancel(confirm) || !confirm) {
|
||||||
prompts.outro("Cancelled")
|
prompts.outro("Cancelled")
|
||||||
return
|
return
|
||||||
@@ -274,9 +266,9 @@ export const McpAuthCommand = cmd({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
yield* MCP.Service.use((mcp) => mcp.authenticate(serverName)).pipe(
|
||||||
const status = await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.authenticate(serverName)))
|
Effect.tap((status) =>
|
||||||
|
Effect.sync(() => {
|
||||||
if (status.status === "connected") {
|
if (status.status === "connected") {
|
||||||
spinner.stop("Authentication successful!")
|
spinner.stop("Authentication successful!")
|
||||||
} else if (status.status === "needs_client_registration") {
|
} else if (status.status === "needs_client_registration") {
|
||||||
@@ -300,31 +292,31 @@ export const McpAuthCommand = cmd({
|
|||||||
} else {
|
} else {
|
||||||
spinner.stop("Unexpected status: " + status.status, 1)
|
spinner.stop("Unexpected status: " + status.status, 1)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}),
|
||||||
|
),
|
||||||
|
Effect.catchCause((cause) =>
|
||||||
|
Effect.sync(() => {
|
||||||
spinner.stop("Authentication failed", 1)
|
spinner.stop("Authentication failed", 1)
|
||||||
|
const error = Cause.squash(cause)
|
||||||
prompts.log.error(error instanceof Error ? error.message : String(error))
|
prompts.log.error(error instanceof Error ? error.message : String(error))
|
||||||
} finally {
|
}),
|
||||||
unsubscribe()
|
),
|
||||||
}
|
Effect.ensuring(Effect.sync(() => unsubscribe())),
|
||||||
|
)
|
||||||
|
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
},
|
}),
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const McpAuthListCommand = cmd({
|
export const McpAuthListCommand = effectCmd({
|
||||||
command: "list",
|
command: "list",
|
||||||
aliases: ["ls"],
|
aliases: ["ls"],
|
||||||
describe: "list OAuth-capable MCP servers and their auth status",
|
describe: "list OAuth-capable MCP servers and their auth status",
|
||||||
async handler() {
|
handler: Effect.fn("Cli.mcp.auth.list")(function* () {
|
||||||
await Instance.provide({
|
|
||||||
directory: process.cwd(),
|
|
||||||
async fn() {
|
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Status")
|
prompts.intro("MCP OAuth Status")
|
||||||
|
|
||||||
const { config, auth } = await authState()
|
const { config, auth } = yield* authState()
|
||||||
const servers = oauthServers(config)
|
const servers = oauthServers(config)
|
||||||
|
|
||||||
if (servers.length === 0) {
|
if (servers.length === 0) {
|
||||||
@@ -343,12 +335,10 @@ export const McpAuthListCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
prompts.outro(`${servers.length} OAuth-capable server(s)`)
|
prompts.outro(`${servers.length} OAuth-capable server(s)`)
|
||||||
},
|
}),
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const McpLogoutCommand = cmd({
|
export const McpLogoutCommand = effectCmd({
|
||||||
command: "logout [name]",
|
command: "logout [name]",
|
||||||
describe: "remove OAuth credentials for an MCP server",
|
describe: "remove OAuth credentials for an MCP server",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
@@ -356,14 +346,11 @@ export const McpLogoutCommand = cmd({
|
|||||||
describe: "name of the MCP server",
|
describe: "name of the MCP server",
|
||||||
type: "string",
|
type: "string",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.mcp.logout")(function* (args) {
|
||||||
await Instance.provide({
|
|
||||||
directory: process.cwd(),
|
|
||||||
async fn() {
|
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Logout")
|
prompts.intro("MCP OAuth Logout")
|
||||||
|
|
||||||
const credentials = await AppRuntime.runPromise(McpAuth.Service.use((auth) => auth.all()))
|
const credentials = yield* McpAuth.Service.use((auth) => auth.all())
|
||||||
const serverNames = Object.keys(credentials)
|
const serverNames = Object.keys(credentials)
|
||||||
|
|
||||||
if (serverNames.length === 0) {
|
if (serverNames.length === 0) {
|
||||||
@@ -374,7 +361,8 @@ export const McpLogoutCommand = cmd({
|
|||||||
|
|
||||||
let serverName = args.name
|
let serverName = args.name
|
||||||
if (!serverName) {
|
if (!serverName) {
|
||||||
const selected = await prompts.select({
|
const selected = yield* Effect.promise(() =>
|
||||||
|
prompts.select({
|
||||||
message: "Select MCP server to logout",
|
message: "Select MCP server to logout",
|
||||||
options: serverNames.map((name) => {
|
options: serverNames.map((name) => {
|
||||||
const entry = credentials[name]
|
const entry = credentials[name]
|
||||||
@@ -390,7 +378,8 @@ export const McpLogoutCommand = cmd({
|
|||||||
hint,
|
hint,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
||||||
serverName = selected
|
serverName = selected
|
||||||
}
|
}
|
||||||
@@ -401,12 +390,10 @@ export const McpLogoutCommand = cmd({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.removeAuth(serverName)))
|
yield* MCP.Service.use((mcp) => mcp.removeAuth(serverName))
|
||||||
prompts.log.success(`Removed OAuth credentials for ${serverName}`)
|
prompts.log.success(`Removed OAuth credentials for ${serverName}`)
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
},
|
}),
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
async function resolveConfigPath(baseDir: string, global = false) {
|
async function resolveConfigPath(baseDir: string, global = false) {
|
||||||
@@ -444,21 +431,22 @@ async function addMcpToConfig(name: string, mcpConfig: ConfigMCP.Info, configPat
|
|||||||
return configPath
|
return configPath
|
||||||
}
|
}
|
||||||
|
|
||||||
export const McpAddCommand = cmd({
|
export const McpAddCommand = effectCmd({
|
||||||
command: "add",
|
command: "add",
|
||||||
describe: "add an MCP server",
|
describe: "add an MCP server",
|
||||||
async handler() {
|
handler: Effect.fn("Cli.mcp.add")(function* () {
|
||||||
await Instance.provide({
|
const maybeCtx = yield* InstanceRef
|
||||||
directory: process.cwd(),
|
if (!maybeCtx) return yield* Effect.die("InstanceRef not provided")
|
||||||
async fn() {
|
const ctx = maybeCtx
|
||||||
|
yield* Effect.promise(async () => {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("Add MCP server")
|
prompts.intro("Add MCP server")
|
||||||
|
|
||||||
const project = Instance.project
|
const project = ctx.project
|
||||||
|
|
||||||
// Resolve config paths eagerly for hints
|
// Resolve config paths eagerly for hints
|
||||||
const [projectConfigPath, globalConfigPath] = await Promise.all([
|
const [projectConfigPath, globalConfigPath] = await Promise.all([
|
||||||
resolveConfigPath(Instance.worktree),
|
resolveConfigPath(ctx.worktree),
|
||||||
resolveConfigPath(Global.Path.config, true),
|
resolveConfigPath(Global.Path.config, true),
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -603,12 +591,11 @@ export const McpAddCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
prompts.outro("MCP server added successfully")
|
prompts.outro("MCP server added successfully")
|
||||||
},
|
|
||||||
})
|
})
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const McpDebugCommand = cmd({
|
export const McpDebugCommand = effectCmd({
|
||||||
command: "debug <name>",
|
command: "debug <name>",
|
||||||
describe: "debug OAuth connection for an MCP server",
|
describe: "debug OAuth connection for an MCP server",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
@@ -617,14 +604,14 @@ export const McpDebugCommand = cmd({
|
|||||||
type: "string",
|
type: "string",
|
||||||
demandOption: true,
|
demandOption: true,
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.mcp.debug")(function* (args) {
|
||||||
await Instance.provide({
|
const config = yield* Config.Service.use((cfg) => cfg.get())
|
||||||
directory: process.cwd(),
|
const mcp = yield* MCP.Service
|
||||||
async fn() {
|
const auth = yield* McpAuth.Service
|
||||||
|
yield* Effect.promise(async () => {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Debug")
|
prompts.intro("MCP OAuth Debug")
|
||||||
|
|
||||||
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
|
|
||||||
const mcpServers = config.mcp ?? {}
|
const mcpServers = config.mcp ?? {}
|
||||||
const serverName = args.name
|
const serverName = args.name
|
||||||
|
|
||||||
@@ -650,15 +637,11 @@ export const McpDebugCommand = cmd({
|
|||||||
prompts.log.info(`Server: ${serverName}`)
|
prompts.log.info(`Server: ${serverName}`)
|
||||||
prompts.log.info(`URL: ${serverConfig.url}`)
|
prompts.log.info(`URL: ${serverConfig.url}`)
|
||||||
|
|
||||||
// Check stored auth status
|
// Check stored auth status — services already in hand, run inline.
|
||||||
const { authStatus, entry } = await AppRuntime.runPromise(
|
const { authStatus, entry } = await Effect.runPromise(
|
||||||
Effect.gen(function* () {
|
Effect.all({
|
||||||
const mcp = yield* MCP.Service
|
authStatus: mcp.getAuthStatus(serverName),
|
||||||
const auth = yield* McpAuth.Service
|
entry: auth.get(serverName),
|
||||||
return {
|
|
||||||
authStatus: yield* mcp.getAuthStatus(serverName),
|
|
||||||
entry: yield* auth.get(serverName),
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
prompts.log.info(`Auth status: ${getAuthStatusIcon(authStatus)} ${getAuthStatusText(authStatus)}`)
|
prompts.log.info(`Auth status: ${getAuthStatusIcon(authStatus)} ${getAuthStatusText(authStatus)}`)
|
||||||
@@ -718,11 +701,6 @@ export const McpDebugCommand = cmd({
|
|||||||
|
|
||||||
// Try to discover OAuth metadata
|
// Try to discover OAuth metadata
|
||||||
const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined
|
const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined
|
||||||
const auth = await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
return yield* McpAuth.Service
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const authProvider = new McpOAuthProvider(
|
const authProvider = new McpOAuthProvider(
|
||||||
serverName,
|
serverName,
|
||||||
serverConfig.url,
|
serverConfig.url,
|
||||||
@@ -792,7 +770,6 @@ export const McpDebugCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
prompts.outro("Debug complete")
|
prompts.outro("Debug complete")
|
||||||
},
|
|
||||||
})
|
})
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import type { Argv } from "yargs"
|
import { EOL } from "os"
|
||||||
import { Instance } from "../../project/instance"
|
import { Effect } from "effect"
|
||||||
import { Provider } from "@/provider/provider"
|
import { Provider } from "@/provider/provider"
|
||||||
import { ProviderID } from "../../provider/schema"
|
import { ProviderID } from "../../provider/schema"
|
||||||
import { ModelsDev } from "@/provider/models"
|
import { ModelsDev } from "@/provider/models"
|
||||||
import { cmd } from "./cmd"
|
import { effectCmd, fail } from "../effect-cmd"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { EOL } from "os"
|
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
export const ModelsCommand = cmd({
|
export const ModelsCommand = effectCmd({
|
||||||
command: "models [provider]",
|
command: "models [provider]",
|
||||||
describe: "list all available models",
|
describe: "list all available models",
|
||||||
builder: (yargs: Argv) => {
|
builder: (yargs) =>
|
||||||
return yargs
|
yargs
|
||||||
.positional("provider", {
|
.positional("provider", {
|
||||||
describe: "provider ID to filter models by",
|
describe: "provider ID to filter models by",
|
||||||
type: "string",
|
type: "string",
|
||||||
@@ -26,25 +23,19 @@ export const ModelsCommand = cmd({
|
|||||||
.option("refresh", {
|
.option("refresh", {
|
||||||
describe: "refresh the models cache from models.dev",
|
describe: "refresh the models cache from models.dev",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
})
|
}),
|
||||||
},
|
handler: Effect.fn("Cli.models")(function* (args) {
|
||||||
handler: async (args) => {
|
|
||||||
if (args.refresh) {
|
if (args.refresh) {
|
||||||
await ModelsDev.refresh(true)
|
yield* ModelsDev.Service.use((s) => s.refresh(true))
|
||||||
UI.println(UI.Style.TEXT_SUCCESS_BOLD + "Models cache refreshed" + UI.Style.TEXT_NORMAL)
|
UI.println(UI.Style.TEXT_SUCCESS_BOLD + "Models cache refreshed" + UI.Style.TEXT_NORMAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
await Instance.provide({
|
const provider = yield* Provider.Service
|
||||||
directory: process.cwd(),
|
const providers = yield* provider.list()
|
||||||
async fn() {
|
|
||||||
await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const svc = yield* Provider.Service
|
|
||||||
const providers = yield* svc.list()
|
|
||||||
|
|
||||||
const print = (providerID: ProviderID, verbose?: boolean) => {
|
const print = (providerID: ProviderID, verbose?: boolean) => {
|
||||||
const provider = providers[providerID]
|
const p = providers[providerID]
|
||||||
const sorted = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
|
const sorted = Object.entries(p.models).sort(([a], [b]) => a.localeCompare(b))
|
||||||
for (const [modelID, model] of sorted) {
|
for (const [modelID, model] of sorted) {
|
||||||
process.stdout.write(`${providerID}/${modelID}`)
|
process.stdout.write(`${providerID}/${modelID}`)
|
||||||
process.stdout.write(EOL)
|
process.stdout.write(EOL)
|
||||||
@@ -57,13 +48,8 @@ export const ModelsCommand = cmd({
|
|||||||
|
|
||||||
if (args.provider) {
|
if (args.provider) {
|
||||||
const providerID = ProviderID.make(args.provider)
|
const providerID = ProviderID.make(args.provider)
|
||||||
const provider = providers[providerID]
|
if (!providers[providerID]) return yield* fail(`Provider not found: ${args.provider}`)
|
||||||
if (!provider) {
|
print(providerID, args.verbose)
|
||||||
yield* Effect.sync(() => UI.error(`Provider not found: ${args.provider}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
yield* Effect.sync(() => print(providerID, args.verbose))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,14 +61,6 @@ export const ModelsCommand = cmd({
|
|||||||
return a.localeCompare(b)
|
return a.localeCompare(b)
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* Effect.sync(() => {
|
for (const providerID of ids) print(ProviderID.make(providerID), args.verbose)
|
||||||
for (const providerID of ids) {
|
|
||||||
print(ProviderID.make(providerID), args.verbose)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { intro, log, outro, spinner } from "@clack/prompts"
|
import { intro, log, outro, spinner } from "@clack/prompts"
|
||||||
import type { Argv } from "yargs"
|
import { Effect } from "effect"
|
||||||
|
|
||||||
import { ConfigPaths } from "@/config/paths"
|
import { ConfigPaths } from "@/config/paths"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install"
|
import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install"
|
||||||
import { resolvePluginTarget } from "../../plugin/shared"
|
import { resolvePluginTarget } from "../../plugin/shared"
|
||||||
import { Instance } from "../../project/instance"
|
|
||||||
import { errorMessage } from "../../util/error"
|
import { errorMessage } from "../../util/error"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { Process } from "@/util/process"
|
import { Process } from "@/util/process"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { cmd } from "./cmd"
|
import { effectCmd } from "../effect-cmd"
|
||||||
|
import { InstanceRef } from "@/effect/instance-ref"
|
||||||
|
|
||||||
type Spin = {
|
type Spin = {
|
||||||
start: (msg: string) => void
|
start: (msg: string) => void
|
||||||
@@ -175,12 +175,12 @@ export function createPlugTask(input: PlugInput, dep: PlugDeps = defaultPlugDeps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PluginCommand = cmd({
|
export const PluginCommand = effectCmd({
|
||||||
command: "plugin <module>",
|
command: "plugin <module>",
|
||||||
aliases: ["plug"],
|
aliases: ["plug"],
|
||||||
describe: "install plugin and update config",
|
describe: "install plugin and update config",
|
||||||
builder: (yargs: Argv) => {
|
builder: (yargs) =>
|
||||||
return yargs
|
yargs
|
||||||
.positional("module", {
|
.positional("module", {
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "npm module name",
|
describe: "npm module name",
|
||||||
@@ -196,9 +196,8 @@ export const PluginCommand = cmd({
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
default: false,
|
default: false,
|
||||||
describe: "replace existing plugin version",
|
describe: "replace existing plugin version",
|
||||||
})
|
}),
|
||||||
},
|
handler: Effect.fn("Cli.plug")(function* (args) {
|
||||||
handler: async (args) => {
|
|
||||||
const mod = String(args.module ?? "").trim()
|
const mod = String(args.module ?? "").trim()
|
||||||
if (!mod) {
|
if (!mod) {
|
||||||
UI.error("module is required")
|
UI.error("module is required")
|
||||||
@@ -214,20 +213,18 @@ export const PluginCommand = cmd({
|
|||||||
global: Boolean(args.global),
|
global: Boolean(args.global),
|
||||||
force: Boolean(args.force),
|
force: Boolean(args.force),
|
||||||
})
|
})
|
||||||
let ok = true
|
|
||||||
|
|
||||||
await Instance.provide({
|
const ctx = yield* InstanceRef
|
||||||
directory: process.cwd(),
|
if (!ctx) return
|
||||||
fn: async () => {
|
const ok = yield* Effect.promise(() =>
|
||||||
ok = await run({
|
run({
|
||||||
vcs: Instance.project.vcs,
|
vcs: ctx.project.vcs,
|
||||||
worktree: Instance.worktree,
|
worktree: ctx.worktree,
|
||||||
directory: Instance.directory,
|
directory: ctx.directory,
|
||||||
})
|
}),
|
||||||
},
|
)
|
||||||
})
|
|
||||||
|
|
||||||
outro("Done")
|
outro("Done")
|
||||||
if (!ok) process.exitCode = 1
|
if (!ok) process.exitCode = 1
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
import { Effect } from "effect"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { cmd } from "./cmd"
|
import { effectCmd, fail } from "../effect-cmd"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { Git } from "@/git"
|
import { Git } from "@/git"
|
||||||
import { Instance } from "@/project/instance"
|
import { InstanceRef } from "@/effect/instance-ref"
|
||||||
import { Process } from "@/util/process"
|
import { Process } from "@/util/process"
|
||||||
|
|
||||||
export const PrCommand = cmd({
|
export const PrCommand = effectCmd({
|
||||||
command: "pr <number>",
|
command: "pr <number>",
|
||||||
describe: "fetch and checkout a GitHub PR branch, then run opencode",
|
describe: "fetch and checkout a GitHub PR branch, then run opencode",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
@@ -14,35 +14,29 @@ export const PrCommand = cmd({
|
|||||||
describe: "PR number to checkout",
|
describe: "PR number to checkout",
|
||||||
demandOption: true,
|
demandOption: true,
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.pr")(function* (args) {
|
||||||
await Instance.provide({
|
const ctx = yield* InstanceRef
|
||||||
directory: process.cwd(),
|
if (!ctx) return yield* fail("Could not load instance context")
|
||||||
async fn() {
|
if (ctx.project.vcs !== "git") {
|
||||||
const project = Instance.project
|
return yield* fail("Could not find git repository. Please run this command from a git repository.")
|
||||||
if (project.vcs !== "git") {
|
|
||||||
UI.error("Could not find git repository. Please run this command from a git repository.")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const git = yield* Git.Service
|
||||||
|
const worktree = ctx.worktree
|
||||||
|
|
||||||
const prNumber = args.number
|
const prNumber = args.number
|
||||||
const localBranchName = `pr/${prNumber}`
|
const localBranchName = `pr/${prNumber}`
|
||||||
UI.println(`Fetching and checking out PR #${prNumber}...`)
|
UI.println(`Fetching and checking out PR #${prNumber}...`)
|
||||||
|
|
||||||
// Use gh pr checkout with custom branch name
|
const checkout = yield* Effect.promise(() =>
|
||||||
const result = await Process.run(
|
Process.run(["gh", "pr", "checkout", `${prNumber}`, "--branch", localBranchName, "--force"], { nothrow: true }),
|
||||||
["gh", "pr", "checkout", `${prNumber}`, "--branch", localBranchName, "--force"],
|
|
||||||
{
|
|
||||||
nothrow: true,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
if (checkout.code !== 0) {
|
||||||
if (result.code !== 0) {
|
return yield* fail(`Failed to checkout PR #${prNumber}. Make sure you have gh CLI installed and authenticated.`)
|
||||||
UI.error(`Failed to checkout PR #${prNumber}. Make sure you have gh CLI installed and authenticated.`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch PR info for fork handling and session link detection
|
const prInfoResult = yield* Effect.promise(() =>
|
||||||
const prInfoResult = await Process.text(
|
Process.text(
|
||||||
[
|
[
|
||||||
"gh",
|
"gh",
|
||||||
"pr",
|
"pr",
|
||||||
@@ -52,62 +46,44 @@ export const PrCommand = cmd({
|
|||||||
"headRepository,headRepositoryOwner,isCrossRepository,headRefName,body",
|
"headRepository,headRepositoryOwner,isCrossRepository,headRefName,body",
|
||||||
],
|
],
|
||||||
{ nothrow: true },
|
{ nothrow: true },
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
let sessionId: string | undefined
|
let sessionId: string | undefined
|
||||||
|
|
||||||
if (prInfoResult.code === 0) {
|
if (prInfoResult.code === 0 && prInfoResult.text.trim()) {
|
||||||
const prInfoText = prInfoResult.text
|
const prInfo = JSON.parse(prInfoResult.text)
|
||||||
if (prInfoText.trim()) {
|
|
||||||
const prInfo = JSON.parse(prInfoText)
|
|
||||||
|
|
||||||
// Handle fork PRs
|
if (prInfo?.isCrossRepository && prInfo.headRepository && prInfo.headRepositoryOwner) {
|
||||||
if (prInfo && prInfo.isCrossRepository && prInfo.headRepository && prInfo.headRepositoryOwner) {
|
|
||||||
const forkOwner = prInfo.headRepositoryOwner.login
|
const forkOwner = prInfo.headRepositoryOwner.login
|
||||||
const forkName = prInfo.headRepository.name
|
const forkName = prInfo.headRepository.name
|
||||||
const remoteName = forkOwner
|
const remoteName = forkOwner
|
||||||
|
|
||||||
// Check if remote already exists
|
const remotes = (yield* git.run(["remote"], { cwd: worktree })).text().trim()
|
||||||
const remotes = await AppRuntime.runPromise(
|
|
||||||
Git.Service.use((git) => git.run(["remote"], { cwd: Instance.worktree })),
|
|
||||||
).then((x) => x.text().trim())
|
|
||||||
if (!remotes.split("\n").includes(remoteName)) {
|
if (!remotes.split("\n").includes(remoteName)) {
|
||||||
await AppRuntime.runPromise(
|
yield* git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
|
||||||
Git.Service.use((git) =>
|
cwd: worktree,
|
||||||
git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
|
})
|
||||||
cwd: Instance.worktree,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
UI.println(`Added fork remote: ${remoteName}`)
|
UI.println(`Added fork remote: ${remoteName}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set upstream to the fork so pushes go there
|
yield* git.run(["branch", `--set-upstream-to=${remoteName}/${prInfo.headRefName}`, localBranchName], {
|
||||||
const headRefName = prInfo.headRefName
|
cwd: worktree,
|
||||||
await AppRuntime.runPromise(
|
})
|
||||||
Git.Service.use((git) =>
|
|
||||||
git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
|
|
||||||
cwd: Instance.worktree,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for opencode session link in PR body
|
if (prInfo?.body) {
|
||||||
if (prInfo && prInfo.body) {
|
|
||||||
const sessionMatch = prInfo.body.match(/https:\/\/opncd\.ai\/s\/([a-zA-Z0-9_-]+)/)
|
const sessionMatch = prInfo.body.match(/https:\/\/opncd\.ai\/s\/([a-zA-Z0-9_-]+)/)
|
||||||
if (sessionMatch) {
|
if (sessionMatch) {
|
||||||
const sessionUrl = sessionMatch[0]
|
const sessionUrl = sessionMatch[0]
|
||||||
UI.println(`Found opencode session: ${sessionUrl}`)
|
UI.println(`Found opencode session: ${sessionUrl}`)
|
||||||
UI.println(`Importing session...`)
|
UI.println(`Importing session...`)
|
||||||
|
|
||||||
const importResult = await Process.text(["opencode", "import", sessionUrl], {
|
const importResult = yield* Effect.promise(() =>
|
||||||
nothrow: true,
|
Process.text(["opencode", "import", sessionUrl], { nothrow: true }),
|
||||||
})
|
)
|
||||||
if (importResult.code === 0) {
|
if (importResult.code === 0) {
|
||||||
const importOutput = importResult.text.trim()
|
const sessionIdMatch = importResult.text.trim().match(/Imported session: ([a-zA-Z0-9_-]+)/)
|
||||||
// Extract session ID from the output (format: "Imported session: <session-id>")
|
|
||||||
const sessionIdMatch = importOutput.match(/Imported session: ([a-zA-Z0-9_-]+)/)
|
|
||||||
if (sessionIdMatch) {
|
if (sessionIdMatch) {
|
||||||
sessionId = sessionIdMatch[1]
|
sessionId = sessionIdMatch[1]
|
||||||
UI.println(`Session imported: ${sessionId}`)
|
UI.println(`Session imported: ${sessionId}`)
|
||||||
@@ -116,7 +92,6 @@ export const PrCommand = cmd({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
UI.println(`Successfully checked out PR #${prNumber} as branch '${localBranchName}'`)
|
UI.println(`Successfully checked out PR #${prNumber} as branch '${localBranchName}'`)
|
||||||
UI.println()
|
UI.println()
|
||||||
@@ -124,15 +99,17 @@ export const PrCommand = cmd({
|
|||||||
UI.println()
|
UI.println()
|
||||||
|
|
||||||
const opencodeArgs = sessionId ? ["-s", sessionId] : []
|
const opencodeArgs = sessionId ? ["-s", sessionId] : []
|
||||||
const opencodeProcess = Process.spawn(["opencode", ...opencodeArgs], {
|
const code = yield* Effect.promise(
|
||||||
|
() =>
|
||||||
|
Process.spawn(["opencode", ...opencodeArgs], {
|
||||||
stdin: "inherit",
|
stdin: "inherit",
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
stderr: "inherit",
|
stderr: "inherit",
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
})
|
}).exited,
|
||||||
const code = await opencodeProcess.exited
|
)
|
||||||
if (code !== 0) throw new Error(`opencode exited with code ${code}`)
|
// Match legacy throw semantics — propagate as a defect so the top-level
|
||||||
},
|
// index.ts catch handles it identically (exit 1, "Unexpected error" banner).
|
||||||
})
|
if (code !== 0) return yield* Effect.die(new Error(`opencode exited with code ${code}`))
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,56 +1,69 @@
|
|||||||
import { Auth } from "../../auth"
|
import { Auth } from "../../auth"
|
||||||
import { AppRuntime } from "../../effect/app-runtime"
|
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import * as prompts from "@clack/prompts"
|
import { CliError, effectCmd, fail } from "../effect-cmd"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
|
import * as Prompt from "../effect/prompt"
|
||||||
import { ModelsDev } from "@/provider/models"
|
import { ModelsDev } from "@/provider/models"
|
||||||
|
|
||||||
import { map, pipe, sortBy, values } from "remeda"
|
import { map, pipe, sortBy, values } from "remeda"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Plugin } from "../../plugin"
|
import { Plugin } from "../../plugin"
|
||||||
import { Instance } from "../../project/instance"
|
|
||||||
import type { Hooks } from "@opencode-ai/plugin"
|
import type { Hooks } from "@opencode-ai/plugin"
|
||||||
import { Process } from "@/util/process"
|
import { Process } from "@/util/process"
|
||||||
|
import { errorMessage } from "@/util/error"
|
||||||
import { text } from "node:stream/consumers"
|
import { text } from "node:stream/consumers"
|
||||||
import { Effect } from "effect"
|
import { Effect, Option } from "effect"
|
||||||
|
|
||||||
type PluginAuth = NonNullable<Hooks["auth"]>
|
type PluginAuth = NonNullable<Hooks["auth"]>
|
||||||
|
|
||||||
const put = (key: string, info: Auth.Info) =>
|
const promptValue = <Value>(value: Option.Option<Value>) => {
|
||||||
AppRuntime.runPromise(
|
if (Option.isNone(value)) return Effect.die(new UI.CancelledError())
|
||||||
Effect.gen(function* () {
|
return Effect.succeed(value.value)
|
||||||
const auth = yield* Auth.Service
|
|
||||||
yield* auth.set(key, info)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, methodName?: string): Promise<boolean> {
|
|
||||||
let index = 0
|
|
||||||
if (methodName) {
|
|
||||||
const match = plugin.auth.methods.findIndex((x) => x.label.toLowerCase() === methodName.toLowerCase())
|
|
||||||
if (match === -1) {
|
|
||||||
prompts.log.error(
|
|
||||||
`Unknown method "${methodName}" for ${provider}. Available: ${plugin.auth.methods.map((x) => x.label).join(", ")}`,
|
|
||||||
)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
}
|
||||||
index = match
|
|
||||||
} else if (plugin.auth.methods.length > 1) {
|
const put = Effect.fn("Cli.providers.put")(function* (key: string, info: Auth.Info) {
|
||||||
const method = await prompts.select({
|
const auth = yield* Auth.Service
|
||||||
|
yield* Effect.orDie(auth.set(key, info))
|
||||||
|
})
|
||||||
|
|
||||||
|
const cliTry = <Value>(message: string, fn: () => PromiseLike<Value>) =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: fn,
|
||||||
|
catch: (error) => new CliError({ message: message + errorMessage(error) }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const handlePluginAuth = Effect.fn("Cli.providers.pluginAuth")(function* (
|
||||||
|
plugin: { auth: PluginAuth },
|
||||||
|
provider: string,
|
||||||
|
methodName?: string,
|
||||||
|
) {
|
||||||
|
const index = yield* Effect.gen(function* () {
|
||||||
|
if (!methodName) {
|
||||||
|
if (plugin.auth.methods.length <= 1) return 0
|
||||||
|
return yield* promptValue(
|
||||||
|
yield* Prompt.select({
|
||||||
message: "Login method",
|
message: "Login method",
|
||||||
options: plugin.auth.methods.map((x, index) => ({
|
options: plugin.auth.methods.map((x, index) => ({
|
||||||
label: x.label,
|
label: x.label,
|
||||||
value: index.toString(),
|
value: index,
|
||||||
})),
|
})),
|
||||||
})
|
}),
|
||||||
if (prompts.isCancel(method)) throw new UI.CancelledError()
|
)
|
||||||
index = parseInt(method)
|
|
||||||
}
|
}
|
||||||
|
const match = plugin.auth.methods.findIndex((x) => x.label.toLowerCase() === methodName.toLowerCase())
|
||||||
|
if (match === -1) {
|
||||||
|
return yield* fail(
|
||||||
|
`Unknown method "${methodName}" for ${provider}. Available: ${plugin.auth.methods.map((x) => x.label).join(", ")}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
})
|
||||||
const method = plugin.auth.methods[index]
|
const method = plugin.auth.methods[index]
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 10))
|
yield* Effect.sleep("10 millis")
|
||||||
const inputs: Record<string, string> = {}
|
const inputs: Record<string, string> = {}
|
||||||
if (method.prompts) {
|
if (method.prompts) {
|
||||||
for (const prompt of method.prompts) {
|
for (const prompt of method.prompts) {
|
||||||
@@ -62,46 +75,44 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
}
|
}
|
||||||
if (prompt.condition && !prompt.condition(inputs)) continue
|
if (prompt.condition && !prompt.condition(inputs)) continue
|
||||||
if (prompt.type === "select") {
|
if (prompt.type === "select") {
|
||||||
const value = await prompts.select({
|
const value = yield* Prompt.select({
|
||||||
message: prompt.message,
|
message: prompt.message,
|
||||||
options: prompt.options,
|
options: prompt.options,
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(value)) throw new UI.CancelledError()
|
inputs[prompt.key] = yield* promptValue(value)
|
||||||
inputs[prompt.key] = value
|
continue
|
||||||
} else {
|
}
|
||||||
const value = await prompts.text({
|
const value = yield* Prompt.text({
|
||||||
message: prompt.message,
|
message: prompt.message,
|
||||||
placeholder: prompt.placeholder,
|
placeholder: prompt.placeholder,
|
||||||
validate: prompt.validate ? (v) => prompt.validate!(v ?? "") : undefined,
|
validate: prompt.validate ? (v) => prompt.validate!(v ?? "") : undefined,
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(value)) throw new UI.CancelledError()
|
inputs[prompt.key] = yield* promptValue(value)
|
||||||
inputs[prompt.key] = value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method.type === "oauth") {
|
if (method.type === "oauth") {
|
||||||
const authorize = await method.authorize(inputs)
|
const authorize = yield* cliTry("Failed to authorize: ", () => method.authorize(inputs))
|
||||||
|
|
||||||
if (authorize.url) {
|
if (authorize.url) {
|
||||||
prompts.log.info("Go to: " + authorize.url)
|
yield* Prompt.log.info("Go to: " + authorize.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authorize.method === "auto") {
|
if (authorize.method === "auto") {
|
||||||
if (authorize.instructions) {
|
if (authorize.instructions) {
|
||||||
prompts.log.info(authorize.instructions)
|
yield* Prompt.log.info(authorize.instructions)
|
||||||
}
|
}
|
||||||
const spinner = prompts.spinner()
|
const spinner = Prompt.spinner()
|
||||||
spinner.start("Waiting for authorization...")
|
yield* spinner.start("Waiting for authorization...")
|
||||||
const result = await authorize.callback()
|
const result = yield* cliTry("Failed to authorize: ", () => authorize.callback())
|
||||||
if (result.type === "failed") {
|
if (result.type === "failed") {
|
||||||
spinner.stop("Failed to authorize", 1)
|
yield* spinner.stop("Failed to authorize", 1)
|
||||||
}
|
}
|
||||||
if (result.type === "success") {
|
if (result.type === "success") {
|
||||||
const saveProvider = result.provider ?? provider
|
const saveProvider = result.provider ?? provider
|
||||||
if ("refresh" in result) {
|
if ("refresh" in result) {
|
||||||
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
|
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
|
||||||
await put(saveProvider, {
|
yield* put(saveProvider, {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
refresh,
|
refresh,
|
||||||
access,
|
access,
|
||||||
@@ -110,30 +121,30 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if ("key" in result) {
|
if ("key" in result) {
|
||||||
await put(saveProvider, {
|
yield* put(saveProvider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key: result.key,
|
key: result.key,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
spinner.stop("Login successful")
|
yield* spinner.stop("Login successful")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authorize.method === "code") {
|
if (authorize.method === "code") {
|
||||||
const code = await prompts.text({
|
const code = yield* Prompt.text({
|
||||||
message: "Paste the authorization code here: ",
|
message: "Paste the authorization code here: ",
|
||||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(code)) throw new UI.CancelledError()
|
const authorizationCode = yield* promptValue(code)
|
||||||
const result = await authorize.callback(code)
|
const result = yield* cliTry("Failed to authorize: ", () => authorize.callback(authorizationCode))
|
||||||
if (result.type === "failed") {
|
if (result.type === "failed") {
|
||||||
prompts.log.error("Failed to authorize")
|
yield* Prompt.log.error("Failed to authorize")
|
||||||
}
|
}
|
||||||
if (result.type === "success") {
|
if (result.type === "success") {
|
||||||
const saveProvider = result.provider ?? provider
|
const saveProvider = result.provider ?? provider
|
||||||
if ("refresh" in result) {
|
if ("refresh" in result) {
|
||||||
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
|
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
|
||||||
await put(saveProvider, {
|
yield* put(saveProvider, {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
refresh,
|
refresh,
|
||||||
access,
|
access,
|
||||||
@@ -142,56 +153,57 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if ("key" in result) {
|
if ("key" in result) {
|
||||||
await put(saveProvider, {
|
yield* put(saveProvider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key: result.key,
|
key: result.key,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
prompts.log.success("Login successful")
|
yield* Prompt.log.success("Login successful")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prompts.outro("Done")
|
yield* Prompt.outro("Done")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method.type === "api") {
|
if (method.type === "api") {
|
||||||
const key = await prompts.password({
|
const key = yield* Prompt.password({
|
||||||
message: "Enter your API key",
|
message: "Enter your API key",
|
||||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
const apiKey = yield* promptValue(key)
|
||||||
|
|
||||||
const metadata = Object.keys(inputs).length ? { metadata: inputs } : {}
|
const metadata = Object.keys(inputs).length ? { metadata: inputs } : {}
|
||||||
if (!method.authorize) {
|
const authorizeApi = method.authorize
|
||||||
await put(provider, {
|
if (!authorizeApi) {
|
||||||
|
yield* put(provider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key,
|
key: apiKey,
|
||||||
...metadata,
|
...metadata,
|
||||||
})
|
})
|
||||||
prompts.outro("Done")
|
yield* Prompt.outro("Done")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await method.authorize(inputs)
|
const result = yield* cliTry("Failed to authorize: ", () => authorizeApi(inputs))
|
||||||
if (result.type === "failed") {
|
if (result.type === "failed") {
|
||||||
prompts.log.error("Failed to authorize")
|
yield* Prompt.log.error("Failed to authorize")
|
||||||
}
|
}
|
||||||
if (result.type === "success") {
|
if (result.type === "success") {
|
||||||
const saveProvider = result.provider ?? provider
|
const saveProvider = result.provider ?? provider
|
||||||
await put(saveProvider, {
|
yield* put(saveProvider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key: result.key ?? key,
|
key: result.key ?? apiKey,
|
||||||
...metadata,
|
...metadata,
|
||||||
})
|
})
|
||||||
prompts.log.success("Login successful")
|
yield* Prompt.log.success("Login successful")
|
||||||
}
|
}
|
||||||
prompts.outro("Done")
|
yield* Prompt.outro("Done")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
})
|
||||||
|
|
||||||
export function resolvePluginProviders(input: {
|
export function resolvePluginProviders(input: {
|
||||||
hooks: Hooks[]
|
hooks: Hooks[]
|
||||||
@@ -229,30 +241,30 @@ export const ProvidersCommand = cmd({
|
|||||||
async handler() {},
|
async handler() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ProvidersListCommand = cmd({
|
export const ProvidersListCommand = effectCmd({
|
||||||
command: "list",
|
command: "list",
|
||||||
aliases: ["ls"],
|
aliases: ["ls"],
|
||||||
describe: "list providers and credentials",
|
describe: "list providers and credentials",
|
||||||
async handler(_args) {
|
// Lists global credentials + provider env vars; no project instance needed.
|
||||||
|
instance: false,
|
||||||
|
handler: Effect.fn("Cli.providers.list")(function* (_args) {
|
||||||
|
const authSvc = yield* Auth.Service
|
||||||
|
const modelsDev = yield* ModelsDev.Service
|
||||||
|
|
||||||
UI.empty()
|
UI.empty()
|
||||||
const authPath = path.join(Global.Path.data, "auth.json")
|
const authPath = path.join(Global.Path.data, "auth.json")
|
||||||
const homedir = os.homedir()
|
const homedir = os.homedir()
|
||||||
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
|
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
|
||||||
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
|
yield* Prompt.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
|
||||||
const results = await AppRuntime.runPromise(
|
const results = Object.entries(yield* Effect.orDie(authSvc.all()))
|
||||||
Effect.gen(function* () {
|
const database = yield* modelsDev.get()
|
||||||
const auth = yield* Auth.Service
|
|
||||||
return Object.entries(yield* auth.all())
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const database = await ModelsDev.get()
|
|
||||||
|
|
||||||
for (const [providerID, result] of results) {
|
for (const [providerID, result] of results) {
|
||||||
const name = database[providerID]?.name || providerID
|
const name = database[providerID]?.name || providerID
|
||||||
prompts.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
|
yield* Prompt.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
prompts.outro(`${results.length} credentials`)
|
yield* Prompt.outro(`${results.length} credentials`)
|
||||||
|
|
||||||
const activeEnvVars: Array<{ provider: string; envVar: string }> = []
|
const activeEnvVars: Array<{ provider: string; envVar: string }> = []
|
||||||
|
|
||||||
@@ -269,18 +281,18 @@ export const ProvidersListCommand = cmd({
|
|||||||
|
|
||||||
if (activeEnvVars.length > 0) {
|
if (activeEnvVars.length > 0) {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("Environment")
|
yield* Prompt.intro("Environment")
|
||||||
|
|
||||||
for (const { provider, envVar } of activeEnvVars) {
|
for (const { provider, envVar } of activeEnvVars) {
|
||||||
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
|
yield* Prompt.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
prompts.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"))
|
yield* Prompt.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"))
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ProvidersLoginCommand = cmd({
|
export const ProvidersLoginCommand = effectCmd({
|
||||||
command: "login [url]",
|
command: "login [url]",
|
||||||
describe: "log in to a provider",
|
describe: "log in to a provider",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
@@ -299,63 +311,56 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
describe: "login method label (skips method selection)",
|
describe: "login method label (skips method selection)",
|
||||||
type: "string",
|
type: "string",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
handler: Effect.fn("Cli.providers.login")(function* (args) {
|
||||||
await Instance.provide({
|
const authSvc = yield* Auth.Service
|
||||||
directory: process.cwd(),
|
|
||||||
async fn() {
|
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("Add credential")
|
yield* Prompt.intro("Add credential")
|
||||||
if (args.url) {
|
if (args.url) {
|
||||||
const url = args.url.replace(/\/+$/, "")
|
const url = args.url.replace(/\/+$/, "")
|
||||||
const wellknown = (await fetch(`${url}/.well-known/opencode`).then((x) => x.json())) as {
|
const wellknown = (yield* cliTry(`Failed to load auth provider metadata from ${url}: `, () =>
|
||||||
|
fetch(`${url}/.well-known/opencode`).then((x) => x.json()),
|
||||||
|
)) as {
|
||||||
auth: { command: string[]; env: string }
|
auth: { command: string[]; env: string }
|
||||||
}
|
}
|
||||||
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
|
yield* Prompt.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
|
||||||
const proc = Process.spawn(wellknown.auth.command, {
|
const abort = new AbortController()
|
||||||
stdout: "pipe",
|
const proc = Process.spawn(wellknown.auth.command, { stdout: "pipe", stderr: "inherit", abort: abort.signal })
|
||||||
})
|
|
||||||
if (!proc.stdout) {
|
if (!proc.stdout) {
|
||||||
prompts.log.error("Failed")
|
yield* Prompt.log.error("Failed")
|
||||||
prompts.outro("Done")
|
yield* Prompt.outro("Done")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const [exit, token] = await Promise.all([proc.exited, text(proc.stdout)])
|
const [exit, token] = yield* cliTry("Failed to run auth provider command: ", () =>
|
||||||
|
Promise.all([proc.exited, text(proc.stdout!)]),
|
||||||
|
).pipe(Effect.ensuring(Effect.sync(() => abort.abort())))
|
||||||
if (exit !== 0) {
|
if (exit !== 0) {
|
||||||
prompts.log.error("Failed")
|
yield* Prompt.log.error("Failed")
|
||||||
prompts.outro("Done")
|
yield* Prompt.outro("Done")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await put(url, {
|
yield* Effect.orDie(authSvc.set(url, { type: "wellknown", key: wellknown.auth.env, token: token.trim() }))
|
||||||
type: "wellknown",
|
yield* Prompt.log.success("Logged into " + url)
|
||||||
key: wellknown.auth.env,
|
yield* Prompt.outro("Done")
|
||||||
token: token.trim(),
|
|
||||||
})
|
|
||||||
prompts.log.success("Logged into " + url)
|
|
||||||
prompts.outro("Done")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await ModelsDev.refresh(true).catch(() => {})
|
|
||||||
|
|
||||||
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
|
const cfgSvc = yield* Config.Service
|
||||||
|
const pluginSvc = yield* Plugin.Service
|
||||||
|
const modelsDev = yield* ModelsDev.Service
|
||||||
|
yield* Effect.ignore(modelsDev.refresh(true))
|
||||||
|
|
||||||
|
const config = yield* cfgSvc.get()
|
||||||
|
|
||||||
const disabled = new Set(config.disabled_providers ?? [])
|
const disabled = new Set(config.disabled_providers ?? [])
|
||||||
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
||||||
|
|
||||||
const providers = await ModelsDev.get().then((x) => {
|
const allProviders = yield* modelsDev.get()
|
||||||
const filtered: Record<string, (typeof x)[string]> = {}
|
const providers: Record<string, (typeof allProviders)[string]> = {}
|
||||||
for (const [key, value] of Object.entries(x)) {
|
for (const [key, value] of Object.entries(allProviders)) {
|
||||||
if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
|
if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) providers[key] = value
|
||||||
filtered[key] = value
|
|
||||||
}
|
}
|
||||||
}
|
const hooks = yield* pluginSvc.list()
|
||||||
return filtered
|
|
||||||
})
|
|
||||||
const hooks = await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const plugin = yield* Plugin.Service
|
|
||||||
return yield* plugin.list()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const priority: Record<string, number> = {
|
const priority: Record<string, number> = {
|
||||||
opencode: 0,
|
opencode: 0,
|
||||||
@@ -404,53 +409,46 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
const byName = options.find((x) => x.label.toLowerCase() === input.toLowerCase())
|
const byName = options.find((x) => x.label.toLowerCase() === input.toLowerCase())
|
||||||
const match = byID ?? byName
|
const match = byID ?? byName
|
||||||
if (!match) {
|
if (!match) {
|
||||||
prompts.log.error(`Unknown provider "${input}"`)
|
return yield* fail(`Unknown provider "${input}"`)
|
||||||
process.exit(1)
|
|
||||||
}
|
}
|
||||||
provider = match.value
|
provider = match.value
|
||||||
} else {
|
} else {
|
||||||
const selected = await prompts.autocomplete({
|
provider = yield* promptValue(
|
||||||
|
yield* Prompt.autocomplete({
|
||||||
message: "Select provider",
|
message: "Select provider",
|
||||||
maxItems: 8,
|
maxItems: 8,
|
||||||
options: [
|
options: [...options, { value: "other", label: "Other" }],
|
||||||
...options,
|
}),
|
||||||
{
|
)
|
||||||
value: "other",
|
|
||||||
label: "Other",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
|
||||||
provider = selected as string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugin = hooks.findLast((x) => x.auth?.provider === provider)
|
const plugin = hooks.findLast((x) => x.auth?.provider === provider)
|
||||||
if (plugin && plugin.auth) {
|
if (plugin && plugin.auth) {
|
||||||
const handled = await handlePluginAuth({ auth: plugin.auth }, provider, args.method)
|
const handled = yield* handlePluginAuth({ auth: plugin.auth! }, provider, args.method)
|
||||||
if (handled) return
|
if (handled) return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider === "other") {
|
if (provider === "other") {
|
||||||
const custom = await prompts.text({
|
provider = (yield* promptValue(
|
||||||
|
yield* Prompt.text({
|
||||||
message: "Enter provider id",
|
message: "Enter provider id",
|
||||||
validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
|
validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
|
||||||
})
|
}),
|
||||||
if (prompts.isCancel(custom)) throw new UI.CancelledError()
|
)).replace(/^@ai-sdk\//, "")
|
||||||
provider = custom.replace(/^@ai-sdk\//, "")
|
|
||||||
|
|
||||||
const customPlugin = hooks.findLast((x) => x.auth?.provider === provider)
|
const customPlugin = hooks.findLast((x) => x.auth?.provider === provider)
|
||||||
if (customPlugin && customPlugin.auth) {
|
if (customPlugin && customPlugin.auth) {
|
||||||
const handled = await handlePluginAuth({ auth: customPlugin.auth }, provider, args.method)
|
const handled = yield* handlePluginAuth({ auth: customPlugin.auth! }, provider, args.method)
|
||||||
if (handled) return
|
if (handled) return
|
||||||
}
|
}
|
||||||
|
|
||||||
prompts.log.warn(
|
yield* Prompt.log.warn(
|
||||||
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
|
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider === "amazon-bedrock") {
|
if (provider === "amazon-bedrock") {
|
||||||
prompts.log.info(
|
yield* Prompt.log.info(
|
||||||
"Amazon Bedrock authentication priority:\n" +
|
"Amazon Bedrock authentication priority:\n" +
|
||||||
" 1. Bearer token (AWS_BEARER_TOKEN_BEDROCK or /connect)\n" +
|
" 1. Bearer token (AWS_BEARER_TOKEN_BEDROCK or /connect)\n" +
|
||||||
" 2. AWS credential chain (profile, access keys, IAM roles, EKS IRSA)\n\n" +
|
" 2. AWS credential chain (profile, access keys, IAM roles, EKS IRSA)\n\n" +
|
||||||
@@ -460,67 +458,55 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (provider === "opencode") {
|
if (provider === "opencode") {
|
||||||
prompts.log.info("Create an api key at https://opencode.ai/auth")
|
yield* Prompt.log.info("Create an api key at https://opencode.ai/auth")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider === "vercel") {
|
if (provider === "vercel") {
|
||||||
prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token")
|
yield* Prompt.log.info("You can create an api key at https://vercel.link/ai-gateway-token")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["cloudflare", "cloudflare-ai-gateway"].includes(provider)) {
|
if (["cloudflare", "cloudflare-ai-gateway"].includes(provider)) {
|
||||||
prompts.log.info(
|
yield* Prompt.log.info(
|
||||||
"Cloudflare AI Gateway can be configured with CLOUDFLARE_GATEWAY_ID, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN environment variables. Read more: https://opencode.ai/docs/providers/#cloudflare-ai-gateway",
|
"Cloudflare AI Gateway can be configured with CLOUDFLARE_GATEWAY_ID, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN environment variables. Read more: https://opencode.ai/docs/providers/#cloudflare-ai-gateway",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = await prompts.password({
|
const key = yield* Prompt.password({
|
||||||
message: "Enter your API key",
|
message: "Enter your API key",
|
||||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
const apiKey = yield* promptValue(key)
|
||||||
await put(provider, {
|
yield* Effect.orDie(authSvc.set(provider, { type: "api", key: apiKey }))
|
||||||
type: "api",
|
|
||||||
key,
|
yield* Prompt.outro("Done")
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
prompts.outro("Done")
|
export const ProvidersLogoutCommand = effectCmd({
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const ProvidersLogoutCommand = cmd({
|
|
||||||
command: "logout",
|
command: "logout",
|
||||||
describe: "log out from a configured provider",
|
describe: "log out from a configured provider",
|
||||||
async handler(_args) {
|
// Removes a global auth credential; no project instance needed.
|
||||||
|
instance: false,
|
||||||
|
handler: Effect.fn("Cli.providers.logout")(function* (_args) {
|
||||||
|
const authSvc = yield* Auth.Service
|
||||||
|
const modelsDev = yield* ModelsDev.Service
|
||||||
|
|
||||||
UI.empty()
|
UI.empty()
|
||||||
const credentials: Array<[string, Auth.Info]> = await AppRuntime.runPromise(
|
const credentials: Array<[string, Auth.Info]> = Object.entries(yield* Effect.orDie(authSvc.all()))
|
||||||
Effect.gen(function* () {
|
yield* Prompt.intro("Remove credential")
|
||||||
const auth = yield* Auth.Service
|
|
||||||
return Object.entries(yield* auth.all())
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
prompts.intro("Remove credential")
|
|
||||||
if (credentials.length === 0) {
|
if (credentials.length === 0) {
|
||||||
prompts.log.error("No credentials found")
|
yield* Prompt.log.error("No credentials found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const database = await ModelsDev.get()
|
const database = yield* modelsDev.get()
|
||||||
const selected = await prompts.select({
|
const selected = yield* Prompt.select({
|
||||||
message: "Select provider",
|
message: "Select provider",
|
||||||
options: credentials.map(([key, value]) => ({
|
options: credentials.map(([key, value]) => ({
|
||||||
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
|
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
|
||||||
value: key,
|
value: key,
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
yield* Effect.orDie(authSvc.remove(yield* promptValue(selected)))
|
||||||
const providerID = selected as string
|
yield* Prompt.outro("Logout successful")
|
||||||
await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const auth = yield* Auth.Service
|
|
||||||
yield* auth.remove(providerID)
|
|
||||||
}),
|
}),
|
||||||
)
|
|
||||||
prompts.outro("Logout successful")
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { Argv } from "yargs"
|
import type { Argv } from "yargs"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { pathToFileURL } from "url"
|
import { pathToFileURL } from "url"
|
||||||
|
import { Effect } from "effect"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { cmd } from "./cmd"
|
import { effectCmd } from "../effect-cmd"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { bootstrap } from "../bootstrap"
|
import { ServerAuth } from "@/server/auth"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
|
||||||
@@ -22,10 +23,10 @@ import { WriteTool } from "../../tool/write"
|
|||||||
import { WebSearchTool } from "../../tool/websearch"
|
import { WebSearchTool } from "../../tool/websearch"
|
||||||
import { TaskTool } from "../../tool/task"
|
import { TaskTool } from "../../tool/task"
|
||||||
import { SkillTool } from "../../tool/skill"
|
import { SkillTool } from "../../tool/skill"
|
||||||
import { BashTool } from "../../tool/bash"
|
import { ShellTool } from "../../tool/shell"
|
||||||
|
import { ShellID } from "../../tool/shell/id"
|
||||||
import { TodoWriteTool } from "../../tool/todo"
|
import { TodoWriteTool } from "../../tool/todo"
|
||||||
import { Locale } from "@/util/locale"
|
import { Locale } from "@/util/locale"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
type ToolProps<T> = {
|
type ToolProps<T> = {
|
||||||
input: Tool.InferParameters<T>
|
input: Tool.InferParameters<T>
|
||||||
@@ -175,7 +176,7 @@ function skill(info: ToolProps<typeof SkillTool>) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function bash(info: ToolProps<typeof BashTool>) {
|
function shell(info: ToolProps<typeof ShellTool>) {
|
||||||
const output = info.part.state.status === "completed" ? info.part.state.output?.trim() : undefined
|
const output = info.part.state.status === "completed" ? info.part.state.output?.trim() : undefined
|
||||||
block(
|
block(
|
||||||
{
|
{
|
||||||
@@ -202,11 +203,17 @@ function normalizePath(input?: string) {
|
|||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RunCommand = cmd({
|
export const RunCommand = effectCmd({
|
||||||
command: "run [message..]",
|
command: "run [message..]",
|
||||||
describe: "run opencode with a message",
|
describe: "run opencode with a message",
|
||||||
builder: (yargs: Argv) => {
|
// --attach connects to a remote server (no local instance needed); the
|
||||||
return yargs
|
// default path runs an in-process server and needs the project instance.
|
||||||
|
instance: (args) => !args.attach,
|
||||||
|
// For --dir without --attach, load instance for the resolved target dir.
|
||||||
|
// The handler also chdirs (preserving the legacy order: chdir → file resolution).
|
||||||
|
directory: (args) => (args.dir && !args.attach ? path.resolve(process.cwd(), args.dir) : process.cwd()),
|
||||||
|
builder: (yargs: Argv) =>
|
||||||
|
yargs
|
||||||
.positional("message", {
|
.positional("message", {
|
||||||
describe: "message to send",
|
describe: "message to send",
|
||||||
type: "string",
|
type: "string",
|
||||||
@@ -269,6 +276,11 @@ export const RunCommand = cmd({
|
|||||||
type: "string",
|
type: "string",
|
||||||
describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)",
|
describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)",
|
||||||
})
|
})
|
||||||
|
.option("username", {
|
||||||
|
alias: ["u"],
|
||||||
|
type: "string",
|
||||||
|
describe: "basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')",
|
||||||
|
})
|
||||||
.option("dir", {
|
.option("dir", {
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "directory to run in, path on remote server if attaching",
|
describe: "directory to run in, path on remote server if attaching",
|
||||||
@@ -290,9 +302,10 @@ export const RunCommand = cmd({
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
|
describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
|
||||||
default: false,
|
default: false,
|
||||||
})
|
}),
|
||||||
},
|
handler: Effect.fn("Cli.run")(function* (args) {
|
||||||
handler: async (args) => {
|
const agentSvc = yield* Agent.Service
|
||||||
|
yield* Effect.promise(async () => {
|
||||||
let message = [...args.message, ...(args["--"] || [])]
|
let message = [...args.message, ...(args["--"] || [])]
|
||||||
.map((arg) => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg))
|
.map((arg) => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg))
|
||||||
.join(" ")
|
.join(" ")
|
||||||
@@ -400,7 +413,7 @@ export const RunCommand = cmd({
|
|||||||
async function execute(sdk: OpencodeClient) {
|
async function execute(sdk: OpencodeClient) {
|
||||||
function tool(part: ToolPart) {
|
function tool(part: ToolPart) {
|
||||||
try {
|
try {
|
||||||
if (part.tool === "bash") return bash(props<typeof BashTool>(part))
|
if (part.tool === ShellID.ToolID) return shell(props<typeof ShellTool>(part))
|
||||||
if (part.tool === "glob") return glob(props<typeof GlobTool>(part))
|
if (part.tool === "glob") return glob(props<typeof GlobTool>(part))
|
||||||
if (part.tool === "grep") return grep(props<typeof GrepTool>(part))
|
if (part.tool === "grep") return grep(props<typeof GrepTool>(part))
|
||||||
if (part.tool === "read") return read(props<typeof ReadTool>(part))
|
if (part.tool === "read") return read(props<typeof ReadTool>(part))
|
||||||
@@ -595,7 +608,7 @@ export const RunCommand = cmd({
|
|||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.get(name)))
|
const entry = await Effect.runPromise(agentSvc.get(name))
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
UI.println(
|
UI.println(
|
||||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||||
@@ -649,18 +662,11 @@ export const RunCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.attach) {
|
if (args.attach) {
|
||||||
const headers = (() => {
|
const headers = ServerAuth.headers({ password: args.password, username: args.username })
|
||||||
const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD
|
|
||||||
if (!password) return undefined
|
|
||||||
const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode"
|
|
||||||
const auth = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
|
|
||||||
return { Authorization: auth }
|
|
||||||
})()
|
|
||||||
const sdk = createOpencodeClient({ baseUrl: args.attach, directory, headers })
|
const sdk = createOpencodeClient({ baseUrl: args.attach, directory, headers })
|
||||||
return await execute(sdk)
|
return await execute(sdk)
|
||||||
}
|
}
|
||||||
|
|
||||||
await bootstrap(process.cwd(), async () => {
|
|
||||||
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
const request = new Request(input, init)
|
const request = new Request(input, init)
|
||||||
return Server.Default().app.fetch(request)
|
return Server.Default().app.fetch(request)
|
||||||
@@ -668,5 +674,5 @@ export const RunCommand = cmd({
|
|||||||
const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
|
const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
|
||||||
await execute(sdk)
|
await execute(sdk)
|
||||||
})
|
})
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
|
import { Effect } from "effect"
|
||||||
import { Server } from "../../server/server"
|
import { Server } from "../../server/server"
|
||||||
import { cmd } from "./cmd"
|
import { effectCmd } from "../effect-cmd"
|
||||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
|
|
||||||
export const ServeCommand = cmd({
|
export const ServeCommand = effectCmd({
|
||||||
command: "serve",
|
command: "serve",
|
||||||
builder: (yargs) => withNetworkOptions(yargs),
|
builder: (yargs) => withNetworkOptions(yargs),
|
||||||
describe: "starts a headless opencode server",
|
describe: "starts a headless opencode server",
|
||||||
handler: async (args) => {
|
// Server loads instances per-request via x-opencode-directory header — no
|
||||||
|
// need for an ambient project InstanceContext at startup.
|
||||||
|
instance: false,
|
||||||
|
handler: Effect.fn("Cli.serve")(function* (args) {
|
||||||
if (!Flag.OPENCODE_SERVER_PASSWORD) {
|
if (!Flag.OPENCODE_SERVER_PASSWORD) {
|
||||||
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
|
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
|
||||||
}
|
}
|
||||||
const opts = await resolveNetworkOptions(args)
|
const opts = yield* Effect.promise(() => resolveNetworkOptions(args))
|
||||||
const server = await Server.listen(opts)
|
const server = yield* Effect.promise(() => Server.listen(opts))
|
||||||
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
||||||
|
|
||||||
await new Promise(() => {})
|
yield* Effect.never
|
||||||
await server.stop()
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { Argv } from "yargs"
|
import type { Argv } from "yargs"
|
||||||
|
import { Effect } from "effect"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
|
import { effectCmd, fail } from "../effect-cmd"
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
import { SessionID } from "../../session/schema"
|
import { SessionID } from "../../session/schema"
|
||||||
import { bootstrap } from "../bootstrap"
|
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { Locale } from "@/util/locale"
|
import { Locale } from "@/util/locale"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
@@ -11,7 +12,6 @@ import { Process } from "@/util/process"
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { which } from "../../util/which"
|
import { which } from "../../util/which"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
function pagerCmd(): string[] {
|
function pagerCmd(): string[] {
|
||||||
const lessOptions = ["-R", "-S"]
|
const lessOptions = ["-R", "-S"]
|
||||||
@@ -47,36 +47,30 @@ export const SessionCommand = cmd({
|
|||||||
async handler() {},
|
async handler() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const SessionDeleteCommand = cmd({
|
export const SessionDeleteCommand = effectCmd({
|
||||||
command: "delete <sessionID>",
|
command: "delete <sessionID>",
|
||||||
describe: "delete a session",
|
describe: "delete a session",
|
||||||
builder: (yargs: Argv) => {
|
builder: (yargs) =>
|
||||||
return yargs.positional("sessionID", {
|
yargs.positional("sessionID", {
|
||||||
describe: "session ID to delete",
|
describe: "session ID to delete",
|
||||||
type: "string",
|
type: "string",
|
||||||
demandOption: true,
|
demandOption: true,
|
||||||
})
|
}),
|
||||||
},
|
handler: Effect.fn("Cli.session.delete")(function* (args) {
|
||||||
handler: async (args) => {
|
const svc = yield* Session.Service
|
||||||
await bootstrap(process.cwd(), async () => {
|
|
||||||
const sessionID = SessionID.make(args.sessionID)
|
const sessionID = SessionID.make(args.sessionID)
|
||||||
try {
|
// Match legacy try/catch — Session.get surfaces NotFoundError as a defect.
|
||||||
await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID)))
|
yield* svc.get(sessionID).pipe(Effect.catchCause(() => fail(`Session not found: ${args.sessionID}`)))
|
||||||
} catch {
|
yield* svc.remove(sessionID)
|
||||||
UI.error(`Session not found: ${args.sessionID}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(sessionID)))
|
|
||||||
UI.println(UI.Style.TEXT_SUCCESS_BOLD + `Session ${args.sessionID} deleted` + UI.Style.TEXT_NORMAL)
|
UI.println(UI.Style.TEXT_SUCCESS_BOLD + `Session ${args.sessionID} deleted` + UI.Style.TEXT_NORMAL)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const SessionListCommand = cmd({
|
export const SessionListCommand = effectCmd({
|
||||||
command: "list",
|
command: "list",
|
||||||
describe: "list sessions",
|
describe: "list sessions",
|
||||||
builder: (yargs: Argv) => {
|
builder: (yargs) =>
|
||||||
return yargs
|
yargs
|
||||||
.option("max-count", {
|
.option("max-count", {
|
||||||
alias: "n",
|
alias: "n",
|
||||||
describe: "limit to N most recent sessions",
|
describe: "limit to N most recent sessions",
|
||||||
@@ -87,28 +81,18 @@ export const SessionListCommand = cmd({
|
|||||||
type: "string",
|
type: "string",
|
||||||
choices: ["table", "json"],
|
choices: ["table", "json"],
|
||||||
default: "table",
|
default: "table",
|
||||||
})
|
}),
|
||||||
},
|
handler: Effect.fn("Cli.session.list")(function* (args) {
|
||||||
handler: async (args) => {
|
const sessions = yield* Session.Service.use((svc) => svc.list({ roots: true, limit: args.maxCount }))
|
||||||
await bootstrap(process.cwd(), async () => {
|
|
||||||
const sessions = await AppRuntime.runPromise(
|
|
||||||
Session.Service.use((svc) => svc.list({ roots: true, limit: args.maxCount })),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (sessions.length === 0) {
|
if (sessions.length === 0) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let output: string
|
const output = args.format === "json" ? formatSessionJSON(sessions) : formatSessionTable(sessions)
|
||||||
if (args.format === "json") {
|
|
||||||
output = formatSessionJSON(sessions)
|
|
||||||
} else {
|
|
||||||
output = formatSessionTable(sessions)
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldPaginate = process.stdout.isTTY && !args.maxCount && args.format === "table"
|
const shouldPaginate = process.stdout.isTTY && !args.maxCount && args.format === "table"
|
||||||
|
|
||||||
if (shouldPaginate) {
|
if (shouldPaginate) {
|
||||||
|
yield* Effect.promise(async () => {
|
||||||
const proc = Process.spawn(pagerCmd(), {
|
const proc = Process.spawn(pagerCmd(), {
|
||||||
stdin: "pipe",
|
stdin: "pipe",
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
@@ -123,11 +107,11 @@ export const SessionListCommand = cmd({
|
|||||||
proc.stdin.write(output)
|
proc.stdin.write(output)
|
||||||
proc.stdin.end()
|
proc.stdin.end()
|
||||||
await proc.exited
|
await proc.exited
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
console.log(output)
|
console.log(output)
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function formatSessionTable(sessions: Session.Info[]): string {
|
function formatSessionTable(sessions: Session.Info[]): string {
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import type { Argv } from "yargs"
|
import { Effect } from "effect"
|
||||||
import { cmd } from "./cmd"
|
import { effectCmd } from "../effect-cmd"
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
import { bootstrap } from "../bootstrap"
|
|
||||||
import { Database } from "@/storage/db"
|
import { Database } from "@/storage/db"
|
||||||
import { SessionTable } from "../../session/session.sql"
|
import { SessionTable } from "../../session/session.sql"
|
||||||
import { Project } from "@/project/project"
|
import { Project } from "@/project/project"
|
||||||
import { Instance } from "../../project/instance"
|
import { InstanceRef } from "@/effect/instance-ref"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
interface SessionStats {
|
interface SessionStats {
|
||||||
totalSessions: number
|
totalSessions: number
|
||||||
@@ -47,11 +45,11 @@ interface SessionStats {
|
|||||||
medianTokensPerSession: number
|
medianTokensPerSession: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StatsCommand = cmd({
|
export const StatsCommand = effectCmd({
|
||||||
command: "stats",
|
command: "stats",
|
||||||
describe: "show token usage and cost statistics",
|
describe: "show token usage and cost statistics",
|
||||||
builder: (yargs: Argv) => {
|
builder: (yargs) =>
|
||||||
return yargs
|
yargs
|
||||||
.option("days", {
|
.option("days", {
|
||||||
describe: "show stats for the last N days (default: all time)",
|
describe: "show stats for the last N days (default: all time)",
|
||||||
type: "number",
|
type: "number",
|
||||||
@@ -66,35 +64,32 @@ export const StatsCommand = cmd({
|
|||||||
.option("project", {
|
.option("project", {
|
||||||
describe: "filter by project (default: all projects, empty string: current project)",
|
describe: "filter by project (default: all projects, empty string: current project)",
|
||||||
type: "string",
|
type: "string",
|
||||||
})
|
}),
|
||||||
},
|
handler: Effect.fn("Cli.stats")(function* (args) {
|
||||||
handler: async (args) => {
|
const ctx = yield* InstanceRef
|
||||||
await bootstrap(process.cwd(), async () => {
|
if (!ctx) return
|
||||||
const stats = await aggregateSessionStats(args.days, args.project)
|
const stats = yield* aggregateSessionStats(args.days, args.project, ctx.project)
|
||||||
|
|
||||||
let modelLimit: number | undefined
|
let modelLimit: number | undefined
|
||||||
if (args.models === true) {
|
if (args.models === true) {
|
||||||
modelLimit = Infinity
|
modelLimit = Infinity
|
||||||
} else if (typeof args.models === "number") {
|
} else if (typeof args.models === "number") {
|
||||||
modelLimit = args.models
|
modelLimit = args.models
|
||||||
}
|
}
|
||||||
|
|
||||||
displayStats(stats, args.tools, modelLimit)
|
displayStats(stats, args.tools, modelLimit)
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
async function getCurrentProject(): Promise<Project.Info> {
|
const getAllSessions = Effect.sync(() =>
|
||||||
return Instance.project
|
Database.use((db) => db.select().from(SessionTable).all()).map((row) => Session.fromRow(row)),
|
||||||
}
|
)
|
||||||
|
|
||||||
async function getAllSessions(): Promise<Session.Info[]> {
|
const aggregateSessionStats = Effect.fn("Cli.stats.aggregate")(function* (
|
||||||
const rows = Database.use((db) => db.select().from(SessionTable).all())
|
days?: number,
|
||||||
return rows.map((row) => Session.fromRow(row))
|
projectFilter?: string,
|
||||||
}
|
currentProject?: Project.Info,
|
||||||
|
) {
|
||||||
export async function aggregateSessionStats(days?: number, projectFilter?: string): Promise<SessionStats> {
|
const svc = yield* Session.Service
|
||||||
const sessions = await getAllSessions()
|
const sessions = yield* getAllSessions
|
||||||
const MS_IN_DAY = 24 * 60 * 60 * 1000
|
const MS_IN_DAY = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
const cutoffTime = (() => {
|
const cutoffTime = (() => {
|
||||||
@@ -117,7 +112,7 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
|||||||
|
|
||||||
if (projectFilter !== undefined) {
|
if (projectFilter !== undefined) {
|
||||||
if (projectFilter === "") {
|
if (projectFilter === "") {
|
||||||
const currentProject = await getCurrentProject()
|
if (!currentProject) throw new Error("currentProject required when projectFilter is empty string")
|
||||||
filteredSessions = filteredSessions.filter((session) => session.projectID === currentProject.id)
|
filteredSessions = filteredSessions.filter((session) => session.projectID === currentProject.id)
|
||||||
} else {
|
} else {
|
||||||
filteredSessions = filteredSessions.filter((session) => session.projectID === projectFilter)
|
filteredSessions = filteredSessions.filter((session) => session.projectID === projectFilter)
|
||||||
@@ -163,14 +158,11 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
|||||||
|
|
||||||
const sessionTotalTokens: number[] = []
|
const sessionTotalTokens: number[] = []
|
||||||
|
|
||||||
const BATCH_SIZE = 20
|
const results = yield* Effect.forEach(
|
||||||
for (let i = 0; i < filteredSessions.length; i += BATCH_SIZE) {
|
filteredSessions,
|
||||||
const batch = filteredSessions.slice(i, i + BATCH_SIZE)
|
(session) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
const batchPromises = batch.map(async (session) => {
|
const messages = yield* svc.messages({ sessionID: session.id })
|
||||||
const messages = await AppRuntime.runPromise(
|
|
||||||
Session.Service.use((svc) => svc.messages({ sessionID: session.id })),
|
|
||||||
)
|
|
||||||
|
|
||||||
let sessionCost = 0
|
let sessionCost = 0
|
||||||
let sessionTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
|
let sessionTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
|
||||||
@@ -179,14 +171,7 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
|||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
messages: number
|
messages: number
|
||||||
tokens: {
|
tokens: { input: number; output: number; cache: { read: number; write: number } }
|
||||||
input: number
|
|
||||||
output: number
|
|
||||||
cache: {
|
|
||||||
read: number
|
|
||||||
write: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cost: number
|
cost: number
|
||||||
}
|
}
|
||||||
> = {}
|
> = {}
|
||||||
@@ -243,11 +228,11 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
|||||||
earliestTime: cutoffTime > 0 ? session.time.updated : session.time.created,
|
earliestTime: cutoffTime > 0 ? session.time.updated : session.time.created,
|
||||||
latestTime: session.time.updated,
|
latestTime: session.time.updated,
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
{ concurrency: 20 },
|
||||||
|
)
|
||||||
|
|
||||||
const batchResults = await Promise.all(batchPromises)
|
for (const result of results) {
|
||||||
|
|
||||||
for (const result of batchResults) {
|
|
||||||
earliestTime = Math.min(earliestTime, result.earliestTime)
|
earliestTime = Math.min(earliestTime, result.earliestTime)
|
||||||
latestTime = Math.max(latestTime, result.latestTime)
|
latestTime = Math.max(latestTime, result.latestTime)
|
||||||
sessionTotalTokens.push(result.sessionTotalTokens)
|
sessionTotalTokens.push(result.sessionTotalTokens)
|
||||||
@@ -280,7 +265,6 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
|||||||
stats.modelUsage[model].cost += usage.cost
|
stats.modelUsage[model].cost += usage.cost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const rangeDays = Math.max(1, Math.ceil((latestTime - earliestTime) / MS_IN_DAY))
|
const rangeDays = Math.max(1, Math.ceil((latestTime - earliestTime) / MS_IN_DAY))
|
||||||
const effectiveDays = windowDays ?? rangeDays
|
const effectiveDays = windowDays ?? rangeDays
|
||||||
@@ -307,7 +291,7 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
|||||||
: sessionTotalTokens[mid]
|
: sessionTotalTokens[mid]
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
}
|
})
|
||||||
|
|
||||||
export function displayStats(stats: SessionStats, toolLimit?: number, modelLimit?: number) {
|
export function displayStats(stats: SessionStats, toolLimit?: number, modelLimit?: number) {
|
||||||
const width = 56
|
const width = 56
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { useEvent } from "@tui/context/event"
|
|||||||
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
||||||
import { StartupLoading } from "@tui/component/startup-loading"
|
import { StartupLoading } from "@tui/component/startup-loading"
|
||||||
import { SyncProvider, useSync } from "@tui/context/sync"
|
import { SyncProvider, useSync } from "@tui/context/sync"
|
||||||
|
import { SyncProviderV2 } from "@tui/context/sync-v2"
|
||||||
import { LocalProvider, useLocal } from "@tui/context/local"
|
import { LocalProvider, useLocal } from "@tui/context/local"
|
||||||
import { DialogModel } from "@tui/component/dialog-model"
|
import { DialogModel } from "@tui/component/dialog-model"
|
||||||
import { useConnected } from "@tui/component/use-connected"
|
import { useConnected } from "@tui/component/use-connected"
|
||||||
@@ -133,6 +134,8 @@ export function tui(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderer = await createCliRenderer(rendererConfig(input.config))
|
const renderer = await createCliRenderer(rendererConfig(input.config))
|
||||||
|
// Prewarm palette before ThemeProvider mounts so `system` theme avoids a first-paint fallback flash.
|
||||||
|
void renderer.getPalette({ size: 16 }).catch(() => undefined)
|
||||||
const mode = (await renderer.waitForThemeMode(1000)) ?? "dark"
|
const mode = (await renderer.waitForThemeMode(1000)) ?? "dark"
|
||||||
|
|
||||||
await render(() => {
|
await render(() => {
|
||||||
@@ -166,6 +169,7 @@ export function tui(input: {
|
|||||||
>
|
>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<SyncProvider>
|
<SyncProvider>
|
||||||
|
<SyncProviderV2>
|
||||||
<ThemeProvider mode={mode}>
|
<ThemeProvider mode={mode}>
|
||||||
<LocalProvider>
|
<LocalProvider>
|
||||||
<KeybindProvider>
|
<KeybindProvider>
|
||||||
@@ -187,6 +191,7 @@ export function tui(input: {
|
|||||||
</KeybindProvider>
|
</KeybindProvider>
|
||||||
</LocalProvider>
|
</LocalProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</SyncProviderV2>
|
||||||
</SyncProvider>
|
</SyncProvider>
|
||||||
</ProjectProvider>
|
</ProjectProvider>
|
||||||
</SDKProvider>
|
</SDKProvider>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
|||||||
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
||||||
import { errorMessage } from "@/util/error"
|
import { errorMessage } from "@/util/error"
|
||||||
import { validateSession } from "./validate-session"
|
import { validateSession } from "./validate-session"
|
||||||
|
import { ServerAuth } from "@/server/auth"
|
||||||
|
|
||||||
export const AttachCommand = cmd({
|
export const AttachCommand = cmd({
|
||||||
command: "attach <url>",
|
command: "attach <url>",
|
||||||
@@ -38,6 +39,11 @@ export const AttachCommand = cmd({
|
|||||||
alias: ["p"],
|
alias: ["p"],
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)",
|
describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)",
|
||||||
|
})
|
||||||
|
.option("username", {
|
||||||
|
alias: ["u"],
|
||||||
|
type: "string",
|
||||||
|
describe: "basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')",
|
||||||
}),
|
}),
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
const unguard = win32InstallCtrlCGuard()
|
const unguard = win32InstallCtrlCGuard()
|
||||||
@@ -60,12 +66,7 @@ export const AttachCommand = cmd({
|
|||||||
return args.dir
|
return args.dir
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
const headers = (() => {
|
const headers = ServerAuth.headers({ password: args.password, username: args.username })
|
||||||
const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD
|
|
||||||
if (!password) return undefined
|
|
||||||
const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
|
|
||||||
return { Authorization: auth }
|
|
||||||
})()
|
|
||||||
const config = await TuiConfig.get()
|
const config = await TuiConfig.get()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { MessageID, PartID } from "@/session/schema"
|
|||||||
import { createStore, produce, unwrap } from "solid-js/store"
|
import { createStore, produce, unwrap } from "solid-js/store"
|
||||||
import { useKeybind } from "@tui/context/keybind"
|
import { useKeybind } from "@tui/context/keybind"
|
||||||
import { usePromptHistory, type PromptInfo } from "./history"
|
import { usePromptHistory, type PromptInfo } from "./history"
|
||||||
|
import { computePromptTraits } from "./traits"
|
||||||
import { assign } from "./part"
|
import { assign } from "./part"
|
||||||
import { usePromptStash } from "./stash"
|
import { usePromptStash } from "./stash"
|
||||||
import { DialogStash } from "../dialog-stash"
|
import { DialogStash } from "../dialog-stash"
|
||||||
@@ -557,17 +558,11 @@ export function Prompt(props: PromptProps) {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!input || input.isDestroyed) return
|
if (!input || input.isDestroyed) return
|
||||||
const capture =
|
input.traits = computePromptTraits({
|
||||||
store.mode === "normal"
|
mode: store.mode,
|
||||||
? auto()?.visible
|
disabled: !!props.disabled,
|
||||||
? (["escape", "navigate", "submit", "tab"] as const)
|
autocompleteVisible: !!auto()?.visible,
|
||||||
: (["tab"] as const)
|
})
|
||||||
: undefined
|
|
||||||
input.traits = {
|
|
||||||
capture,
|
|
||||||
suspend: !!props.disabled || store.mode === "shell",
|
|
||||||
status: store.mode === "shell" ? "SHELL" : undefined,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function restoreExtmarksFromParts(parts: PromptInfo["parts"]) {
|
function restoreExtmarksFromParts(parts: PromptInfo["parts"]) {
|
||||||
@@ -755,9 +750,18 @@ export function Prompt(props: PromptProps) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const variant = local.model.variant.current()
|
||||||
let sessionID = props.sessionID
|
let sessionID = props.sessionID
|
||||||
if (sessionID == null) {
|
if (sessionID == null) {
|
||||||
const res = await sdk.client.session.create({ workspace: props.workspaceID })
|
const res = await sdk.client.session.create({
|
||||||
|
workspace: props.workspaceID,
|
||||||
|
agent: agent.name,
|
||||||
|
model: {
|
||||||
|
providerID: selectedModel.providerID,
|
||||||
|
id: selectedModel.modelID,
|
||||||
|
variant,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
console.log("Creating a session failed:", res.error)
|
console.log("Creating a session failed:", res.error)
|
||||||
@@ -797,7 +801,6 @@ export function Prompt(props: PromptProps) {
|
|||||||
|
|
||||||
// Capture mode before it gets reset
|
// Capture mode before it gets reset
|
||||||
const currentMode = store.mode
|
const currentMode = store.mode
|
||||||
const variant = local.model.variant.current()
|
|
||||||
const editorSelection = editorContext()
|
const editorSelection = editorContext()
|
||||||
const currentEditorSelectionKey = editorSelectionKey(editorSelection)
|
const currentEditorSelectionKey = editorSelectionKey(editorSelection)
|
||||||
const editorParts =
|
const editorParts =
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { EditorTraits } from "@opentui/core"
|
||||||
|
|
||||||
|
export type PromptMode = "normal" | "shell"
|
||||||
|
|
||||||
|
export interface PromptTraitsInput {
|
||||||
|
mode: PromptMode
|
||||||
|
disabled: boolean
|
||||||
|
autocompleteVisible: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the textarea editor traits for the prompt.
|
||||||
|
*
|
||||||
|
* `traits.suspend` gates the textarea's keybinding actions (backspace,
|
||||||
|
* delete-word, arrow movement, undo/redo, etc.). Shell mode is an active
|
||||||
|
* editing mode — only `disabled` should suspend the textarea, otherwise
|
||||||
|
* users can type in shell mode but cannot delete or move the cursor.
|
||||||
|
*/
|
||||||
|
export function computePromptTraits(input: PromptTraitsInput): EditorTraits {
|
||||||
|
const capture =
|
||||||
|
input.mode === "normal"
|
||||||
|
? input.autocompleteVisible
|
||||||
|
? (["escape", "navigate", "submit", "tab"] as const)
|
||||||
|
: (["tab"] as const)
|
||||||
|
: undefined
|
||||||
|
return {
|
||||||
|
capture,
|
||||||
|
suspend: input.disabled,
|
||||||
|
status: input.mode === "shell" ? "SHELL" : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,16 +68,61 @@ function normalize(raw: Record<string, unknown>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolvePlugins(config: Info, configFilepath: string) {
|
const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory: string }) {
|
||||||
if (!config.plugin) return config
|
const afs = yield* AppFileSystem.Service
|
||||||
for (let i = 0; i < config.plugin.length; i++) {
|
|
||||||
config.plugin[i] = await ConfigPlugin.resolvePluginSpec(config.plugin[i], configFilepath)
|
const resolvePlugins = (config: Info, configFilepath: string): Effect.Effect<Info> =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugins = config.plugin
|
||||||
|
if (!plugins) return config
|
||||||
|
for (let i = 0; i < plugins.length; i++) {
|
||||||
|
plugins[i] = yield* Effect.promise(() => ConfigPlugin.resolvePluginSpec(plugins[i], configFilepath))
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}
|
})
|
||||||
|
|
||||||
async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) {
|
const load = (text: string, configFilepath: string): Effect.Effect<Info> =>
|
||||||
const data = await loadFile(file)
|
Effect.gen(function* () {
|
||||||
|
const expanded = yield* Effect.promise(() =>
|
||||||
|
ConfigVariable.substitute({ text, type: "path", path: configFilepath, missing: "empty" }),
|
||||||
|
)
|
||||||
|
const data = ConfigParse.jsonc(expanded, configFilepath)
|
||||||
|
if (!isRecord(data)) return {} as Info
|
||||||
|
// Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json
|
||||||
|
// (mirroring the old opencode.json shape) still get their settings applied.
|
||||||
|
const validated = ConfigParse.schema(Info, normalize(data), configFilepath)
|
||||||
|
return yield* resolvePlugins(validated, configFilepath)
|
||||||
|
}).pipe(
|
||||||
|
// catchCause (not tapErrorCause + orElseSucceed) because ConfigParse.jsonc/.schema
|
||||||
|
// can sync-throw — those become defects, which orElseSucceed wouldn't catch.
|
||||||
|
Effect.catchCause((cause) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
log.warn("invalid tui config", { path: configFilepath, cause })
|
||||||
|
return {} as Info
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const loadFile = (filepath: string): Effect.Effect<Info> =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
// Silent-swallow non-NotFound read errors (perms, EISDIR, IO) → log + skip.
|
||||||
|
// Matches how parse/schema/plugin failures in load() are handled — every
|
||||||
|
// broken-config path degrades gracefully rather than crashing TUI startup.
|
||||||
|
const text = yield* afs.readFileStringSafe(filepath).pipe(
|
||||||
|
Effect.catchCause((cause) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
log.warn("failed to read tui config", { path: filepath, cause })
|
||||||
|
return undefined
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (!text) return {} as Info
|
||||||
|
return yield* load(text, filepath)
|
||||||
|
})
|
||||||
|
|
||||||
|
const mergeFile = (acc: Acc, file: string) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const data = yield* loadFile(file)
|
||||||
acc.result = mergeDeep(acc.result, data)
|
acc.result = mergeDeep(acc.result, data)
|
||||||
if (!data.plugin?.length) return
|
if (!data.plugin?.length) return
|
||||||
|
|
||||||
@@ -88,9 +133,8 @@ async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) {
|
|||||||
])
|
])
|
||||||
acc.result.plugin = plugins.map((item) => item.spec)
|
acc.result.plugin = plugins.map((item) => item.spec)
|
||||||
acc.result.plugin_origins = plugins
|
acc.result.plugin_origins = plugins
|
||||||
}
|
})
|
||||||
|
|
||||||
const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory: string }) {
|
|
||||||
// Every config dir we may read from: global config dir, any `.opencode`
|
// Every config dir we may read from: global config dir, any `.opencode`
|
||||||
// folders between cwd and home, and OPENCODE_CONFIG_DIR.
|
// folders between cwd and home, and OPENCODE_CONFIG_DIR.
|
||||||
const directories = yield* ConfigPaths.directories(ctx.directory)
|
const directories = yield* ConfigPaths.directories(ctx.directory)
|
||||||
@@ -104,19 +148,19 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
|
|||||||
|
|
||||||
// 1. Global tui config (lowest precedence).
|
// 1. Global tui config (lowest precedence).
|
||||||
for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) {
|
for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) {
|
||||||
yield* Effect.promise(() => mergeFile(acc, file, ctx)).pipe(Effect.orDie)
|
yield* mergeFile(acc, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Explicit OPENCODE_TUI_CONFIG override, if set.
|
// 2. Explicit OPENCODE_TUI_CONFIG override, if set.
|
||||||
if (Flag.OPENCODE_TUI_CONFIG) {
|
if (Flag.OPENCODE_TUI_CONFIG) {
|
||||||
const configFile = Flag.OPENCODE_TUI_CONFIG
|
const configFile = Flag.OPENCODE_TUI_CONFIG
|
||||||
yield* Effect.promise(() => mergeFile(acc, configFile, ctx)).pipe(Effect.orDie)
|
yield* mergeFile(acc, configFile)
|
||||||
log.debug("loaded custom tui config", { path: configFile })
|
log.debug("loaded custom tui config", { path: configFile })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Project tui files, applied root-first so the closest file wins.
|
// 3. Project tui files, applied root-first so the closest file wins.
|
||||||
for (const file of projectFiles) {
|
for (const file of projectFiles) {
|
||||||
yield* Effect.promise(() => mergeFile(acc, file, ctx)).pipe(Effect.orDie)
|
yield* mergeFile(acc, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. `.opencode` directories (and OPENCODE_CONFIG_DIR) discovered while
|
// 4. `.opencode` directories (and OPENCODE_CONFIG_DIR) discovered while
|
||||||
@@ -127,7 +171,7 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
|
|||||||
for (const dir of dirs) {
|
for (const dir of dirs) {
|
||||||
if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue
|
if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue
|
||||||
for (const file of ConfigPaths.fileInDirectory(dir, "tui")) {
|
for (const file of ConfigPaths.fileInDirectory(dir, "tui")) {
|
||||||
yield* Effect.promise(() => mergeFile(acc, file, ctx)).pipe(Effect.orDie)
|
yield* mergeFile(acc, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,29 +236,3 @@ export async function waitForDependencies() {
|
|||||||
export async function get() {
|
export async function get() {
|
||||||
return runPromise((svc) => svc.get())
|
return runPromise((svc) => svc.get())
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadFile(filepath: string): Promise<Info> {
|
|
||||||
const text = await ConfigPaths.readFile(filepath)
|
|
||||||
if (!text) return {}
|
|
||||||
return load(text, filepath).catch((error) => {
|
|
||||||
log.warn("failed to load tui config", { path: filepath, error })
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load(text: string, configFilepath: string): Promise<Info> {
|
|
||||||
return ConfigVariable.substitute({ text, type: "path", path: configFilepath, missing: "empty" })
|
|
||||||
.then((expanded) => ConfigParse.jsonc(expanded, configFilepath))
|
|
||||||
.then((data) => {
|
|
||||||
if (!isRecord(data)) return {}
|
|
||||||
|
|
||||||
// Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json
|
|
||||||
// (mirroring the old opencode.json shape) still get their settings applied.
|
|
||||||
return ConfigParse.schema(Info, normalize(data), configFilepath)
|
|
||||||
})
|
|
||||||
.then((data) => resolvePlugins(data, configFilepath))
|
|
||||||
.catch((error) => {
|
|
||||||
log.warn("invalid tui config", { path: configFilepath, error })
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,307 @@
|
|||||||
|
import { useEvent } from "@tui/context/event"
|
||||||
|
import type {
|
||||||
|
SessionMessage,
|
||||||
|
SessionMessageAssistant,
|
||||||
|
SessionMessageAssistantReasoning,
|
||||||
|
SessionMessageAssistantText,
|
||||||
|
SessionMessageAssistantTool,
|
||||||
|
} from "@opencode-ai/sdk/v2"
|
||||||
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
|
import { createSimpleContext } from "./helper"
|
||||||
|
import { useSDK } from "./sdk"
|
||||||
|
|
||||||
|
function activeAssistant(messages: SessionMessage[]) {
|
||||||
|
const index = messages.findLastIndex((message) => message.type === "assistant" && !message.time.completed)
|
||||||
|
if (index < 0) return
|
||||||
|
const assistant = messages[index]
|
||||||
|
return assistant?.type === "assistant" ? assistant : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeCompaction(messages: SessionMessage[]) {
|
||||||
|
const index = messages.findLastIndex((message) => message.type === "compaction")
|
||||||
|
if (index < 0) return
|
||||||
|
const compaction = messages[index]
|
||||||
|
return compaction?.type === "compaction" ? compaction : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeShell(messages: SessionMessage[], callID: string) {
|
||||||
|
const index = messages.findLastIndex((message) => message.type === "shell" && message.callID === callID)
|
||||||
|
if (index < 0) return
|
||||||
|
const shell = messages[index]
|
||||||
|
return shell?.type === "shell" ? shell : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) {
|
||||||
|
return assistant?.content.findLast(
|
||||||
|
(item): item is SessionMessageAssistantTool => item.type === "tool" && (callID === undefined || item.id === callID),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function latestText(assistant: SessionMessageAssistant | undefined) {
|
||||||
|
return assistant?.content.findLast((item): item is SessionMessageAssistantText => item.type === "text")
|
||||||
|
}
|
||||||
|
|
||||||
|
function latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) {
|
||||||
|
return assistant?.content.findLast(
|
||||||
|
(item): item is SessionMessageAssistantReasoning => item.type === "reasoning" && item.id === reasoningID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext({
|
||||||
|
name: "SyncV2",
|
||||||
|
init: () => {
|
||||||
|
const [store, setStore] = createStore<{
|
||||||
|
messages: {
|
||||||
|
[sessionID: string]: SessionMessage[]
|
||||||
|
}
|
||||||
|
}>({
|
||||||
|
messages: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const event = useEvent()
|
||||||
|
const sdk = useSDK()
|
||||||
|
|
||||||
|
function update(sessionID: string, fn: (messages: SessionMessage[]) => void) {
|
||||||
|
setStore(
|
||||||
|
"messages",
|
||||||
|
produce((draft) => {
|
||||||
|
fn((draft[sessionID] ??= []))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
event.subscribe((event) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case "session.next.prompted": {
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
draft.push({
|
||||||
|
id: event.id,
|
||||||
|
type: "user",
|
||||||
|
text: event.properties.prompt.text,
|
||||||
|
files: event.properties.prompt.files,
|
||||||
|
agents: event.properties.prompt.agents,
|
||||||
|
time: { created: event.properties.timestamp },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "session.next.synthetic":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
draft.push({
|
||||||
|
id: event.id,
|
||||||
|
type: "synthetic",
|
||||||
|
sessionID: event.properties.sessionID,
|
||||||
|
text: event.properties.text,
|
||||||
|
time: { created: event.properties.timestamp },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.shell.started":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
draft.push({
|
||||||
|
id: event.id,
|
||||||
|
type: "shell",
|
||||||
|
callID: event.properties.callID,
|
||||||
|
command: event.properties.command,
|
||||||
|
output: "",
|
||||||
|
time: { created: event.properties.timestamp },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.shell.ended":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
const match = activeShell(draft, event.properties.callID)
|
||||||
|
if (!match) return
|
||||||
|
match.output = event.properties.output
|
||||||
|
match.time.completed = event.properties.timestamp
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.step.started":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
const currentAssistant = activeAssistant(draft)
|
||||||
|
if (currentAssistant) currentAssistant.time.completed = event.properties.timestamp
|
||||||
|
draft.push({
|
||||||
|
id: event.id,
|
||||||
|
type: "assistant",
|
||||||
|
agent: event.properties.agent,
|
||||||
|
model: event.properties.model,
|
||||||
|
content: [],
|
||||||
|
snapshot: event.properties.snapshot ? { start: event.properties.snapshot } : undefined,
|
||||||
|
time: { created: event.properties.timestamp },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.step.ended":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
const currentAssistant = activeAssistant(draft)
|
||||||
|
if (!currentAssistant) return
|
||||||
|
currentAssistant.time.completed = event.properties.timestamp
|
||||||
|
currentAssistant.finish = event.properties.finish
|
||||||
|
currentAssistant.cost = event.properties.cost
|
||||||
|
currentAssistant.tokens = event.properties.tokens
|
||||||
|
if (event.properties.snapshot)
|
||||||
|
currentAssistant.snapshot = { ...currentAssistant.snapshot, end: event.properties.snapshot }
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.step.failed":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
const currentAssistant = activeAssistant(draft)
|
||||||
|
if (!currentAssistant) return
|
||||||
|
currentAssistant.time.completed = event.properties.timestamp
|
||||||
|
currentAssistant.finish = "error"
|
||||||
|
currentAssistant.error = event.properties.error
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.text.started":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
activeAssistant(draft)?.content.push({ type: "text", text: "" })
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.text.delta":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
const match = latestText(activeAssistant(draft))
|
||||||
|
if (match) match.text += event.properties.delta
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.text.ended":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
const match = latestText(activeAssistant(draft))
|
||||||
|
if (match) match.text = event.properties.text
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.tool.input.started":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
activeAssistant(draft)?.content.push({
|
||||||
|
type: "tool",
|
||||||
|
id: event.properties.callID,
|
||||||
|
name: event.properties.name,
|
||||||
|
time: { created: event.properties.timestamp },
|
||||||
|
state: { status: "pending", input: "" },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.tool.input.delta":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
const match = latestTool(activeAssistant(draft), event.properties.callID)
|
||||||
|
if (match?.state.status === "pending") match.state.input += event.properties.delta
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.tool.input.ended":
|
||||||
|
break
|
||||||
|
case "session.next.tool.called":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
const match = latestTool(activeAssistant(draft), event.properties.callID)
|
||||||
|
if (!match) return
|
||||||
|
match.time.ran = event.properties.timestamp
|
||||||
|
match.provider = event.properties.provider
|
||||||
|
match.state = { status: "running", input: event.properties.input, structured: {}, content: [] }
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.tool.progress":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
const match = latestTool(activeAssistant(draft), event.properties.callID)
|
||||||
|
if (match?.state.status !== "running") return
|
||||||
|
match.state.structured = event.properties.structured
|
||||||
|
match.state.content = [...event.properties.content]
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.tool.success":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
const match = latestTool(activeAssistant(draft), event.properties.callID)
|
||||||
|
if (match?.state.status !== "running") return
|
||||||
|
match.state = {
|
||||||
|
status: "completed",
|
||||||
|
input: match.state.input,
|
||||||
|
structured: event.properties.structured,
|
||||||
|
content: [...event.properties.content],
|
||||||
|
}
|
||||||
|
match.provider = event.properties.provider
|
||||||
|
match.time.completed = event.properties.timestamp
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.tool.failed":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
const match = latestTool(activeAssistant(draft), event.properties.callID)
|
||||||
|
if (match?.state.status !== "running") return
|
||||||
|
match.state = {
|
||||||
|
status: "error",
|
||||||
|
error: event.properties.error,
|
||||||
|
input: match.state.input,
|
||||||
|
structured: match.state.structured,
|
||||||
|
content: match.state.content,
|
||||||
|
}
|
||||||
|
match.provider = event.properties.provider
|
||||||
|
match.time.completed = event.properties.timestamp
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.reasoning.started":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
activeAssistant(draft)?.content.push({
|
||||||
|
type: "reasoning",
|
||||||
|
id: event.properties.reasoningID,
|
||||||
|
text: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.reasoning.delta":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID)
|
||||||
|
if (match) match.text += event.properties.delta
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.reasoning.ended":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID)
|
||||||
|
if (match) match.text = event.properties.text
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.retried":
|
||||||
|
break
|
||||||
|
case "session.next.compaction.started":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
draft.push({
|
||||||
|
id: event.id,
|
||||||
|
type: "compaction",
|
||||||
|
reason: event.properties.reason,
|
||||||
|
summary: "",
|
||||||
|
time: { created: event.properties.timestamp },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.compaction.delta":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
const match = activeCompaction(draft)
|
||||||
|
if (match) match.summary += event.properties.text
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "session.next.compaction.ended":
|
||||||
|
update(event.properties.sessionID, (draft) => {
|
||||||
|
const match = activeCompaction(draft)
|
||||||
|
if (!match) return
|
||||||
|
match.summary = event.properties.text
|
||||||
|
match.include = event.properties.include
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
data: store,
|
||||||
|
session: {
|
||||||
|
message: {
|
||||||
|
async sync(sessionID: string) {
|
||||||
|
const response = await sdk.client.v2.session.messages({ sessionID })
|
||||||
|
setStore("messages", sessionID, reconcile(response.data?.items ?? []))
|
||||||
|
},
|
||||||
|
fromSession(sessionID: string) {
|
||||||
|
const messages = store.messages[sessionID]
|
||||||
|
if (!messages) return []
|
||||||
|
return messages
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -416,12 +416,16 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||||||
|
|
||||||
const values = createMemo(() => {
|
const values = createMemo(() => {
|
||||||
const active = store.themes[store.active]
|
const active = store.themes[store.active]
|
||||||
if (active) return resolveTheme(active, store.mode)
|
if (active) {
|
||||||
|
return resolveTheme(active, store.mode)
|
||||||
|
}
|
||||||
|
|
||||||
const saved = kv.get("theme")
|
const saved = kv.get("theme")
|
||||||
if (typeof saved === "string") {
|
if (typeof saved === "string") {
|
||||||
const theme = store.themes[saved]
|
const theme = store.themes[saved]
|
||||||
if (theme) return resolveTheme(theme, store.mode)
|
if (theme) {
|
||||||
|
return resolveTheme(theme, store.mode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveTheme(store.themes.opencode, store.mode)
|
return resolveTheme(store.themes.opencode, store.mode)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,9 @@ import SidebarTodo from "../feature-plugins/sidebar/todo"
|
|||||||
import SidebarFiles from "../feature-plugins/sidebar/files"
|
import SidebarFiles from "../feature-plugins/sidebar/files"
|
||||||
import SidebarFooter from "../feature-plugins/sidebar/footer"
|
import SidebarFooter from "../feature-plugins/sidebar/footer"
|
||||||
import PluginManager from "../feature-plugins/system/plugins"
|
import PluginManager from "../feature-plugins/system/plugins"
|
||||||
|
import SessionV2Debug from "../feature-plugins/system/session-v2"
|
||||||
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
||||||
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
|
|
||||||
export type InternalTuiPlugin = TuiPluginModule & {
|
export type InternalTuiPlugin = TuiPluginModule & {
|
||||||
id: string
|
id: string
|
||||||
@@ -24,4 +26,5 @@ export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [
|
|||||||
SidebarFiles,
|
SidebarFiles,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
PluginManager,
|
PluginManager,
|
||||||
|
...(Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM ? [SessionV2Debug] : []),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
|||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { errorData, errorMessage } from "@/util/error"
|
import { errorData, errorMessage } from "@/util/error"
|
||||||
import { isRecord } from "@/util/record"
|
import { isRecord } from "@/util/record"
|
||||||
import { Instance } from "@/project/instance"
|
import { WithInstance } from "@/project/with-instance"
|
||||||
import {
|
import {
|
||||||
readPackageThemes,
|
readPackageThemes,
|
||||||
readPluginId,
|
readPluginId,
|
||||||
@@ -790,7 +790,7 @@ async function addPluginBySpec(state: RuntimeState | undefined, raw: string) {
|
|||||||
state.pending.delete(spec)
|
state.pending.delete(spec)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const ready = await Instance.provide({
|
const ready = await WithInstance.provide({
|
||||||
directory: state.directory,
|
directory: state.directory,
|
||||||
fn: () => resolveExternalPlugins([cfg], () => TuiConfig.waitForDependencies()),
|
fn: () => resolveExternalPlugins([cfg], () => TuiConfig.waitForDependencies()),
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@@ -986,7 +986,7 @@ async function load(input: { api: Api; config: TuiConfig.Info }) {
|
|||||||
}
|
}
|
||||||
runtime = next
|
runtime = next
|
||||||
try {
|
try {
|
||||||
await Instance.provide({
|
await WithInstance.provide({
|
||||||
directory: cwd,
|
directory: cwd,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const records = Flag.OPENCODE_PURE ? [] : (config.plugin_origins ?? [])
|
const records = Flag.OPENCODE_PURE ? [] : (config.plugin_origins ?? [])
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ import { Locale } from "@/util/locale"
|
|||||||
import type { Tool } from "@/tool/tool"
|
import type { Tool } from "@/tool/tool"
|
||||||
import type { ReadTool } from "@/tool/read"
|
import type { ReadTool } from "@/tool/read"
|
||||||
import type { WriteTool } from "@/tool/write"
|
import type { WriteTool } from "@/tool/write"
|
||||||
import { BashTool } from "@/tool/bash"
|
import { ShellTool } from "@/tool/shell"
|
||||||
|
import { ShellID } from "@/tool/shell/id"
|
||||||
import type { GlobTool } from "@/tool/glob"
|
import type { GlobTool } from "@/tool/glob"
|
||||||
import { TodoWriteTool } from "@/tool/todo"
|
import { TodoWriteTool } from "@/tool/todo"
|
||||||
import type { GrepTool } from "@/tool/grep"
|
import type { GrepTool } from "@/tool/grep"
|
||||||
@@ -1552,8 +1553,8 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess
|
|||||||
return (
|
return (
|
||||||
<Show when={!shouldHide()}>
|
<Show when={!shouldHide()}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.part.tool === "bash"}>
|
<Match when={props.part.tool === ShellID.ToolID}>
|
||||||
<Bash {...toolprops} />
|
<Shell {...toolprops} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.part.tool === "glob"}>
|
<Match when={props.part.tool === "glob"}>
|
||||||
<Glob {...toolprops} />
|
<Glob {...toolprops} />
|
||||||
@@ -1784,7 +1785,7 @@ function BlockTool(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Bash(props: ToolProps<typeof BashTool>) {
|
function Shell(props: ToolProps<typeof ShellTool>) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const isRunning = createMemo(() => props.part.state.status === "running")
|
const isRunning = createMemo(() => props.part.state.status === "running")
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
|||||||
import { Keybind } from "@/util/keybind"
|
import { Keybind } from "@/util/keybind"
|
||||||
import { Locale } from "@/util/locale"
|
import { Locale } from "@/util/locale"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
|
import { ShellID } from "@/tool/shell/id"
|
||||||
import { useDialog } from "../../ui/dialog"
|
import { useDialog } from "../../ui/dialog"
|
||||||
import { getScrollAcceleration } from "../../util/scroll"
|
import { getScrollAcceleration } from "../../util/scroll"
|
||||||
import { useTuiConfig } from "../../context/tui-config"
|
import { useTuiConfig } from "../../context/tui-config"
|
||||||
@@ -287,7 +288,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permission === "bash") {
|
if (permission === ShellID.ToolID) {
|
||||||
const title =
|
const title =
|
||||||
typeof data.description === "string" && data.description ? data.description : "Shell command"
|
typeof data.description === "string" && data.description ? data.description : "Shell command"
|
||||||
const command = typeof data.command === "string" ? data.command : ""
|
const command = typeof data.command === "string" ? data.command : ""
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user