Compare commits

...

15 Commits

Author SHA1 Message Date
Simon Klee d4807d3ff3 bump opentui snapshot to 0.0.0-20260717-0afb0c25 2026-07-17 07:16:40 +02:00
Simon Klee bfcee9872f cli: resolve ws from package root 2026-07-16 11:53:02 +02:00
Simon Klee 68af4f7463 tui: drop Bun-only sleep and width APIs 2026-07-16 11:09:21 +02:00
Simon Klee 6fd04de318 fixup! bump opentui snapshot to 0.0.0-20260716-d8afb9e5 2026-07-16 10:43:11 +02:00
Simon Klee ea033154b2 bump opentui snapshot to 0.0.0-20260716-d8afb9e5 2026-07-16 08:52:18 +02:00
Simon Klee 098aaa1b84 feat(cli): publish Node builds as opencode2-node 2026-07-16 08:44:36 +02:00
Simon Klee 19951c08f3 fix(core): update built-in provider imports 2026-07-16 08:44:36 +02:00
Simon Klee bd6f1add9a cli: allow custom build output directory
Hardcoding dist forces every build into the same tree.
2026-07-16 08:44:36 +02:00
Simon Klee 0881c78e28 feat(cli): build Node SEA artifacts 2026-07-16 08:44:36 +02:00
Simon Klee f145d8040e feat(cli): add Node runtime bootstrap 2026-07-16 08:44:36 +02:00
Simon Klee 0c2efdbf5c refactor(cli): remove Bun-only runtime APIs 2026-07-16 08:44:36 +02:00
Simon Klee 87a488e3ce feat(tui): add Node runtime adapters 2026-07-16 08:44:36 +02:00
Simon Klee 12a294c5d0 feat(core): add Node runtime support 2026-07-16 08:44:36 +02:00
Kit Langton 4678bd1049 feat(plugin): expose synthetic session input (#37212)
publish / version (push) Has been cancelled
publish / build-cli (push) Has been cancelled
publish / sign-cli-windows (push) Has been cancelled
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=arm64 host:macos-26 platform_flag:--mac --arm64 target:aarch64-apple-darwin]) (push) Has been cancelled
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=x64 host:macos-26-intel platform_flag:--mac --x64 target:x86_64-apple-darwin]) (push) Has been cancelled
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404 platform_flag:--linux target:x86_64-unknown-linux-gnu]) (push) Has been cancelled
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404-arm platform_flag:--linux --arm64 target:aarch64-unknown-linux-gnu]) (push) Has been cancelled
publish / build-electron (map[host:blacksmith-4vcpu-windows-2025 platform_flag:--win target:x86_64-pc-windows-msvc]) (push) Has been cancelled
publish / build-electron (map[host:windows-2025 platform_flag:--win --arm64 target:aarch64-pc-windows-msvc]) (push) Has been cancelled
publish / publish (push) Has been cancelled
typecheck / typecheck (push) Has been cancelled
2026-07-15 22:55:40 -04:00
Dax Raad 5fb0470b44 feat(api): add experimental wellknown connections 2026-07-15 22:53:38 -04:00
114 changed files with 2665 additions and 528 deletions
+54
View File
@@ -121,6 +121,53 @@ jobs:
outputs:
version: ${{ needs.version.outputs.version }}
build-node-cli:
needs: version
if: github.repository == 'anomalyco/opencode'
strategy:
fail-fast: false
matrix:
settings:
- target: linux-arm64
host: blacksmith-4vcpu-ubuntu-2404-arm
- target: linux-x64
host: blacksmith-4vcpu-ubuntu-2404
- target: darwin-arm64
host: macos-26
- target: windows-arm64
host: windows-2025
- target: windows-x64
host: blacksmith-4vcpu-windows-2025
runs-on: ${{ matrix.settings.host }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/setup-bun
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "26.4.0"
- name: Build
run: bun packages/cli/script/build-node.ts --target=${{ matrix.settings.target }} --outdir=dist/node
env:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
- name: Verify service lifecycle
if: matrix.settings.target != 'windows-arm64'
working-directory: packages/cli
run: bun run script/service-smoke.ts --node
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: opencode-node-cli-${{ matrix.settings.target }}
path: packages/cli/dist/node/cli-node-*
if-no-files-found: error
sign-cli-windows:
needs:
- build-cli
@@ -413,6 +460,7 @@ jobs:
needs:
- version
- build-cli
- build-node-cli
- sign-cli-windows
- build-electron
if: always() && !failure() && !cancelled()
@@ -461,6 +509,12 @@ jobs:
name: opencode-preview-cli
path: packages/cli/dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: opencode-node-cli-*
path: packages/cli/dist/node
merge-multiple: true
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: needs.version.outputs.release
with:
+14
View File
@@ -78,6 +78,20 @@ jobs:
bun run script/build.ts --single --skip-install
bun run script/service-smoke.ts
- name: Setup Node build runtime
if: always()
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "26.4.0"
- name: Verify Node build
if: always()
timeout-minutes: 15
working-directory: packages/cli
run: |
bun run script/build-node.ts --single --skip-install --outdir=dist/node
bun run script/service-smoke.ts --node
- name: Check generated client
if: runner.os == 'Linux'
working-directory: packages/client
+2
View File
@@ -11,6 +11,7 @@ node_modules
playground
tmp
dist
dist-node
ts-dist
.turbo
.typecheck-profiles
@@ -25,6 +26,7 @@ Session.vim
a.out
target
.scripts
.cache
.direnv/
# Local dev files
+42 -20
View File
@@ -145,14 +145,28 @@
"solid-js": "catalog:",
"strip-ansi": "7.1.2",
"uqr": "0.1.3",
"ws": "8.21.0",
},
"devDependencies": {
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.12",
"@lydell/node-pty-darwin-x64": "1.2.0-beta.12",
"@lydell/node-pty-linux-arm64": "1.2.0-beta.12",
"@lydell/node-pty-linux-x64": "1.2.0-beta.12",
"@lydell/node-pty-win32-arm64": "1.2.0-beta.12",
"@lydell/node-pty-win32-x64": "1.2.0-beta.12",
"@opencode-ai/protocol": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@types/semver": "catalog:",
"@typescript/native-preview": "catalog:",
"vite": "catalog:",
"vite-plugin-solid": "catalog:",
},
},
"packages/client": {
@@ -387,6 +401,7 @@
"mime-types": "3.0.2",
"minimatch": "10.2.5",
"npm-package-arg": "13.0.2",
"resolve.exports": "catalog:",
"semver": "^7.6.3",
"turndown": "7.2.0",
"venice-ai-sdk-provider": "2.1.1",
@@ -739,9 +754,9 @@
"typescript": "catalog:",
},
"peerDependencies": {
"@opentui/core": ">=0.4.3",
"@opentui/keymap": ">=0.4.3",
"@opentui/solid": ">=0.4.3",
"@opentui/core": "0.0.0-20260717-0afb0c25",
"@opentui/keymap": "0.0.0-20260717-0afb0c25",
"@opentui/solid": "0.0.0-20260717-0afb0c25",
},
"optionalPeers": [
"@opentui/core",
@@ -1021,10 +1036,12 @@
"diff": "catalog:",
"effect": "catalog:",
"fuzzysort": "catalog:",
"get-east-asian-width": "catalog:",
"open": "10.1.2",
"opentui-spinner": "catalog:",
"remeda": "catalog:",
"solid-js": "catalog:",
"string-width": "catalog:",
"strip-ansi": "7.1.2",
"uqr": "0.1.3",
},
@@ -1171,9 +1188,9 @@
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
},
"overrides": {
"@opentui/core": "catalog:",
"@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:",
"@opentui/core": "0.0.0-20260717-0afb0c25",
"@opentui/keymap": "0.0.0-20260717-0afb0c25",
"@opentui/solid": "0.0.0-20260717-0afb0c25",
"@types/bun": "catalog:",
"@types/node": "catalog:",
},
@@ -1190,9 +1207,9 @@
"@npmcli/arborist": "9.4.0",
"@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opentui/core": "0.4.3",
"@opentui/keymap": "0.4.3",
"@opentui/solid": "0.4.3",
"@opentui/core": "0.0.0-20260717-0afb0c25",
"@opentui/keymap": "0.0.0-20260717-0afb0c25",
"@opentui/solid": "0.0.0-20260717-0afb0c25",
"@pierre/diffs": "1.2.10",
"@playwright/test": "1.59.1",
"@sentry/solid": "10.36.0",
@@ -1220,6 +1237,7 @@
"drizzle-orm": "1.0.0-rc.2",
"effect": "4.0.0-beta.83",
"fuzzysort": "3.1.0",
"get-east-asian-width": "1.6.0",
"hono": "4.10.7",
"hono-openapi": "1.1.2",
"luxon": "3.6.1",
@@ -1228,11 +1246,13 @@
"opentui-spinner": "0.0.7",
"remeda": "2.26.0",
"remend": "1.3.0",
"resolve.exports": "2.0.3",
"semver": "7.7.4",
"shiki": "4.2.0",
"solid-js": "1.9.10",
"solid-list": "0.3.0",
"sst": "4.13.1",
"string-width": "7.2.0",
"tailwindcss": "4.1.11",
"typescript": "5.8.2",
"ulid": "3.0.1",
@@ -2273,27 +2293,27 @@
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
"@opentui/core": ["@opentui/core@0.4.3", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.3", "@opentui/core-darwin-x64": "0.4.3", "@opentui/core-linux-arm64": "0.4.3", "@opentui/core-linux-arm64-musl": "0.4.3", "@opentui/core-linux-x64": "0.4.3", "@opentui/core-linux-x64-musl": "0.4.3", "@opentui/core-win32-arm64": "0.4.3", "@opentui/core-win32-x64": "0.4.3" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-rrJfAk13tALDqldYjhc78eWQ+aKq1iknJgffIOg3OwyZoqQo+p6gtuqyhmWvXIfQzlNUbpgpCPcxbXlhMnlaHQ=="],
"@opentui/core": ["@opentui/core@0.0.0-20260717-0afb0c25", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.0.0-20260717-0afb0c25", "@opentui/core-darwin-x64": "0.0.0-20260717-0afb0c25", "@opentui/core-linux-arm64": "0.0.0-20260717-0afb0c25", "@opentui/core-linux-arm64-musl": "0.0.0-20260717-0afb0c25", "@opentui/core-linux-x64": "0.0.0-20260717-0afb0c25", "@opentui/core-linux-x64-musl": "0.0.0-20260717-0afb0c25", "@opentui/core-win32-arm64": "0.0.0-20260717-0afb0c25", "@opentui/core-win32-x64": "0.0.0-20260717-0afb0c25" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-C27k1sqoh6VP1F3i+fjhT6AOvfgzHaXe+nOtIR0s0T9OmTANQ4VmEW2ryo3+zC4zOSfraUD0Jya/YHECowBRMg=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p5+7AAxpxGuDGagyQfewKtmTFnN7THvTVY4FyKqUtJomNaHdQXPHztapNNzMx0DGWbwOUbVKzpL+yc3CZY3chQ=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20260717-0afb0c25", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LNJeXNtqtNd/FGI/4O0deL0paWhTT7d8m2XidgTiVH8BydYZDLewFMSHo4AHYeHViI3pKR/97rxhty3FZLySYA=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-+fh0vEUE0lwVC7RW5ijYLRlTLp5NfvCRj8SzxDVd7IL2j2ssB6YXcfIbXq2EW7UGnrejwPRXf1tgUrIXW9KmOw=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20260717-0afb0c25", "", { "os": "darwin", "cpu": "x64" }, "sha512-hgAmy1WS/g2K8t7iLXQDiWJaNo6P1QcyIZDHCK1HZz0/3Zaz8igCXggqo4JrZ53s2rWPV2Eo/yDtIy3034yErg=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gl6qA5QJy6u8Cbt7gOtHbhhfMZ4qQDb0kEwFXHcMGmbnKzz4OHoq74D6tNjyvSQB9saoC7C6C0tvn2DcJOuNog=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20260717-0afb0c25", "", { "os": "linux", "cpu": "arm64" }, "sha512-oFWULMWrHquagwZ7avpSScvhStP0dn+kAWz4Zrj0VVcVAw9DiVc7RxRGw/r6jVAo1W8wR2TSMsDbJp3FkbLCTg=="],
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-8p8g8/AEq/xFGpQ7XcIFKcAqjc0QwsZcv+Ll9RbCDpUA56FGH6jfLDir0KYTNTgYXJTIrBIENI9K46VuxMUMQA=="],
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.0.0-20260717-0afb0c25", "", { "os": "linux", "cpu": "arm64" }, "sha512-5ieoKyjV1yx+asSBYIL/8np3ExK+O4s9WhryRlRvAMvZSa93iEeCrYVZCtmKkP/Az/C8XUu6dSldoQIbw/BI2A=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-dXpJitiZdYE3hq2Pvx6e9I0uPQSOcnaLLp1pDgWAHv+3kvKSHEX//9Yr/pV/Ua6qqT7p+2D/K4vXNap/NKVo2w=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20260717-0afb0c25", "", { "os": "linux", "cpu": "x64" }, "sha512-fmEm52sLLedU4o4qTdfDAkCKH6GVCo0saaZv/tpa/Ey6AbSs+y9xMI6yiltqXPhmTPb7pd7yMFnm5Yo/oeYGXw=="],
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/QiFpCrpU2O7vy8QYmLIQYbvAtKDgmqcVjR7dGtqSzkiQk3ktNJoo5RozG7ueXnjung1Wp0nKldKxo2Csg/OrA=="],
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.0.0-20260717-0afb0c25", "", { "os": "linux", "cpu": "x64" }, "sha512-nrE616u1icMTqUgxP3gO4JyZzJytFafBjwX+v104iTfsS0YMxX/ailJghb465zkVlzmZ4spSOOnTPrfmYl5MhA=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Mx2zuOjrhm/z2SDS6RExIyjP/SnN/8QhhagxURUw0jQi/NssGSeAllu1cBAFFnhobJL5QLTE4FU4CRhUK9svgg=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20260717-0afb0c25", "", { "os": "win32", "cpu": "arm64" }, "sha512-03muWP2Dsk2QJZXK3fRoq6ld5VATA2daBjU+nmfhpJZiayxwToBh4RYxCRm+pGmS+XjxET8PdY2YTq5Ne3Wltw=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.3", "", { "os": "win32", "cpu": "x64" }, "sha512-NuoqvWKGXaYnmlqvu7Gg2lLI6yVMnS9OfWBvxp+7Q+McSgHFSTQmYBXaPpvQ8HikpQXE1nCeMPtuSG4PdZHe2w=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20260717-0afb0c25", "", { "os": "win32", "cpu": "x64" }, "sha512-qov2smWOjpiARZzCaCPjpsM+Qw0pcnKDOGLSpKrNXTwrdO3pt32q4vJYKdAoN99lC70YfcaxXaWg/ol2axJV1Q=="],
"@opentui/keymap": ["@opentui/keymap@0.4.3", "", { "dependencies": { "@opentui/core": "0.4.3" }, "peerDependencies": { "@opentui/react": "0.4.3", "@opentui/solid": "0.4.3", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-sinX0pyQBRrEvo89PSSUbSUDIYpL3xWo81VEfec58VFoVRB5FG48/deAtvRTQfJ8w1kgbzN8hzdOXdSm61zBmw=="],
"@opentui/keymap": ["@opentui/keymap@0.0.0-20260717-0afb0c25", "", { "dependencies": { "@opentui/core": "0.0.0-20260717-0afb0c25" }, "peerDependencies": { "@opentui/react": "0.0.0-20260717-0afb0c25", "@opentui/solid": "0.0.0-20260717-0afb0c25", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-byAjR2aOZLJ4nLoHnUo45wtkP458Nlusqahxp0oZvyzYasOu2OrtZK4AUQyuFg7/ncLMP0V04KoQqT3GwBOqkg=="],
"@opentui/solid": ["@opentui/solid@0.4.3", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.3", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-RcV0+S8HMdXOASyr7HmJUBuTUIaFPzAxMDa44VftS5C2JUgrmAuWo0Njv1q3TWRB1owjHnyKhEfWGKq7A82wxw=="],
"@opentui/solid": ["@opentui/solid@0.0.0-20260717-0afb0c25", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20260717-0afb0c25", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-GIAwS1a+ZFMSMR8W5KyvErcVNgddqo5U/KRDoL8/8wUjdKRP/3XyZQj8a23AXTJX3Yv8C5DkTQEKQymkXq2Wlg=="],
"@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="],
@@ -5595,6 +5615,8 @@
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
+9 -6
View File
@@ -46,9 +46,9 @@
"@octokit/rest": "22.0.0",
"@hono/standard-validator": "0.2.0",
"@hono/zod-validator": "0.4.2",
"@opentui/core": "0.4.3",
"@opentui/keymap": "0.4.3",
"@opentui/solid": "0.4.3",
"@opentui/core": "0.0.0-20260717-0afb0c25",
"@opentui/keymap": "0.0.0-20260717-0afb0c25",
"@opentui/solid": "0.0.0-20260717-0afb0c25",
"@tanstack/solid-virtual": "3.13.32",
"@shikijs/stream": "4.2.0",
"ulid": "3.0.1",
@@ -75,6 +75,7 @@
"hono": "4.10.7",
"hono-openapi": "1.1.2",
"fuzzysort": "3.1.0",
"get-east-asian-width": "1.6.0",
"luxon": "3.6.1",
"marked": "17.0.6",
"marked-shiki": "1.2.1",
@@ -85,9 +86,11 @@
"@typescript/native-preview": "7.0.0-dev.20251207.1",
"zod": "4.1.8",
"remeda": "2.26.0",
"resolve.exports": "2.0.3",
"sst": "4.13.1",
"shiki": "4.2.0",
"solid-list": "0.3.0",
"string-width": "7.2.0",
"tailwindcss": "4.1.11",
"vite": "7.1.4",
"@solidjs/meta": "0.29.4",
@@ -145,9 +148,9 @@
"electron"
],
"overrides": {
"@opentui/core": "catalog:",
"@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:",
"@opentui/core": "0.0.0-20260717-0afb0c25",
"@opentui/keymap": "0.0.0-20260717-0afb0c25",
"@opentui/solid": "0.0.0-20260717-0afb0c25",
"@types/bun": "catalog:",
"@types/node": "catalog:"
},
+7 -4
View File
@@ -31,11 +31,13 @@ function run(target) {
const envPath = process.env.OPENCODE_BIN_PATH
const scriptDir = path.dirname(fs.realpathSync(__filename))
const cached = path.join(scriptDir, ".opencode2")
const command = path.basename(__filename).replace(/\.cjs$/, "")
const nodeBuild = command === "opencode2-node"
const cached = path.join(scriptDir, `.${command}`)
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
const base = "@opencode-ai/cli-" + platform + "-" + arch
const binary = platform === "windows" ? "opencode2.exe" : "opencode2"
const base = `@opencode-ai/cli${nodeBuild ? "-node" : ""}-` + platform + "-" + arch
const binary = platform === "windows" ? `${command}.exe` : command
function supportsAvx2() {
if (arch !== "x64") return false
@@ -77,6 +79,7 @@ function supportsAvx2() {
}
const names = (() => {
if (nodeBuild) return [base]
const baseline = arch === "x64" && !supportsAvx2()
if (platform === "linux") {
const musl = (() => {
@@ -121,7 +124,7 @@ function findBinary(startDir) {
const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
if (!resolved) {
console.error(
"It seems that your package manager failed to install the right opencode2 CLI package. Try manually installing " +
`It seems that your package manager failed to install the right ${command} CLI package. Try manually installing ` +
names.map((name) => `"${name}"`).join(" or ") +
" package",
)
+17 -2
View File
@@ -26,6 +26,7 @@
},
"scripts": {
"build": "bun run script/build.ts",
"build:node": "bun run script/build-node.ts",
"dev": "bun run src/index.ts",
"test": "bun test --timeout 30000 --only-failures",
"typecheck": "tsgo --noEmit"
@@ -51,7 +52,8 @@
"semver": "catalog:",
"solid-js": "catalog:",
"strip-ansi": "7.1.2",
"uqr": "0.1.3"
"uqr": "0.1.3",
"ws": "8.21.0"
},
"devDependencies": {
"@opencode-ai/script": "workspace:*",
@@ -59,6 +61,19 @@
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@types/semver": "catalog:",
"@typescript/native-preview": "catalog:"
"@typescript/native-preview": "catalog:",
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.12",
"@lydell/node-pty-darwin-x64": "1.2.0-beta.12",
"@lydell/node-pty-linux-arm64": "1.2.0-beta.12",
"@lydell/node-pty-linux-x64": "1.2.0-beta.12",
"@lydell/node-pty-win32-arm64": "1.2.0-beta.12",
"@lydell/node-pty-win32-x64": "1.2.0-beta.12",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1",
"vite": "catalog:",
"vite-plugin-solid": "catalog:"
}
}
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env bun
import { spawnSync } from "node:child_process"
import { createHash } from "node:crypto"
import { chmod, copyFile, mkdir, mkdtemp, realpath, rename, rm, stat, writeFile } from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { build } from "vite"
import { Script } from "@opencode-ai/script"
import pkg from "../package.json"
import { modelsData } from "./generate"
import { collectNodeAssets, copyNodeAssets, hashNodeAssets, seaAssetMap } from "./node-assets"
import { mainConfig } from "../vite.node.config"
import { nodeExecArgv, nodeTarget, type NodeTarget } from "../src/node/target"
const NODE_VERSION = "26.4.0"
const dir = path.resolve(import.meta.dirname, "..")
const outdir = path.resolve(
dir,
process.argv.find((arg) => arg.startsWith("--outdir="))?.slice("--outdir=".length) ?? "dist",
)
if (outdir === dir) throw new Error("--outdir must not be the package directory")
if (outdir === path.join(dir, "dist-node")) {
throw new Error("--outdir must not be dist-node because it contains temporary files")
}
const bundleOnly = process.argv.includes("--bundle-only")
const single = process.argv.includes("--single")
const skipInstall = process.argv.includes("--skip-install")
const requested = process.argv.find((arg) => arg.startsWith("--target="))?.slice("--target=".length)
const allTargets = [
nodeTarget("linux", "arm64"),
nodeTarget("linux", "x64"),
nodeTarget("darwin", "arm64"),
nodeTarget("win32", "arm64"),
nodeTarget("win32", "x64"),
]
const targets = requested
? allTargets.filter((target) => targetName(target) === requested)
: single || bundleOnly
? [nodeTarget(process.platform, process.arch)]
: allTargets
if (targets.length === 0) {
if (requested === "darwin-x64") throw new Error("Node 26.4 SEA does not support macOS x64")
throw new Error(`Unknown Node target: ${requested}`)
}
if (!bundleOnly && targets.some((target) => target.platform === "darwin" && target.arch === "x64")) {
throw new Error("Node 26.4 SEA does not support macOS x64")
}
process.chdir(dir)
if (!skipInstall) run(process.execPath, ["install", "--os=*", "--cpu=*"])
if (!bundleOnly) await rm(outdir, { recursive: true, force: true })
const builder =
!bundleOnly || targets.some((target) => target.platform === process.platform && target.arch === process.arch)
? await resolveHostNode()
: undefined
for (const target of targets) {
console.log(`building cli-node-${targetName(target)}`)
const assets = await collectNodeAssets(target)
await rm("dist-node", { recursive: true, force: true })
const assetHash = await hashNodeAssets(assets)
const input = { version: Script.version, channel: Script.channel, models: modelsData, assetHash, target }
await build(mainConfig(input))
await copyNodeAssets(assets)
const host = target.platform === process.platform && target.arch === process.arch
if (host) {
if (!builder) throw new Error("Node SEA builder is unavailable")
run(builder, [...nodeExecArgv, "dist-node/opencode.mjs", "--version"])
run(builder, [...nodeExecArgv, "dist-node/opencode.mjs", "--help"])
}
if (bundleOnly) continue
const name = `cli-node-${targetName(target)}`
const binary = target.platform === "win32" ? "opencode2-node.exe" : "opencode2-node"
const output = path.join(outdir, name, "bin", binary)
if (!builder) throw new Error("Node SEA builder is unavailable")
await mkdir(path.dirname(output), { recursive: true })
const config = {
main: "dist-node/opencode.mjs",
mainFormat: "module",
executable: await resolveTargetNode(target, builder),
output: path.relative(dir, output),
disableExperimentalSEAWarning: true,
useSnapshot: false,
useCodeCache: false,
execArgv: nodeExecArgv,
execArgvExtension: "none",
assets: await seaAssetMap(),
}
await writeFile("dist-node/sea.json", `${JSON.stringify(config, null, 2)}\n`)
run(builder, ["--build-sea", "dist-node/sea.json"])
if (target.platform !== "win32") await chmod(output, 0o755)
if (target.platform === "darwin" && process.platform === "darwin") run("codesign", ["--sign", "-", output])
if (target.platform === "darwin" && process.platform !== "darwin") {
console.warn(`${output} must be signed on macOS before it can run`)
}
await writeFile(
path.join(outdir, name, "package.json"),
`${JSON.stringify(
{
name: `@opencode-ai/${name}`,
version: Script.version,
license: pkg.license,
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
os: [target.platform],
cpu: [target.arch],
},
null,
2,
)}\n`,
)
if (host) await smoke(output)
}
async function resolveHostNode() {
const candidates = [process.env.NODE_BIN, "node"].filter((item): item is string => Boolean(item))
for (const candidate of candidates) {
const result = spawnSync(
candidate,
["-p", "JSON.stringify({version:process.versions.node,path:process.execPath})"],
{
encoding: "utf8",
},
)
if (result.status !== 0) continue
const info = JSON.parse(result.stdout) as { version: string; path: string }
if (info.version === NODE_VERSION) return realpath(info.path)
}
return resolveTargetNode(nodeTarget(process.platform, process.arch))
}
async function resolveTargetNode(target: NodeTarget, host?: string) {
if (host && target.platform === process.platform && target.arch === process.arch) return host
const cache = path.resolve(dir, ".cache", "node")
const platform = target.platform === "win32" ? "win" : target.platform
const archiveName = `node-v${NODE_VERSION}-${platform}-${target.arch}`
const targetDirectory = path.join(cache, archiveName)
const executable = path.join(targetDirectory, target.platform === "win32" ? "node.exe" : "bin/node")
if (
(await stat(executable).then(
() => true,
() => false,
)) &&
(await stat(path.join(targetDirectory, ".verified")).then(
() => true,
() => false,
))
)
return realpath(executable)
await mkdir(cache, { recursive: true })
const extension = target.platform === "win32" ? "zip" : "tar.gz"
const filename = `${archiveName}.${extension}`
const archive = path.join(cache, filename)
const base = `https://nodejs.org/dist/v${NODE_VERSION}`
const [response, sums] = await Promise.all([fetch(`${base}/${filename}`), fetch(`${base}/SHASUMS256.txt`)])
if (!response.ok) throw new Error(`Failed to download Node ${NODE_VERSION}: ${response.status}`)
if (!sums.ok) throw new Error(`Failed to download Node ${NODE_VERSION} checksums: ${sums.status}`)
const data = new Uint8Array(await response.arrayBuffer())
const expected = (await sums.text())
.split("\n")
.find((line) => line.endsWith(` ${filename}`))
?.split(/\s+/)[0]
if (!expected) throw new Error(`Missing checksum for ${filename}`)
if (createHash("sha256").update(data).digest("hex") !== expected) throw new Error(`Checksum mismatch for ${filename}`)
await writeFile(archive, data)
const temporary = path.join(cache, `${archiveName}.${process.pid}.tmp`)
await rm(temporary, { recursive: true, force: true })
await mkdir(temporary)
if (target.platform !== "win32") run("tar", ["-xzf", archive, "-C", temporary])
else if (process.platform === "win32") run("tar", ["-xf", archive, "-C", temporary])
else run("unzip", ["-q", archive, "-d", temporary])
await rm(targetDirectory, { recursive: true, force: true })
await rename(path.join(temporary, archiveName), targetDirectory)
await writeFile(path.join(targetDirectory, ".verified"), `${expected}\n`)
await rm(temporary, { recursive: true, force: true })
await rm(archive, { force: true })
return realpath(executable)
}
async function smoke(output: string) {
const root = await mkdtemp(path.join(os.tmpdir(), "opencode-node-smoke-"))
const executable = path.join(root, path.basename(output))
await copyFile(output, executable)
if (process.platform !== "win32") await chmod(executable, 0o755)
run(executable, ["--version"], root)
run(executable, ["--help"], root)
await rm(root, { recursive: true, force: true })
}
function targetName(target: NodeTarget) {
return `${target.platform === "win32" ? "windows" : target.platform}-${target.arch}`
}
function run(command: string, args: readonly string[], cwd = dir) {
const result = spawnSync(command, args, { cwd, stdio: "inherit", env: process.env })
if (result.error) throw result.error
if (result.status !== 0) throw new Error(`${command} exited with status ${result.status ?? "unknown"}`)
}
+9 -13
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env bun
import { $ } from "bun"
import fs from "fs"
import { rm } from "fs/promises"
import path from "path"
import { Script } from "@opencode-ai/script"
@@ -11,9 +10,14 @@ import { modelsData } from "./generate"
const dir = path.resolve(import.meta.dirname, "..")
const binary = "opencode2"
const outdir = path.resolve(
dir,
process.argv.find((arg) => arg.startsWith("--outdir="))?.slice("--outdir=".length) ?? "dist",
)
if (outdir === dir) throw new Error("--outdir must not be the package directory")
process.chdir(dir)
await rm("dist", { recursive: true, force: true })
await rm(outdir, { recursive: true, force: true })
const singleFlag = process.argv.includes("--single")
const baselineFlag = process.argv.includes("--baseline")
@@ -50,10 +54,6 @@ const targets = singleFlag
if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
const localParserWorker = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
const rootParserWorker = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
const parserWorker = fs.realpathSync(fs.existsSync(localParserWorker) ? localParserWorker : rootParserWorker)
for (const item of targets) {
const target = [
binary,
@@ -67,7 +67,7 @@ for (const item of targets) {
const name = target.replace(binary, "cli")
console.log(`building ${name}`)
const result = await Bun.build({
entrypoints: ["./src/index.ts", parserWorker],
entrypoints: ["./src/index.ts"],
tsconfig: "./tsconfig.json",
plugins: [plugin],
external: ["node-gyp"],
@@ -81,7 +81,7 @@ for (const item of targets) {
autoloadTsconfig: true,
autoloadPackageJson: true,
target: target.replace(binary, "bun") as Bun.Build.CompileTarget,
outfile: `./dist/${name}/bin/${binary}`,
outfile: path.join(outdir, name, "bin", binary),
execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--"],
windows: {},
},
@@ -93,10 +93,6 @@ for (const item of targets) {
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
// FFF_LIBC selects the fff native lib variant: "musl" or "gnu".
FFF_LIBC: item.os === "linux" ? `'${item.abi ?? "gnu"}'` : "undefined",
OTUI_TREE_SITTER_WORKER_PATH:
(item.os === "win32" ? '"B:/~BUN/root/' : '"/$bunfs/root/') +
path.relative(dir, parserWorker).replaceAll("\\", "/") +
'"',
...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}),
},
})
@@ -107,7 +103,7 @@ for (const item of targets) {
}
await Bun.write(
`./dist/${name}/package.json`,
path.join(outdir, name, "package.json"),
JSON.stringify(
{
name: `@opencode-ai/${name}`,
+3 -1
View File
@@ -1,7 +1,9 @@
import { readFile } from "node:fs/promises"
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
export const modelsData = process.env.MODELS_DEV_API_JSON
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
? await readFile(process.env.MODELS_DEV_API_JSON, "utf8")
: await fetch(`${modelsUrl}/api.json`).then((response) => response.text())
console.log("Loaded models.dev snapshot")
+83
View File
@@ -0,0 +1,83 @@
import { createHash } from "node:crypto"
import { copyFile, mkdir, readdir, readFile, stat } from "node:fs/promises"
import { createRequire } from "node:module"
import path from "node:path"
import { fileURLToPath } from "node:url"
import { getNodeAssets } from "@opentui/core/node-assets"
import { attentionSoundAssets, type NodeTarget, photonWasmAsset } from "../src/node/target"
const dir = path.resolve(import.meta.dirname, "..")
// Bun's compiler discovers file imports and embeds them in its virtual filesystem. Vite only bundles the JavaScript
// portion of the Node executable, while SEA embeds only the assets explicitly listed in its build configuration.
// Collect and stage those files under stable keys so the SEA prelude can extract them to real paths at startup;
// native addons, helper executables, and other path-based consumers cannot use assets directly from SEA memory.
export type NodeAsset = {
readonly key: string
readonly source: string
}
async function files(root: string, current = root): Promise<string[]> {
return (
await Promise.all(
(await readdir(current, { withFileTypes: true })).map((entry) => {
const target = path.join(current, entry.name)
return entry.isDirectory() ? files(root, target) : [path.relative(root, target)]
}),
)
).flat()
}
export async function collectNodeAssets(target: NodeTarget) {
const ptyEntry = fileURLToPath(import.meta.resolve(target.nodePtyPackage))
const ptyRoot = path.resolve(path.dirname(ptyEntry), "..")
const assets: NodeAsset[] = [
...getNodeAssets({
platform: target.platform,
arch: target.arch,
...(target.platform === "linux" ? { libc: "glibc" as const } : {}),
}),
{ key: target.parcelWatcherAsset, source: fileURLToPath(import.meta.resolve(target.parcelWatcherPackage)) },
{
key: photonWasmAsset,
source: createRequire(path.resolve(dir, "../core/package.json")).resolve(photonWasmAsset),
},
...attentionSoundAssets.map((key) => ({
key,
source: path.resolve(dir, "../ui/src/assets/audio", path.basename(key)),
})),
...(await files(ptyRoot))
.filter((relative) => !relative.endsWith(".map") && !relative.endsWith(".pdb"))
.map((relative) => ({
key: `${target.nodePtyPackage}/${relative}`,
source: path.join(ptyRoot, relative),
})),
]
await Promise.all(assets.map((asset) => stat(asset.source)))
return assets
}
export async function hashNodeAssets(assets: readonly NodeAsset[]) {
const hash = createHash("sha256")
for (const asset of assets.toSorted((left, right) => left.key.localeCompare(right.key))) {
hash.update(asset.key)
hash.update(await readFile(asset.source))
}
return hash.digest("hex").slice(0, 16)
}
export async function copyNodeAssets(assets: readonly NodeAsset[]) {
const root = path.join(dir, "dist-node", "assets")
await Promise.all(
assets.map(async (asset) => {
const target = path.join(root, asset.key)
await mkdir(path.dirname(target), { recursive: true })
await copyFile(asset.source, target)
}),
)
}
export async function seaAssetMap() {
const root = path.join(dir, "dist-node", "assets")
return Object.fromEntries((await files(root)).map((key) => [key.replaceAll(path.sep, "/"), path.join(root, key)]))
}
+50 -32
View File
@@ -18,37 +18,55 @@ async function publish(dir: string, name: string, version: string) {
await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir)
}
const binaries: Record<string, string> = {}
for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) {
const item = await Bun.file(`./dist/${filepath}`).json()
binaries[item.name] = item.version
async function publishDistribution(input: { root: string; name: string; binary: string; packagePrefix: string }) {
const binaries: Record<string, string> = {}
for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: input.root })) {
const item = await Bun.file(`${input.root}/${filepath}`).json()
if (!item.name.startsWith(input.packagePrefix)) continue
binaries[item.name] = item.version
}
console.log(input.name, "binaries", binaries)
const versions = new Set(Object.values(binaries))
if (versions.size > 1) throw new Error(`Binary package versions do not match for ${input.name}`)
const version = versions.values().next().value
if (!version) throw new Error(`No binary packages found for ${input.name}`)
await $`mkdir -p ${input.root}/${input.name}/bin`
await $`cp ./bin/opencode2.cjs ${input.root}/${input.name}/bin/${input.binary}`
await Bun.file(`${input.root}/${input.name}/package.json`).write(
JSON.stringify(
{
name: input.name,
bin: { [input.binary]: `./bin/${input.binary}` },
version,
license: pkg.license,
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
os: ["darwin", "linux", "win32"],
cpu: ["arm64", "x64"],
optionalDependencies: binaries,
},
null,
2,
),
)
await Promise.all(
Object.entries(binaries).map(([name, version]) =>
publish(`${input.root}/${name.replace("@opencode-ai/", "")}`, name, version),
),
)
await publish(`${input.root}/${input.name}`, input.name, version)
}
console.log("binaries", binaries)
const version = Object.values(binaries)[0]
const name = pkg.name
await $`mkdir -p ./dist/${name}/bin`
await $`cp ./bin/opencode2.cjs ./dist/${name}/bin/opencode2`
await Bun.file(`./dist/${name}/package.json`).write(
JSON.stringify(
{
name,
bin: { opencode2: "./bin/opencode2" },
version,
license: pkg.license,
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
os: ["darwin", "linux", "win32"],
cpu: ["arm64", "x64"],
optionalDependencies: binaries,
},
null,
2,
),
)
await Promise.all(
Object.entries(binaries).map(([name, version]) =>
publish(`./dist/${name.replace("@opencode-ai/", "")}`, name, version),
),
)
await publish(`./dist/${name}`, name, version)
await publishDistribution({
root: "./dist",
name: pkg.name,
binary: "opencode2",
packagePrefix: "@opencode-ai/cli-",
})
await publishDistribution({
root: "./dist/node",
name: "opencode2-node",
binary: "opencode2-node",
packagePrefix: "@opencode-ai/cli-node-",
})
+4 -3
View File
@@ -7,9 +7,10 @@ import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
const target = `cli-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`
const directory = path.join(import.meta.dir, "..", "dist", target, "bin")
const binary = path.join(directory, `opencode2${process.platform === "win32" ? ".exe" : ""}`)
const nodeBuild = process.argv.includes("--node")
const target = `cli${nodeBuild ? "-node" : ""}-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`
const directory = path.join(import.meta.dir, "..", "dist", ...(nodeBuild ? ["node"] : []), target, "bin")
const binary = path.join(directory, `opencode2${nodeBuild ? "-node" : ""}${process.platform === "win32" ? ".exe" : ""}`)
if (!(await Bun.file(binary).exists())) throw new Error(`Missing compiled CLI in ${directory}`)
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-smoke-"))
+12 -4
View File
@@ -66,6 +66,17 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
}),
],
}),
Spec.make("auth", {
description: "Manage authentication",
commands: [
Spec.make("connect", {
description: "Connect to a wellknown authentication provider",
params: {
url: Argument.string("url").pipe(Argument.withDescription("Wellknown provider URL")),
},
}),
],
}),
Spec.make("mcp", {
description: "Manage MCP (Model Context Protocol) servers",
commands: [
@@ -178,10 +189,7 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
Flag.atMost(100),
),
title: Flag.string("title").pipe(Flag.withDescription("Session title"), Flag.optional),
thinking: Flag.boolean("thinking").pipe(
Flag.withDescription("Show thinking blocks"),
Flag.withDefault(false),
),
thinking: Flag.boolean("thinking").pipe(Flag.withDescription("Show thinking blocks"), Flag.withDefault(false)),
auto: Flag.boolean("auto").pipe(
Flag.withDescription("Auto-approve permissions that are not explicitly denied"),
Flag.withDefault(false),
@@ -0,0 +1,52 @@
import { EOL } from "node:os"
import { Effect } from "effect"
import { Service } from "@opencode-ai/client/effect/service"
import { OpenCode, type IntegrationCommandStatusOutput, type OpenCodeClient } from "@opencode-ai/client/promise"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { ServiceConfig } from "../../../services/service-config"
const location = { directory: process.cwd() }
export default Runtime.handler(
Commands.commands.auth.commands.connect,
Effect.fn("cli.auth.connect")(function* (input) {
process.stdout.write("Connecting..." + EOL + EOL)
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
yield* request(() => client.integration.wellknown.add({ url: input.url, location }))
const integrationID = input.url.replace(/\/+$/, "")
const started = yield* request(() =>
client.integration.command.connect({ integrationID, methodID: "login", location }),
)
yield* Effect.addFinalizer(() =>
request(() =>
client.integration.command.cancel({ integrationID, attemptID: started.data.attemptID, location }),
).pipe(Effect.ignore),
)
const status = yield* wait(client, integrationID, started.data.attemptID)
if (status.status === "failed") return yield* Effect.fail(new Error(status.message))
if (status.status === "expired") return yield* Effect.fail(new Error("Authentication expired"))
process.stdout.write("Connected" + EOL)
}),
)
const wait = (
client: OpenCodeClient,
integrationID: string,
attemptID: string,
shown = false,
): Effect.Effect<Exclude<IntegrationCommandStatusOutput["data"], { status: "pending" }>, unknown> =>
Effect.gen(function* () {
const response = yield* request(() => client.integration.command.status({ integrationID, attemptID, location }))
if (response.data.status !== "pending") return response.data
const output = response.data.message?.trim()
if (!shown && output) process.stdout.write(output + EOL + EOL)
yield* Effect.sleep(500)
return yield* wait(client, integrationID, attemptID, shown || !!output)
})
function request<A>(task: () => Promise<A>) {
return Effect.tryPromise({ try: task, catch: (cause) => cause })
}
+14 -5
View File
@@ -1,5 +1,6 @@
import { EOL } from "node:os"
import path from "node:path"
import { readFile, stat, writeFile } from "node:fs/promises"
import { Effect, Option } from "effect"
import { applyEdits, modify } from "jsonc-parser"
import { Global } from "@opencode-ai/core/global"
@@ -35,7 +36,7 @@ export default Runtime.handler(
}),
)
async function resolveConfigPath(directory: string) {
export async function resolveConfigPath(directory: string) {
const candidates = [
path.join(directory, "opencode.json"),
path.join(directory, "opencode.jsonc"),
@@ -43,16 +44,24 @@ async function resolveConfigPath(directory: string) {
path.join(directory, ".opencode", "opencode.jsonc"),
]
for (const candidate of candidates) {
if (await Bun.file(candidate).exists()) return candidate
if (
await stat(candidate).then(
(info) => info.isFile(),
() => false,
)
)
return candidate
}
return candidates[0]
}
async function write(configPath: string, name: string, server: unknown) {
const file = Bun.file(configPath)
const text = (await file.exists()) ? await file.text() : "{}"
const text = await readFile(configPath, "utf8").catch((error) => {
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return "{}"
throw error
})
const edits = modify(text, ["mcp", "servers", name], server, {
formattingOptions: { tabSize: 2, insertSpaces: true },
})
await Bun.write(configPath, applyEdits(text, edits))
await writeFile(configPath, applyEdits(text, edits))
}
+3
View File
@@ -17,6 +17,9 @@ import { Npm } from "@opencode-ai/core/npm"
const Handlers = Runtime.handlers(Commands, {
$: () => import("./commands/handlers/default"),
api: () => import("./commands/handlers/api"),
auth: {
connect: () => import("./commands/handlers/auth/connect"),
},
debug: {
agents: () => import("./commands/handlers/debug/agents"),
},
+4 -3
View File
@@ -4,6 +4,7 @@ import { useTerminalDimensions } from "@opentui/solid"
import { createEffect, createMemo, createSignal, type Accessor } from "solid-js"
import { transparent, type RunFooterTheme } from "./theme"
import { Locale } from "@opencode-ai/tui/util/locale"
import { stringWidth } from "@opencode-ai/tui/util/string-width"
export const FOOTER_MENU_ROWS = 8
@@ -196,7 +197,7 @@ export function RunFooterMenu(props: {
...props
.items()
.filter((item) => item.description)
.map((item) => Bun.stringWidth(item.display)),
.map((item) => stringWidth(item.display)),
)
return width === 0 ? 0 : width + 2
})
@@ -205,14 +206,14 @@ export function RunFooterMenu(props: {
return ""
}
return " ".repeat(Math.max(1, descriptionColumn() - Bun.stringWidth(item.display)))
return " ".repeat(Math.max(1, descriptionColumn() - stringWidth(item.display)))
}
const descriptionText = (item: RunFooterMenuItem) => {
if (!item.description) {
return
}
const footerWidth = item.footer ? Bun.stringWidth(item.footer) + 1 : 0
const footerWidth = item.footer ? stringWidth(item.footer) + 1 : 0
const available =
term().width -
(border() ? 1 : 0) -
+9 -12
View File
@@ -5,14 +5,15 @@
// It produces a PromptState that RunPromptBody renders as a slim single-line
// composer while the footer view renders any active menus below it.
/** @jsxImportSource @opentui/solid */
import { pathToFileURL } from "bun"
import { StyledText, fg, type ColorInput, type KeyEvent, type TextareaRenderable } from "@opentui/core"
import { useRenderer } from "@opentui/solid"
import { normalizePromptContent } from "@opencode-ai/tui/prompt/content"
import fuzzysort from "fuzzysort"
import path from "path"
import { pathToFileURL } from "node:url"
import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
import { Locale } from "@opencode-ai/tui/util/locale"
import { stringWidth } from "@opencode-ai/tui/util/string-width"
import {
createPromptHistory,
displayCharAt,
@@ -602,7 +603,7 @@ export function createPromptState(input: PromptInput): PromptState {
})
}
const restore = (value: RunPrompt, cursor = Bun.stringWidth(value.text)) => {
const restore = (value: RunPrompt, cursor = stringWidth(value.text)) => {
draft = clonePrompt(value)
setShell(value.mode === "shell")
if (!area || area.isDestroyed) {
@@ -612,7 +613,7 @@ export function createPromptState(input: PromptInput): PromptState {
hide()
area.setText(value.text)
restoreParts(value.parts)
area.cursorOffset = Math.min(cursor, Bun.stringWidth(area.plainText))
area.cursorOffset = Math.min(cursor, stringWidth(area.plainText))
scheduleRows()
area.focus()
}
@@ -643,7 +644,7 @@ export function createPromptState(input: PromptInput): PromptState {
area.setText(text)
clearParts()
draft = shell() ? { text: area.plainText, parts: [], mode: "shell" } : { text: area.plainText, parts: [] }
area.cursorOffset = Math.min(Bun.stringWidth(text), Bun.stringWidth(area.plainText))
area.cursorOffset = Math.min(stringWidth(text), stringWidth(area.plainText))
scheduleRows()
area.focus()
}
@@ -777,7 +778,7 @@ export function createPromptState(input: PromptInput): PromptState {
if (move(dir, event)) return
if (!area || area.isDestroyed) return false
const endOffset = Bun.stringWidth(area.plainText)
const endOffset = stringWidth(area.plainText)
if (dir === -1) {
if (area.cursorOffset === 0) return false
if (area.visualCursor.visualRow === 0) {
@@ -886,16 +887,12 @@ export function createPromptState(input: PromptInput): PromptState {
area.cursorOffset = 0
const start = area.logicalCursor
area.cursorOffset =
shell() || !head
? cursor
: local
? Bun.stringWidth(area.plainText)
: Bun.stringWidth(area.plainText.slice(0, head.end))
shell() || !head ? cursor : local ? stringWidth(area.plainText) : stringWidth(area.plainText.slice(0, head.end))
const end = area.logicalCursor
area.deleteRange(start.row, start.col, end.row, end.col)
area.insertText(text)
area.cursorOffset = Bun.stringWidth(text)
area.cursorOffset = stringWidth(text)
hide()
syncDraft()
if (!shell()) {
@@ -920,7 +917,7 @@ export function createPromptState(input: PromptInput): PromptState {
const text = "@" + next.value
const startOffset = at()
const endOffset = startOffset + Bun.stringWidth(text)
const endOffset = startOffset + stringWidth(text)
const part = structuredClone(next.part)
if (part.type === "agent") {
part.source = {
+4 -2
View File
@@ -4,6 +4,8 @@ import { ServerConnection } from "../services/server-connection"
import { waitForCatalogReady } from "./catalog.shared"
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./runtime.stdin"
import type { RunInput, RunTuiConfig } from "./types"
import { readStdin } from "../util/io"
import { setTimeout } from "node:timers/promises"
export type MiniCommandInput = {
server: ServerConnection.Resolved
@@ -22,7 +24,7 @@ export type MiniCommandInput = {
type Session = Awaited<ReturnType<OpenCodeClient["session"]["get"]>>
export async function runMini(input: MiniCommandInput) {
validate(input)
const initialInput = mergeInput(process.stdin.isTTY ? undefined : await Bun.stdin.text(), input.prompt)
const initialInput = mergeInput(process.stdin.isTTY ? undefined : await readStdin(), input.prompt)
const runtimeTask = import("./runtime")
const directory = localDirectory()
@@ -123,7 +125,7 @@ async function validateAgent(sdk: OpenCodeClient, directory: string, name?: stri
return
}
if (agent) return name
await Bun.sleep(25)
await setTimeout(25)
}
if (!agents) {
warning("failed to list agents. Falling back to default agent")
+3 -2
View File
@@ -1,6 +1,7 @@
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
import { SessionMessage } from "@opencode-ai/schema/session-message"
import { EOL } from "node:os"
import { readFile } from "node:fs/promises"
import { UI } from "./ui"
import type { MiniToolPart } from "./types"
@@ -478,11 +479,11 @@ async function prepareFile(file: File) {
if (file.mime !== "text/plain") {
const uri = file.url.startsWith("data:")
? file.url
: `data:${file.mime};base64,${Buffer.from(await Bun.file(new URL(file.url)).arrayBuffer()).toString("base64")}`
: `data:${file.mime};base64,${(await readFile(new URL(file.url))).toString("base64")}`
return { attachment: { uri, mime: file.mime, name: file.filename } }
}
const content = file.url.startsWith("data:")
? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8")
: await Bun.file(new URL(file.url)).text()
: await readFile(new URL(file.url), "utf8")
return { text: `<file name="${file.filename}">\n${content}\n</file>` }
}
+4 -3
View File
@@ -8,6 +8,7 @@
// the current draft is saved and history begins. Arrowing past the end
// restores the draft.
export { displayCharAt, displaySlice, mentionTriggerIndex } from "@opencode-ai/tui/prompt/display"
import { stringWidth } from "@opencode-ai/tui/util/string-width"
import type { RunPrompt } from "./types"
const HISTORY_LIMIT = 200
@@ -102,7 +103,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text:
return { state, apply: false }
}
if (dir === 1 && cursor !== Bun.stringWidth(text)) {
if (dir === 1 && cursor !== stringWidth(text)) {
return { state, apply: false }
}
@@ -136,7 +137,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text:
index: null,
},
text: state.draft,
cursor: Bun.stringWidth(state.draft),
cursor: stringWidth(state.draft),
apply: true,
}
}
@@ -147,7 +148,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text:
index: idx,
},
text: state.items[idx].text,
cursor: dir === -1 ? 0 : Bun.stringWidth(state.items[idx].text),
cursor: dir === -1 ? 0 : stringWidth(state.items[idx].text),
apply: true,
}
}
+2 -1
View File
@@ -4,6 +4,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { Model } from "@opencode-ai/schema/model"
import { open } from "node:fs/promises"
import path from "node:path"
import { readStdin } from "../util/io"
import { ServerConnection } from "../services/server-connection"
import { loadRunAgents, waitForCatalogReady } from "./catalog.shared"
import { runNonInteractivePrompt } from "./noninteractive"
@@ -48,7 +49,7 @@ async function run(input: RunCommandInput) {
if (input.fork && !input.continue && !input.session) fail("--fork requires --continue or --session")
const root = process.env.PWD ?? process.cwd()
const directory = localDirectory(root)
const message = mergeInput(formatMessage(input.message), process.stdin.isTTY ? undefined : await Bun.stdin.text())
const message = mergeInput(formatMessage(input.message), process.stdin.isTTY ? undefined : await readStdin())
if (!message?.trim()) fail("You must provide a message")
const files = await Promise.all(input.file.map((file) => prepareFile(file, root)))
const prepared = { directory, message, files }
+2 -1
View File
@@ -1,3 +1,4 @@
import { readFile } from "node:fs/promises"
import type {
EventSubscribeOutput,
OpenCodeClient,
@@ -161,7 +162,7 @@ async function prepareFile(file: RunFilePart) {
if (file.mime !== "text/plain") return { attachment: { uri: file.url, name: file.filename } }
const content = file.url.startsWith("data:")
? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8")
: await Bun.file(new URL(file.url)).text()
: await readFile(new URL(file.url), "utf8")
return { text: `<file name="${file.filename}">\n${content}\n</file>` }
}
+3
View File
@@ -0,0 +1,3 @@
import "./plugin-runtime.promise"
import "./plugin-runtime.effect"
import "../index"
@@ -0,0 +1,28 @@
import {
Agent,
Command,
Connection,
Credential,
Integration,
Model,
Plugin,
Provider,
Reference,
Skill,
} from "@opencode-ai/plugin/v2/effect"
import { Tool } from "@opencode-ai/plugin/v2/effect/tool"
const key = Symbol.for("opencode.plugin.v2.effect")
;(globalThis as typeof globalThis & { [key]?: unknown })[key] = {
Agent,
Command,
Connection,
Credential,
Integration,
Model,
Plugin,
Provider,
Reference,
Skill,
Tool,
}
@@ -0,0 +1,26 @@
import {
Agent,
Command,
Connection,
Credential,
Integration,
Model,
Plugin,
Provider,
Reference,
Skill,
} from "@opencode-ai/plugin/v2"
const key = Symbol.for("opencode.plugin.v2.promise")
;(globalThis as typeof globalThis & { [key]?: unknown })[key] = {
Agent,
Command,
Connection,
Credential,
Integration,
Model,
Plugin,
Provider,
Reference,
Skill,
}
+34
View File
@@ -0,0 +1,34 @@
const platforms = ["darwin", "linux", "win32"] as const
export type NodeTarget = ReturnType<typeof nodeTarget>
export function nodeTarget(platform: string, arch: string) {
if (!platforms.includes(platform as (typeof platforms)[number]) || (arch !== "arm64" && arch !== "x64")) {
throw new Error(`Unsupported Node executable target: ${platform}-${arch}`)
}
const targetPlatform = platform as (typeof platforms)[number]
const targetArch = arch as "arm64" | "x64"
const nodePtyPackage = `@lydell/node-pty-${targetPlatform}-${targetArch}`
const parcelWatcherPackage = `@parcel/watcher-${targetPlatform}-${targetArch}${targetPlatform === "linux" ? "-glibc" : ""}`
return {
platform: targetPlatform,
arch: targetArch,
nodePtyPackage,
nodePtyEntryAsset: `${nodePtyPackage}/lib/index.js`,
parcelWatcherPackage,
parcelWatcherAsset: `${parcelWatcherPackage}/watcher.node`,
}
}
export const photonWasmAsset = "@silvia-odwyer/photon-node/photon_rs_bg.wasm"
export const nodeExecArgv = ["--experimental-ffi", "--use-system-ca", "--disable-warning=ExperimentalWarning"] as const
export const attentionSoundAssets = [
"@opencode-ai/ui/audio/bip-bop-01.mp3",
"@opencode-ai/ui/audio/bip-bop-03.mp3",
"@opencode-ai/ui/audio/staplebops-06.mp3",
"@opencode-ai/ui/audio/nope-03.mp3",
"@opencode-ai/ui/audio/yup-01.mp3",
] as const
+2 -4
View File
@@ -5,6 +5,7 @@ import { Service } from "@opencode-ai/client/effect/service"
import { Effect, FileSystem, Option, Schema } from "effect"
import { randomBytes } from "crypto"
import path from "path"
import { selfCommand } from "../util/process"
// The CLI's service configuration file, plus the Service.EnsureOptions binding that
// points the client package's service operations at this CLI: which
@@ -78,13 +79,10 @@ const paths = Effect.gen(function* () {
export const options = Effect.fnUntraced(function* () {
const { file, legacyFile } = yield* paths
yield* migrateRegistration(legacyFile, file)
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
const entrypoint = compiled ? undefined : process.argv[1]
if (!compiled && entrypoint === undefined) return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint"))
return {
file,
version: InstallationVersion,
command: [process.execPath, ...(entrypoint ? [entrypoint] : []), "serve", "--service"],
command: [...selfCommand(), "serve", "--service"],
}
})
+2 -5
View File
@@ -4,7 +4,7 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Effect, Schema, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { randomBytes } from "node:crypto"
import path from "node:path"
import { selfCommand } from "../util/process"
const Ready = Schema.Struct({ url: Schema.String })
const decodeReady = Schema.decodeUnknownPromise(Schema.fromJsonString(Ready))
@@ -14,10 +14,7 @@ type Options = {
}
function command(password: string, options: Options) {
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
const entrypoint = compiled ? [] : process.argv[1] ? [process.argv[1]] : []
if (!compiled && entrypoint.length === 0) throw new Error("Failed to resolve CLI entrypoint")
const [executable, ...args] = options.command ?? [process.execPath, ...entrypoint, "serve"]
const [executable, ...args] = options.command ?? [...selfCommand(), "serve"]
if (!executable) throw new Error("Failed to resolve standalone server command")
return ChildProcess.make(executable, [...args, "--stdio", "--port", "0"], {
cwd: process.cwd(),
@@ -7,6 +7,7 @@ import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { registerOpencodeSpinner } from "@opencode-ai/tui/component/register-spinner"
import { SPINNER_FRAMES } from "@opencode-ai/tui/component/spinner"
import { go } from "@opencode-ai/tui/logo"
import { setTimeout } from "node:timers/promises"
import {
batch,
createEffect,
@@ -123,7 +124,7 @@ async function open(from?: string): Promise<Session> {
let shownAt = performance.now()
const waitForStage = async () => {
const remaining = stageFloor - (performance.now() - shownAt)
if (remaining > 0) await Bun.sleep(remaining)
if (remaining > 0) await setTimeout(remaining)
}
const advance = async (stage: number) => {
await waitForStage()
@@ -140,11 +141,11 @@ async function open(from?: string): Promise<Session> {
setOutcome(next)
const completed = await Promise.race([
settled.promise.then(() => true),
Bun.sleep(transitionDuration + 500).then(() => false),
setTimeout(transitionDuration + 500).then(() => false),
])
resolveOutcome = undefined
setAnimating(false)
if (completed) await Bun.sleep(hold)
if (completed) await setTimeout(hold)
}
let closing: Promise<void> | undefined
let transferred = false
@@ -154,7 +155,7 @@ async function open(from?: string): Promise<Session> {
setAnimating(false)
if (renderer.isDestroyed) return
renderer.pause()
await Promise.race([renderer.idle(), Bun.sleep(500)])
await Promise.race([renderer.idle(), setTimeout(500)])
renderer.destroy()
})())
let loading: Promise<void> | undefined
@@ -178,7 +179,7 @@ async function open(from?: string): Promise<Session> {
renderer.screenMode = "alternate-screen"
renderer.consoleMode = "console-overlay"
renderer.requestRender()
await Promise.race([renderer.idle(), Bun.sleep(500)])
await Promise.race([renderer.idle(), setTimeout(500)])
transferred = true
return {
renderer,
+6 -1
View File
@@ -12,11 +12,16 @@ import { parse, type ParseError } from "jsonc-parser"
import path from "node:path"
import semver from "semver"
declare const OPENCODE_CLI_NAME: string | undefined
export type Policy = boolean | "notify"
export type Action = "none" | "upgrade"
type Method = "npm" | "pnpm" | "bun" | "yarn"
const packageName = "@opencode-ai/cli"
const packageName =
typeof OPENCODE_CLI_NAME === "string" && OPENCODE_CLI_NAME === "opencode2-node"
? OPENCODE_CLI_NAME
: "@opencode-ai/cli"
export interface Interface {
readonly check: () => Effect.Effect<void>
+5
View File
@@ -0,0 +1,5 @@
import { text } from "node:stream/consumers"
export function readStdin() {
return text(process.stdin)
}
+26
View File
@@ -0,0 +1,26 @@
import path from "node:path"
export function selfCommand() {
const runtime = path.basename(process.execPath, path.extname(process.execPath)).toLowerCase()
if (runtime !== "bun" && runtime !== "node" && runtime !== "nodejs") return [process.execPath]
if (!process.argv[1]) throw new Error("Failed to resolve CLI entrypoint")
if (runtime === "node" || runtime === "nodejs") return [process.execPath, ...nodeFlags(), process.argv[1]]
return [process.execPath, process.argv[1]]
}
function nodeFlags() {
return process.execArgv.flatMap((arg, index, args) => {
if (index > 0 && args[index - 1] === "--conditions") return []
if (arg === "--conditions") return args[index + 1] ? [arg, args[index + 1]] : []
if (arg.startsWith("--conditions=")) return [arg]
if (
arg === "--experimental-ffi" ||
arg === "--use-system-ca" ||
arg === "--enable-source-maps" ||
arg === "--no-addons"
)
return [arg]
if (arg === "--no-warnings" || arg.startsWith("--disable-warning=")) return [arg]
return []
})
}
+2 -1
View File
@@ -6,5 +6,6 @@
"jsxImportSource": "@opentui/solid",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"noUncheckedIndexedAccess": false
}
},
"exclude": ["dist", "dist-node"]
}
+220
View File
@@ -0,0 +1,220 @@
import path from "node:path"
import { readFile } from "node:fs/promises"
import { createRequire } from "node:module"
import { defineConfig, type Plugin, type UserConfig } from "vite"
import solid from "vite-plugin-solid"
import { nodeExecArgv, nodeTarget, type NodeTarget, photonWasmAsset } from "./src/node/target"
const dir = import.meta.dirname
function rawTextPlugin(): Plugin {
return {
name: "opencode:raw-text",
async load(id) {
if (!id.endsWith(".md")) return
return `export default ${JSON.stringify(await readFile(id, "utf8"))}`
},
}
}
function runtimeRequirePlugin(): Plugin {
return {
name: "opencode:runtime-require",
enforce: "pre",
transform(code, id) {
if (!id.endsWith("turndown/lib/turndown.es.js")) return
const transformed = code.replace(" var domino = require('@mixmark-io/domino');", "")
if (transformed === code) this.error("Failed to rewrite Turndown's Domino require")
return `import domino from "@mixmark-io/domino"\n${transformed}`
},
}
}
const resolve = {
alias: [
{ find: /^solid-js\/store$/, replacement: "solid-js/store/dist/store.js" },
{ find: /^solid-js$/, replacement: "solid-js/dist/solid.js" },
{
find: /^ws$/,
replacement: path.join(path.dirname(createRequire(import.meta.url).resolve("ws/package.json")), "wrapper.mjs"),
},
],
conditions: ["node"],
}
const output = (entryFileNames: string, banner?: string) => ({
format: "esm" as const,
entryFileNames,
inlineDynamicImports: true,
banner,
})
function nodePrelude(input: NodeBuildInput) {
const nodePtySpawnHelper =
input.target.platform === "darwin"
? `${input.target.nodePtyPackage}/prebuilds/darwin-${input.target.arch}/spawn-helper`
: undefined
const promiseModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")]
if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable")
export const Agent = sdk.Agent
export const Command = sdk.Command
export const Connection = sdk.Connection
export const Credential = sdk.Credential
export const Integration = sdk.Integration
export const Model = sdk.Model
export const Plugin = sdk.Plugin
export const Provider = sdk.Provider
export const Reference = sdk.Reference
export const Skill = sdk.Skill`
const effectModule = promiseModule
.replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
.replace("Promise plugin", "Effect plugin")
const promisePluginModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")]
if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable")
export const define = sdk.Plugin.define`
const effectPluginModule = promisePluginModule
.replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
.replace("Promise plugin", "Effect plugin")
const effectToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.effect")]
if (!sdk) throw new Error("OpenCode Effect plugin SDK is unavailable")
export const Tool = sdk.Tool
export const Failure = sdk.Tool.Failure
export const RegistrationError = sdk.Tool.RegistrationError
export const make = sdk.Tool.make
export const validateName = sdk.Tool.validateName
export const registrationEntries = sdk.Tool.registrationEntries
export const withPermission = sdk.Tool.withPermission
export const permission = sdk.Tool.permission
export const definition = sdk.Tool.definition
export const settle = sdk.Tool.settle`
return `#!/usr/bin/env -S node ${nodeExecArgv.join(" ")}
import __cjs_mod__ from "node:module"
import { chmodSync as __ocChmod, existsSync as __ocExists, lstatSync as __ocLstat, mkdirSync as __ocMkdir, renameSync as __ocRename, rmSync as __ocRm, writeFileSync as __ocWrite } from "node:fs"
import { tmpdir as __ocTmpdir } from "node:os"
import __ocPath from "node:path"
import { getAssetKeys as __ocAssetKeys, getRawAsset as __ocRawAsset, isSea as __ocIsSea } from "node:sea"
import { fileURLToPath as __ocFileURLToPath } from "node:url"
const __filename = import.meta.filename
const __dirname = import.meta.dirname
const require = __cjs_mod__.createRequire(import.meta.url)
const __ocPluginModules = ${JSON.stringify({
"@opencode-ai/plugin/v2": "opencode:plugin-v2",
"@opencode-ai/plugin/v2/plugin": "opencode:plugin-v2-plugin",
"@opencode-ai/plugin/v2/effect": "opencode:plugin-v2-effect",
"@opencode-ai/plugin/v2/effect/plugin": "opencode:plugin-v2-effect-plugin",
"@opencode-ai/plugin/v2/effect/tool": "opencode:plugin-v2-effect-tool",
})}
const __ocPluginSources = ${JSON.stringify({
"opencode:plugin-v2": promiseModule,
"opencode:plugin-v2-plugin": promisePluginModule,
"opencode:plugin-v2-effect": effectModule,
"opencode:plugin-v2-effect-plugin": effectPluginModule,
"opencode:plugin-v2-effect-tool": effectToolModule,
})}
__cjs_mod__.registerHooks({
resolve(__ocSpecifier, __ocContext, __ocNextResolve) {
const __ocUrl = __ocPluginModules[__ocSpecifier]
return __ocUrl ? { url: __ocUrl, shortCircuit: true } : __ocNextResolve(__ocSpecifier, __ocContext)
},
load(__ocUrl, __ocContext, __ocNextLoad) {
const __ocSource = __ocPluginSources[__ocUrl]
return __ocSource
? { format: "module", source: __ocSource, shortCircuit: true }
: __ocNextLoad(__ocUrl, __ocContext)
},
})
const __ocUid = typeof process.getuid === "function" ? process.getuid() : undefined
const __ocCacheRoot = __ocPath.join(__ocTmpdir(), \`opencode-node-\${__ocUid ?? "user"}\`)
if (__ocIsSea()) {
try {
__ocMkdir(__ocCacheRoot, { mode: 0o700 })
} catch (__ocError) {
if (!__ocExists(__ocCacheRoot)) throw __ocError
}
const __ocCacheInfo = __ocLstat(__ocCacheRoot)
if (!__ocCacheInfo.isDirectory() || __ocCacheInfo.isSymbolicLink()) throw new Error("Unsafe Node asset cache path")
if (__ocUid !== undefined && __ocCacheInfo.uid !== __ocUid) throw new Error("Node asset cache is owned by another user")
if (__ocUid !== undefined) __ocChmod(__ocCacheRoot, 0o700)
}
const __ocAssetRoot = __ocIsSea()
? __ocPath.join(__ocCacheRoot, ${JSON.stringify(`${input.assetHash}-${input.target.platform}-${input.target.arch}`)})
: __ocFileURLToPath(new URL("./assets/", import.meta.url))
if (__ocIsSea()) {
for (const __ocKey of __ocAssetKeys()) {
const __ocTarget = __ocPath.join(__ocAssetRoot, __ocKey)
if (__ocExists(__ocTarget)) continue
__ocMkdir(__ocPath.dirname(__ocTarget), { recursive: true })
const __ocTemporary = \`${"${__ocTarget}"}.${"${process.pid}"}.${"${crypto.randomUUID()}"}.tmp\`
__ocWrite(__ocTemporary, new Uint8Array(__ocRawAsset(__ocKey)))
try {
__ocRename(__ocTemporary, __ocTarget)
} catch (__ocError) {
__ocRm(__ocTemporary, { force: true })
if (!__ocExists(__ocTarget)) throw __ocError
}
}
const __ocPtySpawnHelper = ${JSON.stringify(nodePtySpawnHelper)}
if (__ocPtySpawnHelper) __ocChmod(__ocPath.join(__ocAssetRoot, __ocPtySpawnHelper), 0o755)
}
process.env.OPENCODE_NODE_ASSETS_DIR = __ocAssetRoot
process.env.OTUI_ASSET_ROOT = __ocAssetRoot
process.env.OPENCODE_NODE_PTY_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.nodePtyEntryAsset)})
process.env.OPENCODE_PARCEL_WATCHER_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.parcelWatcherAsset)})
process.env.OPENCODE_PHOTON_WASM_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(photonWasmAsset)})
globalThis.__OPENCODE_PHOTON_WASM_PATH = process.env.OPENCODE_PHOTON_WASM_PATH
if (process.platform === "linux") process.env.OPENTUI_LIBC = "glibc"`
}
export type NodeBuildInput = {
readonly version: string
readonly channel: string
readonly models: string
readonly assetHash: string
readonly target: NodeTarget
}
export function mainConfig(input: NodeBuildInput): UserConfig {
return defineConfig({
root: dir,
plugins: [
rawTextPlugin(),
runtimeRequirePlugin(),
solid({
solid: {
generate: "universal",
moduleName: "@opentui/solid",
},
}),
],
resolve,
esbuild: { jsx: "automatic" },
define: {
OPENCODE_VERSION: JSON.stringify(input.version),
OPENCODE_CLI_NAME: JSON.stringify("opencode2-node"),
OPENCODE_MODELS_DEV: input.models,
OPENCODE_CHANNEL: JSON.stringify(input.channel),
OPENCODE_LIBC: input.target.platform === "linux" ? JSON.stringify("glibc") : "undefined",
FFF_LIBC: input.target.platform === "linux" ? JSON.stringify("gnu") : "undefined",
},
ssr: { noExternal: true },
build: {
ssr: "src/node/index.ts",
target: "node26",
outDir: "dist-node",
emptyOutDir: false,
minify: true,
rollupOptions: {
external: [/^@opencode-ai\/simulation(?:\/|$)/],
output: output("opencode.mjs", nodePrelude(input)),
},
},
})
}
export default mainConfig({
version: process.env.OPENCODE_VERSION ?? "local",
channel: process.env.OPENCODE_CHANNEL ?? "local",
models: "undefined",
assetHash: "local",
target: nodeTarget(process.platform, process.arch),
})
+45 -34
View File
@@ -407,102 +407,113 @@ export type Endpoint10_1Input = {
export type Endpoint10_1Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.get"]>>
export type IntegrationGetOperation<E = never> = (input: Endpoint10_1Input) => Effect.Effect<Endpoint10_1Output, E>
type Endpoint10_2Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
type Endpoint10_2Request = Parameters<RawClient["server.integration"]["integration.wellknown.add"]>[0]
export type Endpoint10_2Input = {
readonly integrationID: Endpoint10_2Request["params"]["integrationID"]
readonly location?: Endpoint10_2Request["query"]["location"]
readonly key: Endpoint10_2Request["payload"]["key"]
readonly label?: Endpoint10_2Request["payload"]["label"]
readonly url: Endpoint10_2Request["payload"]["url"]
}
export type Endpoint10_2Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.key"]>>
export type IntegrationConnectKeyOperation<E = never> = (
export type Endpoint10_2Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.wellknown.add"]>>
export type IntegrationWellknownAddOperation<E = never> = (
input: Endpoint10_2Input,
) => Effect.Effect<Endpoint10_2Output, E>
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.oauth.connect"]>[0]
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
export type Endpoint10_3Input = {
readonly integrationID: Endpoint10_3Request["params"]["integrationID"]
readonly location?: Endpoint10_3Request["query"]["location"]
readonly methodID: Endpoint10_3Request["payload"]["methodID"]
readonly inputs: Endpoint10_3Request["payload"]["inputs"]
readonly key: Endpoint10_3Request["payload"]["key"]
readonly label?: Endpoint10_3Request["payload"]["label"]
}
export type Endpoint10_3Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.connect"]>>
export type IntegrationOauthConnectOperation<E = never> = (
export type Endpoint10_3Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.key"]>>
export type IntegrationConnectKeyOperation<E = never> = (
input: Endpoint10_3Input,
) => Effect.Effect<Endpoint10_3Output, E>
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.oauth.status"]>[0]
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.oauth.connect"]>[0]
export type Endpoint10_4Input = {
readonly integrationID: Endpoint10_4Request["params"]["integrationID"]
readonly attemptID: Endpoint10_4Request["params"]["attemptID"]
readonly location?: Endpoint10_4Request["query"]["location"]
readonly methodID: Endpoint10_4Request["payload"]["methodID"]
readonly inputs: Endpoint10_4Request["payload"]["inputs"]
readonly label?: Endpoint10_4Request["payload"]["label"]
}
export type Endpoint10_4Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.status"]>>
export type IntegrationOauthStatusOperation<E = never> = (
export type Endpoint10_4Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.connect"]>>
export type IntegrationOauthConnectOperation<E = never> = (
input: Endpoint10_4Input,
) => Effect.Effect<Endpoint10_4Output, E>
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.oauth.complete"]>[0]
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.oauth.status"]>[0]
export type Endpoint10_5Input = {
readonly integrationID: Endpoint10_5Request["params"]["integrationID"]
readonly attemptID: Endpoint10_5Request["params"]["attemptID"]
readonly location?: Endpoint10_5Request["query"]["location"]
readonly code?: Endpoint10_5Request["payload"]["code"]
}
export type Endpoint10_5Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.complete"]>>
export type IntegrationOauthCompleteOperation<E = never> = (
export type Endpoint10_5Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.status"]>>
export type IntegrationOauthStatusOperation<E = never> = (
input: Endpoint10_5Input,
) => Effect.Effect<Endpoint10_5Output, E>
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.oauth.cancel"]>[0]
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.oauth.complete"]>[0]
export type Endpoint10_6Input = {
readonly integrationID: Endpoint10_6Request["params"]["integrationID"]
readonly attemptID: Endpoint10_6Request["params"]["attemptID"]
readonly location?: Endpoint10_6Request["query"]["location"]
readonly code?: Endpoint10_6Request["payload"]["code"]
}
export type Endpoint10_6Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.cancel"]>>
export type IntegrationOauthCancelOperation<E = never> = (
export type Endpoint10_6Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.complete"]>>
export type IntegrationOauthCompleteOperation<E = never> = (
input: Endpoint10_6Input,
) => Effect.Effect<Endpoint10_6Output, E>
type Endpoint10_7Request = Parameters<RawClient["server.integration"]["integration.command.connect"]>[0]
type Endpoint10_7Request = Parameters<RawClient["server.integration"]["integration.oauth.cancel"]>[0]
export type Endpoint10_7Input = {
readonly integrationID: Endpoint10_7Request["params"]["integrationID"]
readonly attemptID: Endpoint10_7Request["params"]["attemptID"]
readonly location?: Endpoint10_7Request["query"]["location"]
readonly methodID: Endpoint10_7Request["payload"]["methodID"]
readonly label?: Endpoint10_7Request["payload"]["label"]
}
export type Endpoint10_7Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.connect"]>>
export type IntegrationCommandConnectOperation<E = never> = (
export type Endpoint10_7Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.cancel"]>>
export type IntegrationOauthCancelOperation<E = never> = (
input: Endpoint10_7Input,
) => Effect.Effect<Endpoint10_7Output, E>
type Endpoint10_8Request = Parameters<RawClient["server.integration"]["integration.command.status"]>[0]
type Endpoint10_8Request = Parameters<RawClient["server.integration"]["integration.command.connect"]>[0]
export type Endpoint10_8Input = {
readonly integrationID: Endpoint10_8Request["params"]["integrationID"]
readonly attemptID: Endpoint10_8Request["params"]["attemptID"]
readonly location?: Endpoint10_8Request["query"]["location"]
readonly methodID: Endpoint10_8Request["payload"]["methodID"]
readonly label?: Endpoint10_8Request["payload"]["label"]
}
export type Endpoint10_8Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.status"]>>
export type IntegrationCommandStatusOperation<E = never> = (
export type Endpoint10_8Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.connect"]>>
export type IntegrationCommandConnectOperation<E = never> = (
input: Endpoint10_8Input,
) => Effect.Effect<Endpoint10_8Output, E>
type Endpoint10_9Request = Parameters<RawClient["server.integration"]["integration.command.cancel"]>[0]
type Endpoint10_9Request = Parameters<RawClient["server.integration"]["integration.command.status"]>[0]
export type Endpoint10_9Input = {
readonly integrationID: Endpoint10_9Request["params"]["integrationID"]
readonly attemptID: Endpoint10_9Request["params"]["attemptID"]
readonly location?: Endpoint10_9Request["query"]["location"]
}
export type Endpoint10_9Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.cancel"]>>
export type IntegrationCommandCancelOperation<E = never> = (
export type Endpoint10_9Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.status"]>>
export type IntegrationCommandStatusOperation<E = never> = (
input: Endpoint10_9Input,
) => Effect.Effect<Endpoint10_9Output, E>
type Endpoint10_10Request = Parameters<RawClient["server.integration"]["integration.command.cancel"]>[0]
export type Endpoint10_10Input = {
readonly integrationID: Endpoint10_10Request["params"]["integrationID"]
readonly attemptID: Endpoint10_10Request["params"]["attemptID"]
readonly location?: Endpoint10_10Request["query"]["location"]
}
export type Endpoint10_10Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.cancel"]>>
export type IntegrationCommandCancelOperation<E = never> = (
input: Endpoint10_10Input,
) => Effect.Effect<Endpoint10_10Output, E>
export interface IntegrationApi<E = never> {
readonly list: IntegrationListOperation<E>
readonly get: IntegrationGetOperation<E>
readonly wellknown: { readonly add: IntegrationWellknownAddOperation<E> }
readonly connect: { readonly key: IntegrationConnectKeyOperation<E> }
readonly oauth: {
readonly connect: IntegrationOauthConnectOperation<E>
+68 -57
View File
@@ -504,106 +504,116 @@ const Endpoint10_1 = (raw: RawClient["server.integration"]) => (input: Endpoint1
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_2Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
type Endpoint10_2Request = Parameters<RawClient["server.integration"]["integration.wellknown.add"]>[0]
type Endpoint10_2Input = {
readonly integrationID: Endpoint10_2Request["params"]["integrationID"]
readonly location?: Endpoint10_2Request["query"]["location"]
readonly key: Endpoint10_2Request["payload"]["key"]
readonly label?: Endpoint10_2Request["payload"]["label"]
readonly url: Endpoint10_2Request["payload"]["url"]
}
const Endpoint10_2 = (raw: RawClient["server.integration"]) => (input: Endpoint10_2Input) =>
raw["integration.wellknown.add"]({ query: { location: input["location"] }, payload: { url: input["url"] } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
type Endpoint10_3Input = {
readonly integrationID: Endpoint10_3Request["params"]["integrationID"]
readonly location?: Endpoint10_3Request["query"]["location"]
readonly key: Endpoint10_3Request["payload"]["key"]
readonly label?: Endpoint10_3Request["payload"]["label"]
}
const Endpoint10_3 = (raw: RawClient["server.integration"]) => (input: Endpoint10_3Input) =>
raw["integration.connect.key"]({
params: { integrationID: input["integrationID"] },
query: { location: input["location"] },
payload: { key: input["key"], label: input["label"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.oauth.connect"]>[0]
type Endpoint10_3Input = {
readonly integrationID: Endpoint10_3Request["params"]["integrationID"]
readonly location?: Endpoint10_3Request["query"]["location"]
readonly methodID: Endpoint10_3Request["payload"]["methodID"]
readonly inputs: Endpoint10_3Request["payload"]["inputs"]
readonly label?: Endpoint10_3Request["payload"]["label"]
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.oauth.connect"]>[0]
type Endpoint10_4Input = {
readonly integrationID: Endpoint10_4Request["params"]["integrationID"]
readonly location?: Endpoint10_4Request["query"]["location"]
readonly methodID: Endpoint10_4Request["payload"]["methodID"]
readonly inputs: Endpoint10_4Request["payload"]["inputs"]
readonly label?: Endpoint10_4Request["payload"]["label"]
}
const Endpoint10_3 = (raw: RawClient["server.integration"]) => (input: Endpoint10_3Input) =>
const Endpoint10_4 = (raw: RawClient["server.integration"]) => (input: Endpoint10_4Input) =>
raw["integration.oauth.connect"]({
params: { integrationID: input["integrationID"] },
query: { location: input["location"] },
payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.oauth.status"]>[0]
type Endpoint10_4Input = {
readonly integrationID: Endpoint10_4Request["params"]["integrationID"]
readonly attemptID: Endpoint10_4Request["params"]["attemptID"]
readonly location?: Endpoint10_4Request["query"]["location"]
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.oauth.status"]>[0]
type Endpoint10_5Input = {
readonly integrationID: Endpoint10_5Request["params"]["integrationID"]
readonly attemptID: Endpoint10_5Request["params"]["attemptID"]
readonly location?: Endpoint10_5Request["query"]["location"]
}
const Endpoint10_4 = (raw: RawClient["server.integration"]) => (input: Endpoint10_4Input) =>
const Endpoint10_5 = (raw: RawClient["server.integration"]) => (input: Endpoint10_5Input) =>
raw["integration.oauth.status"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.oauth.complete"]>[0]
type Endpoint10_5Input = {
readonly integrationID: Endpoint10_5Request["params"]["integrationID"]
readonly attemptID: Endpoint10_5Request["params"]["attemptID"]
readonly location?: Endpoint10_5Request["query"]["location"]
readonly code?: Endpoint10_5Request["payload"]["code"]
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.oauth.complete"]>[0]
type Endpoint10_6Input = {
readonly integrationID: Endpoint10_6Request["params"]["integrationID"]
readonly attemptID: Endpoint10_6Request["params"]["attemptID"]
readonly location?: Endpoint10_6Request["query"]["location"]
readonly code?: Endpoint10_6Request["payload"]["code"]
}
const Endpoint10_5 = (raw: RawClient["server.integration"]) => (input: Endpoint10_5Input) =>
const Endpoint10_6 = (raw: RawClient["server.integration"]) => (input: Endpoint10_6Input) =>
raw["integration.oauth.complete"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] },
payload: { code: input["code"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.oauth.cancel"]>[0]
type Endpoint10_6Input = {
readonly integrationID: Endpoint10_6Request["params"]["integrationID"]
readonly attemptID: Endpoint10_6Request["params"]["attemptID"]
readonly location?: Endpoint10_6Request["query"]["location"]
type Endpoint10_7Request = Parameters<RawClient["server.integration"]["integration.oauth.cancel"]>[0]
type Endpoint10_7Input = {
readonly integrationID: Endpoint10_7Request["params"]["integrationID"]
readonly attemptID: Endpoint10_7Request["params"]["attemptID"]
readonly location?: Endpoint10_7Request["query"]["location"]
}
const Endpoint10_6 = (raw: RawClient["server.integration"]) => (input: Endpoint10_6Input) =>
const Endpoint10_7 = (raw: RawClient["server.integration"]) => (input: Endpoint10_7Input) =>
raw["integration.oauth.cancel"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_7Request = Parameters<RawClient["server.integration"]["integration.command.connect"]>[0]
type Endpoint10_7Input = {
readonly integrationID: Endpoint10_7Request["params"]["integrationID"]
readonly location?: Endpoint10_7Request["query"]["location"]
readonly methodID: Endpoint10_7Request["payload"]["methodID"]
readonly label?: Endpoint10_7Request["payload"]["label"]
type Endpoint10_8Request = Parameters<RawClient["server.integration"]["integration.command.connect"]>[0]
type Endpoint10_8Input = {
readonly integrationID: Endpoint10_8Request["params"]["integrationID"]
readonly location?: Endpoint10_8Request["query"]["location"]
readonly methodID: Endpoint10_8Request["payload"]["methodID"]
readonly label?: Endpoint10_8Request["payload"]["label"]
}
const Endpoint10_7 = (raw: RawClient["server.integration"]) => (input: Endpoint10_7Input) =>
const Endpoint10_8 = (raw: RawClient["server.integration"]) => (input: Endpoint10_8Input) =>
raw["integration.command.connect"]({
params: { integrationID: input["integrationID"] },
query: { location: input["location"] },
payload: { methodID: input["methodID"], label: input["label"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_8Request = Parameters<RawClient["server.integration"]["integration.command.status"]>[0]
type Endpoint10_8Input = {
readonly integrationID: Endpoint10_8Request["params"]["integrationID"]
readonly attemptID: Endpoint10_8Request["params"]["attemptID"]
readonly location?: Endpoint10_8Request["query"]["location"]
}
const Endpoint10_8 = (raw: RawClient["server.integration"]) => (input: Endpoint10_8Input) =>
raw["integration.command.status"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_9Request = Parameters<RawClient["server.integration"]["integration.command.cancel"]>[0]
type Endpoint10_9Request = Parameters<RawClient["server.integration"]["integration.command.status"]>[0]
type Endpoint10_9Input = {
readonly integrationID: Endpoint10_9Request["params"]["integrationID"]
readonly attemptID: Endpoint10_9Request["params"]["attemptID"]
readonly location?: Endpoint10_9Request["query"]["location"]
}
const Endpoint10_9 = (raw: RawClient["server.integration"]) => (input: Endpoint10_9Input) =>
raw["integration.command.status"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_10Request = Parameters<RawClient["server.integration"]["integration.command.cancel"]>[0]
type Endpoint10_10Input = {
readonly integrationID: Endpoint10_10Request["params"]["integrationID"]
readonly attemptID: Endpoint10_10Request["params"]["attemptID"]
readonly location?: Endpoint10_10Request["query"]["location"]
}
const Endpoint10_10 = (raw: RawClient["server.integration"]) => (input: Endpoint10_10Input) =>
raw["integration.command.cancel"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] },
@@ -612,14 +622,15 @@ const Endpoint10_9 = (raw: RawClient["server.integration"]) => (input: Endpoint1
const adaptGroup10 = (raw: RawClient["server.integration"]) => ({
list: Endpoint10_0(raw),
get: Endpoint10_1(raw),
connect: { key: Endpoint10_2(raw) },
wellknown: { add: Endpoint10_2(raw) },
connect: { key: Endpoint10_3(raw) },
oauth: {
connect: Endpoint10_3(raw),
status: Endpoint10_4(raw),
complete: Endpoint10_5(raw),
cancel: Endpoint10_6(raw),
connect: Endpoint10_4(raw),
status: Endpoint10_5(raw),
complete: Endpoint10_6(raw),
cancel: Endpoint10_7(raw),
},
command: { connect: Endpoint10_7(raw), status: Endpoint10_8(raw), cancel: Endpoint10_9(raw) },
command: { connect: Endpoint10_8(raw), status: Endpoint10_9(raw), cancel: Endpoint10_10(raw) },
})
type Endpoint11_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
@@ -82,6 +82,8 @@ import type {
IntegrationListOutput,
IntegrationGetInput,
IntegrationGetOutput,
IntegrationWellknownAddInput,
IntegrationWellknownAddOutput,
IntegrationConnectKeyInput,
IntegrationConnectKeyOutput,
IntegrationOauthConnectInput,
@@ -893,6 +895,21 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
wellknown: {
add: (input: IntegrationWellknownAddInput, requestOptions?: RequestOptions) =>
request<IntegrationWellknownAddOutput>(
{
method: "POST",
path: `/api/experimental/integration/wellknown`,
query: { location: input["location"] },
body: { url: input["url"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
},
requestOptions,
),
},
connect: {
key: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) =>
request<IntegrationConnectKeyOutput>(
@@ -3304,6 +3304,15 @@ export type IntegrationGetOutput = {
data: IntegrationInfo | null
}
export type IntegrationWellknownAddInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly url: { readonly url: string }["url"]
}
export type IntegrationWellknownAddOutput = void
export type IntegrationConnectKeyInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: {
+24 -1
View File
@@ -36,7 +36,8 @@ test("exposes every standard HTTP API group", () => {
expect(Object.keys(client.debug)).toEqual(["location"])
expect(Object.keys(client.debug.location)).toEqual(["list", "evict"])
expect(Object.keys(client.message)).toEqual(["list"])
expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "oauth", "command"])
expect(Object.keys(client.integration)).toEqual(["list", "get", "wellknown", "connect", "oauth", "command"])
expect(Object.keys(client.integration.wellknown)).toEqual(["add"])
expect(Object.keys(client.integration.connect)).toEqual(["key"])
expect(Object.keys(client.integration.oauth)).toEqual(["connect", "status", "complete", "cancel"])
expect(Object.keys(client.integration.command)).toEqual(["connect", "status", "cancel"])
@@ -62,6 +63,28 @@ test("server.get uses the public HTTP contract", async () => {
expect(request?.url).toBe("http://localhost:3000/api/server")
})
test("experimental wellknown integration add uses the public HTTP contract", async () => {
let request: Request | undefined
const client = OpenCode.make({
baseUrl: "http://localhost:3000",
fetch: async (input, init) => {
request = input instanceof Request ? input : new Request(input, init)
return new Response(null, { status: 204 })
},
})
await client.integration.wellknown.add({
url: "https://example.com",
location: { directory: "/tmp/project" },
})
expect(request?.method).toBe("POST")
expect(request?.url).toBe(
"http://localhost:3000/api/experimental/integration/wellknown?location%5Bdirectory%5D=%2Ftmp%2Fproject",
)
expect(await request?.json()).toEqual({ url: "https://example.com" })
})
test("health.stop sends exact replacement identity", async () => {
let request: Request | undefined
const client = OpenCode.make({
+16
View File
@@ -37,6 +37,21 @@
"bun": "./src/filesystem/fff.bun.ts",
"node": "./src/filesystem/fff.node.ts",
"default": "./src/filesystem/fff.bun.ts"
},
"#photon-wasm": {
"bun": "./src/image/photon-wasm.bun.ts",
"node": "./src/image/photon-wasm.node.ts",
"default": "./src/image/photon-wasm.bun.ts"
},
"#runtime-import": {
"bun": "./src/runtime/import.bun.ts",
"node": "./src/runtime/import.node.ts",
"default": "./src/runtime/import.bun.ts"
},
"#process-lock-ffi": {
"bun": "./src/util/process-lock-ffi.bun.ts",
"node": "./src/util/process-lock-ffi.node.ts",
"default": "./src/util/process-lock-ffi.bun.ts"
}
},
"devDependencies": {
@@ -121,6 +136,7 @@
"mime-types": "3.0.2",
"minimatch": "10.2.5",
"npm-package-arg": "13.0.2",
"resolve.exports": "catalog:",
"semver": "^7.6.3",
"turndown": "7.2.0",
"venice-ai-sdk-provider": "2.1.1",
+55 -2
View File
@@ -1,9 +1,9 @@
{
"version": "7",
"dialect": "sqlite",
"id": "5f0a1db8-d4bf-42c3-becb-96b46fe66bed",
"id": "a4ba73b4-21bc-41ab-a415-94e2ca38d798",
"prevIds": [
"666138ef-82cb-4a9a-a765-e6669a436ff3"
"5f0a1db8-d4bf-42c3-becb-96b46fe66bed"
],
"ddl": [
{
@@ -38,6 +38,10 @@
"name": "event",
"entityType": "tables"
},
{
"name": "kv",
"entityType": "tables"
},
{
"name": "permission",
"entityType": "tables"
@@ -556,6 +560,46 @@
"entityType": "columns",
"table": "event"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "key",
"entityType": "columns",
"table": "kv"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "value",
"entityType": "columns",
"table": "kv"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "kv"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "kv"
},
{
"type": "text",
"notNull": false,
@@ -1844,6 +1888,15 @@
"table": "event",
"entityType": "pks"
},
{
"columns": [
"key"
],
"nameExplicit": false,
"name": "kv_pk",
"table": "kv",
"entityType": "pks"
},
{
"columns": [
"id"
+87 -17
View File
@@ -6,6 +6,8 @@ import { type ParseError, parse } from "jsonc-parser"
import { Context, Effect, Fiber, Layer, Option, PubSub, Schema, Stream } from "effect"
import { Permission } from "@opencode-ai/schema/permission"
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
import { Integration } from "@opencode-ai/schema/integration"
import { Credential } from "./credential"
import { EventV2 } from "./event"
import { Watcher } from "./filesystem/watcher"
import { FSUtil } from "./fs-util"
@@ -28,6 +30,7 @@ import { ConfigVariable } from "./config/variable"
import { ConfigWatcher } from "./config/watcher"
import { ConfigV1 } from "./v1/config/config"
import { ConfigMigrateV1 } from "./v1/config/migrate"
import { WellKnown } from "./wellknown"
export class Info extends Schema.Class<Info>("Config.Info")({
$schema: Schema.optional(Schema.String).annotate({
@@ -157,29 +160,67 @@ const layer = Layer.effect(
const location = yield* Location.Service
const watcher = yield* Watcher.Service
const events = yield* EventV2.Service
const credentials = yield* Credential.Service
const wellknown = yield* WellKnown.Service
const names = ["opencode.json", "opencode.jsonc"]
const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions)
const decodeV1Info = Schema.decodeUnknownOption(ConfigV1.Info, decodeOptions)
const loadFile = Effect.fnUntraced(function* (filepath: string) {
const text = yield* fs.readFileStringSafe(filepath)
if (!text) return
const substituted = yield* ConfigVariable.substitute({ type: "path", path: filepath, text })
const parseInfo = (text: string) => {
const errors: ParseError[] = []
const input: unknown = parse(substituted, errors, { allowTrailingComma: true })
const input: unknown = parse(text, errors, { allowTrailingComma: true })
if (errors.length) return
const info = Option.getOrUndefined(
return Option.getOrUndefined(
ConfigMigrateV1.isV1(input)
? decodeV1Info(input).pipe(Option.map(ConfigMigrateV1.migrate), Option.flatMap(decodeInfo))
: decodeInfo(input),
)
}
const loadFile = Effect.fnUntraced(function* (filepath: string) {
const text = yield* fs.readFileStringSafe(filepath)
if (!text) return
const substituted = yield* ConfigVariable.substitute({ type: "path", path: filepath, text })
const info = parseInfo(substituted)
if (!info) return
return new Document({ type: "document", path: filepath, info })
})
const loadWellknown = Effect.fn("Config.loadWellknown")(function* () {
const entries = yield* wellknown
.entries()
.pipe(
Effect.catch((error) =>
Effect.logWarning("failed to discover wellknown config", { error }).pipe(Effect.as([] as const)),
),
)
return yield* Effect.forEach(entries, (entry) =>
Effect.gen(function* () {
const auth = entry.manifest.auth
if (!auth) return []
const credential = (yield* credentials.list(entry.integrationID)).findLast(
(credential) => credential.value.type === "key",
)
if (!credential || credential.value.type !== "key") return []
const variables = { [auth.env]: credential.value.key }
const configs = yield* wellknown.resolve(entry, variables).pipe(Effect.orDie)
return yield* Effect.forEach(configs, (config) =>
ConfigVariable.substitute({
type: "virtual",
source: entry.origin,
dir: entry.origin,
text: JSON.stringify(config),
env: variables,
}).pipe(
Effect.map(parseInfo),
Effect.map((info) => (info ? new Document({ type: "document", info }) : undefined)),
),
).pipe(Effect.map((documents) => documents.filter((document) => document !== undefined)))
}),
).pipe(Effect.map((documents) => documents.flat()))
})
const loadDirectory = Effect.fnUntraced(function* (directory: AbsolutePath) {
return [
...(yield* Effect.forEach(names, (file) => loadFile(path.join(directory, file))).pipe(
@@ -201,7 +242,7 @@ const layer = Layer.effect(
targets: [".opencode", ".claude", ".agents", ...names.toReversed()],
start: location.directory,
})
.pipe(Effect.orDie)
.pipe(Effect.orDie)
// We load certain files from a few other folders in the ecosystem
const claude = [
@@ -244,7 +285,14 @@ const layer = Layer.effect(
)
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
return [...claude, ...agents, ...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()]
return [
...claude,
...agents,
...(supplementary[0] ?? []),
...direct,
...supplementary.slice(1).flat(),
...(yield* loadWellknown().pipe(Effect.orDie)),
]
})
const initial = yield* discover()
@@ -276,15 +324,37 @@ const layer = Layer.effect(
}
})
const reload = Effect.fn("Config.reload")(function* () {
const next = yield* discover()
configs = next
yield* reconcile(next)
yield* events.publish(ConfigSchema.Event.Updated, {})
})
yield* Stream.fromPubSub(updates).pipe(
Stream.debounce("100 millis"),
Stream.runForEach((update) =>
Effect.gen(function* () {
const next = yield* discover()
configs = next
yield* reconcile(next)
yield* events.publish(ConfigSchema.Event.Updated, {})
}).pipe(Effect.catchCause((cause) => Effect.logError("failed to reload config", { path: update.path, cause }))),
reload().pipe(
Effect.catchCause((cause) => Effect.logError("failed to reload config", { path: update.path, cause })),
),
),
Effect.forkScoped({ startImmediately: true }),
)
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filterEffect((event) =>
wellknown.entries().pipe(
Effect.map((entries) => entries.some((entry) => entry.integrationID === event.data.integrationID)),
Effect.catch(() => Effect.succeed(false)),
),
),
Stream.runForEach(() =>
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown config", { cause }))),
),
Effect.forkScoped({ startImmediately: true }),
)
yield* wellknown.changes.pipe(
Stream.runForEach(() =>
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown sources", { cause }))),
),
Effect.forkScoped({ startImmediately: true }),
)
@@ -301,5 +371,5 @@ const layer = Layer.effect(
export const node = makeLocationNode({
service: Service,
layer,
deps: [Watcher.node, EventV2.node, FSUtil.node, Global.node, Location.node],
deps: [Watcher.node, EventV2.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node],
})
+1
View File
@@ -54,5 +54,6 @@ export const migrations = (
import("./migration/20260709163752_time_suspended"),
import("./migration/20260709190621_session_pending_table"),
import("./migration/20260710025429_instruction_sync"),
import("./migration/20260716020354_kv"),
])
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
@@ -0,0 +1,18 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration"
export default {
id: "20260716020354_kv",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`
CREATE TABLE \`kv\` (
\`key\` text PRIMARY KEY,
\`value\` text NOT NULL,
\`time_created\` integer NOT NULL,
\`time_updated\` integer NOT NULL
);
`)
})
},
} satisfies DatabaseMigration.Migration
+8
View File
@@ -87,6 +87,14 @@ export default {
CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE
);
`)
yield* tx.run(`
CREATE TABLE \`kv\` (
\`key\` text PRIMARY KEY,
\`value\` text NOT NULL,
\`time_created\` integer NOT NULL,
\`time_updated\` integer NOT NULL
);
`)
yield* tx.run(`
CREATE TABLE \`permission\` (
\`id\` text PRIMARY KEY,
+4 -1
View File
@@ -11,10 +11,12 @@ import { Flag } from "../flag/flag"
import { lazy } from "../util/lazy"
import { watch as watchFileSystem } from "node:fs"
import path from "path"
import { createRequire } from "node:module"
declare const OPENCODE_LIBC: string | undefined
const SUBSCRIBE_TIMEOUT_MS = 10_000
const require = createRequire(import.meta.url)
export const Event = { Updated: FileSystem.Event.Changed }
@@ -22,7 +24,8 @@ const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
try {
const libc = typeof OPENCODE_LIBC === "undefined" ? undefined : OPENCODE_LIBC
const binding = require(
`@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${libc || "glibc"}` : ""}`,
process.env.OPENCODE_PARCEL_WATCHER_PATH ??
`@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${libc || "glibc"}` : ""}`,
)
return createWrapper(binding) as typeof import("@parcel/watcher")
} catch {
@@ -0,0 +1,4 @@
// @ts-ignore Bun embeds static file imports when compiling the CLI.
import photonWasm from "@silvia-odwyer/photon-node/photon_rs_bg.wasm" with { type: "file" }
export default photonWasm
@@ -0,0 +1,4 @@
import { createRequire } from "node:module"
export default process.env.OPENCODE_PHOTON_WASM_PATH ??
createRequire(import.meta.url).resolve("@silvia-odwyer/photon-node/photon_rs_bg.wasm")
+1 -2
View File
@@ -1,5 +1,4 @@
// @ts-ignore Bun's static file import is embedded by `bun build --compile`; some consumers also declare *.wasm.
import photonWasm from "@silvia-odwyer/photon-node/photon_rs_bg.wasm" with { type: "file" }
import photonWasm from "#photon-wasm"
import { Effect } from "effect"
import path from "node:path"
import { fileURLToPath } from "node:url"
+23 -16
View File
@@ -8,6 +8,7 @@ import {
Duration,
Effect,
Exit,
Fiber,
Layer,
Schedule,
Schema,
@@ -617,34 +618,40 @@ const layer = Layer.effect(
}),
)
yield* processes
.runStream(
yield* Effect.gen(function* () {
const handle = yield* processes.spawn(
ChildProcess.make(method.command[0], method.command.slice(1), {
extendEnv: true,
stdin: "ignore",
}),
{ okExitCodes: [0] },
)
.pipe(
Stream.tap((line) =>
const stdout = yield* AppProcess.collectStream(handle.stdout, undefined).pipe(Effect.forkScoped)
yield* handle.stderr.pipe(
Stream.decodeText,
Stream.tap((chunk) =>
SynchronizedRef.update(commandAttempts, (current) => {
const attempt = current.get(attemptID)
if (!attempt || attempt.status !== "pending") return current
const message = attempt.message ? `${attempt.message}\n${line}` : line
const message = (attempt.message ?? "") + chunk
return new Map(current).set(attemptID, { ...attempt, message })
}),
),
Stream.runCollect,
Effect.flatMap((lines) => {
const credential = Array.from(lines).at(-1)
return credential
? Effect.succeed(credential)
: Effect.fail(new Error("Authentication command returned no credential"))
}),
Effect.exit,
Effect.flatMap((exit) => settleCommand(attemptID, exit)),
Effect.forkIn(attemptScope, { startImmediately: true }),
Stream.runDrain,
)
const exitCode = yield* handle.exitCode
if (exitCode !== 0) {
const attempt = (yield* SynchronizedRef.get(commandAttempts)).get(attemptID)
return yield* Effect.fail(new Error(attempt?.message?.trim() || `Authentication command exited ${exitCode}`))
}
const credential = (yield* Fiber.join(stdout)).buffer.toString("utf8").trim()
if (!credential) return yield* Effect.fail(new Error("Authentication command returned no credential"))
return credential
}).pipe(
Scope.provide(attemptScope),
Effect.exit,
Effect.flatMap((exit) => settleCommand(attemptID, exit)),
Effect.forkIn(attemptScope, { startImmediately: true }),
)
return CommandAttempt.make({ attemptID, time })
})
+47
View File
@@ -0,0 +1,47 @@
export * as KV from "./kv"
import { eq } from "drizzle-orm"
import { Context, Effect, Layer, Schema } from "effect"
import { Database } from "./database/database"
import { makeGlobalNode } from "./effect/app-node"
import { KVTable } from "./kv/sql"
export type Value = Schema.Json
export interface Interface {
readonly get: (key: string) => Effect.Effect<Value | undefined>
readonly set: (key: string, value: Value) => Effect.Effect<void>
readonly remove: (key: string) => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/KV") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const { db } = yield* Database.Service
return Service.of({
get: Effect.fn("KV.get")(function* (key) {
return (yield* db
.select({ value: KVTable.value })
.from(KVTable)
.where(eq(KVTable.key, key))
.get()
.pipe(Effect.orDie))?.value
}),
set: Effect.fn("KV.set")(function* (key, value) {
yield* db
.insert(KVTable)
.values({ key, value })
.onConflictDoUpdate({ target: KVTable.key, set: { value, time_updated: Date.now() } })
.run()
.pipe(Effect.orDie)
}),
remove: Effect.fn("KV.remove")(function* (key) {
yield* db.delete(KVTable).where(eq(KVTable.key, key)).run().pipe(Effect.orDie)
}),
})
}),
)
export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] })
+9
View File
@@ -0,0 +1,9 @@
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
import { Timestamps } from "../database/schema.sql"
import type { KV } from "../kv"
export const KVTable = sqliteTable("kv", {
key: text().primaryKey(),
value: text({ mode: "json" }).$type<KV.Value>().notNull(),
...Timestamps,
})
+18
View File
@@ -0,0 +1,18 @@
declare module "node:ffi" {
type Signature = {
readonly arguments?: readonly string[]
readonly return?: string
}
type ForeignFunction = (...args: ReadonlyArray<unknown>) => number | bigint
export function dlopen(
path: string,
definitions: Readonly<Record<string, Signature>>,
): {
readonly lib: { close(): void }
readonly functions: Readonly<Record<string, ForeignFunction>>
}
export function getInt32(pointer: number | bigint, offset?: number): number
}
+2 -3
View File
@@ -12,6 +12,7 @@ import { filesystem } from "./effect/app-node-platform"
import { LayerNode } from "./effect/layer-node"
import { makeRuntime } from "./effect/runtime"
import { NpmConfig } from "./npm-config"
import { resolveModule } from "#runtime-import"
export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", {
add: Schema.Array(Schema.String).pipe(Schema.optional),
@@ -54,9 +55,7 @@ const resolveEntryPoint = (name: string, dir: string, subpaths: readonly string[
const entrypoint = subpaths
.map((subpath) => {
try {
return typeof Bun !== "undefined"
? import.meta.resolve([name, subpath].filter(Boolean).join("/"), dir)
: import.meta.resolve(dir)
return resolveModule([name, subpath].filter(Boolean).join("/"), dir)
} catch {
return undefined
}
+1
View File
@@ -425,6 +425,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
get: (input) => runtime.session.get(input.sessionID),
prompt: runtime.session.prompt,
command: runtime.session.command,
synthetic: runtime.session.synthetic,
interrupt: (input) => runtime.session.interrupt(input.sessionID),
},
} satisfies Plugin.Context
+5
View File
@@ -43,6 +43,7 @@ import { SubagentTool } from "../tool/subagent"
import { Tools } from "../tool/tools"
import { WebFetchTool } from "../tool/webfetch"
import { WebSearchTool } from "../tool/websearch"
import { WellKnown } from "../wellknown"
import { WriteTool } from "../tool/write"
import { AgentPlugin } from "./agent"
import { CommandPlugin } from "./command"
@@ -52,6 +53,7 @@ import { PluginRuntime } from "./runtime"
import { SkillPlugin } from "./skill"
import { SystemPromptPlugin } from "./system-prompt"
import { VariantPlugin } from "./variant"
import { WellKnownPlugin } from "../wellknown/plugin"
const services = Effect.fn("PluginInternal.services")(function* () {
const agent = yield* AgentV2.Service
@@ -81,6 +83,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
const skill = yield* SkillV2.Service
const tools = yield* Tools.Service
const websearch = yield* WebSearchTool.ConfigService
const wellknown = yield* WellKnown.Service
return Context.mergeAll(
Context.make(AgentV2.Service, agent),
Context.make(Catalog.Service, catalog),
@@ -109,6 +112,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
Context.make(SkillV2.Service, skill),
Context.make(Tools.Service, tools),
Context.make(WebSearchTool.ConfigService, websearch),
Context.make(WellKnown.Service, wellknown),
)
})
@@ -119,6 +123,7 @@ export type Requirements = ContextServices<Effect.Success<ReturnType<typeof serv
export type InternalPlugin = Plugin<Requirements | Scope.Scope>
const pre = [
WellKnownPlugin.Plugin,
AgentPlugin.Plugin,
CommandPlugin.Plugin,
SkillPlugin.Plugin,
+11
View File
@@ -244,6 +244,17 @@ export function fromPromise(plugin: Plugin) {
resume: input.resume ?? undefined,
}),
),
synthetic: (input) =>
run(
host.session.synthetic({
...input,
sessionID: Session.ID.make(input.sessionID),
id: input.id == null ? undefined : SessionMessage.ID.make(input.id),
description: input.description ?? undefined,
delivery: input.delivery ?? undefined,
resume: input.resume ?? undefined,
}),
),
interrupt: (input) => run(host.session.interrupt({ sessionID: Session.ID.make(input.sessionID) })),
},
}
+4 -5
View File
@@ -2,6 +2,7 @@ import { Effect } from "effect"
import { pathToFileURL } from "url"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Npm } from "../../npm"
import { importModule } from "#runtime-import"
export const DynamicProviderPlugin = define({
id: "opencode.provider.dynamic",
@@ -17,11 +18,9 @@ export const DynamicProviderPlugin = define({
: (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
const mod = yield* Effect.promise(async () => {
return (await import(
installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href
)) as Record<string, (options: any) => any>
}).pipe(Effect.orDie)
const mod = (yield* Effect.promise(() =>
importModule(installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href),
).pipe(Effect.orDie)) as Record<string, (options: any) => any>
const match = Object.keys(mod).find((name) => name.startsWith("create"))
if (!match) throw new Error(`Package ${evt.package} has no provider factory export`)
@@ -3,6 +3,7 @@ import { pathToFileURL } from "url"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Npm } from "../../npm"
import { ProviderV2 } from "../../provider"
import { importModule } from "#runtime-import"
export const SapAICorePlugin = define({
id: "opencode.provider.sap-ai-core",
@@ -22,9 +23,9 @@ export const SapAICorePlugin = define({
: (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint
if (!installedPath) return yield* Effect.die(new Error(`Package ${evt.package} has no import entrypoint`))
const mod: Record<string, unknown> = yield* Effect.promise(
() => import(installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href),
)
const mod = (yield* Effect.promise(() =>
importModule(installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href),
)) as Record<string, unknown>
const match = Object.keys(mod).find((name) => name.startsWith("create"))
if (!match) return yield* Effect.die(new Error(`Package ${evt.package} has no provider factory export`))
const factory = mod[match]
+7 -2
View File
@@ -35,9 +35,11 @@ import { SkillV2 } from "../skill"
import { ReadToolFileSystem } from "../tool/read-filesystem"
import { ToolRegistry } from "../tool/registry"
import { WebSearchTool } from "../tool/websearch"
import { WellKnown } from "../wellknown"
import { PluginInternal } from "./internal"
import { PluginRuntime } from "./runtime"
import { SdkPlugins } from "./sdk"
import { importModule } from "#runtime-import"
const PluginModule = Schema.Struct({
default: Schema.Union([
@@ -165,9 +167,11 @@ const load = Effect.fn("PluginSupervisor.load")(function* (operation: Extract<Op
const source =
operation.mtime === undefined
? entrypoint
: `${operation.target.replaceAll("\\", "/")}?mtime=${operation.mtime}`
: typeof Bun !== "undefined"
? `${operation.target.replaceAll("\\", "/")}?mtime=${operation.mtime}`
: `${entrypoint}?mtime=${operation.mtime}`
yield* Effect.log({ msg: "loading plugin", id: operation.target, entrypoint: source })
const mod = yield* Effect.promise(() => import(source))
const mod = yield* Effect.promise(() => importModule(source))
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
return {
@@ -296,6 +300,7 @@ export const node = makeLocationNode({
SkillV2.node,
ToolRegistry.toolsNode,
WebSearchTool.configNode,
WellKnown.node,
],
})
+25 -4
View File
@@ -1,11 +1,11 @@
export * as ProviderV2 from "./provider"
import { Effect, Schema } from "effect"
import { pathToFileURL } from "url"
import { Provider } from "@opencode-ai/schema/provider"
import type { ProviderPackageDefinition } from "@opencode-ai/ai"
import { Npm } from "./npm"
import type { DeepMutable } from "./schema"
import { importModule, resolveModule } from "#runtime-import"
export const ID = Provider.ID
export type ID = typeof ID.Type
@@ -32,8 +32,24 @@ export class LoadError extends Schema.TaggedErrorClass<LoadError>()("ProviderV2.
export type ProviderPackage = ProviderPackageDefinition
const packages = new Map<string, Promise<unknown>>()
const builtins = new Map<string, () => Promise<unknown>>([
["@opencode-ai/ai/providers/amazon-bedrock", () => import("@opencode-ai/ai/providers/amazon-bedrock")],
["@opencode-ai/ai/providers/anthropic", () => import("@opencode-ai/ai/providers/anthropic")],
["@opencode-ai/ai/providers/azure", () => import("@opencode-ai/ai/providers/azure")],
["@opencode-ai/ai/providers/azure/chat", () => import("@opencode-ai/ai/providers/azure/chat")],
["@opencode-ai/ai/providers/azure/responses", () => import("@opencode-ai/ai/providers/azure/responses")],
["@opencode-ai/ai/providers/google", () => import("@opencode-ai/ai/providers/google")],
["@opencode-ai/ai/providers/openai", () => import("@opencode-ai/ai/providers/openai")],
["@opencode-ai/ai/providers/openai/chat", () => import("@opencode-ai/ai/providers/openai/chat")],
["@opencode-ai/ai/providers/openai/responses", () => import("@opencode-ai/ai/providers/openai/responses")],
["@opencode-ai/ai/providers/openai-compatible", () => import("@opencode-ai/ai/providers/openai-compatible")],
["@opencode-ai/ai/providers/openrouter", () => import("@opencode-ai/ai/providers/openrouter")],
["@opencode-ai/ai/providers/xai", () => import("@opencode-ai/ai/providers/xai")],
])
export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specifier: string, npm?: Npm.Interface) {
const builtin = builtins.get(specifier)
if (builtin) return yield* importPackage(specifier, specifier, builtin)
const resolved = yield* Effect.sync(() => {
if (specifier.startsWith("file://") || specifier.startsWith("@opencode-ai/ai/")) return specifier
try {
@@ -53,7 +69,8 @@ export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specif
const root = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : (parts[0] ?? specifier)
const installed = yield* npm.add(root).pipe(Effect.mapError((cause) => new LoadError({ package: specifier, cause })))
const entrypoint = yield* Effect.try({
try: () => import.meta.resolve(specifier, pathToFileURL(`${installed.directory}/`).href),
try: () =>
specifier === root && installed.entrypoint ? installed.entrypoint : resolveModule(specifier, installed.directory),
catch: (cause) => new LoadError({ package: specifier, cause }),
})
return yield* importPackage(specifier, entrypoint)
@@ -116,12 +133,16 @@ export type Info = Provider.Info
export type MutableInfo = DeepMutable<Info>
const importPackage = Effect.fn("ProviderV2.importPackage")(function* (specifier: string, entrypoint: string) {
const importPackage = Effect.fn("ProviderV2.importPackage")(function* (
specifier: string,
entrypoint: string,
load = () => importModule(entrypoint),
) {
const module = yield* Effect.tryPromise({
try: () => {
const existing = packages.get(entrypoint)
if (existing) return existing
const loaded = import(entrypoint)
const loaded = load()
packages.set(entrypoint, loaded)
return loaded
},
+7 -3
View File
@@ -1,11 +1,15 @@
// ast-grep-ignore: no-star-import
import * as pty from "@lydell/node-pty"
import { createRequire } from "node:module"
import { isSea } from "node:sea"
import type { Opts, Proc } from "./pty"
export type { Disp, Exit, Opts, Proc } from "./pty"
const pty = createRequire(import.meta.url)(
process.env.OPENCODE_NODE_PTY_PATH ?? "@lydell/node-pty",
) as typeof import("@lydell/node-pty")
export function spawn(file: string, args: string[], opts: Opts): Proc {
const proc = pty.spawn(file, args, opts)
const proc = pty.spawn(file, args, process.platform === "win32" && isSea() ? { ...opts, useConptyDll: true } : opts)
return {
pid: proc.pid,
onData(listener) {
+7
View File
@@ -0,0 +1,7 @@
export function importModule(specifier: string) {
return import(specifier) as Promise<unknown>
}
export function resolveModule(specifier: string, directory: string) {
return import.meta.resolve(specifier, directory)
}
+39
View File
@@ -0,0 +1,39 @@
import { Script, constants } from "node:vm"
import { createRequire, registerHooks } from "node:module"
import path from "node:path"
import { pathToFileURL } from "node:url"
import { resolve, type Package } from "resolve.exports"
let conditions: readonly string[] = []
const conditionHooks = registerHooks({
resolve(specifier, context, nextResolve) {
conditions = context.conditions
return nextResolve(specifier, context)
},
})
await new Script('import("node:module")', {
importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
}).runInThisContext()
conditionHooks.deregister()
export async function importModule(specifier: string) {
const imported = (await new Script(`import(${JSON.stringify(specifier)})`, {
importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
}).runInThisContext()) as unknown
if (typeof imported !== "object" || imported === null) return imported
const module = imported as Record<string, unknown>
const exports = module["module.exports"]
if (exports !== module.default || (typeof exports !== "object" && typeof exports !== "function") || exports === null)
return imported
return Object.assign({}, module, exports)
}
export function resolveModule(specifier: string, directory: string) {
const pkg = createRequire(import.meta.url)(path.join(directory, "package.json")) as Package
const target = resolve(pkg, specifier, { conditions, unsafe: true })?.[0]
if (target) return pathToFileURL(path.resolve(directory, target)).href
const legacyTarget =
specifier === pkg.name ? directory : path.resolve(directory, specifier.slice(pkg.name.length + 1))
return pathToFileURL(createRequire(path.join(directory, "package.json")).resolve(legacyTarget)).href
}
+2 -1
View File
@@ -8,6 +8,7 @@ import { Global } from "../global"
import { makeGlobalNode } from "../effect/app-node"
import { httpClient } from "../effect/app-node-platform"
import { AbsolutePath } from "../schema"
import { Hash } from "../util/hash"
const skillConcurrency = 4
const fileConcurrency = 8
@@ -110,7 +111,7 @@ const layer = Layer.effect(
)
if (!data) return []
const sourceRoot = path.resolve(global.cache, "skills", Bun.hash(base).toString(16))
const sourceRoot = path.resolve(global.cache, "skills", Hash.fast(base))
return yield* Effect.forEach(
data.skills.flatMap((skill) => {
if (!isSafeSegment(skill.name)) {
@@ -0,0 +1,50 @@
import { dlopen, read, type Pointer } from "bun:ffi"
import { existsSync } from "node:fs"
export type LockResult =
| { readonly acquired: true }
| { readonly acquired: false; readonly held: true }
| { readonly acquired: false; readonly held: false; readonly code: number }
const LOCK_EX = 2
const LOCK_NB = 4
const DARWIN_EWOULDBLOCK = 35
const LINUX_EWOULDBLOCK = 11
export function lockDarwin(fd: number): LockResult {
const library = dlopen("/usr/lib/libSystem.B.dylib", {
flock: { args: ["i32", "i32"], returns: "i32" },
__error: { args: [], returns: "ptr" },
})
try {
const result = library.symbols.flock(fd, LOCK_EX | LOCK_NB)
const code = result === 0 ? 0 : errorCode(library.symbols.__error())
if (result === 0) return { acquired: true }
if (code === DARWIN_EWOULDBLOCK) return { acquired: false, held: true }
return { acquired: false, held: false, code }
} finally {
library.close()
}
}
export function lockLinux(fd: number): LockResult {
const musl = `/lib/libc.musl-${process.arch === "arm64" ? "aarch64" : "x86_64"}.so.1`
const library = dlopen(existsSync(musl) ? musl : "libc.so.6", {
flock: { args: ["i32", "i32"], returns: "i32" },
__errno_location: { args: [], returns: "ptr" },
})
try {
const result = library.symbols.flock(fd, LOCK_EX | LOCK_NB)
const code = result === 0 ? 0 : errorCode(library.symbols.__errno_location())
if (result === 0) return { acquired: true }
if (code === LINUX_EWOULDBLOCK) return { acquired: false, held: true }
return { acquired: false, held: false, code }
} finally {
library.close()
}
}
function errorCode(pointer: Pointer | null) {
if (pointer === null) throw new Error("Failed to read process lock error code")
return read.i32(pointer, 0)
}
@@ -0,0 +1,43 @@
import { dlopen, getInt32 } from "node:ffi"
export type LockResult =
| { readonly acquired: true }
| { readonly acquired: false; readonly held: true }
| { readonly acquired: false; readonly held: false; readonly code: number }
const LOCK_EX = 2
const LOCK_NB = 4
const DARWIN_EWOULDBLOCK = 35
const LINUX_EWOULDBLOCK = 11
export function lockDarwin(fd: number): LockResult {
const library = dlopen("/usr/lib/libSystem.B.dylib", {
flock: { arguments: ["int32", "int32"], return: "int32" },
__error: { arguments: [], return: "pointer" },
})
try {
const result = library.functions.flock(fd, LOCK_EX | LOCK_NB)
const code = result === 0 ? 0 : getInt32(library.functions.__error(), 0)
if (result === 0) return { acquired: true }
if (code === DARWIN_EWOULDBLOCK) return { acquired: false, held: true }
return { acquired: false, held: false, code }
} finally {
library.lib.close()
}
}
export function lockLinux(fd: number): LockResult {
const library = dlopen("libc.so.6", {
flock: { arguments: ["int32", "int32"], return: "int32" },
__errno_location: { arguments: [], return: "pointer" },
})
try {
const result = library.functions.flock(fd, LOCK_EX | LOCK_NB)
const code = result === 0 ? 0 : getInt32(library.functions.__errno_location(), 0)
if (result === 0) return { acquired: true }
if (code === LINUX_EWOULDBLOCK) return { acquired: false, held: true }
return { acquired: false, held: false, code }
} finally {
library.lib.close()
}
}
+3 -51
View File
@@ -1,5 +1,5 @@
import { dlopen, read, type Pointer } from "bun:ffi"
import { closeSync, existsSync, mkdirSync, openSync } from "node:fs"
import { lockDarwin, lockLinux, type LockResult } from "#process-lock-ffi"
import { closeSync, mkdirSync, openSync } from "node:fs"
import { connect, createServer, type Server, type Socket } from "node:net"
import path from "node:path"
import { Effect, Schema } from "effect"
@@ -76,60 +76,12 @@ export namespace ProcessLock {
})
}
type Result =
| { readonly acquired: true }
| { readonly acquired: false; readonly held: true }
| { readonly acquired: false; readonly held: false; readonly code: number }
const LOCK_EX = 2
const LOCK_NB = 4
const DARWIN_EWOULDBLOCK = 35
const LINUX_EWOULDBLOCK = 11
function lock(fd: number): Result {
function lock(fd: number): LockResult {
if (process.platform === "darwin") return lockDarwin(fd)
if (process.platform === "linux") return lockLinux(fd)
throw new Error(`Unsupported process lock platform: ${process.platform}`)
}
function lockDarwin(fd: number): Result {
const library = dlopen("/usr/lib/libSystem.B.dylib", {
flock: { args: ["i32", "i32"], returns: "i32" },
__error: { args: [], returns: "ptr" },
})
try {
const result = library.symbols.flock(fd, LOCK_EX | LOCK_NB)
const code = result === 0 ? 0 : errorCode(library.symbols.__error())
if (result === 0) return { acquired: true }
if (code === DARWIN_EWOULDBLOCK) return { acquired: false, held: true }
return { acquired: false, held: false, code }
} finally {
library.close()
}
}
function lockLinux(fd: number): Result {
const musl = `/lib/libc.musl-${process.arch === "arm64" ? "aarch64" : "x86_64"}.so.1`
const library = dlopen(existsSync(musl) ? musl : "libc.so.6", {
flock: { args: ["i32", "i32"], returns: "i32" },
__errno_location: { args: [], returns: "ptr" },
})
try {
const result = library.symbols.flock(fd, LOCK_EX | LOCK_NB)
const code = result === 0 ? 0 : errorCode(library.symbols.__errno_location())
if (result === 0) return { acquired: true }
if (code === LINUX_EWOULDBLOCK) return { acquired: false, held: true }
return { acquired: false, held: false, code }
} finally {
library.close()
}
}
function errorCode(pointer: Pointer | null) {
if (pointer === null) throw new Error("Failed to read process lock error code")
return read.i32(pointer, 0)
}
function acquireWindows(file: string) {
return Effect.callback<Server, ProcessLock.LockError>((resume) => {
const server = createServer()
+166
View File
@@ -0,0 +1,166 @@
export * as WellKnown from "./wellknown"
import { Integration } from "@opencode-ai/schema/integration"
import { Context, Effect, Layer, PubSub, Ref, Schema, Semaphore, Stream } from "effect"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { makeGlobalNode } from "./effect/app-node"
import { httpClient } from "./effect/app-node-platform"
import { KV } from "./kv"
export interface Auth extends Schema.Schema.Type<typeof Auth> {}
export const Auth = Schema.Struct({
command: Schema.Array(Schema.String),
env: Schema.String,
}).annotate({ identifier: "WellKnown.Auth" })
export interface RemoteConfig extends Schema.Schema.Type<typeof RemoteConfig> {}
export const RemoteConfig = Schema.Struct({
url: Schema.String,
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
}).annotate({ identifier: "WellKnown.RemoteConfig" })
export interface Config extends Schema.Schema.Type<typeof Config> {}
export const Config = Schema.Record(Schema.String, Schema.Json).annotate({ identifier: "WellKnown.Config" })
export interface Manifest extends Schema.Schema.Type<typeof Manifest> {}
export const Manifest = Schema.Struct({
auth: Schema.optional(Auth),
config: Schema.optional(Schema.NullOr(Config)),
remote_config: Schema.optional(RemoteConfig),
}).annotate({ identifier: "WellKnown.Manifest" })
export interface ResolveInput {
readonly origin: string
readonly variables?: Readonly<Record<string, string>>
}
export interface Entry {
readonly origin: string
readonly integrationID: Integration.ID
readonly manifest: Manifest
}
export interface Interface {
readonly entries: () => Effect.Effect<readonly Entry[], Error>
readonly snapshot: () => readonly Entry[]
readonly add: (origin: string) => Effect.Effect<Entry, Error>
readonly remove: (origin: string) => Effect.Effect<void>
readonly changes: Stream.Stream<void>
readonly resolve: (entry: Entry, variables: Readonly<Record<string, string>>) => Effect.Effect<Config[], Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/WellKnown") {}
export const inspect = Effect.fn("WellKnown.inspect")(function* (origin: string) {
const url = `${origin.replace(/\/+$/, "")}/.well-known/opencode`
const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient)
return yield* http.execute(HttpClientRequest.get(url).pipe(HttpClientRequest.acceptJson)).pipe(
Effect.flatMap(HttpClientResponse.schemaBodyJson(Manifest)),
Effect.mapError((cause) => new Error(`Failed to load wellknown manifest from ${url}`, { cause })),
)
})
export const resolve = Effect.fn("WellKnown.resolve")(function* (input: ResolveInput) {
const manifest = yield* inspect(input.origin)
return yield* resolveEntry(
{ origin: input.origin, integrationID: Integration.ID.make(input.origin.replace(/\/+$/, "")), manifest },
input.variables ?? {},
)
})
const resolveEntry = Effect.fnUntraced(function* (entry: Entry, variables: Readonly<Record<string, string>>) {
const configs = entry.manifest.config ? [entry.manifest.config] : []
if (!entry.manifest.remote_config) return configs
const substitute = (value: string) =>
value.replace(/\{env:([^}]+)\}/g, (_, name: string) => variables[name] ?? process.env[name] ?? "")
const url = substitute(entry.manifest.remote_config.url)
const headers = Object.fromEntries(
Object.entries(entry.manifest.remote_config.headers ?? {}).map(([key, value]) => [key, substitute(value)]),
)
const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient)
const remote = yield* http
.execute(HttpClientRequest.get(url).pipe(HttpClientRequest.acceptJson, HttpClientRequest.setHeaders(headers)))
.pipe(
Effect.flatMap(HttpClientResponse.schemaBodyJson(Config)),
Effect.mapError((cause) => new Error(`Failed to load wellknown remote config from ${url}`, { cause })),
)
if (Schema.is(Config)(remote.config)) return [...configs, remote.config]
return [...configs, remote]
})
const sourcesKey = "wellknown:sources"
const Sources = Schema.Array(Schema.String)
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const http = yield* HttpClient.HttpClient
const kv = yield* KV.Service
const cache = yield* Ref.make(new Map<string, Entry>())
const changes = yield* PubSub.unbounded<void>()
const lock = Semaphore.makeUnsafe(1)
const load = Effect.fn("WellKnown.load")(function* () {
const value = yield* kv.get(sourcesKey)
const origins = Schema.is(Sources)(value) ? value : []
const current = yield* Ref.get(cache)
const entries = yield* Effect.forEach(origins, (origin) => {
const cached = current.get(origin)
if (cached) return Effect.succeed(cached)
return inspect(origin).pipe(
Effect.provideService(HttpClient.HttpClient, http),
Effect.map((manifest) => ({ origin, integrationID: Integration.ID.make(origin), manifest })),
)
})
yield* Ref.set(cache, new Map(entries.map((entry) => [entry.origin, entry])))
return entries
})
return Service.of({
entries: load,
snapshot: () => Array.from(Ref.getUnsafe(cache).values()),
add: Effect.fn("WellKnown.add")(function* (value) {
return yield* lock.withPermit(
Effect.gen(function* () {
const origin = value.replace(/\/+$/, "")
const manifest = yield* inspect(origin).pipe(Effect.provideService(HttpClient.HttpClient, http))
if (!manifest.auth) return yield* Effect.fail(new Error(`No authentication method found at ${origin}`))
const entry = { origin, integrationID: Integration.ID.make(origin), manifest }
const sources = yield* kv.get(sourcesKey)
const origins = Schema.is(Sources)(sources) ? sources : []
yield* kv.set(sourcesKey, Array.from(new Set([...origins, origin])))
yield* Ref.update(cache, (current) => new Map(current).set(origin, entry))
yield* PubSub.publish(changes, undefined)
return entry
}),
)
}),
remove: Effect.fn("WellKnown.remove")(function* (value) {
yield* lock.withPermit(
Effect.gen(function* () {
const origin = value.replace(/\/+$/, "")
const sources = yield* kv.get(sourcesKey)
const origins = Schema.is(Sources)(sources) ? sources : []
yield* kv.set(
sourcesKey,
origins.filter((item) => item !== origin),
)
yield* Ref.update(cache, (current) => {
const next = new Map(current)
next.delete(origin)
return next
})
yield* PubSub.publish(changes, undefined)
}),
)
}),
changes: Stream.fromPubSub(changes),
resolve: Effect.fn("WellKnown.resolveEntry")(function* (entry, variables) {
return yield* resolveEntry(entry, variables).pipe(Effect.provideService(HttpClient.HttpClient, http))
}),
})
}),
)
export const node = makeGlobalNode({ service: Service, layer, deps: [httpClient, KV.node] })
+34
View File
@@ -0,0 +1,34 @@
export * as WellKnownPlugin from "./plugin"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Effect, Stream } from "effect"
import { WellKnown } from "../wellknown"
export const Plugin = define({
id: "opencode.wellknown",
effect: Effect.fn(function* (ctx) {
const wellknown = yield* WellKnown.Service
yield* wellknown.entries().pipe(Effect.orDie)
yield* ctx.integration.transform((draft) => {
wellknown.snapshot().forEach((entry) => {
if (!entry.manifest.auth) return
draft.update(entry.integrationID, (integration) => {
integration.name = new URL(entry.origin).hostname
})
draft.method.update({
integrationID: entry.integrationID,
method: {
id: "login",
type: "command",
label: "Log in",
command: [...entry.manifest.auth.command],
},
})
})
})
yield* wellknown.changes.pipe(
Stream.runForEach(() => ctx.integration.reload()),
Effect.forkScoped({ startImmediately: true }),
)
}),
})
+121 -3
View File
@@ -8,7 +8,9 @@ import { ConfigModel } from "@opencode-ai/core/config/model"
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
import { ConfigProvider } from "@opencode-ai/core/config/provider"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Credential } from "@opencode-ai/core/credential"
import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { FSUtil } from "@opencode-ai/core/fs-util"
@@ -19,6 +21,8 @@ import { Location } from "@opencode-ai/core/location"
import { Project } from "@opencode-ai/core/project"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { WellKnown } from "@opencode-ai/core/wellknown"
import { Integration } from "@opencode-ai/schema/integration"
import { location } from "../fixture/location"
import { tmpdir } from "../fixture/tmpdir"
import { testEffect } from "../lib/effect"
@@ -26,12 +30,46 @@ import { testEffect } from "../lib/effect"
const it = testEffect(Layer.empty)
const selection = Schema.decodeUnknownSync(ConfigModel.Selection)
const emptyCredentialNode = makeGlobalNode({
service: Credential.Service,
layer: Layer.succeed(
Credential.Service,
Credential.Service.of({
all: () => Effect.succeed([]),
list: () => Effect.succeed([]),
get: () => Effect.succeed(undefined),
create: () => Effect.die("unused Credential.create"),
update: () => Effect.die("unused Credential.update"),
remove: () => Effect.die("unused Credential.remove"),
}),
),
deps: [],
})
const emptyWellknownNode = makeGlobalNode({
service: WellKnown.Service,
layer: Layer.succeed(
WellKnown.Service,
WellKnown.Service.of({
entries: () => Effect.succeed([]),
snapshot: () => [],
add: () => Effect.die("unused Wellknown.add"),
remove: () => Effect.die("unused Wellknown.remove"),
changes: Stream.empty,
resolve: () => Effect.die("unused Wellknown.resolve"),
}),
),
deps: [],
})
function testLayer(
directory: string,
globalDirectory = path.join(directory, "global"),
projectDirectory = directory,
vcs?: Project.Vcs,
watcher?: Layer.Layer<Watcher.Service>,
credentialNode = emptyCredentialNode,
wellknownNode = emptyWellknownNode,
) {
const locationLayer = Layer.succeed(
Location.Service,
@@ -45,6 +83,8 @@ function testLayer(
return AppNodeBuilder.build(LayerNode.group([Config.node, EventV2.node]), [
[Location.node, locationLayer],
[Global.node, Global.layerWith({ config: globalDirectory, home: path.join(globalDirectory, "home") })],
[Credential.node, credentialNode],
[WellKnown.node, wellknownNode],
...(watcher ? ([[Watcher.node, watcher]] as const) : []),
])
}
@@ -125,6 +165,86 @@ describe("Config", () => {
}),
)
it.live("loads authenticated wellknown config at highest priority", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) =>
Effect.gen(function* () {
const global = path.join(tmp.path, "global")
const project = path.join(tmp.path, "project")
yield* Effect.promise(async () => {
await fs.mkdir(global, { recursive: true })
await fs.mkdir(project, { recursive: true })
await fs.writeFile(path.join(global, "opencode.json"), JSON.stringify({ shell: "global" }))
await fs.writeFile(path.join(project, "opencode.json"), JSON.stringify({ shell: "project" }))
})
const integrationID = Integration.ID.make("https://example.com")
let key = "secret"
const credentialNode = makeGlobalNode({
service: Credential.Service,
layer: Layer.succeed(
Credential.Service,
Credential.Service.of({
all: () => Effect.die("unused Credential.all"),
list: () =>
Effect.succeed([
new Credential.Info({
id: Credential.ID.create(),
integrationID,
label: "default",
value: Credential.Key.make({ type: "key", key }),
}),
]),
get: () => Effect.die("unused Credential.get"),
create: () => Effect.die("unused Credential.create"),
update: () => Effect.die("unused Credential.update"),
remove: () => Effect.die("unused Credential.remove"),
}),
),
deps: [],
})
const entry: WellKnown.Entry = {
origin: "https://example.com",
integrationID,
manifest: { auth: { command: ["login"], env: "TOKEN" } },
}
const wellknownNode = makeGlobalNode({
service: WellKnown.Service,
layer: Layer.succeed(
WellKnown.Service,
WellKnown.Service.of({
entries: () => Effect.succeed([entry]),
snapshot: () => [entry],
add: () => Effect.die("unused Wellknown.add"),
remove: () => Effect.die("unused Wellknown.remove"),
changes: Stream.empty,
resolve: (_entry, variables) => Effect.succeed([{ shell: variables.TOKEN }]),
}),
),
deps: [],
})
return yield* Effect.gen(function* () {
const config = yield* Config.Service
const events = yield* EventV2.Service
expect(Config.latest(yield* config.entries(), "shell")).toBe("secret")
const updated = yield* events
.subscribe(ConfigSchema.Event.Updated)
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow
key = "next"
yield* events.publish(Integration.Event.ConnectionUpdated, { integrationID })
expect(yield* Fiber.join(updated)).toHaveLength(1)
expect(Config.latest(yield* config.entries(), "shell")).toBe("next")
}).pipe(
Effect.provide(testLayer(project, global, project, undefined, undefined, credentialNode, wellknownNode)),
)
}),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
it.effect("detects v1 configuration from any v1-only top-level key", () =>
Effect.sync(() => {
expect(ConfigMigrateV1.isV1({ snapshot: false })).toBe(true)
@@ -282,9 +402,7 @@ describe("Config", () => {
const config = yield* Config.Service
yield* config.entries()
expect(targets).toEqual([
{ type: "directory", path: AbsolutePath.make(path.join(tmp.path, "global")) },
])
expect(targets).toEqual([{ type: "directory", path: AbsolutePath.make(path.join(tmp.path, "global")) }])
}).pipe(Effect.provide(testLayer(tmp.path, undefined, undefined, undefined, watcher)))
}),
),
+2 -2
View File
@@ -181,7 +181,7 @@ describe("Integration", () => {
command: [
process.execPath,
"-e",
'console.log("https://example.com/login"); await Bun.sleep(50); console.log("secret")',
'console.error("https://example.com/login"); await Bun.sleep(50); console.log("secret")',
],
},
}),
@@ -192,7 +192,7 @@ describe("Integration", () => {
integrations.command.status({ integrationID, attemptID: attempt.attemptID }),
(status) => status.status === "pending" && status.message?.includes("https://example.com/login") === true,
)
expect(pending).toMatchObject({ status: "pending", message: "https://example.com/login" })
expect(pending).toMatchObject({ status: "pending", message: "https://example.com/login\n" })
expect(
yield* eventually(
+25
View File
@@ -0,0 +1,25 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { KV } from "@opencode-ai/core/kv"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { testEffect } from "./lib/effect"
const it = testEffect(LayerNode.compile(KV.node))
describe("KV", () => {
it.effect("stores, replaces, and removes JSON values", () =>
Effect.gen(function* () {
const kv = yield* KV.Service
expect(yield* kv.get("wellknown:sources")).toBeUndefined()
yield* kv.set("wellknown:sources", ["https://example.com"])
expect(yield* kv.get("wellknown:sources")).toEqual(["https://example.com"])
yield* kv.set("wellknown:sources", ["https://example.com", "https://example.org"])
expect(yield* kv.get("wellknown:sources")).toEqual(["https://example.com", "https://example.org"])
yield* kv.remove("wellknown:sources")
expect(yield* kv.get("wellknown:sources")).toBeUndefined()
}),
)
})
+1
View File
@@ -97,6 +97,7 @@ export function host(overrides: Overrides = {}): PluginContext {
get: overrides.session?.get ?? (() => Effect.die("unused session.get")),
prompt: overrides.session?.prompt ?? (() => Effect.die("unused session.prompt")),
command: overrides.session?.command ?? (() => Effect.die("unused session.command")),
synthetic: overrides.session?.synthetic ?? (() => Effect.die("unused session.synthetic")),
interrupt: overrides.session?.interrupt ?? (() => Effect.die("unused session.interrupt")),
},
}
+55 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect } from "bun:test"
import { Message, SystemPart } from "@opencode-ai/ai"
import { Effect, Schema } from "effect"
import { DateTime, Effect, Schema } from "effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog"
import { ModelV2 } from "@opencode-ai/core/model"
@@ -10,6 +10,7 @@ import { PluginHost } from "@opencode-ai/core/plugin/host"
import { PluginPromise } from "@opencode-ai/core/plugin/promise"
import { SessionV2 } from "@opencode-ai/core/session"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { SessionPending } from "@opencode-ai/core/session/pending"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Plugin } from "@opencode-ai/plugin/v2"
@@ -18,10 +19,63 @@ import { Model } from "@opencode-ai/schema/model"
import { Provider } from "@opencode-ai/schema/provider"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
import { host as testHost } from "./host"
const it = testEffect(PluginTestLayer)
describe("fromPromise", () => {
it.effect("forwards synthetic session input", () =>
Effect.gen(function* () {
const input = {
sessionID: "ses_synthetic",
id: "msg_synthetic",
text: "Background work completed",
description: null,
metadata: { shellID: "shell_1" },
delivery: null,
resume: null,
}
let seen: unknown
const host = testHost({
session: {
synthetic: (value) => {
seen = value
return Effect.succeed(
SessionPending.Synthetic.make({
admittedSeq: 1,
id: SessionMessage.ID.make(input.id),
sessionID: SessionV2.ID.make(input.sessionID),
timeCreated: DateTime.makeUnsafe(0),
type: "synthetic",
data: {
text: input.text,
metadata: input.metadata,
},
delivery: "queue",
}),
)
},
},
})
yield* PluginPromise.fromPromise(
Plugin.define({
id: "promise-session-synthetic",
setup: async (ctx) => {
await ctx.session.synthetic(input)
},
}),
).effect(host)
expect(seen).toEqual({
...input,
description: undefined,
delivery: undefined,
resume: undefined,
})
}),
)
it.effect("forwards standard client reads", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
+82
View File
@@ -0,0 +1,82 @@
import { expect } from "bun:test"
import { Effect, Fiber, Stream } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { KV } from "@opencode-ai/core/kv"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { WellKnown } from "@opencode-ai/core/wellknown"
import { testEffect } from "./lib/effect"
const it = testEffect(FetchHttpClient.layer)
const serviceIt = testEffect(LayerNode.compile(LayerNode.group([WellKnown.node, KV.node])))
it.live("loads embedded and remote configuration", () =>
Effect.acquireUseRelease(
Effect.sync(() =>
Bun.serve({
port: 0,
fetch(request) {
const url = new URL(request.url)
if (url.pathname === "/.well-known/opencode") {
return Response.json({
auth: { command: ["login"], env: "TOKEN" },
config: { model: "embedded/model" },
remote_config: {
url: `${url.origin}/config/{env:TOKEN}`,
headers: { authorization: "Bearer {env:TOKEN}" },
},
})
}
if (url.pathname === "/config/secret" && request.headers.get("authorization") === "Bearer secret") {
return Response.json({ config: { model: "remote/model" } })
}
return new Response("Not found", { status: 404 })
},
}),
),
(server) =>
Effect.gen(function* () {
const origin = server.url.origin
expect(yield* WellKnown.inspect(`${origin}/`)).toEqual({
auth: { command: ["login"], env: "TOKEN" },
config: { model: "embedded/model" },
remote_config: {
url: `${origin}/config/{env:TOKEN}`,
headers: { authorization: "Bearer {env:TOKEN}" },
},
})
expect(yield* WellKnown.resolve({ origin, variables: { TOKEN: "secret" } })).toEqual([
{ model: "embedded/model" },
{ model: "remote/model" },
])
}),
(server) => Effect.promise(() => server.stop(true)),
),
)
serviceIt.live("persists sources in one KV value", () =>
Effect.acquireUseRelease(
Effect.sync(() =>
Bun.serve({
port: 0,
fetch: () => Response.json({ auth: { command: ["login"], env: "TOKEN" } }),
}),
),
(server) =>
Effect.gen(function* () {
const wellknown = yield* WellKnown.Service
const kv = yield* KV.Service
const changed = yield* wellknown.changes.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
const entry = yield* wellknown.add(`${server.url.origin}/`)
expect(entry.origin).toBe(server.url.origin)
expect(yield* kv.get("wellknown:sources")).toEqual([server.url.origin])
expect(yield* wellknown.entries()).toEqual([entry])
expect(yield* Fiber.join(changed)).toHaveLength(1)
yield* wellknown.remove(server.url.origin)
expect(yield* kv.get("wellknown:sources")).toEqual([])
expect(yield* wellknown.entries()).toEqual([])
}),
(server) => Effect.promise(() => server.stop(true)),
),
)
+1 -1
View File
@@ -177,7 +177,7 @@ and plugin options.
| `ctx.integration` | `list`, `get`, `connect`, `attempt`, `transform`, `reload`, and connection lookup/resolution |
| `ctx.plugin` | `list` currently active plugin IDs |
| `ctx.reference` | `list`, `transform`, `reload` |
| `ctx.session` | `create`, `get`, `prompt`, `command`, `interrupt`, and `hook` |
| `ctx.session` | `create`, `get`, `prompt`, `command`, `synthetic`, `interrupt`, and `hook` |
| `ctx.skill` | `list`, `transform`, `reload` |
| `ctx.tool` | `transform` and `hook` |
| `ctx.aisdk` | `hook` |
@@ -1528,7 +1528,7 @@ describe("session.message-v2.fromError", () => {
test("classifies ZlibError from fetch as retryable APIError", () => {
const zlibError = new Error(
'ZlibError fetching "https://opencode.cloudflare.dev/anthropic/messages". For more information, pass `verbose: true` in the second argument to fetch()',
'ZlibError fetching "https://example.com/anthropic/messages". For more information, pass `verbose: true` in the second argument to fetch()',
)
;(zlibError as any).code = "ZlibError"
;(zlibError as any).errno = 0
@@ -1543,7 +1543,7 @@ describe("session.message-v2.fromError", () => {
test("classifies ZlibError as AbortedError when abort context is provided", () => {
const zlibError = new Error(
'ZlibError fetching "https://opencode.cloudflare.dev/anthropic/messages". For more information, pass `verbose: true` in the second argument to fetch()',
'ZlibError fetching "https://example.com/anthropic/messages". For more information, pass `verbose: true` in the second argument to fetch()',
)
;(zlibError as any).code = "ZlibError"
;(zlibError as any).errno = 0
+3 -3
View File
@@ -34,9 +34,9 @@
"zod": "catalog:"
},
"peerDependencies": {
"@opentui/core": ">=0.4.3",
"@opentui/keymap": ">=0.4.3",
"@opentui/solid": ">=0.4.3"
"@opentui/core": "0.0.0-20260717-0afb0c25",
"@opentui/keymap": "0.0.0-20260717-0afb0c25",
"@opentui/solid": "0.0.0-20260717-0afb0c25"
},
"peerDependenciesMeta": {
"@opentui/core": {
+1 -1
View File
@@ -61,7 +61,7 @@ export interface IntegrationDraft {
}
}
export interface IntegrationDomain extends IntegrationApi<unknown> {
export interface IntegrationDomain extends Omit<IntegrationApi<unknown>, "wellknown"> {
readonly transform: Transform<IntegrationDraft>
readonly reload: () => Effect.Effect<void>
readonly connection: {
+4 -1
View File
@@ -19,6 +19,9 @@ export interface SessionHooks {
readonly context: SessionContext
}
export type SessionDomain = Pick<SessionApi<unknown>, "create" | "get" | "prompt" | "command" | "interrupt"> & {
export type SessionDomain = Pick<
SessionApi<unknown>,
"create" | "get" | "prompt" | "command" | "synthetic" | "interrupt"
> & {
readonly hook: Hooks<SessionHooks>
}
@@ -5,7 +5,7 @@ import type { Transform } from "./registration.js"
export type { IntegrationDraft, IntegrationMethodRegistration }
export interface IntegrationDomain extends IntegrationApi {
export interface IntegrationDomain extends Omit<IntegrationApi, "wellknown"> {
readonly transform: Transform<IntegrationDraft>
readonly reload: () => Promise<void>
readonly connection: {
+1 -1
View File
@@ -19,6 +19,6 @@ export interface SessionHooks {
readonly context: SessionContext
}
export type SessionDomain = Pick<SessionApi, "create" | "get" | "prompt" | "command" | "interrupt"> & {
export type SessionDomain = Pick<SessionApi, "create" | "get" | "prompt" | "command" | "synthetic" | "interrupt"> & {
readonly hook: Hooks<SessionHooks>
}
@@ -37,6 +37,22 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
}),
),
)
.add(
HttpApiEndpoint.post("integration.wellknown.add", "/api/experimental/integration/wellknown", {
query: LocationQuery,
payload: Schema.Struct({ url: Schema.String }),
success: HttpApiSchema.NoContent,
error: InvalidRequestError,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.experimental.integration.wellknown.add",
summary: "Add wellknown integration",
description: "Discover and persist an experimental wellknown integration source.",
}),
),
)
.add(
HttpApiEndpoint.post("integration.connect.key", "/api/integration/:integrationID/connect/key", {
params: { integrationID: Integration.ID },
+65
View File
@@ -280,6 +280,8 @@ import type {
V2DebugLocationListResponses,
V2EventSubscribeErrors,
V2EventSubscribeResponses,
V2ExperimentalIntegrationWellknownAddErrors,
V2ExperimentalIntegrationWellknownAddResponses,
V2FormRequestListErrors,
V2FormRequestListResponses,
V2FsFindErrors,
@@ -7209,6 +7211,64 @@ export class Integration extends HeyApiClient {
}
}
export class Wellknown extends HeyApiClient {
/**
* Add wellknown integration
*
* Discover and persist an experimental wellknown integration source.
*/
public add<ThrowOnError extends boolean = false>(
parameters?: {
location?: {
directory?: string | null
workspace?: string | null
} | null
url?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "location" },
{ in: "body", key: "url" },
],
},
],
)
return (options?.client ?? this.client).post<
V2ExperimentalIntegrationWellknownAddResponses,
V2ExperimentalIntegrationWellknownAddErrors,
ThrowOnError
>({
url: "/api/experimental/integration/wellknown",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
}
export class Integration2 extends HeyApiClient {
private _wellknown?: Wellknown
get wellknown(): Wellknown {
return (this._wellknown ??= new Wellknown({ client: this.client }))
}
}
export class Experimental2 extends HeyApiClient {
private _integration?: Integration2
get integration(): Integration2 {
return (this._integration ??= new Integration2({ client: this.client }))
}
}
export class Resource2 extends HeyApiClient {
/**
* List MCP resources
@@ -8500,6 +8560,11 @@ export class V2 extends HeyApiClient {
return (this._integration ??= new Integration({ client: this.client }))
}
private _experimental?: Experimental2
get experimental(): Experimental2 {
return (this._experimental ??= new Experimental2({ client: this.client }))
}
private _mcp?: Mcp2
get mcp(): Mcp2 {
return (this._mcp ??= new Mcp2({ client: this.client }))
+38
View File
@@ -16859,6 +16859,44 @@ export type V2IntegrationGetResponses = {
export type V2IntegrationGetResponse = V2IntegrationGetResponses[keyof V2IntegrationGetResponses]
export type V2ExperimentalIntegrationWellknownAddData = {
body: {
url: string
}
path?: never
query?: {
location?: {
directory?: string | null
workspace?: string | null
} | null
}
url: "/api/experimental/integration/wellknown"
}
export type V2ExperimentalIntegrationWellknownAddErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError1 | InvalidRequestErrorV2
/**
* UnauthorizedError
*/
401: UnauthorizedError
}
export type V2ExperimentalIntegrationWellknownAddError =
V2ExperimentalIntegrationWellknownAddErrors[keyof V2ExperimentalIntegrationWellknownAddErrors]
export type V2ExperimentalIntegrationWellknownAddResponses = {
/**
* <No Content>
*/
204: void
}
export type V2ExperimentalIntegrationWellknownAddResponse =
V2ExperimentalIntegrationWellknownAddResponses[keyof V2ExperimentalIntegrationWellknownAddResponses]
export type V2IntegrationConnectKeyData = {
body: {
key: string
@@ -4,6 +4,7 @@ import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import { Api } from "../api"
import { InvalidRequestError } from "@opencode-ai/protocol/errors"
import { response } from "../location"
import { WellKnown } from "@opencode-ai/core/wellknown"
const authorize = <A, R>(effect: Effect.Effect<A, Integration.AuthorizationError, R>) =>
effect.pipe(
@@ -33,6 +34,22 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
return yield* response(service.get(ctx.params.integrationID))
}),
)
.handle(
"integration.wellknown.add",
Effect.fn(function* (ctx) {
const wellknown = yield* WellKnown.Service
const integration = yield* Integration.Service
yield* wellknown
.add(ctx.payload.url)
.pipe(
Effect.mapError(
(error) => new InvalidRequestError({ message: error.message, kind: "well_known_discovery" }),
),
)
yield* integration.reload()
return HttpApiSchema.NoContent.make()
}),
)
.handle(
"integration.connect.key",
Effect.fn(function* (ctx) {
+4 -3
View File
@@ -16,6 +16,7 @@ import { SessionRestart } from "@opencode-ai/core/session/execution/restart"
import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { WellKnown } from "@opencode-ai/core/wellknown"
import { HttpRouter, HttpServer } from "effect/unstable/http"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Context, Effect, Layer, Option } from "effect"
@@ -44,6 +45,7 @@ const applicationServices = LayerNode.group([
PermissionSaved.node,
PtyTicket.node,
Credential.node,
WellKnown.node,
PtyEnvironment.node,
LocationServiceMap.node,
SessionRestart.node,
@@ -85,7 +87,7 @@ function makeRoutes<AuthError, AuthServices>(
Layer.flatMap((context) => {
const services = Layer.succeedContext(context)
const requestServices = Layer.merge(
Layer.succeedContext(Context.pick(PermissionSaved.Service, Project.Service)(context)),
Layer.succeedContext(Context.pick(PermissionSaved.Service, Project.Service, WellKnown.Service)(context)),
ServerInfo.layer(serviceURLs),
)
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
@@ -109,5 +111,4 @@ function simulateEnabled() {
return !!process.env.OPENCODE_SIMULATE
}
export const webHandler = () =>
HttpRouter.toWebHandler(createRoutes().pipe(Layer.provide(HttpServer.layerServices)))
export const webHandler = () => HttpRouter.toWebHandler(createRoutes().pipe(Layer.provide(HttpServer.layerServices)))
+25
View File
@@ -43,6 +43,7 @@
"./util/persistence": "./src/util/persistence.ts",
"./util/record": "./src/util/record.ts",
"./util/session": "./src/util/session.ts",
"./util/string-width": "./src/util/string-width.ts",
"./logo": "./src/logo.ts",
"./ui/dialog": "./src/ui/dialog.tsx",
"./ui/spinner": "./src/ui/spinner.ts",
@@ -50,6 +51,28 @@
"./component/spinner": "./src/component/spinner.tsx",
"./component/register-spinner": "./src/component/register-spinner.ts"
},
"imports": {
"#attention-sounds": {
"bun": "./src/attention-sounds.bun.ts",
"node": "./src/attention-sounds.node.ts",
"default": "./src/attention-sounds.bun.ts"
},
"#terminal-win32": {
"bun": "./src/terminal-win32.bun.ts",
"node": "./src/terminal-win32.node.ts",
"default": "./src/terminal-win32.bun.ts"
},
"#string-width": {
"bun": "./src/util/string-width.bun.ts",
"node": "./src/util/string-width.node.ts",
"default": "./src/util/string-width.bun.ts"
},
"#zed-sqlite": {
"bun": "./src/editor-zed-sqlite.bun.ts",
"node": "./src/editor-zed-sqlite.node.ts",
"default": "./src/editor-zed-sqlite.bun.ts"
}
},
"dependencies": {
"@opencode-ai/client": "workspace:*",
"@opencode-ai/core": "workspace:*",
@@ -64,10 +87,12 @@
"diff": "catalog:",
"effect": "catalog:",
"fuzzysort": "catalog:",
"get-east-asian-width": "catalog:",
"open": "10.1.2",
"opentui-spinner": "catalog:",
"remeda": "catalog:",
"solid-js": "catalog:",
"string-width": "catalog:",
"strip-ansi": "7.1.2",
"uqr": "0.1.3"
},
+8
View File
@@ -0,0 +1,8 @@
/// <reference path="./audio.d.ts" />
import defaultSoundPath from "@opencode-ai/ui/audio/bip-bop-01.mp3" with { type: "file" }
import questionSoundPath from "@opencode-ai/ui/audio/bip-bop-03.mp3" with { type: "file" }
import permissionSoundPath from "@opencode-ai/ui/audio/staplebops-06.mp3" with { type: "file" }
import errorSoundPath from "@opencode-ai/ui/audio/nope-03.mp3" with { type: "file" }
import subagentDoneSoundPath from "@opencode-ai/ui/audio/yup-01.mp3" with { type: "file" }
export { defaultSoundPath, questionSoundPath, permissionSoundPath, errorSoundPath, subagentDoneSoundPath }
+16
View File
@@ -0,0 +1,16 @@
import { createRequire } from "node:module"
import path from "node:path"
const require = createRequire(import.meta.url)
const resolve = (name: string) => {
const key = `@opencode-ai/ui/audio/${name}`
return process.env.OPENCODE_NODE_ASSETS_DIR
? path.join(process.env.OPENCODE_NODE_ASSETS_DIR, key)
: require.resolve(key)
}
export const defaultSoundPath = resolve("bip-bop-01.mp3")
export const questionSoundPath = resolve("bip-bop-03.mp3")
export const permissionSoundPath = resolve("staplebops-06.mp3")
export const errorSoundPath = resolve("nope-03.mp3")
export const subagentDoneSoundPath = resolve("yup-01.mp3")
+8 -7
View File
@@ -14,12 +14,13 @@ import { AttentionSoundName, type Config } from "./config"
import { Schema } from "effect"
import stripAnsi from "strip-ansi"
import * as TuiAudio from "./audio"
import defaultSoundPath from "@opencode-ai/ui/audio/bip-bop-01.mp3" with { type: "file" }
import questionSoundPath from "@opencode-ai/ui/audio/bip-bop-03.mp3" with { type: "file" }
import permissionSoundPath from "@opencode-ai/ui/audio/staplebops-06.mp3" with { type: "file" }
import errorSoundPath from "@opencode-ai/ui/audio/nope-03.mp3" with { type: "file" }
import doneSoundPath from "@opencode-ai/ui/audio/bip-bop-01.mp3" with { type: "file" }
import subagentDoneSoundPath from "@opencode-ai/ui/audio/yup-01.mp3" with { type: "file" }
import {
defaultSoundPath,
questionSoundPath,
permissionSoundPath,
errorSoundPath,
subagentDoneSoundPath,
} from "#attention-sounds"
type FocusState = "unknown" | "focused" | "blurred"
@@ -51,7 +52,7 @@ const BUILTIN_PACK: RegisteredSoundPack = {
question: questionSoundPath,
permission: permissionSoundPath,
error: errorSoundPath,
done: doneSoundPath,
done: defaultSoundPath,
subagent_done: subagentDoneSoundPath,
},
}
@@ -185,6 +185,8 @@ function CommandStarting(props: {
const dialog = useDialog()
const client = useClient()
const toast = useToast()
let closed = false
let handedOff = false
onMount(() => {
void client.api.integration.command
@@ -193,7 +195,16 @@ function CommandStarting(props: {
methodID: props.method.id,
location: location(data),
})
.then((result) =>
.then((result) => {
if (closed) {
void client.api.integration.command.cancel({
integrationID: props.integration.id,
attemptID: result.data.attemptID,
location: location(data),
})
return
}
handedOff = true
dialog.replace(() => (
<CommandPending
integration={props.integration}
@@ -201,13 +212,17 @@ function CommandStarting(props: {
attempt={result.data}
onConnected={props.onConnected}
/>
)),
)
))
})
.catch((cause) => {
if (closed) return
toast.show({ variant: "error", message: message(cause) })
dialog.clear()
})
})
onCleanup(() => {
if (!handedOff) closed = true
})
return <CommandView title={props.method.label} output="" message="Starting command..." />
}
@@ -275,9 +290,10 @@ function CommandPending(props: {
function CommandView(props: { title: string; output: string; message: string }) {
const dialog = useDialog()
const { theme } = useTheme()
onMount(() => dialog.setSize("large"))
return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<box gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
<text attributes={TextAttributes.BOLD} fg={theme.text}>
{props.title}
</text>
@@ -285,10 +301,12 @@ function CommandView(props: { title: string; output: string; message: string })
esc close
</text>
</box>
<box backgroundColor={theme.backgroundElement} paddingLeft={1} paddingRight={1}>
<text fg={theme.text}>{props.output}</text>
<box backgroundColor={theme.backgroundElement} paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
<text fg={theme.text}>{props.output.trim()}</text>
</box>
<box paddingLeft={2} paddingRight={2}>
<text fg={theme.textMuted}>{props.message}</text>
</box>
<text fg={theme.textMuted}>{props.message}</text>
</box>
)
}
@@ -1,5 +1,5 @@
import type { BoxRenderable, TextareaRenderable, ScrollBoxRenderable } from "@opentui/core"
import { pathToFileURL } from "bun"
import { pathToFileURL } from "node:url"
import fuzzysort from "fuzzysort"
import path from "path"
import { firstBy } from "remeda"
@@ -22,6 +22,7 @@ import { useBindings } from "../../keymap"
import { Keymap } from "../../context/keymap"
import { displayCharAt, mentionTriggerIndex } from "../../prompt/display"
import type { FileSystemEntry } from "@opencode-ai/client"
import { stringWidth } from "../../util/string-width"
function removeLineRange(input: string) {
const hashIndex = input.lastIndexOf("#")
@@ -189,7 +190,7 @@ export function Autocomplete(props: {
const virtualText = "@" + text
const extmarkStart = store.index
const extmarkEnd = extmarkStart + Bun.stringWidth(virtualText)
const extmarkEnd = extmarkStart + stringWidth(virtualText)
const styleId = part.type === "file" ? props.fileStyleId : props.agentStyleId
@@ -432,7 +433,7 @@ export function Autocomplete(props: {
const cursor = props.input().logicalCursor
props.input().deleteRange(0, 0, cursor.row, cursor.col)
props.input().insertText(newText)
props.input().cursorOffset = Bun.stringWidth(newText)
props.input().cursorOffset = stringWidth(newText)
}
const commands = createMemo((): AutocompleteOption[] => {
+2 -1
View File
@@ -28,6 +28,7 @@ import { editorSelectionKey, useEditorContext, type EditorSelection } from "../.
import { normalizePromptContent, openEditor } from "../../editor"
import { useExit } from "../../context/exit"
import { promptOffsetWidth } from "../../prompt/display"
import { stringWidth } from "../../util/string-width"
import { createStore, produce, unwrap } from "solid-js/store"
import { emptyPrompt, usePromptHistory, type PromptInfo, type PromptPartRef } from "../../prompt/history"
import { computePromptTraits } from "../../prompt/traits"
@@ -530,7 +531,7 @@ export function Prompt(props: PromptProps) {
pasted: [],
})
restoreExtmarksFromPrompt(store.prompt)
input.cursorOffset = Bun.stringWidth(normalized)
input.cursorOffset = stringWidth(normalized)
},
},
{
@@ -0,0 +1 @@
export { Database } from "bun:sqlite"
@@ -0,0 +1,21 @@
import { DatabaseSync, type SQLInputValue } from "node:sqlite"
export class Database {
readonly #database: DatabaseSync
constructor(file: string, options?: { readonly?: boolean }) {
this.#database = new DatabaseSync(file, { readOnly: options?.readonly })
}
query(sql: string) {
const statement = this.#database.prepare(sql)
return {
all: (parameters?: Record<string, SQLInputValue>) => (parameters ? statement.all(parameters) : statement.all()),
get: (parameters?: Record<string, SQLInputValue>) => (parameters ? statement.get(parameters) : statement.get()),
}
}
close() {
this.#database.close()
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { Database } from "bun:sqlite"
import { Database } from "#zed-sqlite"
import { statSync } from "node:fs"
import { readFile as readFileAsync } from "node:fs/promises"
import os from "node:os"

Some files were not shown because too many files have changed in this diff Show More