Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/workflows/build-cli.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Build CLI Binaries

on:
workflow_dispatch:
release:
types:
- published

permissions:
contents: write

jobs:
build-all:
if: ${{ github.event_name != 'release' || startsWith(github.event.release.tag_name, 'v') }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Fetch tags
run: git fetch --force --tags

- name: Setup Bun and install dependencies
uses: ./.github/actions/setup-bun

- name: Install platform-specific dependencies
run: |
cd packages/opencode
bun install --os="*" --cpu="*" @opentui/core@$(node -p "require('./package.json').dependencies['@opentui/core']")
bun install --os="*" --cpu="*" @parcel/watcher@$(node -p "require('./package.json').dependencies['@parcel/watcher']")

- name: Build release platforms
run: |
cd packages/opencode
bun run script/build.ts --skip-install \
--target opencode-linux-x64-baseline \
--target opencode-windows-x64-baseline

- name: Create archives for GitHub release
run: |
cd packages/opencode
bun run script/archive.ts
ls -lh dist/release

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: costrict-cli-baseline-platforms
path: |
packages/opencode/dist/release/*.tar.gz
packages/opencode/dist/release/*.zip
retention-days: 7
if-no-files-found: error

- name: Upload release assets
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.release.tag_name }}
files: |
packages/opencode/dist/release/*.tar.gz
packages/opencode/dist/release/*.zip
fail_on_unmatched_files: true
overwrite_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Build Summary
if: always()
env:
STATUS: ${{ job.status }}
run: |
if [[ "$STATUS" == "success" ]]; then
echo "## 🎉 CoStrict CLI Build Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ CLI binaries built successfully for the selected release targets." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📦 Built Packages" >> $GITHUB_STEP_SUMMARY
echo "- opencode-linux-x64-baseline.tar.gz" >> $GITHUB_STEP_SUMMARY
echo "- opencode-windows-x64-baseline.zip" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📥 Download" >> $GITHUB_STEP_SUMMARY
echo "Visit the [Actions run page](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) to download the compiled binaries." >> $GITHUB_STEP_SUMMARY
exit 0
fi

echo "## ❌ CoStrict CLI Build Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "The build or asset upload step failed. Check the failed step logs in this run for details." >> $GITHUB_STEP_SUMMARY
32 changes: 31 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
.DS_Store
node_modules
history_message
.history_message
.worktrees
.sst
.env
Expand Down Expand Up @@ -28,6 +30,7 @@ target
opencode-dev
logs/
*.bun-build
trae_agent

.claude
.opencode
Expand All @@ -36,4 +39,31 @@ logs/
tsconfig.tsbuildinfo
.bunfig.toml
.cache
skills
skills

.history
.cursor
.cspell

.fdignore
.rgignore
agent-git
.agent-git/
explore_result/
agent-git.bat
.tmp-bash-tool-test/

docs/
test-project/
.agent-git
.agent-worktrees
proposal/
.memory_bank.md
__pycache__/
.pytest_cache/
.venv/
venv/
testcase/
nul
packages/opencode/testcase/
packages/opencode/proposal/
6,067 changes: 3,059 additions & 3,008 deletions bun.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"type": "module",
"packageManager": "bun@1.3.10",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"debug:server": "bun run --cwd packages/opencode --inspect=ws://localhost:6499/ ./src/index.ts serve --port 4096",
"dev": "bun run --preload @opentui/solid/preload --cwd packages/opencode --conditions=browser src/index.ts",
"debug:server": "bun run --preload @opentui/solid/preload --cwd packages/opencode --inspect=ws://localhost:6499/ ./src/index.ts serve --port 4096",
"debug:tui": "bun run dev attach http://localhost:4096",
"dev:desktop": "bun --cwd packages/desktop tauri dev",
"dev:web": "bun --cwd packages/app dev",
Expand Down
217 changes: 217 additions & 0 deletions packages/app/src/utils/proposal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { OpencodeClient } from "@opencode-ai/sdk/v2/client"

export type ChangeAgent = "coding" | "FixAgent" | "taskcheck"

export type ProposalInfo = {
changeId: string
description?: string
taskPath: string
}

function normalizePath(value: string) {
return value.replaceAll("\\", "/")
}

function trimExcerpt(value: string) {
const text = value.trim()
if (text.length <= 500) return text
return text.slice(0, 497) + "..."
}

function extractTaskcheckUserInput(content: string) {
const match = content.match(/##\s*(背景|需求|用户需求|目标)[^\n]*\n([\s\S]*?)(?=\n##|$)/i)
if (match?.[2]) return trimExcerpt(match[2])

const paragraphs = content
.split("\n\n")
.filter((line) => line.trim() && !line.trim().startsWith("#"))
.slice(0, 2)
.join("\n\n")
.trim()

if (paragraphs.length > 0) return trimExcerpt(paragraphs)
return "(无法提取用户原始需求)"
}

async function list(client: OpencodeClient, path: string) {
return client.file
.list({ path })
.then((x) => x.data ?? [])
.catch(() => [])
}

async function read(client: OpencodeClient, path: string) {
return client.file
.read({ path })
.then((x) => {
const content = x.data
if (!content || content.type !== "text") return ""
return content.content
})
.catch(() => "")
}

function resolveTaskPath(paths: string[]) {
return paths.find((item) => item.endsWith("/tasks.md")) ?? paths.find((item) => item.endsWith("/task.md"))
}

export function isChangeAgent(name?: string): name is ChangeAgent {
return name === "coding" || name === "FixAgent" || name === "taskcheck"
}

export async function scanProposals(client: OpencodeClient) {
const root = await list(client, "proposal")
const dirs = root.filter((item) => item.type === "directory")

const items = await Promise.all(
dirs.map(async (item) => {
const entries = await list(client, item.path)
const taskPath = resolveTaskPath(entries.filter((entry) => entry.type === "file").map((entry) => entry.path))
if (!taskPath) return

const proposalPath = entries.find((entry) => entry.type === "file" && entry.name === "proposal.md")?.path
const description = proposalPath ? proposalTag(await read(client, proposalPath)) : undefined

return {
changeId: item.name,
description,
taskPath,
} satisfies ProposalInfo
}),
)

return items.flatMap((item) => (item ? [item] : []))
}

export async function buildProposalPrompt(
client: OpencodeClient,
input: {
agent: ChangeAgent
changeId: string
project: string
},
) {
const proposalDir = `proposal/${input.changeId}`
const entries = await list(client, proposalDir)
const taskPath = resolveTaskPath(entries.filter((item) => item.type === "file").map((item) => item.path))
if (!taskPath) return

const projectText = normalizePath(input.project)
const taskText = normalizePath(taskPath)

if (input.agent === "coding") {
return `项目路径:\`${projectText}\`\n任务文件路径:\`${taskText}\`\n\n请确保本次编码任务高质量完成`
}

if (input.agent === "FixAgent") {
return `项目路径:\`${projectText}\`\n任务文件路径:\`${taskText}\`\n\n请认真收集用户反馈并进行代码修复和改进`
}

const proposalPath = `${proposalDir}/proposal.md`
const userInputPath = `${proposalDir}/user_input.md`
const proposalContent = await read(client, proposalPath)
const userInput = await read(client, userInputPath)
const userTaskText = userInput.trim() ? trimExcerpt(userInput) : extractTaskcheckUserInput(proposalContent)

return `## 用户原始需求

\`\`\`text
${userTaskText}
\`\`\`

## 任务上下文

项目路径:\`${projectText}\`
proposal.md路径:\`${normalizePath(proposalPath)}\`
task.md路径:\`${taskText}\`

请以"用户原始需求"为覆盖基准,认真检查 task.md 文件是否需要调整。`
}

export function proposalTag(content: string) {
const title = heading(content)
if (title) return title

const scope = section(content, ["需求拆解"])
const item = scope ? bullet(scope) : undefined
if (item) return tag("子需", item)

const goal = section(content, ["背景与目标", "背景", "需求", "目标"])
const line = goal ? sentence(goal) : undefined
if (line) return tag("背景", line)

return undefined
}

function heading(content: string) {
const line = content
.split("\n")
.map((item) => item.trim())
.find((item) => item.startsWith("# "))

if (!line) return undefined
const text = line.replace(/^#\s+/, "")
const cut = text.split(/[::]/)
const title = cut.length > 1 ? cut.slice(1).join(":").trim() : text
return clean(title || text)
}

function section(content: string, names: string[]) {
const lines = content.split("\n")

for (const name of names) {
const start = lines.findIndex((line) => new RegExp(`^##\\s*${escape(name)}(?:\\s|$)`).test(line.trim()))
if (start === -1) continue

const body = []
for (const line of lines.slice(start + 1)) {
if (line.trim().startsWith("## ")) break
body.push(line)
}

const text = body.join("\n").trim()
if (text) return text
}

return undefined
}

function bullet(content: string) {
const line = content
.split("\n")
.map((item) => item.trim())
.filter(Boolean)
.find((item) => item.startsWith("- ") && !item.startsWith("- 功能描述") && !item.startsWith("- 影响范围"))

if (!line) return undefined
return clean(line.replace(/^-+\s*/, "").replace(/^子需求\s*\d+\s*[::]\s*/, ""))
}

function sentence(content: string) {
return content
.split("\n")
.map((item) => clean(item))
.find(Boolean)
}

function clean(text: string) {
const line = text
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/[`*_~]/g, "")
.replace(/^[0-9]+[.)]\s*/, "")
.replace(/^[-*]\s*/, "")
.replace(/^(功能描述|影响范围|修改对象|修改目的|修改内容)\s*[::]\s*/, "")
.replace(/\s+/g, " ")
.trim()

if (!line) return undefined
return line
}

function tag(name: string, text: string) {
return `${name} ${text}`
}

function escape(text: string) {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
11 changes: 10 additions & 1 deletion packages/opencode/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,13 @@ gen
app.log
src/provider/models-snapshot.ts
src/costrict/agent/builtin.ts
src/costrict/skill/builtin.ts
src/costrict/skill/builtin.ts
# Lint tools - extracted binaries (bundles are committed)
resources/lint/*/
!resources/lint/bundles/
resources/lint/bundles/*.tmp

# Search tools - extracted binaries (bundles are committed)
resources/search/*/
!resources/search/bundles/
resources/search/bundles/*.tmp
Loading
Loading