Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8eaa77bf1 | |||
| a07f37073b | |||
| 4d760a1984 | |||
| 6b7058fe1c | |||
| 1149b984d9 | |||
| 81fb1b313e | |||
| 3a7a2a838e | |||
| 10ae43a121 | |||
| c85b970903 | |||
| 7044662cfa | |||
| 92656fdf29 | |||
| c65e7aff86 | |||
| e97613ef9f | |||
| 827469c725 | |||
| 613b5fbe48 | |||
| 7ed05962db | |||
| 250a86ec52 | |||
| 0795a577e0 | |||
| 8e5607f9c0 | |||
| d6b3bb0807 |
@@ -6,7 +6,11 @@ on:
|
||||
|
||||
jobs:
|
||||
opencode:
|
||||
if: startsWith(github.event.comment.body, 'hey opencode')
|
||||
if: |
|
||||
startsWith(github.event.comment.body, 'opencode') ||
|
||||
startsWith(github.event.comment.body, 'hi opencode') ||
|
||||
startsWith(github.event.comment.body, 'hey opencode') ||
|
||||
contains(github.event.comment.body, '@opencode-agent')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -17,7 +21,7 @@ jobs:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/sdks/github@github-v1
|
||||
uses: sst/opencode/github@latest
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
|
||||
@@ -27,4 +27,4 @@ jobs:
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
./script/publish
|
||||
working-directory: ./sdks/github
|
||||
working-directory: ./github
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@clack/prompts": "0.11.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@modelcontextprotocol/sdk": "1.15.1",
|
||||
@@ -54,6 +56,7 @@
|
||||
"devDependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "2.2.10",
|
||||
"@ai-sdk/anthropic": "1.2.12",
|
||||
"@octokit/webhooks-types": "7.6.1",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@tsconfig/bun": "1.0.7",
|
||||
"@types/bun": "latest",
|
||||
@@ -134,6 +137,16 @@
|
||||
"zod": "3.25.49",
|
||||
},
|
||||
"packages": {
|
||||
"@actions/core": ["@actions/core@1.11.1", "", { "dependencies": { "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1" } }, "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A=="],
|
||||
|
||||
"@actions/exec": ["@actions/exec@1.1.1", "", { "dependencies": { "@actions/io": "^1.0.1" } }, "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w=="],
|
||||
|
||||
"@actions/github": ["@actions/github@6.0.1", "", { "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", "@octokit/plugin-paginate-rest": "^9.2.2", "@octokit/plugin-rest-endpoint-methods": "^10.4.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "undici": "^5.28.5" } }, "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw=="],
|
||||
|
||||
"@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="],
|
||||
|
||||
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@2.2.10", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icLGO7Q0NinnHIPgT+y1QjHVwH4HwV+brWbvM+FfCG2Afpa89PyKa3Ret91kGjZpBgM/xnj1B7K5eM+rRlsXQA=="],
|
||||
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
|
||||
@@ -512,6 +525,8 @@
|
||||
|
||||
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||
|
||||
"@octokit/webhooks-types": ["@octokit/webhooks-types@7.6.1", "", {}, "sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw=="],
|
||||
|
||||
"@openauthjs/openauth": ["@openauthjs/openauth@0.4.3", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-RlnjqvHzqcbFVymEwhlUEuac4utA5h4nhSK/i2szZuQmxTIqbGUxZ+nM+avM+VV4Ing+/ZaNLKILoXS3yrkOOw=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk"],
|
||||
@@ -1016,6 +1031,8 @@
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||
@@ -2158,6 +2175,8 @@
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
||||
|
||||
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||
|
||||
"turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
|
||||
@@ -2332,6 +2351,16 @@
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@actions/github/@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="],
|
||||
|
||||
"@actions/github/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@9.2.2", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ=="],
|
||||
|
||||
"@actions/github/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@10.4.1", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg=="],
|
||||
|
||||
"@actions/github/@octokit/request": ["@octokit/request@8.4.1", "", { "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw=="],
|
||||
|
||||
"@actions/github/@octokit/request-error": ["@octokit/request-error@5.1.1", "", { "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
|
||||
@@ -2604,6 +2633,28 @@
|
||||
|
||||
"yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
||||
|
||||
"@actions/github/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="],
|
||||
|
||||
"@actions/github/@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="],
|
||||
|
||||
"@actions/github/@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
"@actions/github/@octokit/core/before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="],
|
||||
|
||||
"@actions/github/@octokit/core/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
|
||||
|
||||
"@actions/github/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||
|
||||
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||
|
||||
"@actions/github/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="],
|
||||
|
||||
"@actions/github/@octokit/request/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
"@actions/github/@octokit/request/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
|
||||
|
||||
"@actions/github/@octokit/request-error/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
@@ -2764,6 +2815,16 @@
|
||||
|
||||
"wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
|
||||
|
||||
"@actions/github/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@actions/github/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
|
||||
|
||||
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
|
||||
|
||||
"@actions/github/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@actions/github/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# opencode GitHub Action
|
||||
|
||||
A GitHub Action that integrates [opencode](https://opencode.ai) directly into your GitHub workflow.
|
||||
|
||||
Start your comment with `hey opencode`, and opencode will take action via your GitHub Actions runner.
|
||||
|
||||
## Features
|
||||
|
||||
#### Triage and explain issues
|
||||
|
||||
```
|
||||
hey opencode, explain this issue
|
||||
```
|
||||
|
||||
#### Fix or implement issues - opencode will create a PR with the changes.
|
||||
|
||||
```
|
||||
hi opencode, fix this
|
||||
```
|
||||
|
||||
- Review PRs and make changes
|
||||
|
||||
```
|
||||
Delete the attachment from S3 when the note is removed @opencode-agent
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Run the following command in the terminal from your GitHub repo:
|
||||
|
||||
```
|
||||
opencode github install
|
||||
```
|
||||
|
||||
This will walk you through installing the GitHub app, configuring the workflow, and setting up secrets.
|
||||
|
||||
### Manual Setup
|
||||
|
||||
1. Install the GitHub app https://github.com/apps/opencode-agent. Make sure it is installed on the target repository.
|
||||
2. Add the following workflow file to `.github/workflows/opencode.yml` in your repo. Set the appropriate `model` and required API keys in `env`.
|
||||
|
||||
```yml
|
||||
name: opencode
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
opencode:
|
||||
if: |
|
||||
startsWith(github.event.comment.body, 'opencode') ||
|
||||
startsWith(github.event.comment.body, 'hi opencode') ||
|
||||
startsWith(github.event.comment.body, 'hey opencode') ||
|
||||
contains(github.event.comment.body, '@opencode-agent')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
```
|
||||
|
||||
3. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys.
|
||||
|
||||
## Support
|
||||
|
||||
This is an early release. If you encounter issues or have feedback, please create an issue at https://github.com/sst/opencode/issues.
|
||||
|
||||
## Development
|
||||
|
||||
To test locally:
|
||||
|
||||
1. Navigate to a test repo (e.g. `hello-world`):
|
||||
|
||||
```
|
||||
cd hello-world
|
||||
```
|
||||
|
||||
2. Run:
|
||||
|
||||
```
|
||||
MODEL=anthropic/claude-sonnet-4-20250514 \
|
||||
ANTHROPIC_API_KEY=sk-ant-api03-1234567890 \
|
||||
GITHUB_RUN_ID=dummy \
|
||||
bun /path/to/opencode/packages/opencode/src/index.ts github run \
|
||||
--token 'github_pat_1234567890' \
|
||||
--event '{"eventName":"issue_comment",...}'
|
||||
```
|
||||
|
||||
- `MODEL`: The model used by opencode. Same as the `MODEL` defined in the GitHub workflow.
|
||||
- `ANTHROPIC_API_KEY`: Your model provider API key. Same as the keys defined in the GitHub workflow.
|
||||
- `GITHUB_RUN_ID`: Dummy value to emulate GitHub action environment.
|
||||
- `/path/to/opencode`: Path to your cloned opencode repo. `bun /path/to/opencode/packages/opencode/src/index.ts` runs your local version of `opencode`.
|
||||
- `--token`: A GitHub persontal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens).
|
||||
- `--event`: Mock GitHub event payload (see templates below).
|
||||
|
||||
#### Issue comment event
|
||||
|
||||
```
|
||||
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
|
||||
```
|
||||
|
||||
Replace:
|
||||
|
||||
- `"owner":"sst"` with repo owner
|
||||
- `"repo":"hello-world"` with repo name
|
||||
- `"actor":"fwang"` with the GitHub username of commentor
|
||||
- `"number":4` with the GitHub issue id
|
||||
- `"body":"hey opencode, summarize thread"` with comment body
|
||||
|
||||
#### Issue comment with image attachment.
|
||||
|
||||
```
|
||||
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, what is in my image "}}}'
|
||||
```
|
||||
|
||||
Replace the image URL `https://github.com/user-attachments/assets/xxxxxxxx` with a valid GitHub attachment (you can generate one by commenting with an image in any issue).
|
||||
|
||||
#### PR comment event
|
||||
|
||||
```
|
||||
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4,"pull_request":{}},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
name: "opencode GitHub Action"
|
||||
description: "Run opencode in GitHub Actions workflows"
|
||||
branding:
|
||||
icon: "code"
|
||||
color: "orange"
|
||||
|
||||
inputs:
|
||||
model:
|
||||
description: "Model to use"
|
||||
required: false
|
||||
|
||||
share:
|
||||
description: "Share the opencode session (defaults to true for public repos)"
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install opencode
|
||||
shell: bash
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Run opencode
|
||||
shell: bash
|
||||
id: run_opencode
|
||||
run: opencode github run
|
||||
env:
|
||||
MODEL: ${{ inputs.model }}
|
||||
SHARE: ${{ inputs.share }}
|
||||
@@ -8,8 +8,8 @@ if [ -z "$latest_tag" ]; then
|
||||
fi
|
||||
echo "Latest tag: $latest_tag"
|
||||
|
||||
# Update github-v1 to latest
|
||||
git tag -d github-v1
|
||||
git push origin :refs/tags/github-v1
|
||||
git tag -a github-v1 $latest_tag -m "Update github-v1 to $latest_tag"
|
||||
git push origin github-v1
|
||||
# Update latest tag
|
||||
git tag -d latest
|
||||
git push origin :refs/tags/latest
|
||||
git tag -a latest $latest_tag -m "Update latest to $latest_tag"
|
||||
git push origin latest
|
||||
@@ -20,6 +20,10 @@
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"context7": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.context7.com/sse"
|
||||
},
|
||||
"weather": {
|
||||
"type": "local",
|
||||
"command": ["opencode", "x", "@h1deya/mcp-server-weather"]
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"devDependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "2.2.10",
|
||||
"@ai-sdk/anthropic": "1.2.12",
|
||||
"@octokit/webhooks-types": "7.6.1",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@tsconfig/bun": "1.0.7",
|
||||
"@types/bun": "latest",
|
||||
@@ -27,6 +28,8 @@
|
||||
"zod-to-json-schema": "3.24.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@clack/prompts": "0.11.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@modelcontextprotocol/sdk": "1.15.1",
|
||||
|
||||
@@ -44,7 +44,10 @@ export namespace Agent {
|
||||
item = result[key] = {
|
||||
name: key,
|
||||
description: "",
|
||||
tools: {},
|
||||
tools: {
|
||||
todowrite: false,
|
||||
todoread: false,
|
||||
},
|
||||
}
|
||||
const model = value.model ?? cfg.model
|
||||
if (model) item.model = Provider.parseModel(model)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,221 +0,0 @@
|
||||
import { $ } from "bun"
|
||||
import path from "path"
|
||||
import { exec } from "child_process"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { map, pipe, sortBy, values } from "remeda"
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import { ModelsDev } from "../../provider/models"
|
||||
import { App } from "../../app/app"
|
||||
|
||||
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
|
||||
|
||||
export const InstallGithubCommand = cmd({
|
||||
command: "install-github",
|
||||
describe: "install the GitHub agent",
|
||||
async handler() {
|
||||
await App.provide({ cwd: process.cwd() }, async () => {
|
||||
UI.empty()
|
||||
prompts.intro("Install GitHub agent")
|
||||
const app = await getAppInfo()
|
||||
await installGitHubApp()
|
||||
|
||||
const providers = await ModelsDev.get()
|
||||
const provider = await promptProvider()
|
||||
const model = await promptModel()
|
||||
//const key = await promptKey()
|
||||
|
||||
await addWorkflowFiles()
|
||||
printNextSteps()
|
||||
|
||||
function printNextSteps() {
|
||||
let step2
|
||||
if (provider === "amazon-bedrock") {
|
||||
step2 =
|
||||
"Configure OIDC in AWS - https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services"
|
||||
} else {
|
||||
const url = `https://github.com/organizations/${app.owner}/settings/secrets/actions`
|
||||
const env = providers[provider].env
|
||||
const envStr =
|
||||
env.length === 1
|
||||
? `\`${env[0]}\` secret`
|
||||
: `\`${[env.slice(0, -1).join("\`, \`"), ...env.slice(-1)].join("\` and \`")}\` secrets`
|
||||
step2 = `Add ${envStr} for ${providers[provider].name} - ${url}`
|
||||
}
|
||||
|
||||
prompts.outro(
|
||||
[
|
||||
"Next steps:",
|
||||
` 1. Commit "${WORKFLOW_FILE}" file and push`,
|
||||
` 2. ${step2}`,
|
||||
" 3. Learn how to use the GitHub agent - https://docs.opencode.ai/docs/github/getting-started",
|
||||
].join("\n"),
|
||||
)
|
||||
}
|
||||
|
||||
async function getAppInfo() {
|
||||
const app = App.info()
|
||||
if (!app.git) {
|
||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||
throw new UI.CancelledError()
|
||||
}
|
||||
|
||||
// Get repo info
|
||||
const info = await $`git remote get-url origin`.quiet().nothrow().text()
|
||||
// match https or git pattern
|
||||
// ie. https://github.com/sst/opencode.git
|
||||
// ie. git@github.com:sst/opencode.git
|
||||
const parsed = info.match(/git@github\.com:(.*)\.git/) ?? info.match(/github\.com\/(.*)\.git/)
|
||||
if (!parsed) {
|
||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||
throw new UI.CancelledError()
|
||||
}
|
||||
const [owner, repo] = parsed[1].split("/")
|
||||
return { owner, repo, root: app.path.root }
|
||||
}
|
||||
|
||||
async function promptProvider() {
|
||||
const priority: Record<string, number> = {
|
||||
anthropic: 0,
|
||||
"github-copilot": 1,
|
||||
openai: 2,
|
||||
google: 3,
|
||||
}
|
||||
let provider = await prompts.select({
|
||||
message: "Select provider",
|
||||
maxItems: 8,
|
||||
options: pipe(
|
||||
providers,
|
||||
values(),
|
||||
sortBy(
|
||||
(x) => priority[x.id] ?? 99,
|
||||
(x) => x.name ?? x.id,
|
||||
),
|
||||
map((x) => ({
|
||||
label: x.name,
|
||||
value: x.id,
|
||||
hint: priority[x.id] === 0 ? "recommended" : undefined,
|
||||
})),
|
||||
),
|
||||
})
|
||||
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
async function promptModel() {
|
||||
const providerData = providers[provider]!
|
||||
|
||||
const model = await prompts.select({
|
||||
message: "Select model",
|
||||
maxItems: 8,
|
||||
options: pipe(
|
||||
providerData.models,
|
||||
values(),
|
||||
sortBy((x) => x.name ?? x.id),
|
||||
map((x) => ({
|
||||
label: x.name ?? x.id,
|
||||
value: x.id,
|
||||
})),
|
||||
),
|
||||
})
|
||||
|
||||
if (prompts.isCancel(model)) throw new UI.CancelledError()
|
||||
return model
|
||||
}
|
||||
|
||||
async function installGitHubApp() {
|
||||
const s = prompts.spinner()
|
||||
s.start("Installing GitHub app")
|
||||
|
||||
// Get installation
|
||||
const installation = await getInstallation()
|
||||
if (installation) return s.stop("GitHub app already installed")
|
||||
|
||||
// Open browser
|
||||
const url = "https://github.com/apps/opencode-agent"
|
||||
const command =
|
||||
process.platform === "darwin"
|
||||
? `open "${url}"`
|
||||
: process.platform === "win32"
|
||||
? `start "${url}"`
|
||||
: `xdg-open "${url}"`
|
||||
|
||||
exec(command, (error) => {
|
||||
if (error) {
|
||||
prompts.log.warn(`Could not open browser. Please visit: ${url}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Wait for installation
|
||||
s.message("Waiting for GitHub app to be installed")
|
||||
const MAX_RETRIES = 60
|
||||
let retries = 0
|
||||
do {
|
||||
const installation = await getInstallation()
|
||||
if (installation) break
|
||||
|
||||
if (retries > MAX_RETRIES) {
|
||||
s.stop(
|
||||
`Failed to detect GitHub app installation. Make sure to install the app for the \`${app.owner}/${app.repo}\` repository.`,
|
||||
)
|
||||
throw new UI.CancelledError()
|
||||
}
|
||||
|
||||
retries++
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
} while (true)
|
||||
|
||||
s.stop("Installed GitHub app")
|
||||
|
||||
async function getInstallation() {
|
||||
return await fetch(`https://api.opencode.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => data.installation)
|
||||
}
|
||||
}
|
||||
|
||||
async function addWorkflowFiles() {
|
||||
const envStr =
|
||||
provider === "amazon-bedrock"
|
||||
? ""
|
||||
: `\n env:${providers[provider].env.map((e) => `\n ${e}: \${{ secrets.${e} }}`).join("")}`
|
||||
|
||||
await Bun.write(
|
||||
path.join(app.root, WORKFLOW_FILE),
|
||||
`
|
||||
name: opencode
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
opencode:
|
||||
if: |
|
||||
startsWith(github.event.comment.body, 'opencode') ||
|
||||
startsWith(github.event.comment.body, 'hi opencode') ||
|
||||
startsWith(github.event.comment.body, 'hey opencode') ||
|
||||
contains(github.event.comment.body, '@opencode-agent')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/sdks/github@github-v1${envStr}
|
||||
with:
|
||||
model: ${provider}/${model}
|
||||
`.trim(),
|
||||
)
|
||||
|
||||
prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -99,6 +99,7 @@ export namespace Config {
|
||||
export const Mode = z
|
||||
.object({
|
||||
model: z.string().optional(),
|
||||
temperature: z.number().optional(),
|
||||
prompt: z.string().optional(),
|
||||
tools: z.record(z.string(), z.boolean()).optional(),
|
||||
disable: z.boolean().optional(),
|
||||
|
||||
@@ -17,7 +17,7 @@ import { TuiCommand } from "./cli/cmd/tui"
|
||||
import { DebugCommand } from "./cli/cmd/debug"
|
||||
import { StatsCommand } from "./cli/cmd/stats"
|
||||
import { McpCommand } from "./cli/cmd/mcp"
|
||||
import { InstallGithubCommand } from "./cli/cmd/install-github"
|
||||
import { GithubCommand } from "./cli/cmd/github"
|
||||
import { Trace } from "./trace"
|
||||
|
||||
Trace.init()
|
||||
@@ -78,7 +78,7 @@ const cli = yargs(hideBin(process.argv))
|
||||
.command(ServeCommand)
|
||||
.command(ModelsCommand)
|
||||
.command(StatsCommand)
|
||||
.command(InstallGithubCommand)
|
||||
.command(GithubCommand)
|
||||
.fail((msg) => {
|
||||
if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {
|
||||
cli.showHelp("log")
|
||||
|
||||
@@ -322,4 +322,43 @@ export namespace LSPServer {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const CSharp: Info = {
|
||||
id: "csharp",
|
||||
root: NearestRoot([".sln", ".csproj", "global.json"]),
|
||||
extensions: [".cs"],
|
||||
async spawn(_, root) {
|
||||
let bin = Bun.which("csharp-ls", {
|
||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||
})
|
||||
if (!bin) {
|
||||
if (!Bun.which("dotnet")) {
|
||||
log.error(".NET SDK is required to install csharp-ls")
|
||||
return
|
||||
}
|
||||
|
||||
log.info("installing csharp-ls via dotnet tool")
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin],
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
stdin: "pipe",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0) {
|
||||
log.error("Failed to install csharp-ls")
|
||||
return
|
||||
}
|
||||
|
||||
bin = path.join(Global.Path.bin, "csharp-ls" + (process.platform === "win32" ? ".exe" : ""))
|
||||
log.info(`installed csharp-ls`, { bin })
|
||||
}
|
||||
|
||||
return {
|
||||
process: spawn(bin, {
|
||||
cwd: root,
|
||||
}),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { experimental_createMCPClient, type Tool } from "ai"
|
||||
import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
||||
import { App } from "../app/app"
|
||||
import { Config } from "../config/config"
|
||||
import { Log } from "../util/log"
|
||||
@@ -32,15 +34,28 @@ export namespace MCP {
|
||||
}
|
||||
log.info("found", { key, type: mcp.type })
|
||||
if (mcp.type === "remote") {
|
||||
const client = await experimental_createMCPClient({
|
||||
name: key,
|
||||
transport: {
|
||||
type: "sse",
|
||||
url: mcp.url,
|
||||
headers: mcp.headers,
|
||||
},
|
||||
}).catch(() => {})
|
||||
if (!client) {
|
||||
const transports = [
|
||||
new StreamableHTTPClientTransport(new URL(mcp.url), {
|
||||
requestInit: {
|
||||
headers: mcp.headers,
|
||||
},
|
||||
}),
|
||||
new SSEClientTransport(new URL(mcp.url), {
|
||||
requestInit: {
|
||||
headers: mcp.headers,
|
||||
},
|
||||
}),
|
||||
]
|
||||
for (const transport of transports) {
|
||||
const client = await experimental_createMCPClient({
|
||||
name: key,
|
||||
transport,
|
||||
}).catch(() => {})
|
||||
if (!client) continue
|
||||
clients[key] = client
|
||||
break
|
||||
}
|
||||
if (!clients[key])
|
||||
Bus.publish(Session.Event.Error, {
|
||||
error: {
|
||||
name: "UnknownError",
|
||||
@@ -49,16 +64,13 @@ export namespace MCP {
|
||||
},
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
clients[key] = client
|
||||
}
|
||||
|
||||
if (mcp.type === "local") {
|
||||
const [cmd, ...args] = mcp.command
|
||||
const client = await experimental_createMCPClient({
|
||||
name: key,
|
||||
transport: new Experimental_StdioMCPTransport({
|
||||
transport: new StdioClientTransport({
|
||||
stderr: "ignore",
|
||||
command: cmd,
|
||||
args,
|
||||
|
||||
@@ -5,22 +5,11 @@ import { mergeDeep, sortBy } from "remeda"
|
||||
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
||||
import { Log } from "../util/log"
|
||||
import { BunProc } from "../bun"
|
||||
import { BashTool } from "../tool/bash"
|
||||
import { EditTool } from "../tool/edit"
|
||||
import { WebFetchTool } from "../tool/webfetch"
|
||||
import { GlobTool } from "../tool/glob"
|
||||
import { GrepTool } from "../tool/grep"
|
||||
import { ListTool } from "../tool/ls"
|
||||
import { PatchTool } from "../tool/patch"
|
||||
import { ReadTool } from "../tool/read"
|
||||
import { WriteTool } from "../tool/write"
|
||||
import { TodoReadTool, TodoWriteTool } from "../tool/todo"
|
||||
import { AuthAnthropic } from "../auth/anthropic"
|
||||
import { AuthCopilot } from "../auth/copilot"
|
||||
import { ModelsDev } from "./models"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Auth } from "../auth"
|
||||
import { TaskTool } from "../tool/task"
|
||||
|
||||
export namespace Provider {
|
||||
const log = Log.create({ service: "provider" })
|
||||
@@ -468,137 +457,6 @@ export namespace Provider {
|
||||
}
|
||||
}
|
||||
|
||||
const TOOLS = [
|
||||
BashTool,
|
||||
EditTool,
|
||||
WebFetchTool,
|
||||
GlobTool,
|
||||
GrepTool,
|
||||
ListTool,
|
||||
// LspDiagnosticTool,
|
||||
// LspHoverTool,
|
||||
PatchTool,
|
||||
ReadTool,
|
||||
// MultiEditTool,
|
||||
WriteTool,
|
||||
TodoWriteTool,
|
||||
TodoReadTool,
|
||||
TaskTool,
|
||||
]
|
||||
|
||||
export async function tools(providerID: string) {
|
||||
const result = await Promise.all(TOOLS.map((t) => t()))
|
||||
switch (providerID) {
|
||||
case "anthropic":
|
||||
return result.filter((t) => t.id !== "patch")
|
||||
case "openai":
|
||||
return result.map((t) => ({
|
||||
...t,
|
||||
parameters: optionalToNullable(t.parameters),
|
||||
}))
|
||||
case "azure":
|
||||
return result.map((t) => ({
|
||||
...t,
|
||||
parameters: optionalToNullable(t.parameters),
|
||||
}))
|
||||
case "google":
|
||||
return result.map((t) => ({
|
||||
...t,
|
||||
parameters: sanitizeGeminiParameters(t.parameters),
|
||||
}))
|
||||
default:
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeGeminiParameters(schema: z.ZodTypeAny, visited = new Set()): z.ZodTypeAny {
|
||||
if (!schema || visited.has(schema)) {
|
||||
return schema
|
||||
}
|
||||
visited.add(schema)
|
||||
|
||||
if (schema instanceof z.ZodDefault) {
|
||||
const innerSchema = schema.removeDefault()
|
||||
// Handle Gemini's incompatibility with `default` on `anyOf` (unions).
|
||||
if (innerSchema instanceof z.ZodUnion) {
|
||||
// The schema was `z.union(...).default(...)`, which is not allowed.
|
||||
// We strip the default and return the sanitized union.
|
||||
return sanitizeGeminiParameters(innerSchema, visited)
|
||||
}
|
||||
// Otherwise, the default is on a regular type, which is allowed.
|
||||
// We recurse on the inner type and then re-apply the default.
|
||||
return sanitizeGeminiParameters(innerSchema, visited).default(schema._def.defaultValue())
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodOptional) {
|
||||
return z.optional(sanitizeGeminiParameters(schema.unwrap(), visited))
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodObject) {
|
||||
const newShape: Record<string, z.ZodTypeAny> = {}
|
||||
for (const [key, value] of Object.entries(schema.shape)) {
|
||||
newShape[key] = sanitizeGeminiParameters(value as z.ZodTypeAny, visited)
|
||||
}
|
||||
return z.object(newShape)
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodArray) {
|
||||
return z.array(sanitizeGeminiParameters(schema.element, visited))
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodUnion) {
|
||||
// This schema corresponds to `anyOf` in JSON Schema.
|
||||
// We recursively sanitize each option in the union.
|
||||
const sanitizedOptions = schema.options.map((option: z.ZodTypeAny) => sanitizeGeminiParameters(option, visited))
|
||||
return z.union(sanitizedOptions as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]])
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodString) {
|
||||
const newSchema = z.string({ description: schema.description })
|
||||
const safeChecks = ["min", "max", "length", "regex", "startsWith", "endsWith", "includes", "trim"]
|
||||
// rome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
;(newSchema._def as any).checks = (schema._def as z.ZodStringDef).checks.filter((check) =>
|
||||
safeChecks.includes(check.kind),
|
||||
)
|
||||
return newSchema
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
|
||||
if (schema instanceof z.ZodObject) {
|
||||
const shape = schema.shape
|
||||
const newShape: Record<string, z.ZodTypeAny> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(shape)) {
|
||||
const zodValue = value as z.ZodTypeAny
|
||||
if (zodValue instanceof z.ZodOptional) {
|
||||
newShape[key] = zodValue.unwrap().nullable()
|
||||
} else {
|
||||
newShape[key] = optionalToNullable(zodValue)
|
||||
}
|
||||
}
|
||||
|
||||
return z.object(newShape)
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodArray) {
|
||||
return z.array(optionalToNullable(schema.element))
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodUnion) {
|
||||
return z.union(
|
||||
schema.options.map((option: z.ZodTypeAny) => optionalToNullable(option)) as [
|
||||
z.ZodTypeAny,
|
||||
z.ZodTypeAny,
|
||||
...z.ZodTypeAny[],
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
export const ModelNotFoundError = NamedError.create(
|
||||
"ProviderModelNotFoundError",
|
||||
z.object({
|
||||
|
||||
@@ -7,24 +7,47 @@ export namespace ProviderTransform {
|
||||
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
||||
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
||||
|
||||
const providerOptions = {
|
||||
anthropic: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
openrouter: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
bedrock: {
|
||||
cachePoint: { type: "ephemeral" },
|
||||
},
|
||||
openaiCompatible: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
}
|
||||
|
||||
for (const msg of unique([...system, ...final])) {
|
||||
const shouldUseContentOptions =
|
||||
providerID !== "anthropic" && Array.isArray(msg.content) && msg.content.length > 0
|
||||
|
||||
if (shouldUseContentOptions) {
|
||||
const lastContent = msg.content[msg.content.length - 1]
|
||||
if (lastContent && typeof lastContent === "object") {
|
||||
lastContent.providerOptions = {
|
||||
...lastContent.providerOptions,
|
||||
...providerOptions,
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
msg.providerOptions = {
|
||||
...msg.providerOptions,
|
||||
anthropic: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
openrouter: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
bedrock: {
|
||||
cachePoint: { type: "ephemeral" },
|
||||
},
|
||||
openaiCompatible: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
...providerOptions,
|
||||
}
|
||||
}
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
export function temperature(_providerID: string, modelID: string) {
|
||||
if (modelID.toLowerCase().includes("qwen")) return 0.55
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ import { MessageV2 } from "./message-v2"
|
||||
import { Mode } from "./mode"
|
||||
import { LSP } from "../lsp"
|
||||
import { ReadTool } from "../tool/read"
|
||||
import { splitWhen } from "remeda"
|
||||
import { mergeDeep, pipe, splitWhen } from "remeda"
|
||||
import { ToolRegistry } from "../tool/registry"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -387,6 +388,34 @@ export namespace Session {
|
||||
if (part.type === "file") {
|
||||
const url = new URL(part.url)
|
||||
switch (url.protocol) {
|
||||
case "data:":
|
||||
if (part.mime === "text/plain") {
|
||||
return [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
type: "text",
|
||||
synthetic: true,
|
||||
text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: part.filename })}`,
|
||||
},
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
type: "text",
|
||||
synthetic: true,
|
||||
text: Buffer.from(part.url, "base64url").toString(),
|
||||
},
|
||||
{
|
||||
...part,
|
||||
id: part.id ?? Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
},
|
||||
]
|
||||
}
|
||||
break
|
||||
case "file:":
|
||||
// have to normalize, symbol search returns absolute paths
|
||||
// Decode the pathname since URL constructor doesn't automatically decode it
|
||||
@@ -430,7 +459,7 @@ export namespace Session {
|
||||
}
|
||||
}
|
||||
const args = { filePath, offset, limit }
|
||||
const result = await ReadTool().then((t) =>
|
||||
const result = await ReadTool.init().then((t) =>
|
||||
t.execute(args, {
|
||||
sessionID: input.sessionID,
|
||||
abort: new AbortController().signal,
|
||||
@@ -626,7 +655,6 @@ export namespace Session {
|
||||
return SystemPrompt.provider(input.modelID)
|
||||
})(),
|
||||
)
|
||||
system.push(...(mode.prompt ? [mode.prompt] : SystemPrompt.provider(input.modelID)))
|
||||
system.push(...(await SystemPrompt.environment()))
|
||||
system.push(...(await SystemPrompt.custom()))
|
||||
// max 2 system prompt messages for caching purposes
|
||||
@@ -661,10 +689,13 @@ export namespace Session {
|
||||
|
||||
const processor = createProcessor(assistantMsg, model.info)
|
||||
|
||||
for (const item of await Provider.tools(input.providerID)) {
|
||||
if (mode.tools[item.id] === false) continue
|
||||
if (input.tools?.[item.id] === false) continue
|
||||
if (session.parentID && item.id === "task") continue
|
||||
const enabledTools = pipe(
|
||||
mode.tools,
|
||||
mergeDeep(ToolRegistry.enabled(input.providerID, input.modelID)),
|
||||
mergeDeep(input.tools ?? {}),
|
||||
)
|
||||
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
|
||||
if (enabledTools[item.id] === false) continue
|
||||
tools[item.id] = tool({
|
||||
id: item.id as any,
|
||||
description: item.description,
|
||||
@@ -765,6 +796,7 @@ export namespace Session {
|
||||
},
|
||||
modelID: input.modelID,
|
||||
providerID: input.providerID,
|
||||
mode: inputMode,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
@@ -792,7 +824,9 @@ export namespace Session {
|
||||
),
|
||||
...MessageV2.toModelMessage(msgs),
|
||||
],
|
||||
temperature: model.info.temperature ? 0 : undefined,
|
||||
temperature: model.info.temperature
|
||||
? (mode.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID))
|
||||
: undefined,
|
||||
tools: model.info.tool_call === false ? undefined : tools,
|
||||
model: wrapLanguageModel({
|
||||
model: model.language,
|
||||
@@ -1005,6 +1039,7 @@ export namespace Session {
|
||||
start: Date.now(),
|
||||
end: Date.now(),
|
||||
}
|
||||
currentText.text = currentText.text.trimEnd()
|
||||
await updatePart(currentText)
|
||||
}
|
||||
currentText = undefined
|
||||
|
||||
@@ -7,6 +7,7 @@ export namespace Mode {
|
||||
export const Info = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
temperature: z.number().optional(),
|
||||
model: z
|
||||
.object({
|
||||
modelID: z.string(),
|
||||
@@ -22,13 +23,16 @@ export namespace Mode {
|
||||
export type Info = z.infer<typeof Info>
|
||||
const state = App.state("mode", async () => {
|
||||
const cfg = await Config.get()
|
||||
const model = cfg.model ? Provider.parseModel(cfg.model) : undefined
|
||||
const result: Record<string, Info> = {
|
||||
build: {
|
||||
model,
|
||||
name: "build",
|
||||
tools: {},
|
||||
},
|
||||
plan: {
|
||||
name: "plan",
|
||||
model,
|
||||
tools: {
|
||||
write: false,
|
||||
edit: false,
|
||||
@@ -45,11 +49,9 @@ export namespace Mode {
|
||||
tools: {},
|
||||
}
|
||||
item.name = key
|
||||
const model = value.model ?? cfg.model
|
||||
if (model) {
|
||||
item.model = Provider.parseModel(model)
|
||||
}
|
||||
if (value.model) item.model = Provider.parseModel(value.model)
|
||||
if (value.prompt) item.prompt = value.prompt
|
||||
if (value.temperature) item.temperature = value.temperature
|
||||
if (value.tools)
|
||||
item.tools = {
|
||||
...value.tools,
|
||||
|
||||
@@ -7,8 +7,7 @@ const MAX_OUTPUT_LENGTH = 30000
|
||||
const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
||||
const MAX_TIMEOUT = 10 * 60 * 1000
|
||||
|
||||
export const BashTool = Tool.define({
|
||||
id: "bash",
|
||||
export const BashTool = Tool.define("bash", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
command: z.string().describe("The command to execute"),
|
||||
|
||||
@@ -14,8 +14,7 @@ import { File } from "../file"
|
||||
import { Bus } from "../bus"
|
||||
import { FileTime } from "../file/time"
|
||||
|
||||
export const EditTool = Tool.define({
|
||||
id: "edit",
|
||||
export const EditTool = Tool.define("edit", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||
|
||||
@@ -5,8 +5,7 @@ import { App } from "../app/app"
|
||||
import DESCRIPTION from "./glob.txt"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
|
||||
export const GlobTool = Tool.define({
|
||||
id: "glob",
|
||||
export const GlobTool = Tool.define("glob", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
pattern: z.string().describe("The glob pattern to match files against"),
|
||||
|
||||
@@ -5,8 +5,7 @@ import { Ripgrep } from "../file/ripgrep"
|
||||
|
||||
import DESCRIPTION from "./grep.txt"
|
||||
|
||||
export const GrepTool = Tool.define({
|
||||
id: "grep",
|
||||
export const GrepTool = Tool.define("grep", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
pattern: z.string().describe("The regex pattern to search for in file contents"),
|
||||
|
||||
@@ -33,8 +33,7 @@ export const IGNORE_PATTERNS = [
|
||||
|
||||
const LIMIT = 100
|
||||
|
||||
export const ListTool = Tool.define({
|
||||
id: "list",
|
||||
export const ListTool = Tool.define("list", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(),
|
||||
|
||||
@@ -5,8 +5,7 @@ import { LSP } from "../lsp"
|
||||
import { App } from "../app/app"
|
||||
import DESCRIPTION from "./lsp-diagnostics.txt"
|
||||
|
||||
export const LspDiagnosticTool = Tool.define({
|
||||
id: "lsp_diagnostics",
|
||||
export const LspDiagnosticTool = Tool.define("lsp_diagnostics", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
path: z.string().describe("The path to the file to get diagnostics."),
|
||||
|
||||
@@ -5,8 +5,7 @@ import { LSP } from "../lsp"
|
||||
import { App } from "../app/app"
|
||||
import DESCRIPTION from "./lsp-hover.txt"
|
||||
|
||||
export const LspHoverTool = Tool.define({
|
||||
id: "lsp_hover",
|
||||
export const LspHoverTool = Tool.define("lsp_hover", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
file: z.string().describe("The path to the file to get diagnostics."),
|
||||
|
||||
@@ -5,8 +5,7 @@ import DESCRIPTION from "./multiedit.txt"
|
||||
import path from "path"
|
||||
import { App } from "../app/app"
|
||||
|
||||
export const MultiEditTool = Tool.define({
|
||||
id: "multiedit",
|
||||
export const MultiEditTool = Tool.define("multiedit", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||
@@ -22,7 +21,7 @@ export const MultiEditTool = Tool.define({
|
||||
.describe("Array of edit operations to perform sequentially on the file"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const tool = await EditTool()
|
||||
const tool = await EditTool.init()
|
||||
const results = []
|
||||
for (const [, edit] of params.edits.entries()) {
|
||||
const result = await tool.execute(
|
||||
|
||||
@@ -210,8 +210,7 @@ async function applyCommit(
|
||||
}
|
||||
}
|
||||
|
||||
export const PatchTool = Tool.define({
|
||||
id: "patch",
|
||||
export const PatchTool = Tool.define("patch", {
|
||||
description: DESCRIPTION,
|
||||
parameters: PatchParams,
|
||||
execute: async (params, ctx) => {
|
||||
|
||||
@@ -10,8 +10,7 @@ import { App } from "../app/app"
|
||||
const DEFAULT_READ_LIMIT = 2000
|
||||
const MAX_LINE_LENGTH = 2000
|
||||
|
||||
export const ReadTool = Tool.define({
|
||||
id: "read",
|
||||
export const ReadTool = Tool.define("read", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The path to the file to read"),
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import z from "zod"
|
||||
import { BashTool } from "./bash"
|
||||
import { EditTool } from "./edit"
|
||||
import { GlobTool } from "./glob"
|
||||
import { GrepTool } from "./grep"
|
||||
import { ListTool } from "./ls"
|
||||
import { PatchTool } from "./patch"
|
||||
import { ReadTool } from "./read"
|
||||
import { TaskTool } from "./task"
|
||||
import { TodoWriteTool, TodoReadTool } from "./todo"
|
||||
import { WebFetchTool } from "./webfetch"
|
||||
import { WriteTool } from "./write"
|
||||
|
||||
export namespace ToolRegistry {
|
||||
const ALL = [
|
||||
BashTool,
|
||||
EditTool,
|
||||
WebFetchTool,
|
||||
GlobTool,
|
||||
GrepTool,
|
||||
ListTool,
|
||||
PatchTool,
|
||||
ReadTool,
|
||||
WriteTool,
|
||||
TodoWriteTool,
|
||||
TodoReadTool,
|
||||
TaskTool,
|
||||
]
|
||||
|
||||
export function ids() {
|
||||
return ALL.map((t) => t.id)
|
||||
}
|
||||
|
||||
export async function tools(providerID: string, _modelID: string) {
|
||||
const result = await Promise.all(
|
||||
ALL.map(async (t) => ({
|
||||
id: t.id,
|
||||
...(await t.init()),
|
||||
})),
|
||||
)
|
||||
|
||||
if (providerID === "openai") {
|
||||
return result.map((t) => ({
|
||||
...t,
|
||||
parameters: optionalToNullable(t.parameters),
|
||||
}))
|
||||
}
|
||||
|
||||
if (providerID === "azure") {
|
||||
return result.map((t) => ({
|
||||
...t,
|
||||
parameters: optionalToNullable(t.parameters),
|
||||
}))
|
||||
}
|
||||
|
||||
if (providerID === "google") {
|
||||
return result.map((t) => ({
|
||||
...t,
|
||||
parameters: sanitizeGeminiParameters(t.parameters),
|
||||
}))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function enabled(_providerID: string, modelID: string): Record<string, boolean> {
|
||||
if (modelID.toLowerCase().includes("claude")) {
|
||||
return {
|
||||
patch: false,
|
||||
}
|
||||
}
|
||||
if (modelID.toLowerCase().includes("qwen")) {
|
||||
return {
|
||||
patch: false,
|
||||
todowrite: false,
|
||||
todoread: false,
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
function sanitizeGeminiParameters(schema: z.ZodTypeAny, visited = new Set()): z.ZodTypeAny {
|
||||
if (!schema || visited.has(schema)) {
|
||||
return schema
|
||||
}
|
||||
visited.add(schema)
|
||||
|
||||
if (schema instanceof z.ZodDefault) {
|
||||
const innerSchema = schema.removeDefault()
|
||||
// Handle Gemini's incompatibility with `default` on `anyOf` (unions).
|
||||
if (innerSchema instanceof z.ZodUnion) {
|
||||
// The schema was `z.union(...).default(...)`, which is not allowed.
|
||||
// We strip the default and return the sanitized union.
|
||||
return sanitizeGeminiParameters(innerSchema, visited)
|
||||
}
|
||||
// Otherwise, the default is on a regular type, which is allowed.
|
||||
// We recurse on the inner type and then re-apply the default.
|
||||
return sanitizeGeminiParameters(innerSchema, visited).default(schema._def.defaultValue())
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodOptional) {
|
||||
return z.optional(sanitizeGeminiParameters(schema.unwrap(), visited))
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodObject) {
|
||||
const newShape: Record<string, z.ZodTypeAny> = {}
|
||||
for (const [key, value] of Object.entries(schema.shape)) {
|
||||
newShape[key] = sanitizeGeminiParameters(value as z.ZodTypeAny, visited)
|
||||
}
|
||||
return z.object(newShape)
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodArray) {
|
||||
return z.array(sanitizeGeminiParameters(schema.element, visited))
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodUnion) {
|
||||
// This schema corresponds to `anyOf` in JSON Schema.
|
||||
// We recursively sanitize each option in the union.
|
||||
const sanitizedOptions = schema.options.map((option: z.ZodTypeAny) => sanitizeGeminiParameters(option, visited))
|
||||
return z.union(sanitizedOptions as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]])
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodString) {
|
||||
const newSchema = z.string({ description: schema.description })
|
||||
const safeChecks = ["min", "max", "length", "regex", "startsWith", "endsWith", "includes", "trim"]
|
||||
// rome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
;(newSchema._def as any).checks = (schema._def as z.ZodStringDef).checks.filter((check) =>
|
||||
safeChecks.includes(check.kind),
|
||||
)
|
||||
return newSchema
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
|
||||
if (schema instanceof z.ZodObject) {
|
||||
const shape = schema.shape
|
||||
const newShape: Record<string, z.ZodTypeAny> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(shape)) {
|
||||
const zodValue = value as z.ZodTypeAny
|
||||
if (zodValue instanceof z.ZodOptional) {
|
||||
newShape[key] = zodValue.unwrap().nullable()
|
||||
} else {
|
||||
newShape[key] = optionalToNullable(zodValue)
|
||||
}
|
||||
}
|
||||
|
||||
return z.object(newShape)
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodArray) {
|
||||
return z.array(optionalToNullable(schema.element))
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodUnion) {
|
||||
return z.union(
|
||||
schema.options.map((option: z.ZodTypeAny) => optionalToNullable(option)) as [
|
||||
z.ZodTypeAny,
|
||||
z.ZodTypeAny,
|
||||
...z.ZodTypeAny[],
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,10 @@ import { MessageV2 } from "../session/message-v2"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Agent } from "../agent/agent"
|
||||
|
||||
export const TaskTool = Tool.define(async () => {
|
||||
export const TaskTool = Tool.define("task", async () => {
|
||||
const agents = await Agent.list()
|
||||
const description = DESCRIPTION.replace("{agents}", agents.map((a) => `- ${a.name}: ${a.description}`).join("\n"))
|
||||
return {
|
||||
id: "task",
|
||||
description,
|
||||
parameters: z.object({
|
||||
description: z.string().describe("A short (3-5 words) description of the task"),
|
||||
@@ -53,7 +52,10 @@ export const TaskTool = Tool.define(async () => {
|
||||
providerID: model.providerID,
|
||||
mode: msg.mode,
|
||||
system: agent.prompt,
|
||||
tools: agent.tools,
|
||||
tools: {
|
||||
...agent.tools,
|
||||
task: false,
|
||||
},
|
||||
parts: [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
|
||||
@@ -18,8 +18,7 @@ const state = App.state("todo-tool", () => {
|
||||
return todos
|
||||
})
|
||||
|
||||
export const TodoWriteTool = Tool.define({
|
||||
id: "todowrite",
|
||||
export const TodoWriteTool = Tool.define("todowrite", {
|
||||
description: DESCRIPTION_WRITE,
|
||||
parameters: z.object({
|
||||
todos: z.array(TodoInfo).describe("The updated todo list"),
|
||||
@@ -37,8 +36,7 @@ export const TodoWriteTool = Tool.define({
|
||||
},
|
||||
})
|
||||
|
||||
export const TodoReadTool = Tool.define({
|
||||
id: "todoread",
|
||||
export const TodoReadTool = Tool.define("todoread", {
|
||||
description: "Use this tool to read your todo list",
|
||||
parameters: z.object({}),
|
||||
async execute(_params, opts) {
|
||||
|
||||
@@ -12,21 +12,30 @@ export namespace Tool {
|
||||
}
|
||||
export interface Info<Parameters extends StandardSchemaV1 = StandardSchemaV1, M extends Metadata = Metadata> {
|
||||
id: string
|
||||
description: string
|
||||
parameters: Parameters
|
||||
execute(
|
||||
args: StandardSchemaV1.InferOutput<Parameters>,
|
||||
ctx: Context,
|
||||
): Promise<{
|
||||
title: string
|
||||
metadata: M
|
||||
output: string
|
||||
init: () => Promise<{
|
||||
description: string
|
||||
parameters: Parameters
|
||||
execute(
|
||||
args: StandardSchemaV1.InferOutput<Parameters>,
|
||||
ctx: Context,
|
||||
): Promise<{
|
||||
title: string
|
||||
metadata: M
|
||||
output: string
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
export function define<Parameters extends StandardSchemaV1, Result extends Metadata>(
|
||||
input: Info<Parameters, Result> | (() => Promise<Info<Parameters, Result>>),
|
||||
): () => Promise<Info<Parameters, Result>> {
|
||||
return input instanceof Function ? input : async () => input
|
||||
id: string,
|
||||
init: Info<Parameters, Result>["init"] | Awaited<ReturnType<Info<Parameters, Result>["init"]>>,
|
||||
): Info<Parameters, Result> {
|
||||
return {
|
||||
id,
|
||||
init: async () => {
|
||||
if (init instanceof Function) return init()
|
||||
return init
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
|
||||
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
|
||||
const MAX_TIMEOUT = 120 * 1000 // 2 minutes
|
||||
|
||||
export const WebFetchTool = Tool.define({
|
||||
id: "webfetch",
|
||||
export const WebFetchTool = Tool.define("webfetch", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
url: z.string().describe("The URL to fetch content from"),
|
||||
|
||||
@@ -9,8 +9,7 @@ import { Bus } from "../bus"
|
||||
import { File } from "../file"
|
||||
import { FileTime } from "../file/time"
|
||||
|
||||
export const WriteTool = Tool.define({
|
||||
id: "write",
|
||||
export const WriteTool = Tool.define("write", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"),
|
||||
|
||||
@@ -9,8 +9,8 @@ const ctx = {
|
||||
abort: AbortSignal.any([]),
|
||||
metadata: () => {},
|
||||
}
|
||||
const glob = await GlobTool()
|
||||
const list = await ListTool()
|
||||
const glob = await GlobTool.init()
|
||||
const list = await ListTool.init()
|
||||
|
||||
describe("tool.glob", () => {
|
||||
test("truncate", async () => {
|
||||
|
||||
@@ -670,15 +670,21 @@ func (m *messagesComponent) renderHeader() string {
|
||||
isSubscriptionModel := m.app.Model != nil &&
|
||||
m.app.Model.Cost.Input == 0 && m.app.Model.Cost.Output == 0
|
||||
|
||||
sessionInfoText := formatTokensAndCost(tokens, contextWindow, cost, isSubscriptionModel)
|
||||
sessionInfo = styles.NewStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
Background(t.Background()).
|
||||
Render(formatTokensAndCost(tokens, contextWindow, cost, isSubscriptionModel))
|
||||
Render(sessionInfoText)
|
||||
|
||||
shareEnabled := m.app.Config.Share != opencode.ConfigShareDisabled
|
||||
headerTextWidth := headerWidth
|
||||
if !shareEnabled {
|
||||
// +1 is to ensure there is always at least one space between header and session info
|
||||
headerTextWidth -= len(sessionInfoText) + 1
|
||||
}
|
||||
headerText := util.ToMarkdown(
|
||||
"# "+m.app.Session.Title,
|
||||
headerWidth,
|
||||
headerTextWidth,
|
||||
t.Background(),
|
||||
)
|
||||
|
||||
@@ -705,11 +711,9 @@ func (m *messagesComponent) renderHeader() string {
|
||||
items...,
|
||||
)
|
||||
|
||||
var headerLines []string
|
||||
headerLines := []string{headerRow}
|
||||
if shareEnabled {
|
||||
headerLines = []string{headerText, headerRow}
|
||||
} else {
|
||||
headerLines = []string{headerRow}
|
||||
}
|
||||
|
||||
header := strings.Join(headerLines, "\n")
|
||||
|
||||
@@ -18,7 +18,7 @@ You can define MCP servers in your opencode config under `mcp`.
|
||||
|
||||
### Local
|
||||
|
||||
Add local MCP servers under `mcp` with `"type": "local"`.
|
||||
Add local MCP servers using `"type": "local"` within the MCP object. Multiple MCP servers can be added. The key string for each server can be any arbitrary name.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
@@ -31,7 +31,7 @@ Add local MCP servers under `mcp` with `"type": "local"`.
|
||||
"environment": {
|
||||
"MY_ENV_VAR": "my_env_var_value"
|
||||
}
|
||||
}, {
|
||||
},
|
||||
"my-different-local-mcp-server": {
|
||||
"type": "local",
|
||||
"command": ["bun", "x", "my-other-mcp-command"],
|
||||
@@ -62,3 +62,28 @@ Add remote MCP servers under `mcp` with `"type": "remote"`.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Local and remote servers can be used together within the same `mcp` config object.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"my-local-mcp-server": {
|
||||
"type": "local",
|
||||
"command": ["bun", "x", "my-mcp-command"],
|
||||
"enabled": true,
|
||||
"environment": {
|
||||
"MY_ENV_VAR": "my_env_var_value"
|
||||
}
|
||||
},
|
||||
"my-remote-mcp": {
|
||||
"type": "remote",
|
||||
"url": "https://my-mcp-server.com",
|
||||
"enabled": true,
|
||||
"headers": {
|
||||
"Authorization": "Bearer MY_API_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,51 @@ Use the `model` config to override the default model for this mode. Useful for u
|
||||
|
||||
---
|
||||
|
||||
### Temperature
|
||||
|
||||
Control the randomness and creativity of the AI's responses with the `temperature` config. Lower values make responses more focused and deterministic, while higher values increase creativity and variability.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"mode": {
|
||||
"plan": {
|
||||
"temperature": 0.1
|
||||
},
|
||||
"creative": {
|
||||
"temperature": 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Temperature values typically range from 0.0 to 1.0:
|
||||
|
||||
- **0.0-0.2**: Very focused and deterministic responses, ideal for code analysis and planning
|
||||
- **0.3-0.5**: Balanced responses with some creativity, good for general development tasks
|
||||
- **0.6-1.0**: More creative and varied responses, useful for brainstorming and exploration
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"mode": {
|
||||
"analyze": {
|
||||
"temperature": 0.1,
|
||||
"prompt": "{file:./prompts/analysis.txt}"
|
||||
},
|
||||
"build": {
|
||||
"temperature": 0.3
|
||||
},
|
||||
"brainstorm": {
|
||||
"temperature": 0.7,
|
||||
"prompt": "{file:./prompts/creative.txt}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If no temperature is specified, opencode uses model-specific defaults (typically 0 for most models, 0.55 for Qwen models).
|
||||
|
||||
---
|
||||
|
||||
### Prompt
|
||||
|
||||
Specify a custom system prompt file for this mode with the `prompt` config. The prompt file should contain instructions specific to the mode's purpose.
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
name: "opencode GitHub Action"
|
||||
description: "Run opencode in GitHub Actions workflows"
|
||||
branding:
|
||||
icon: "code"
|
||||
color: "orange"
|
||||
|
||||
inputs:
|
||||
model:
|
||||
description: "Model to use"
|
||||
required: false
|
||||
|
||||
share:
|
||||
description: "Share the opencode session (defaults to true for public repos)"
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
share_url:
|
||||
description: "URL to share the opencode execution"
|
||||
value: ${{ steps.run_opencode.outputs.share_url }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.16
|
||||
|
||||
- name: Install Dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
cd ${GITHUB_ACTION_PATH}
|
||||
bun install
|
||||
|
||||
- name: Install opencode
|
||||
shell: bash
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Run opencode
|
||||
shell: bash
|
||||
id: run_opencode
|
||||
run: |
|
||||
bun run ${GITHUB_ACTION_PATH}/src/index.ts
|
||||
env:
|
||||
INPUT_MODEL: ${{ inputs.model }}
|
||||
INPUT_SHARE: ${{ inputs.share }}
|
||||
|
||||
#- name: Testing
|
||||
# shell: bash
|
||||
# run: |
|
||||
# gh pr comment ${{ github.event.number }} --body "This is an automated comment"
|
||||
# env:
|
||||
# GH_TOKEN: ${{ github.token }}
|
||||
@@ -1,157 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "github",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@octokit/graphql": "^9.0.1",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/webhooks-types": "^7.6.1",
|
||||
"@types/bun": "latest",
|
||||
"@types/node": "^24.0.10",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@actions/core": ["@actions/core@1.11.1", "", { "dependencies": { "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1" } }, "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A=="],
|
||||
|
||||
"@actions/exec": ["@actions/exec@1.1.1", "", { "dependencies": { "@actions/io": "^1.0.1" } }, "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w=="],
|
||||
|
||||
"@actions/github": ["@actions/github@6.0.1", "", { "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", "@octokit/plugin-paginate-rest": "^9.2.2", "@octokit/plugin-rest-endpoint-methods": "^10.4.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "undici": "^5.28.5" } }, "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw=="],
|
||||
|
||||
"@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="],
|
||||
|
||||
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||
|
||||
"@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="],
|
||||
|
||||
"@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="],
|
||||
|
||||
"@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="],
|
||||
|
||||
"@octokit/graphql": ["@octokit/graphql@9.0.1", "", { "dependencies": { "@octokit/request": "^10.0.2", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg=="],
|
||||
|
||||
"@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@9.2.2", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ=="],
|
||||
|
||||
"@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@10.4.1", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg=="],
|
||||
|
||||
"@octokit/request": ["@octokit/request@8.4.1", "", { "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw=="],
|
||||
|
||||
"@octokit/request-error": ["@octokit/request-error@5.1.1", "", { "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g=="],
|
||||
|
||||
"@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
|
||||
|
||||
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||
|
||||
"@octokit/webhooks-types": ["@octokit/webhooks-types@7.6.1", "", {}, "sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
|
||||
|
||||
"@types/node": ["@types/node@24.0.13", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||
|
||||
"before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="],
|
||||
|
||||
"fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
||||
|
||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
|
||||
"universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="],
|
||||
|
||||
"@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
"@octokit/core/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
|
||||
|
||||
"@octokit/endpoint/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
"@octokit/endpoint/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
|
||||
|
||||
"@octokit/graphql/@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core": ["@octokit/core@7.0.3", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||
|
||||
"@octokit/request/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
"@octokit/request/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
|
||||
|
||||
"@octokit/request-error/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
"@octokit/rest/@octokit/core": ["@octokit/core@7.0.3", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ=="],
|
||||
|
||||
"@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.1.1", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw=="],
|
||||
|
||||
"@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.0.0", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g=="],
|
||||
|
||||
"@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@octokit/graphql/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ=="],
|
||||
|
||||
"@octokit/graphql/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
|
||||
|
||||
"@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ=="],
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "github",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@octokit/webhooks-types": "^7.6.1",
|
||||
"@types/bun": "latest",
|
||||
"@types/node": "^24.0.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@octokit/graphql": "^9.0.1",
|
||||
"@octokit/rest": "^22.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,541 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { $ } from "bun"
|
||||
import { Octokit } from "@octokit/rest"
|
||||
import { graphql } from "@octokit/graphql"
|
||||
import * as core from "@actions/core"
|
||||
import * as github from "@actions/github"
|
||||
import type { IssueCommentEvent } from "@octokit/webhooks-types"
|
||||
import type { GitHubIssue, GitHubPullRequest, IssueQueryResponse, PullRequestQueryResponse } from "./types"
|
||||
|
||||
if (github.context.eventName !== "issue_comment") {
|
||||
core.setFailed(`Unsupported event type: ${github.context.eventName}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const { owner, repo } = github.context.repo
|
||||
const payload = github.context.payload as IssueCommentEvent
|
||||
const actor = github.context.actor
|
||||
const issueId = payload.issue.number
|
||||
const body = payload.comment.body
|
||||
|
||||
let appToken: string
|
||||
let octoRest: Octokit
|
||||
let octoGraph: typeof graphql
|
||||
let commentId: number
|
||||
let gitCredentials: string
|
||||
let shareUrl: string | undefined
|
||||
let state:
|
||||
| {
|
||||
type: "issue"
|
||||
issue: GitHubIssue
|
||||
}
|
||||
| {
|
||||
type: "local-pr"
|
||||
pr: GitHubPullRequest
|
||||
}
|
||||
| {
|
||||
type: "fork-pr"
|
||||
pr: GitHubPullRequest
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const match = body.match(/^hey\s*opencode,/)
|
||||
if (!match?.[1]) throw new Error("Command must start with `hey opencode,`")
|
||||
const userPrompt = match[1]
|
||||
|
||||
const oidcToken = await generateGitHubToken()
|
||||
appToken = await exchangeForAppToken(oidcToken)
|
||||
octoRest = new Octokit({ auth: appToken })
|
||||
octoGraph = graphql.defaults({
|
||||
headers: { authorization: `token ${appToken}` },
|
||||
})
|
||||
|
||||
await configureGit(appToken)
|
||||
await assertPermissions()
|
||||
|
||||
const comment = await createComment("opencode started...")
|
||||
commentId = comment.data.id
|
||||
|
||||
// Set state
|
||||
const repoData = await fetchRepo()
|
||||
if (payload.issue.pull_request) {
|
||||
const prData = await fetchPR()
|
||||
state = {
|
||||
type: prData.headRepository.nameWithOwner === prData.baseRepository.nameWithOwner ? "local-pr" : "fork-pr",
|
||||
pr: prData,
|
||||
}
|
||||
} else {
|
||||
state = {
|
||||
type: "issue",
|
||||
issue: await fetchIssue(),
|
||||
}
|
||||
}
|
||||
|
||||
// Setup git branch
|
||||
if (state.type === "local-pr") await checkoutLocalBranch(state.pr)
|
||||
else if (state.type === "fork-pr") await checkoutForkBranch(state.pr)
|
||||
|
||||
// Prompt
|
||||
const share = process.env.INPUT_SHARE === "true" || !repoData.data.private
|
||||
const promptData = state.type === "issue" ? buildPromptDataForIssue(state.issue) : buildPromptDataForPR(state.pr)
|
||||
const responseRet = await runOpencode(`${userPrompt}\n\n${promptData}`, {
|
||||
share,
|
||||
})
|
||||
|
||||
const response = responseRet.stdout
|
||||
shareUrl = responseRet.stderr.match(/https:\/\/opencode\.ai\/s\/\w+/)?.[0]
|
||||
|
||||
// Comment and push changes
|
||||
if (await branchIsDirty()) {
|
||||
const summary =
|
||||
(await runOpencode(`Summarize the following in less than 40 characters:\n\n${response}`, { share: false }))
|
||||
?.stdout || `Fix issue: ${payload.issue.title}`
|
||||
|
||||
if (state.type === "issue") {
|
||||
const branch = await pushToNewBranch(summary)
|
||||
const pr = await createPR(repoData.data.default_branch, branch, summary, `${response}\n\nCloses #${issueId}`)
|
||||
await updateComment(`opencode created pull request #${pr}`)
|
||||
} else if (state.type === "local-pr") {
|
||||
await pushToCurrentBranch(summary)
|
||||
await updateComment(response)
|
||||
} else if (state.type === "fork-pr") {
|
||||
await pushToForkBranch(summary, state.pr)
|
||||
await updateComment(response)
|
||||
}
|
||||
} else {
|
||||
await updateComment(response)
|
||||
}
|
||||
await restoreGitConfig()
|
||||
await revokeAppToken()
|
||||
} catch (e: any) {
|
||||
await restoreGitConfig()
|
||||
await revokeAppToken()
|
||||
console.error(e)
|
||||
let msg = e
|
||||
if (e instanceof $.ShellError) {
|
||||
msg = e.stderr.toString()
|
||||
} else if (e instanceof Error) {
|
||||
msg = e.message
|
||||
}
|
||||
if (commentId) await updateComment(msg)
|
||||
core.setFailed(`opencode failed with error: ${msg}`)
|
||||
// Also output the clean error message for the action to capture
|
||||
//core.setOutput("prepare_error", e.message);
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
run()
|
||||
}
|
||||
|
||||
async function generateGitHubToken() {
|
||||
try {
|
||||
return await core.getIDToken("opencode-github-action")
|
||||
} catch (error) {
|
||||
console.error("Failed to get OIDC token:", error)
|
||||
throw new Error("Could not fetch an OIDC token. Make sure to add `id-token: write` to your workflow permissions.")
|
||||
}
|
||||
}
|
||||
|
||||
async function exchangeForAppToken(oidcToken: string) {
|
||||
const response = await fetch("https://api.opencode.ai/exchange_github_app_token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${oidcToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const responseJson = (await response.json()) as { error?: string }
|
||||
throw new Error(`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`)
|
||||
}
|
||||
|
||||
const responseJson = (await response.json()) as { token: string }
|
||||
return responseJson.token
|
||||
}
|
||||
|
||||
async function configureGit(appToken: string) {
|
||||
console.log("Configuring git...")
|
||||
const config = "http.https://github.com/.extraheader"
|
||||
const ret = await $`git config --local --get ${config}`
|
||||
gitCredentials = ret.stdout.toString().trim()
|
||||
|
||||
const newCredentials = Buffer.from(`x-access-token:${appToken}`, "utf8").toString("base64")
|
||||
|
||||
await $`git config --local --unset-all ${config}`
|
||||
await $`git config --local ${config} "AUTHORIZATION: basic ${newCredentials}"`
|
||||
await $`git config --global user.name "opencode-agent[bot]"`
|
||||
await $`git config --global user.email "opencode-agent[bot]@users.noreply.github.com"`
|
||||
}
|
||||
|
||||
async function checkoutLocalBranch(pr: GitHubPullRequest) {
|
||||
console.log("Checking out local branch...")
|
||||
|
||||
const branch = pr.headRefName
|
||||
const depth = Math.max(pr.commits.totalCount, 20)
|
||||
|
||||
await $`git fetch origin --depth=${depth} ${branch}`
|
||||
await $`git checkout ${branch}`
|
||||
}
|
||||
|
||||
async function checkoutForkBranch(pr: GitHubPullRequest) {
|
||||
console.log("Checking out fork branch...")
|
||||
|
||||
const remoteBranch = pr.headRefName
|
||||
const localBranch = generateBranchName()
|
||||
const depth = Math.max(pr.commits.totalCount, 20)
|
||||
|
||||
await $`git remote add fork https://github.com/${pr.headRepository.nameWithOwner}.git`
|
||||
await $`git fetch fork --depth=${depth} ${remoteBranch}`
|
||||
await $`git checkout -b ${localBranch} fork/${remoteBranch}`
|
||||
}
|
||||
|
||||
async function restoreGitConfig() {
|
||||
if (!gitCredentials) return
|
||||
const config = "http.https://github.com/.extraheader"
|
||||
await $`git config --local ${config} "${gitCredentials}"`
|
||||
}
|
||||
|
||||
async function assertPermissions() {
|
||||
console.log(`Asserting permissions for user ${actor}...`)
|
||||
|
||||
let permission
|
||||
try {
|
||||
const response = await octoRest.repos.getCollaboratorPermissionLevel({
|
||||
owner,
|
||||
repo,
|
||||
username: actor,
|
||||
})
|
||||
|
||||
permission = response.data.permission
|
||||
console.log(` permission: ${permission}`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to check permissions: ${error}`)
|
||||
throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
|
||||
}
|
||||
|
||||
if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
|
||||
}
|
||||
|
||||
function buildComment(content: string) {
|
||||
const runId = process.env.GITHUB_RUN_ID!
|
||||
const runUrl = `/${owner}/${repo}/actions/runs/${runId}`
|
||||
return [content, "\n\n", shareUrl ? `[view session](${shareUrl}) | ` : "", `[view log](${runUrl})`].join("")
|
||||
}
|
||||
|
||||
async function createComment(body: string) {
|
||||
console.log("Creating comment...")
|
||||
return await octoRest.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueId,
|
||||
body: buildComment(body),
|
||||
})
|
||||
}
|
||||
|
||||
async function updateComment(body: string) {
|
||||
console.log("Updating comment...")
|
||||
return await octoRest.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: commentId,
|
||||
body: buildComment(body),
|
||||
})
|
||||
}
|
||||
|
||||
function generateBranchName() {
|
||||
const type = state.type === "issue" ? "issue" : "pr"
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/[:-]/g, "")
|
||||
.replace(/\.\d{3}Z/, "")
|
||||
.split("T")
|
||||
.join("_")
|
||||
return `opencode/${type}${issueId}-${timestamp}`
|
||||
}
|
||||
|
||||
async function pushToCurrentBranch(summary: string) {
|
||||
console.log("Pushing to current branch...")
|
||||
await $`git add .`
|
||||
await $`git commit -m "${summary}
|
||||
|
||||
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
await $`git push`
|
||||
}
|
||||
|
||||
async function pushToForkBranch(summary: string, pr: GitHubPullRequest) {
|
||||
console.log("Pushing to fork branch...")
|
||||
|
||||
const remoteBranch = pr.headRefName
|
||||
|
||||
await $`git add .`
|
||||
await $`git commit -m "${summary}
|
||||
|
||||
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
await $`git push fork HEAD:${remoteBranch}`
|
||||
}
|
||||
|
||||
async function pushToNewBranch(summary: string) {
|
||||
console.log("Pushing to new branch...")
|
||||
const branch = generateBranchName()
|
||||
await $`git checkout -b ${branch}`
|
||||
await $`git add .`
|
||||
await $`git commit -m "${summary}
|
||||
|
||||
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
await $`git push -u origin ${branch}`
|
||||
return branch
|
||||
}
|
||||
|
||||
async function createPR(base: string, branch: string, title: string, body: string) {
|
||||
console.log("Creating pull request...")
|
||||
const pr = await octoRest.rest.pulls.create({
|
||||
owner,
|
||||
repo,
|
||||
head: branch,
|
||||
base,
|
||||
title,
|
||||
body: buildComment(body),
|
||||
})
|
||||
return pr.data.number
|
||||
}
|
||||
|
||||
async function runOpencode(
|
||||
prompt: string,
|
||||
opts?: {
|
||||
share?: boolean
|
||||
},
|
||||
) {
|
||||
console.log("Running opencode...")
|
||||
|
||||
const promptPath = path.join(os.tmpdir(), "PROMPT")
|
||||
await Bun.write(promptPath, prompt)
|
||||
const ret = await $`cat ${promptPath} | opencode run -m ${process.env.INPUT_MODEL} ${opts?.share ? "--share" : ""}`
|
||||
return {
|
||||
stdout: ret.stdout.toString().trim(),
|
||||
stderr: ret.stderr.toString().trim(),
|
||||
}
|
||||
}
|
||||
|
||||
async function branchIsDirty() {
|
||||
console.log("Checking if branch is dirty...")
|
||||
const ret = await $`git status --porcelain`
|
||||
return ret.stdout.toString().trim().length > 0
|
||||
}
|
||||
|
||||
async function fetchRepo() {
|
||||
return await octoRest.rest.repos.get({ owner, repo })
|
||||
}
|
||||
|
||||
async function fetchIssue() {
|
||||
console.log("Fetching prompt data for issue...")
|
||||
const issueResult = await octoGraph<IssueQueryResponse>(
|
||||
`
|
||||
query($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
issue(number: $number) {
|
||||
title
|
||||
body
|
||||
author {
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
state
|
||||
comments(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
databaseId
|
||||
body
|
||||
author {
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
number: issueId,
|
||||
},
|
||||
)
|
||||
|
||||
const issue = issueResult.repository.issue
|
||||
if (!issue) throw new Error(`Issue #${issueId} not found`)
|
||||
|
||||
return issue
|
||||
}
|
||||
|
||||
function buildPromptDataForIssue(issue: GitHubIssue) {
|
||||
const comments = (issue.comments?.nodes || [])
|
||||
.filter((c) => {
|
||||
const id = parseInt(c.databaseId)
|
||||
return id !== commentId && id !== payload.comment.id
|
||||
})
|
||||
.map((c) => ` - ${c.author.login} at ${c.createdAt}: ${c.body}`)
|
||||
|
||||
return [
|
||||
"Here is the context for the issue:",
|
||||
`- Title: ${issue.title}`,
|
||||
`- Body: ${issue.body}`,
|
||||
`- Author: ${issue.author.login}`,
|
||||
`- Created At: ${issue.createdAt}`,
|
||||
`- State: ${issue.state}`,
|
||||
...(comments.length > 0 ? ["- Comments:", ...comments] : []),
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
async function fetchPR() {
|
||||
console.log("Fetching prompt data for PR...")
|
||||
const prResult = await octoGraph<PullRequestQueryResponse>(
|
||||
`
|
||||
query($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $number) {
|
||||
title
|
||||
body
|
||||
author {
|
||||
login
|
||||
}
|
||||
baseRefName
|
||||
headRefName
|
||||
headRefOid
|
||||
createdAt
|
||||
additions
|
||||
deletions
|
||||
state
|
||||
baseRepository {
|
||||
nameWithOwner
|
||||
}
|
||||
headRepository {
|
||||
nameWithOwner
|
||||
}
|
||||
commits(first: 100) {
|
||||
totalCount
|
||||
nodes {
|
||||
commit {
|
||||
oid
|
||||
message
|
||||
author {
|
||||
name
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
files(first: 100) {
|
||||
nodes {
|
||||
path
|
||||
additions
|
||||
deletions
|
||||
changeType
|
||||
}
|
||||
}
|
||||
comments(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
databaseId
|
||||
body
|
||||
author {
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
reviews(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
databaseId
|
||||
author {
|
||||
login
|
||||
}
|
||||
body
|
||||
state
|
||||
submittedAt
|
||||
comments(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
databaseId
|
||||
body
|
||||
path
|
||||
line
|
||||
author {
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
number: issueId,
|
||||
},
|
||||
)
|
||||
|
||||
const pr = prResult.repository.pullRequest
|
||||
if (!pr) throw new Error(`PR #${issueId} not found`)
|
||||
|
||||
return pr
|
||||
}
|
||||
|
||||
function buildPromptDataForPR(pr: GitHubPullRequest) {
|
||||
const comments = (pr.comments?.nodes || [])
|
||||
.filter((c) => {
|
||||
const id = parseInt(c.databaseId)
|
||||
return id !== commentId && id !== payload.comment.id
|
||||
})
|
||||
.map((c) => ` - ${c.author.login} at ${c.createdAt}: ${c.body}`)
|
||||
|
||||
const files = (pr.files.nodes || []).map((f) => ` - ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`)
|
||||
const reviewData = (pr.reviews.nodes || []).map((r) => {
|
||||
const comments = (r.comments.nodes || []).map((c) => ` - ${c.path}:${c.line ?? "?"}: ${c.body}`)
|
||||
return [
|
||||
` - ${r.author.login} at ${r.submittedAt}:`,
|
||||
` - Review body: ${r.body}`,
|
||||
...(comments.length > 0 ? [" - Comments:", ...comments] : []),
|
||||
]
|
||||
})
|
||||
|
||||
return [
|
||||
"Here is the context for the pull request:",
|
||||
`- Title: ${pr.title}`,
|
||||
`- Body: ${pr.body}`,
|
||||
`- Author: ${pr.author.login}`,
|
||||
`- Created At: ${pr.createdAt}`,
|
||||
`- Base Branch: ${pr.baseRefName}`,
|
||||
`- Head Branch: ${pr.headRefName}`,
|
||||
`- State: ${pr.state}`,
|
||||
`- Additions: ${pr.additions}`,
|
||||
`- Deletions: ${pr.deletions}`,
|
||||
`- Total Commits: ${pr.commits.totalCount}`,
|
||||
`- Changed Files: ${pr.files.nodes.length} files`,
|
||||
...(comments.length > 0 ? ["- Comments:", ...comments] : []),
|
||||
...(files.length > 0 ? ["- Changed files:", ...files] : []),
|
||||
...(reviewData.length > 0 ? ["- Reviews:", ...reviewData] : []),
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
async function revokeAppToken() {
|
||||
if (!appToken) return
|
||||
|
||||
await fetch("https://api.github.com/installation/token", {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
Accept: "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
// Types for GitHub GraphQL query responses
|
||||
export type GitHubAuthor = {
|
||||
login: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type GitHubComment = {
|
||||
id: string;
|
||||
databaseId: string;
|
||||
body: string;
|
||||
author: GitHubAuthor;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type GitHubReviewComment = GitHubComment & {
|
||||
path: string;
|
||||
line: number | null;
|
||||
};
|
||||
|
||||
export type GitHubCommit = {
|
||||
oid: string;
|
||||
message: string;
|
||||
author: {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type GitHubFile = {
|
||||
path: string;
|
||||
additions: number;
|
||||
deletions: number;
|
||||
changeType: string;
|
||||
};
|
||||
|
||||
export type GitHubReview = {
|
||||
id: string;
|
||||
databaseId: string;
|
||||
author: GitHubAuthor;
|
||||
body: string;
|
||||
state: string;
|
||||
submittedAt: string;
|
||||
comments: {
|
||||
nodes: GitHubReviewComment[];
|
||||
};
|
||||
};
|
||||
|
||||
export type GitHubPullRequest = {
|
||||
title: string;
|
||||
body: string;
|
||||
author: GitHubAuthor;
|
||||
baseRefName: string;
|
||||
headRefName: string;
|
||||
headRefOid: string;
|
||||
createdAt: string;
|
||||
additions: number;
|
||||
deletions: number;
|
||||
state: string;
|
||||
baseRepository: {
|
||||
nameWithOwner: string;
|
||||
};
|
||||
headRepository: {
|
||||
nameWithOwner: string;
|
||||
};
|
||||
commits: {
|
||||
totalCount: number;
|
||||
nodes: Array<{
|
||||
commit: GitHubCommit;
|
||||
}>;
|
||||
};
|
||||
files: {
|
||||
nodes: GitHubFile[];
|
||||
};
|
||||
comments: {
|
||||
nodes: GitHubComment[];
|
||||
};
|
||||
reviews: {
|
||||
nodes: GitHubReview[];
|
||||
};
|
||||
};
|
||||
|
||||
export type GitHubIssue = {
|
||||
title: string;
|
||||
body: string;
|
||||
author: GitHubAuthor;
|
||||
createdAt: string;
|
||||
state: string;
|
||||
comments: {
|
||||
nodes: GitHubComment[];
|
||||
};
|
||||
};
|
||||
|
||||
export type PullRequestQueryResponse = {
|
||||
repository: {
|
||||
pullRequest: GitHubPullRequest;
|
||||
};
|
||||
};
|
||||
|
||||
export type IssueQueryResponse = {
|
||||
repository: {
|
||||
issue: GitHubIssue;
|
||||
};
|
||||
};
|
||||
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user