Compare commits

..

19 Commits

Author SHA1 Message Date
GitHub Action
3a77f98ead chore: regen sdk 2025-12-07 05:55:22 +00:00
Aiden Cline
c8c29faf4d fix: typo 2025-12-06 23:54:50 -06:00
GitHub Action
e367c8439b chore: format code 2025-12-07 05:47:44 +00:00
Aiden Cline
e52d22039e rebase 2025-12-06 23:47:08 -06:00
Aiden Cline
9425eee09f fix: external dir 2025-12-06 23:47:08 -06:00
GitHub Action
320ebb35b7 chore: regen sdk 2025-12-07 05:46:16 +00:00
Aiden Cline
775ddd8c46 Merge branch 'dev' into bash-tweaks 2025-12-06 21:45:50 -08:00
Aiden Cline
32566a16a5 Revise bash.txt for command usage guidelines
Updated examples to avoid using 'cd' and clarified command usage.
2025-12-06 17:30:10 -06:00
Aiden Cline
414bbd50ac fix example
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-06 17:28:53 -06:00
Aiden Cline
b40bb950ae fix typo
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-06 17:28:43 -06:00
Aiden Cline
fcc256c3c9 fix: test 2025-12-06 17:04:53 -06:00
Aiden Cline
bfbcf978fa tweak: timeout defaults 2025-12-05 16:31:10 -06:00
Aiden Cline
c75dae680a remove max timeout 2025-12-05 16:28:32 -06:00
Aiden Cline
639824b1ea tweak 2025-12-05 16:18:52 -06:00
Aiden Cline
8ede7c59d2 fix 2025-12-05 16:12:40 -06:00
Aiden Cline
61cf0aeee2 fix 2025-12-05 16:12:18 -06:00
Aiden Cline
ae328d338e bash tweaks 2025-12-05 16:11:39 -06:00
Aiden Cline
2f5d6ba447 Merge branch 'dev' into bash-tweaks 2025-12-05 14:42:23 -06:00
Aiden Cline
6cf5fe481b wip 2025-12-05 14:42:10 -06:00
389 changed files with 46609 additions and 11424 deletions

View File

@@ -18,6 +18,7 @@ jobs:
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.head_ref || github.ref }}
- name: Setup Bun
uses: ./.github/actions/setup-bun
@@ -27,5 +28,3 @@ jobs:
./script/format.ts
env:
CI: true
GITHUB_HEAD_REF: ${{ github.head_ref }}
GITHUB_REF_NAME: ${{ github.ref_name }}

View File

@@ -26,7 +26,7 @@ permissions:
jobs:
publish:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'sst/opencode' && github.ref == 'refs/heads/dev'
if: github.repository == 'sst/opencode'
steps:
- uses: actions/checkout@v3
with:
@@ -99,26 +99,6 @@ jobs:
with:
fetch-depth: 0
- uses: apple-actions/import-codesign-certs@v2
if: ${{ runner.os == 'macOS' }}
with:
keychain: build
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Verify Certificate
if: ${{ runner.os == 'macOS' }}
run: |
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported."
- name: Setup Apple API Key
if: ${{ runner.os == 'macOS' }}
run: |
echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
- run: git fetch --force --tags
- uses: ./.github/actions/setup-bun
@@ -164,17 +144,12 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
with:
projectPath: packages/tauri
uploadWorkflowArtifacts: true
tauriScript: ${{ (startsWith(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
args: --target ${{ matrix.settings.target }}
updaterJsonPreferNsis: true
# releaseId: TODO
releaseDraft: true
tagName: ${{ inputs.version }}
releaseName: ${{ inputs.version }}

View File

@@ -162,4 +162,3 @@
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |

100
bun.lock
View File

@@ -324,7 +324,7 @@
"name": "@opencode-ai/sdk",
"version": "1.0.134",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
@@ -468,12 +468,12 @@
"@types/bun": "1.3.3",
"@types/luxon": "3.7.1",
"@types/node": "22.13.9",
"@typescript/native-preview": "7.0.0-dev.20251207.1",
"@typescript/native-preview": "7.0.0-dev.20251014.1",
"ai": "5.0.97",
"diff": "8.0.2",
"fuzzysort": "3.1.0",
"hono": "4.10.7",
"hono-openapi": "1.1.2",
"hono-openapi": "1.1.1",
"luxon": "3.6.1",
"remeda": "2.26.0",
"solid-js": "1.9.10",
@@ -851,11 +851,9 @@
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.3.3", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg=="],
"@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.0.6", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0", "lodash": "^4.17.21" } }, "sha512-yktiFZoWPtEW8QKS65eqKwA5MTKp88CyiL8q72WynrBs/73SAaxlSWlA2zW/DZlywZ5hX1OYzrCC0wFdvO9c2w=="],
"@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA=="],
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.88.1", "", { "dependencies": { "@hey-api/codegen-core": "^0.3.3", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.2", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-x/nDTupOnV9VuSeNIiJpgIpc915GHduhyseJeMTnI0JMsXaObmpa0rgPr3ASVEYMLgpvqozIEG1RTOOnal6zLQ=="],
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.81.0", "", { "dependencies": { "@hey-api/json-schema-ref-parser": "1.0.6", "ansi-colors": "4.1.3", "c12": "2.0.1", "color-support": "1.1.3", "commander": "13.0.0", "handlebars": "4.7.8", "js-yaml": "4.1.0", "open": "10.1.2", "semver": "7.7.2" }, "peerDependencies": { "typescript": "^5.5.3" }, "bin": { "openapi-ts": "bin/index.cjs" } }, "sha512-PoJukNBkUfHOoMDpN33bBETX49TUhy7Hu8Sa0jslOvFndvZ5VjQr4Nl/Dzjb9LG1Lp5HjybyTJMA6a1zYk/q6A=="],
"@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="],
@@ -1765,21 +1763,21 @@
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251207.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251207.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-4QcRnzB0pi9rS0AOvg8kWbmuwHv5X7B2EXHbgcms9+56hsZ8SZrZjNgBJb2rUIodJ4kU5mrkj/xlTTT4r9VcpQ=="],
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251014.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251014.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251014.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251014.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251014.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251014.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251014.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251014.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-IqmX5CYCBqXbfL+HKlcQAMaDlfJ0Z8OhUxvADFV2TENnzSYI4CuhvKxwOB2wFSLXufVsgtAlf3Fjwn24KmMyPQ=="],
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251207.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-waWJnuuvkXh4WdpbTjYf7pyahJzx0ycesV2BylyHrE9OxU9FSKcD/cRLQYvbq3YcBSdF7sZwRLDBer7qTeLsYA=="],
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251014.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7rQoLlerWnwnvrM56hP4rdEbo4xDE4zr7cch+EzgENq/tbXYereGq1fmnR83UNglb1Eyy53OvJZ3O2csYBa2vg=="],
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20251207.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-3bkD9QuIjxETtp6J1l5X2oKgudJ8z+8fwUq0izCjK1JrIs2vW1aQnbzxhynErSyHWH7URGhHHzcsXHbikckAsg=="],
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20251014.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-SF29o9NFRGDM23Jz0nVO4/yS78GQ81rtOemmCVNXuJotoY4bP3npGDyEmfkZQHZgDOXogs2OWy3t7NUJ235ANQ=="],
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20251207.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OjrZBq8XJkB7uCQvT1AZ1FPsp+lT0cHxY5SisE+ZTAU6V0IHAZMwJ7J/mnwlGsBcCKRLBT+lX3hgEuOTSwHr9w=="],
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20251014.1", "", { "os": "linux", "cpu": "arm" }, "sha512-o5cu7h+BBAp6V4qxYY5RWuaYouN3j+MGFLrrUtvvNj4XKM+kbq5qwsgVRsmJZ1LfUvHmzyQs86vt9djAWedzjQ=="],
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20251207.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qhp06OObkwy5B+PlAhAmq+Ls3GVt4LHAovrTRcpLB3Mk3yJ0h9DnIQwPQiayp16TdvTsGHI3jdIX4MGm5L/ghA=="],
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20251014.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+YWbW/JF4uggEUBr+vflqI5i7bL4Z3XInCOyUO1qQEY7VmfDCsPEzIwGi37O1mixfxw9Qj8LQsptCkU+fqKwGw=="],
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20251207.1", "", { "os": "linux", "cpu": "x64" }, "sha512-fPRw0zfTBeVmrkgi5Le+sSwoeAz6pIdvcsa1OYZcrspueS9hn3qSC5bLEc5yX4NJP1vItadBqyGLUQ7u8FJjow=="],
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20251014.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3LC4tgcgi6zWJWBUpBNXOGSY3yISJrQezSP/T+v+mQRApkdoIpTSHIyQAhgaagcs3MOQRaqiIPaLOVrdHXdU6A=="],
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-KxY1i+HxeSFfzZ+HVsKwMGBM79laTRZv1ibFqHu22CEsfSPDt4yiV1QFis8Nw7OBXswNqJG/UGqY47VP8FeTvw=="],
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20251014.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-P0D4UEXwzFZh3pHexe2Ky1tW/HjY/HxTBTIajz2ViDCNPw7uDSEsXSB4H9TTiFJw8gVdTUFbsoAQp1MteTeORA=="],
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "x64" }, "sha512-5l51HlXjX7lXwo65DEl1IaCFLjmkMtL6K3NrSEamPNeNTtTQwZRa3pQ9V65dCglnnCQ0M3+VF1RqzC7FU0iDKg=="],
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251014.1", "", { "os": "win32", "cpu": "x64" }, "sha512-fi53g2ihH7tkQLlz8hZGAb2V+3aNZpcxrZ530CQ4xcWwAqssEj0EaZJX0VLEtIQBar1ttGVK9Pz/wJU9sYyVzg=="],
"@typescript/vfs": ["@typescript/vfs@1.6.2", "", { "dependencies": { "debug": "^4.1.1" }, "peerDependencies": { "typescript": "*" } }, "sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g=="],
@@ -2001,7 +1999,7 @@
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"c12": ["c12@3.3.2", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A=="],
"c12": ["c12@2.0.1", "", { "dependencies": { "chokidar": "^4.0.1", "confbox": "^0.1.7", "defu": "^6.1.4", "dotenv": "^16.4.5", "giget": "^1.2.3", "jiti": "^2.3.0", "mlly": "^1.7.1", "ohash": "^1.1.4", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", "pkg-types": "^1.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A=="],
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
@@ -2083,7 +2081,7 @@
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
"commander": ["commander@13.0.0", "", {}, "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ=="],
"common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
@@ -2091,7 +2089,7 @@
"condense-newlines": ["condense-newlines@0.2.1", "", { "dependencies": { "extend-shallow": "^2.0.1", "is-whitespace": "^0.3.0", "kind-of": "^3.0.2" } }, "sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg=="],
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="],
@@ -2215,7 +2213,7 @@
"dot-prop": ["dot-prop@8.0.2", "", { "dependencies": { "type-fest": "^3.8.0" } }, "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ=="],
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"drizzle-kit": ["drizzle-kit@0.30.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l6dMSE100u7sDaTbLczibrQZjA35jLsHNqIV+jmhNVO3O8jzM6kywMOmV9uOz9ZVSCMPQhAZEFjL/qDPVrqpUA=="],
@@ -2333,8 +2331,6 @@
"expressive-code": ["expressive-code@0.41.3", "", { "dependencies": { "@expressive-code/core": "^0.41.3", "@expressive-code/plugin-frames": "^0.41.3", "@expressive-code/plugin-shiki": "^0.41.3", "@expressive-code/plugin-text-markers": "^0.41.3" } }, "sha512-YLnD62jfgBZYrXIPQcJ0a51Afv9h8VlWqEGK9uU2T5nL/5rb8SnA86+7+mgCZe5D34Tff5RNEA5hjNVJYHzrFg=="],
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
@@ -2397,6 +2393,8 @@
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
"fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -2443,7 +2441,7 @@
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
"giget": ["giget@1.2.5", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.5.4", "pathe": "^2.0.3", "tar": "^6.2.1" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug=="],
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
@@ -2473,6 +2471,8 @@
"h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="],
"handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="],
"happy-dom": ["happy-dom@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "@types/whatwg-mimetype": "^3.0.2", "whatwg-mimetype": "^3.0.0" } }, "sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g=="],
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
@@ -2537,7 +2537,7 @@
"hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="],
"hono-openapi": ["hono-openapi@1.1.2", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g=="],
"hono-openapi": ["hono-openapi@1.1.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.8", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-AC3HNhZYPHhnZdSy2Je7GDoTTNxPos6rKRQKVDBbSilY3cWJPqsxRnN6zA4pU7tfxmQEMTqkiLXbw6sAaemB8Q=="],
"html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
@@ -2645,8 +2645,6 @@
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
"is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="],
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
@@ -2985,6 +2983,8 @@
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
"mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -3003,6 +3003,8 @@
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
"nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="],
@@ -3041,7 +3043,7 @@
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="],
"nypm": ["nypm@0.5.4", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "tinyexec": "^0.3.2", "ufo": "^1.5.4" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
@@ -3157,7 +3159,7 @@
"peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="],
"perfect-debounce": ["perfect-debounce@2.0.0", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="],
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="],
@@ -3175,7 +3177,7 @@
"pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="],
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="],
@@ -3203,8 +3205,6 @@
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
@@ -3459,7 +3459,7 @@
"solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
@@ -3659,6 +3659,8 @@
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
"uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
"ulid": ["ulid@3.0.1", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q=="],
"ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="],
@@ -3787,6 +3789,8 @@
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
"wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="],
"workerd": ["workerd@1.20251118.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251118.0", "@cloudflare/workerd-darwin-arm64": "1.20251118.0", "@cloudflare/workerd-linux-64": "1.20251118.0", "@cloudflare/workerd-linux-arm64": "1.20251118.0", "@cloudflare/workerd-windows-64": "1.20251118.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Om5ns0Lyx/LKtYI04IV0bjIrkBgoFNg0p6urzr2asekJlfP18RqFzyqMFZKf0i9Gnjtz/JfAS/Ol6tjCe5JJsQ=="],
"wrangler": ["wrangler@4.50.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.11", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20251118.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20251118.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251118.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-+nuZuHZxDdKmAyXOSrHlciGshCoAPiy5dM+t6mEohWm7HpXvTHmWQGUf/na9jjWlWJHCJYOWzkA1P5HBJqrIEA=="],
@@ -3799,8 +3803,6 @@
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
"wsl-utils": ["wsl-utils@0.3.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-3sFIGLiaDP7rTO4xh3g+b3AzhYDIUGGywE/WsmqzJWDxus5aJXVnPTNC/6L+r2WzrwXqVOdD262OaO+cEyPMSQ=="],
"xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="],
"xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="],
@@ -3895,6 +3897,8 @@
"@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.9", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.5", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.13.0", "smol-toml": "^1.4.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-hX2cLC/KW74Io1zIbn92kI482j9J7LleBLGCVU9EP3BeH5MVrnFawOnqD0t/q6D1Z+ZNeQG2gNKMslCcO36wng=="],
"@astrojs/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"@astrojs/sitemap/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@astrojs/solid-js/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
@@ -3981,10 +3985,6 @@
"@expressive-code/plugin-shiki/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
"@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
"@hono/zod-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
@@ -4031,6 +4031,8 @@
"@jsx-email/doiuse-email/htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="],
"@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
@@ -4175,7 +4177,9 @@
"boxen/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"c12/ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="],
"c12/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
"compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
@@ -4195,6 +4199,8 @@
"esbuild-plugin-copy/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"estree-util-to-js/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
"express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
@@ -4209,10 +4215,14 @@
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"gaxios/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"giget/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
"glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
@@ -4249,8 +4259,6 @@
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
"nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="],
@@ -4311,8 +4319,6 @@
"sitemap/sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="],
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="],
"sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="],
@@ -4817,6 +4823,14 @@
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"giget/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
"giget/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"giget/tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"giget/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"js-beautify/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
@@ -4993,6 +5007,8 @@
"esbuild-plugin-copy/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"giget/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],

View File

@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-7ItLfqYrXzC6LO2iXZ8m+ZfQH1D7NWtcAcgRMO5NXZI="
"nodeModules": "sha256-7eJpUUtS50/uHa6sU0pl/MbwJykkVlYwEeAoKKNk4+g="
}

View File

@@ -35,11 +35,11 @@
"diff": "8.0.2",
"ai": "5.0.97",
"hono": "4.10.7",
"hono-openapi": "1.1.2",
"hono-openapi": "1.1.1",
"fuzzysort": "3.1.0",
"luxon": "3.6.1",
"typescript": "5.8.2",
"@typescript/native-preview": "7.0.0-dev.20251207.1",
"@typescript/native-preview": "7.0.0-dev.20251014.1",
"zod": "4.1.8",
"remeda": "2.26.0",
"solid-list": "0.3.0",

View File

@@ -235,7 +235,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const abort = () =>
sdk.client.session.abort({
sessionID: session.id!,
path: {
id: session.id!,
},
})
const handleKeyDown = (event: KeyboardEvent) => {
@@ -327,19 +329,21 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
session.prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
sdk.client.session.prompt({
sessionID: existing.id,
agent: local.agent.current()!.name,
model: {
modelID: local.model.current()!.id,
providerID: local.model.current()!.provider.id,
},
parts: [
{
type: "text",
text,
path: { id: existing.id },
body: {
agent: local.agent.current()!.name,
model: {
modelID: local.model.current()!.id,
providerID: local.model.current()!.provider.id,
},
...attachmentParts,
],
parts: [
{
type: "text",
text,
},
...attachmentParts,
],
},
})
}

View File

@@ -74,10 +74,12 @@ export const Terminal = (props: TerminalProps) => {
term.onResize(async (size) => {
if (ws && ws.readyState === WebSocket.OPEN) {
await sdk.client.pty.update({
ptyID: local.pty.id,
size: {
cols: size.cols,
rows: size.rows,
path: { id: local.pty.id },
body: {
size: {
cols: size.cols,
rows: size.rows,
},
},
})
}
@@ -98,10 +100,12 @@ export const Terminal = (props: TerminalProps) => {
ws.addEventListener("open", () => {
console.log("WebSocket connected")
sdk.client.pty.update({
ptyID: local.pty.id,
size: {
cols: term.cols,
rows: term.rows,
path: { id: local.pty.id },
body: {
size: {
cols: term.cols,
rows: term.rows,
},
},
})
})

View File

@@ -1,4 +1,4 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"

View File

@@ -12,7 +12,7 @@ import type {
FileDiff,
Todo,
SessionStatus,
} from "@opencode-ai/sdk/v2"
} from "@opencode-ai/sdk"
import { createStore, produce, reconcile } from "solid-js/store"
import { Binary } from "@opencode-ai/util/binary"
import { createSimpleContext } from "@opencode-ai/ui/context"

View File

@@ -1,7 +1,7 @@
import { createStore, produce, reconcile } from "solid-js/store"
import { batch, createEffect, createMemo } from "solid-js"
import { uniqueBy } from "remeda"
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
@@ -257,7 +257,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const load = async (path: string) => {
const relativePath = relative(path)
sdk.client.file.read({ path: relativePath }).then((x) => {
sdk.client.file.read({ query: { path: relativePath } }).then((x) => {
setStore(
"node",
relativePath,
@@ -305,7 +305,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
const list = async (path: string) => {
return sdk.client.file.list({ path: path + "/" }).then((x) => {
return sdk.client.file.list({ query: { path: path + "/" } }).then((x) => {
setStore(
"node",
produce((draft) => {
@@ -318,9 +318,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
}
const searchFiles = (query: string) => sdk.client.find.files({ query, dirs: "false" }).then((x) => x.data!)
const searchFiles = (query: string) =>
sdk.client.find.files({ query: { query, dirs: "false" } }).then((x) => x.data!)
const searchFilesAndDirectories = (query: string) =>
sdk.client.find.files({ query, dirs: "true" }).then((x) => x.data!)
sdk.client.find.files({ query: { query, dirs: "true" } }).then((x) => x.data!)
sdk.event.listen((e) => {
const event = e.details

View File

@@ -1,4 +1,4 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"

View File

@@ -5,7 +5,7 @@ import { useSync } from "./sync"
import { makePersisted } from "@solid-primitives/storage"
import { TextSelection } from "./local"
import { pipe, sumBy } from "remeda"
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk"
import { useParams } from "@solidjs/router"
import { base64Encode } from "@/utils"
import { useSDK } from "./sdk"
@@ -198,7 +198,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
all: createMemo(() => Object.values(store.terminals.all)),
active: createMemo(() => store.terminals.active),
new() {
sdk.client.pty.create({ title: `Terminal ${store.terminals.all.length + 1}` }).then((pty) => {
sdk.client.pty.create({ body: { title: `Terminal ${store.terminals.all.length + 1}` } }).then((pty) => {
const id = pty.data?.id
if (!id) return
setStore("terminals", "all", [
@@ -214,9 +214,8 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
update(pty: Partial<LocalPTY> & { id: string }) {
setStore("terminals", "all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
sdk.client.pty.update({
ptyID: pty.id,
title: pty.title,
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
path: { id: pty.id },
body: { title: pty.title, size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined },
})
},
async clone(id: string) {
@@ -224,7 +223,9 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
const pty = store.terminals.all[index]
if (!pty) return
const clone = await sdk.client.pty.create({
title: pty.title,
body: {
title: pty.title,
},
})
if (!clone.data) return
setStore("terminals", "all", index, {
@@ -251,7 +252,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
setStore("terminals", "active", previous)
}
})
await sdk.client.pty.remove({ ptyID: id })
await sdk.client.pty.remove({ path: { id } })
},
move(id: string, to: number) {
const index = store.terminals.all.findIndex((f) => f.id === id)

View File

@@ -28,7 +28,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
status: () => sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)),
changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)),
node: () => sdk.client.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
node: () => sdk.client.file.list({ query: { path: "/" } }).then((x) => setStore("node", x.data!)),
}
Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
@@ -49,10 +49,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
},
async sync(sessionID: string, _isRetry = false) {
const [session, messages, todo, diff] = await Promise.all([
sdk.client.session.get({ sessionID }, { throwOnError: true }),
sdk.client.session.messages({ sessionID, limit: 100 }),
sdk.client.session.todo({ sessionID }),
sdk.client.session.diff({ sessionID }),
sdk.client.session.get({ path: { id: sessionID }, throwOnError: true }),
sdk.client.session.messages({ path: { id: sessionID }, query: { limit: 100 } }),
sdk.client.session.todo({ path: { id: sessionID } }),
sdk.client.session.diff({ path: { id: sessionID } }),
])
setStore(
produce((draft) => {

View File

@@ -13,7 +13,7 @@ import { Collapsible } from "@opencode-ai/ui/collapsible"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { getFilename } from "@opencode-ai/util/path"
import { Select } from "@opencode-ai/ui/select"
import { Session } from "@opencode-ai/sdk/v2/client"
import { Session } from "@opencode-ai/sdk/client"
export default function Layout(props: ParentProps) {
const navigate = useNavigate()

View File

@@ -21,12 +21,12 @@ cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.134/opencode-linux-arm64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.134/opencode-linux-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.134/opencode-linux-x64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.134/opencode-linux-x64.zip"
cmd = "./opencode"
args = ["acp"]

View File

@@ -29,7 +29,7 @@ import { MCP } from "@/mcp"
import { Todo } from "@/session/todo"
import { z } from "zod"
import { LoadAPIKeyError } from "ai"
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
import type { OpencodeClient } from "@opencode-ai/sdk"
export namespace ACP {
const log = Log.create({ service: "acp-agent" })
@@ -68,7 +68,7 @@ export namespace ACP {
{ optionId: "always", kind: "allow_always", name: "Always allow" },
{ optionId: "reject", kind: "reject_once", name: "Reject" },
]
this.config.sdk.event.subscribe({ directory }).then(async (events) => {
this.config.sdk.event.subscribe({ query: { directory } }).then(async (events) => {
for await (const event of events.stream) {
switch (event.type) {
case "permission.updated":
@@ -93,29 +93,32 @@ export namespace ACP {
permissionID: permission.id,
sessionID: permission.sessionID,
})
await this.config.sdk.permission.respond({
sessionID: permission.sessionID,
permissionID: permission.id,
response: "reject",
directory,
await this.config.sdk.postSessionIdPermissionsPermissionId({
path: { id: permission.sessionID, permissionID: permission.id },
body: {
response: "reject",
},
query: { directory },
})
return
})
if (!res) return
if (res.outcome.outcome !== "selected") {
await this.config.sdk.permission.respond({
sessionID: permission.sessionID,
permissionID: permission.id,
response: "reject",
directory,
await this.config.sdk.postSessionIdPermissionsPermissionId({
path: { id: permission.sessionID, permissionID: permission.id },
body: {
response: "reject",
},
query: { directory },
})
return
}
await this.config.sdk.permission.respond({
sessionID: permission.sessionID,
permissionID: permission.id,
response: res.outcome.optionId as "once" | "always" | "reject",
directory,
await this.config.sdk.postSessionIdPermissionsPermissionId({
path: { id: permission.sessionID, permissionID: permission.id },
body: {
response: res.outcome.optionId as "once" | "always" | "reject",
},
query: { directory },
})
} catch (err) {
log.error("unexpected error when handling permission", { error: err })
@@ -130,14 +133,14 @@ export namespace ACP {
const { part } = props
const message = await this.config.sdk.session
.message(
{
sessionID: part.sessionID,
.message({
throwOnError: true,
path: {
id: part.sessionID,
messageID: part.messageID,
directory,
},
{ throwOnError: true },
)
query: { directory },
})
.then((x) => x.data)
.catch((err) => {
log.error("unexpected error when fetching message", { error: err })
@@ -417,7 +420,9 @@ export namespace ACP {
const model = await defaultModel(this.config, directory)
const sessionId = params.sessionId
const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
const providers = await this.sdk.config
.providers({ throwOnError: true, query: { directory } })
.then((x) => x.data.providers)
const entries = providers.sort((a, b) => {
const nameA = a.name.toLowerCase()
const nameB = b.name.toLowerCase()
@@ -434,22 +439,22 @@ export namespace ACP {
})
const agents = await this.config.sdk.app
.agents(
{
.agents({
throwOnError: true,
query: {
directory,
},
{ throwOnError: true },
)
.then((resp) => resp.data!)
})
.then((resp) => resp.data)
const commands = await this.config.sdk.command
.list(
{
.list({
throwOnError: true,
query: {
directory,
},
{ throwOnError: true },
)
.then((resp) => resp.data!)
})
.then((resp) => resp.data)
const availableCommands = commands.map((command) => ({
name: command.name,
@@ -498,14 +503,14 @@ export namespace ACP {
await Promise.all(
Object.entries(mcpServers).map(async ([key, mcp]) => {
await this.sdk.mcp
.add(
{
directory,
.add({
throwOnError: true,
query: { directory },
body: {
name: key,
config: mcp,
},
{ throwOnError: true },
)
})
.catch((error) => {
log.error("failed to add mcp server", { name: key, error })
})
@@ -554,7 +559,7 @@ export namespace ACP {
async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> {
this.sessionManager.get(params.sessionId)
await this.config.sdk.app
.agents({}, { throwOnError: true })
.agents({ throwOnError: true })
.then((x) => x.data)
.then((agent) => {
if (!agent) throw new Error(`Agent not found: ${params.modeId}`)
@@ -646,42 +651,50 @@ export namespace ACP {
if (!cmd) {
await this.sdk.session.prompt({
sessionID,
model: {
providerID: model.providerID,
modelID: model.modelID,
path: { id: sessionID },
body: {
model: {
providerID: model.providerID,
modelID: model.modelID,
},
parts,
agent,
},
query: {
directory,
},
parts,
agent,
directory,
})
return done
}
const command = await this.config.sdk.command
.list({ directory }, { throwOnError: true })
.then((x) => x.data!.find((c) => c.name === cmd.name))
.list({ throwOnError: true, query: { directory } })
.then((x) => x.data.find((c) => c.name === cmd.name))
if (command) {
await this.sdk.session.command({
sessionID,
command: command.name,
arguments: cmd.args,
model: model.providerID + "/" + model.modelID,
agent,
directory,
path: { id: sessionID },
body: {
command: command.name,
arguments: cmd.args,
model: model.providerID + "/" + model.modelID,
agent,
},
query: {
directory,
},
})
return done
}
switch (cmd.name) {
case "compact":
await this.config.sdk.session.summarize(
{
sessionID,
await this.config.sdk.session.summarize({
path: { id: sessionID },
throwOnError: true,
query: {
directory,
},
{ throwOnError: true },
)
})
break
}
@@ -690,13 +703,13 @@ export namespace ACP {
async cancel(params: CancelNotification) {
const session = this.sessionManager.get(params.sessionId)
await this.config.sdk.session.abort(
{
sessionID: params.sessionId,
await this.config.sdk.session.abort({
path: { id: params.sessionId },
throwOnError: true,
query: {
directory: session.cwd,
},
{ throwOnError: true },
)
})
}
}
@@ -753,10 +766,10 @@ export namespace ACP {
if (configured) return configured
const model = await sdk.config
.get({ directory: cwd }, { throwOnError: true })
.get({ throwOnError: true, query: { directory: cwd } })
.then((resp) => {
const cfg = resp.data
if (!cfg || !cfg.model) return undefined
if (!cfg.model) return undefined
const parsed = Provider.parseModel(cfg.model)
return {
providerID: parsed.providerID,

View File

@@ -1,7 +1,7 @@
import { RequestError, type McpServer } from "@agentclientprotocol/sdk"
import type { ACPSessionState } from "./types"
import { Log } from "@/util/log"
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
import type { OpencodeClient } from "@opencode-ai/sdk"
const log = Log.create({ service: "acp-session-manager" })
@@ -15,14 +15,16 @@ export class ACPSessionManager {
async create(cwd: string, mcpServers: McpServer[], model?: ACPSessionState["model"]): Promise<ACPSessionState> {
const session = await this.sdk.session
.create(
{
.create({
body: {
title: `ACP Session ${crypto.randomUUID()}`,
},
query: {
directory: cwd,
},
{ throwOnError: true },
)
.then((x) => x.data!)
throwOnError: true,
})
.then((x) => x.data)
const sessionId = session.id
const resolvedModel = model

View File

@@ -1,5 +1,5 @@
import type { McpServer } from "@agentclientprotocol/sdk"
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
import type { OpencodeClient } from "@opencode-ai/sdk"
export interface ACPSessionState {
id: string

View File

@@ -4,7 +4,7 @@ import { cmd } from "./cmd"
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
import { ACP } from "@/acp/agent"
import { Server } from "@/server/server"
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import { createOpencodeClient } from "@opencode-ai/sdk"
const log = Log.create({ service: "acp-command" })

View File

@@ -3,272 +3,13 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { MCP } from "../../mcp"
import { McpAuth } from "../../mcp/auth"
import { Config } from "../../config/config"
import { Instance } from "../../project/instance"
import path from "path"
import os from "os"
import { Global } from "../../global"
export const McpCommand = cmd({
command: "mcp",
builder: (yargs) =>
yargs
.command(McpAddCommand)
.command(McpListCommand)
.command(McpAuthCommand)
.command(McpLogoutCommand)
.demandCommand(),
builder: (yargs) => yargs.command(McpAddCommand).demandCommand(),
async handler() {},
})
export const McpListCommand = cmd({
command: "list",
aliases: ["ls"],
describe: "list MCP servers and their status",
async handler() {
await Instance.provide({
directory: process.cwd(),
async fn() {
UI.empty()
prompts.intro("MCP Servers")
const config = await Config.get()
const mcpServers = config.mcp ?? {}
const statuses = await MCP.status()
if (Object.keys(mcpServers).length === 0) {
prompts.log.warn("No MCP servers configured")
prompts.outro("Add servers with: opencode mcp add")
return
}
for (const [name, serverConfig] of Object.entries(mcpServers)) {
const status = statuses[name]
const hasOAuth = serverConfig.type === "remote" && !!serverConfig.oauth
const hasStoredTokens = await MCP.hasStoredTokens(name)
let statusIcon: string
let statusText: string
let hint = ""
if (!status) {
statusIcon = "○"
statusText = "not initialized"
} else if (status.status === "connected") {
statusIcon = "✓"
statusText = "connected"
if (hasOAuth && hasStoredTokens) {
hint = " (OAuth)"
}
} else if (status.status === "disabled") {
statusIcon = "○"
statusText = "disabled"
} else if (status.status === "needs_auth") {
statusIcon = "⚠"
statusText = "needs authentication"
} else if (status.status === "needs_client_registration") {
statusIcon = "✗"
statusText = "needs client registration"
hint = "\n " + status.error
} else {
statusIcon = "✗"
statusText = "failed"
hint = "\n " + status.error
}
const typeHint = serverConfig.type === "remote" ? serverConfig.url : serverConfig.command.join(" ")
prompts.log.info(
`${statusIcon} ${name} ${UI.Style.TEXT_DIM}${statusText}${hint}\n ${UI.Style.TEXT_DIM}${typeHint}`,
)
}
prompts.outro(`${Object.keys(mcpServers).length} server(s)`)
},
})
},
})
export const McpAuthCommand = cmd({
command: "auth [name]",
describe: "authenticate with an OAuth-enabled MCP server",
builder: (yargs) =>
yargs.positional("name", {
describe: "name of the MCP server",
type: "string",
}),
async handler(args) {
await Instance.provide({
directory: process.cwd(),
async fn() {
UI.empty()
prompts.intro("MCP OAuth Authentication")
const config = await Config.get()
const mcpServers = config.mcp ?? {}
// Get OAuth-enabled servers
const oauthServers = Object.entries(mcpServers).filter(([_, cfg]) => cfg.type === "remote" && !!cfg.oauth)
if (oauthServers.length === 0) {
prompts.log.warn("No OAuth-enabled MCP servers configured")
prompts.log.info("Add OAuth config to a remote MCP server in opencode.json:")
prompts.log.info(`
"mcp": {
"my-server": {
"type": "remote",
"url": "https://example.com/mcp",
"oauth": {
"scope": "tools:read"
}
}
}`)
prompts.outro("Done")
return
}
let serverName = args.name
if (!serverName) {
const selected = await prompts.select({
message: "Select MCP server to authenticate",
options: oauthServers.map(([name, cfg]) => ({
label: name,
value: name,
hint: cfg.type === "remote" ? cfg.url : undefined,
})),
})
if (prompts.isCancel(selected)) throw new UI.CancelledError()
serverName = selected
}
const serverConfig = mcpServers[serverName]
if (!serverConfig) {
prompts.log.error(`MCP server not found: ${serverName}`)
prompts.outro("Done")
return
}
if (serverConfig.type !== "remote" || !serverConfig.oauth) {
prompts.log.error(`MCP server ${serverName} does not have OAuth configured`)
prompts.outro("Done")
return
}
// Check if already authenticated
const hasTokens = await MCP.hasStoredTokens(serverName)
if (hasTokens) {
const confirm = await prompts.confirm({
message: `${serverName} already has stored credentials. Re-authenticate?`,
})
if (prompts.isCancel(confirm) || !confirm) {
prompts.outro("Cancelled")
return
}
}
const spinner = prompts.spinner()
spinner.start("Starting OAuth flow...")
try {
const status = await MCP.authenticate(serverName)
if (status.status === "connected") {
spinner.stop("Authentication successful!")
} else if (status.status === "needs_client_registration") {
spinner.stop("Authentication failed", 1)
prompts.log.error(status.error)
prompts.log.info("Add clientId to your MCP server config:")
prompts.log.info(`
"mcp": {
"${serverName}": {
"type": "remote",
"url": "${serverConfig.url}",
"oauth": {
"clientId": "your-client-id",
"clientSecret": "your-client-secret"
}
}
}`)
} else if (status.status === "failed") {
spinner.stop("Authentication failed", 1)
prompts.log.error(status.error)
} else {
spinner.stop("Unexpected status: " + status.status, 1)
}
} catch (error) {
spinner.stop("Authentication failed", 1)
prompts.log.error(error instanceof Error ? error.message : String(error))
}
prompts.outro("Done")
},
})
},
})
export const McpLogoutCommand = cmd({
command: "logout [name]",
describe: "remove OAuth credentials for an MCP server",
builder: (yargs) =>
yargs.positional("name", {
describe: "name of the MCP server",
type: "string",
}),
async handler(args) {
await Instance.provide({
directory: process.cwd(),
async fn() {
UI.empty()
prompts.intro("MCP OAuth Logout")
const authPath = path.join(Global.Path.data, "mcp-auth.json")
const credentials = await McpAuth.all()
const serverNames = Object.keys(credentials)
if (serverNames.length === 0) {
prompts.log.warn("No MCP OAuth credentials stored")
prompts.outro("Done")
return
}
let serverName = args.name
if (!serverName) {
const selected = await prompts.select({
message: "Select MCP server to logout",
options: serverNames.map((name) => {
const entry = credentials[name]
const hasTokens = !!entry.tokens
const hasClient = !!entry.clientInfo
let hint = ""
if (hasTokens && hasClient) hint = "tokens + client"
else if (hasTokens) hint = "tokens"
else if (hasClient) hint = "client registration"
return {
label: name,
value: name,
hint,
}
}),
})
if (prompts.isCancel(selected)) throw new UI.CancelledError()
serverName = selected
}
if (!credentials[serverName]) {
prompts.log.error(`No credentials found for: ${serverName}`)
prompts.outro("Done")
return
}
await MCP.removeAuth(serverName)
prompts.log.success(`Removed OAuth credentials for ${serverName}`)
prompts.outro("Done")
},
})
},
})
export const McpAddCommand = cmd({
command: "add",
describe: "add an MCP server",
@@ -325,74 +66,13 @@ export const McpAddCommand = cmd({
})
if (prompts.isCancel(url)) throw new UI.CancelledError()
const useOAuth = await prompts.confirm({
message: "Does this server require OAuth authentication?",
initialValue: false,
const client = new Client({
name: "opencode",
version: "1.0.0",
})
if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
if (useOAuth) {
const hasClientId = await prompts.confirm({
message: "Do you have a pre-registered client ID?",
initialValue: false,
})
if (prompts.isCancel(hasClientId)) throw new UI.CancelledError()
if (hasClientId) {
const clientId = await prompts.text({
message: "Enter client ID",
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(clientId)) throw new UI.CancelledError()
const hasSecret = await prompts.confirm({
message: "Do you have a client secret?",
initialValue: false,
})
if (prompts.isCancel(hasSecret)) throw new UI.CancelledError()
let clientSecret: string | undefined
if (hasSecret) {
const secret = await prompts.password({
message: "Enter client secret",
})
if (prompts.isCancel(secret)) throw new UI.CancelledError()
clientSecret = secret
}
prompts.log.info(`Remote MCP server "${name}" configured with OAuth (client ID: ${clientId})`)
prompts.log.info("Add this to your opencode.json:")
prompts.log.info(`
"mcp": {
"${name}": {
"type": "remote",
"url": "${url}",
"oauth": {
"clientId": "${clientId}"${clientSecret ? `,\n "clientSecret": "${clientSecret}"` : ""}
}
}
}`)
} else {
prompts.log.info(`Remote MCP server "${name}" configured with OAuth (dynamic registration)`)
prompts.log.info("Add this to your opencode.json:")
prompts.log.info(`
"mcp": {
"${name}": {
"type": "remote",
"url": "${url}",
"oauth": {}
}
}`)
}
} else {
const client = new Client({
name: "opencode",
version: "1.0.0",
})
const transport = new StreamableHTTPClientTransport(new URL(url))
await client.connect(transport)
prompts.log.info(`Remote MCP server "${name}" configured with URL: ${url}`)
}
const transport = new StreamableHTTPClientTransport(new URL(url))
await client.connect(transport)
prompts.log.info(`Remote MCP server "${name}" configured with URL: ${url}`)
}
prompts.outro("MCP server added successfully")

View File

@@ -7,7 +7,7 @@ import { bootstrap } from "../bootstrap"
import { Command } from "../../command"
import { EOL } from "os"
import { select } from "@clack/prompts"
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2"
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk"
import { Server } from "../../server/server"
import { Provider } from "../../provider/provider"
@@ -212,10 +212,9 @@ export const RunCommand = cmd({
initialValue: "once",
}).catch(() => "reject")
const response = (result.toString().includes("cancel") ? "reject" : result) as "once" | "always" | "reject"
await sdk.permission.respond({
sessionID,
permissionID: permission.id,
response,
await sdk.postSessionIdPermissionsPermissionId({
path: { id: sessionID, permissionID: permission.id },
body: { response },
})
}
}
@@ -223,19 +222,23 @@ export const RunCommand = cmd({
if (args.command) {
await sdk.session.command({
sessionID,
agent: args.agent || "build",
model: args.model,
command: args.command,
arguments: message,
path: { id: sessionID },
body: {
agent: args.agent || "build",
model: args.model,
command: args.command,
arguments: message,
},
})
} else {
const modelParam = args.model ? Provider.parseModel(args.model) : undefined
await sdk.session.prompt({
sessionID,
agent: args.agent || "build",
model: modelParam,
parts: [...fileParts, { type: "text", text: message }],
path: { id: sessionID },
body: {
agent: args.agent || "build",
model: modelParam,
parts: [...fileParts, { type: "text", text: message }],
},
})
}
@@ -260,7 +263,7 @@ export const RunCommand = cmd({
: args.title
: undefined
const result = await sdk.session.create(title ? { title } : {})
const result = await sdk.session.create({ body: title ? { title } : {} })
return result.data?.id
})()
@@ -271,7 +274,7 @@ export const RunCommand = cmd({
const cfgResult = await sdk.config.get()
if (cfgResult.data && (cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)) {
const shareResult = await sdk.session.share({ sessionID }).catch((error) => {
const shareResult = await sdk.session.share({ path: { id: sessionID } }).catch((error) => {
if (error instanceof Error && error.message.includes("disabled")) {
UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message)
}
@@ -312,7 +315,7 @@ export const RunCommand = cmd({
: args.title
: undefined
const result = await sdk.session.create(title ? { title } : {})
const result = await sdk.session.create({ body: title ? { title } : {} })
return result.data?.id
})()
@@ -324,7 +327,7 @@ export const RunCommand = cmd({
const cfgResult = await sdk.config.get()
if (cfgResult.data && (cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)) {
const shareResult = await sdk.session.share({ sessionID }).catch((error) => {
const shareResult = await sdk.session.share({ path: { id: sessionID } }).catch((error) => {
if (error instanceof Error && error.message.includes("disabled")) {
UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message)
}

View File

@@ -11,7 +11,7 @@ import {
} from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { useKeybind } from "@tui/context/keybind"
import type { KeybindsConfig } from "@opencode-ai/sdk/v2"
import type { KeybindsConfig } from "@opencode-ai/sdk"
type Context = ReturnType<typeof init>
const ctx = createContext<Context>()

View File

@@ -7,7 +7,7 @@ import { useSDK } from "../context/sdk"
import { DialogPrompt } from "../ui/dialog-prompt"
import { useTheme } from "../context/theme"
import { TextAttributes } from "@opentui/core"
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2"
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk"
import { DialogModel } from "./dialog-model"
const PROVIDER_PRIORITY: Record<string, number> = {
@@ -64,8 +64,12 @@ export function createDialogProviderOptions() {
const method = methods[index]
if (method.type === "oauth") {
const result = await sdk.client.provider.oauth.authorize({
providerID: provider.id,
method: index,
path: {
id: provider.id,
},
body: {
method: index,
},
})
if (result.data?.method === "code") {
dialog.replace(() => (
@@ -107,8 +111,12 @@ function AutoMethod(props: AutoMethodProps) {
onMount(async () => {
const result = await sdk.client.provider.oauth.callback({
providerID: props.providerID,
method: props.index,
path: {
id: props.providerID,
},
body: {
method: props.index,
},
})
if (result.error) {
dialog.clear()
@@ -153,9 +161,13 @@ function CodeMethod(props: CodeMethodProps) {
placeholder="Authorization code"
onConfirm={async (value) => {
const { error } = await sdk.client.provider.oauth.callback({
providerID: props.providerID,
method: props.index,
code: value,
path: {
id: props.providerID,
},
body: {
method: props.index,
code: value,
},
})
if (!error) {
await sdk.client.instance.dispose()
@@ -207,8 +219,10 @@ function ApiMethod(props: ApiMethodProps) {
onConfirm={async (value) => {
if (!value) return
sdk.client.auth.set({
providerID: props.providerID,
auth: {
path: {
id: props.providerID,
},
body: {
type: "api",
key: value,
},

View File

@@ -74,7 +74,9 @@ export function DialogSessionList() {
onTrigger: async (option) => {
if (toDelete() === option.value) {
sdk.client.session.delete({
sessionID: option.value,
path: {
id: option.value,
},
})
setToDelete(undefined)
// dialog.clear()

View File

@@ -20,8 +20,12 @@ export function DialogSessionRename(props: DialogSessionRenameProps) {
value={session()?.title}
onConfirm={(value) => {
sdk.client.session.update({
sessionID: props.session,
title: value,
path: {
id: props.session,
},
body: {
title: value,
},
})
dialog.clear()
}}

View File

@@ -28,15 +28,11 @@ export function DialogStatus() {
<text
flexShrink={0}
style={{
fg: (
{
connected: theme.success,
failed: theme.error,
disabled: theme.textMuted,
needs_auth: theme.warning,
needs_client_registration: theme.error,
} as Record<string, typeof theme.success>
)[item.status],
fg: {
connected: theme.success,
failed: theme.error,
disabled: theme.textMuted,
}[item.status],
}}
>
@@ -44,16 +40,10 @@ export function DialogStatus() {
<text fg={theme.text} wrapMode="word">
<b>{key}</b>{" "}
<span style={{ fg: theme.textMuted }}>
<Switch fallback={item.status}>
<Switch>
<Match when={item.status === "connected"}>Connected</Match>
<Match when={item.status === "failed" && item}>{(val) => val().error}</Match>
<Match when={item.status === "disabled"}>Disabled in configuration</Match>
<Match when={(item.status as string) === "needs_auth"}>
Needs authentication (run: opencode mcp auth {key})
</Match>
<Match when={(item.status as string) === "needs_client_registration" && item}>
{(val) => (val() as { error: string }).error}
</Match>
</Switch>
</span>
</text>

View File

@@ -16,7 +16,9 @@ export function DialogTag(props: { onSelect?: (value: string) => void }) {
() => [store.filter],
async () => {
const result = await sdk.client.find.files({
query: store.filter,
query: {
query: store.filter,
},
})
if (result.error) return []
const sliced = (result.data ?? []).slice(0, 5)

View File

@@ -1,14 +1,13 @@
import type { BoxRenderable, TextareaRenderable, KeyEvent } from "@opentui/core"
import fuzzysort from "fuzzysort"
import { firstBy } from "remeda"
import { createMemo, createResource, createEffect, onMount, onCleanup, For, Show, createSignal } from "solid-js"
import { createMemo, createResource, createEffect, onMount, For, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { useSDK } from "@tui/context/sdk"
import { useSync } from "@tui/context/sync"
import { useTheme, selectedForeground } from "@tui/context/theme"
import { SplitBorder } from "@tui/component/border"
import { useCommandDialog } from "@tui/component/dialog-command"
import { useTerminalDimensions } from "@opentui/solid"
import { Locale } from "@/util/locale"
import type { PromptInfo } from "./history"
@@ -42,43 +41,13 @@ export function Autocomplete(props: {
const sync = useSync()
const command = useCommandDialog()
const { theme } = useTheme()
const dimensions = useTerminalDimensions()
const [store, setStore] = createStore({
index: 0,
selected: 0,
visible: false as AutocompleteRef["visible"],
position: { x: 0, y: 0, width: 0 },
})
const [positionTick, setPositionTick] = createSignal(0)
createEffect(() => {
if (store.visible) {
let lastPos = { x: 0, y: 0, width: 0 }
const interval = setInterval(() => {
const anchor = props.anchor()
if (anchor.x !== lastPos.x || anchor.y !== lastPos.y || anchor.width !== lastPos.width) {
lastPos = { x: anchor.x, y: anchor.y, width: anchor.width }
setPositionTick((t) => t + 1)
}
}, 50)
onCleanup(() => clearInterval(interval))
}
})
const position = createMemo(() => {
if (!store.visible) return { x: 0, y: 0, width: 0 }
const dims = dimensions()
positionTick()
const anchor = props.anchor()
return {
x: anchor.x,
y: anchor.y,
width: anchor.width,
}
})
const filter = createMemo(() => {
if (!store.visible) return
// Track props.value to make memo reactive to text changes
@@ -140,14 +109,16 @@ export function Autocomplete(props: {
// Get files from SDK
const result = await sdk.client.find.files({
query: query ?? "",
query: {
query: query ?? "",
},
})
const options: AutocompleteOption[] = []
// Add file options
if (!result.error && result.data) {
const width = props.anchor().width - 4
const width = store.position.width - 4
options.push(
...result.data.map(
(item): AutocompleteOption => ({
@@ -390,6 +361,11 @@ export function Autocomplete(props: {
setStore({
visible: mode,
index: props.input().cursorOffset,
position: {
x: props.anchor().x,
y: props.anchor().y,
width: props.anchor().width,
},
})
}
@@ -481,9 +457,9 @@ export function Autocomplete(props: {
<box
visible={store.visible !== false}
position="absolute"
top={position().y - height()}
left={position().x}
width={position().width}
top={store.position.y - height()}
left={store.position.x}
width={store.position.width}
zIndex={100}
{...SplitBorder}
borderColor={theme.border}

View File

@@ -5,7 +5,7 @@ import { createStore, produce } from "solid-js/store"
import { clone } from "remeda"
import { createSimpleContext } from "../../context/helper"
import { appendFile, writeFile } from "fs/promises"
import type { AgentPart, FilePart, TextPart } from "@opencode-ai/sdk/v2"
import type { AgentPart, FilePart, TextPart } from "@opencode-ai/sdk"
export type PromptInfo = {
input: string

View File

@@ -17,7 +17,7 @@ import { useRenderer } from "@opentui/solid"
import { Editor } from "@tui/util/editor"
import { useExit } from "../../context/exit"
import { Clipboard } from "../../util/clipboard"
import type { FilePart } from "@opencode-ai/sdk/v2"
import type { FilePart } from "@opencode-ai/sdk"
import { TuiEvent } from "../../event"
import { iife } from "@/util/iife"
import { Locale } from "@/util/locale"
@@ -170,7 +170,9 @@ export function Prompt(props: PromptProps) {
if (store.interrupt >= 2) {
sdk.client.session.abort({
sessionID: props.sessionID,
path: {
id: props.sessionID,
},
})
setStore("interrupt", 0)
}
@@ -445,13 +447,17 @@ export function Prompt(props: PromptProps) {
if (store.mode === "shell") {
sdk.client.session.shell({
sessionID,
agent: local.agent.current().name,
model: {
providerID: selectedModel.providerID,
modelID: selectedModel.modelID,
path: {
id: sessionID,
},
body: {
agent: local.agent.current().name,
model: {
providerID: selectedModel.providerID,
modelID: selectedModel.modelID,
},
command: inputText,
},
command: inputText,
})
setStore("mode", "normal")
} else if (
@@ -464,31 +470,39 @@ export function Prompt(props: PromptProps) {
) {
let [command, ...args] = inputText.split(" ")
sdk.client.session.command({
sessionID,
command: command.slice(1),
arguments: args.join(" "),
agent: local.agent.current().name,
model: `${selectedModel.providerID}/${selectedModel.modelID}`,
messageID,
path: {
id: sessionID,
},
body: {
command: command.slice(1),
arguments: args.join(" "),
agent: local.agent.current().name,
model: `${selectedModel.providerID}/${selectedModel.modelID}`,
messageID,
},
})
} else {
sdk.client.session.prompt({
sessionID,
...selectedModel,
messageID,
agent: local.agent.current().name,
model: selectedModel,
parts: [
{
id: Identifier.ascending("part"),
type: "text",
text: inputText,
},
...nonTextParts.map((x) => ({
id: Identifier.ascending("part"),
...x,
})),
],
path: {
id: sessionID,
},
body: {
...selectedModel,
messageID,
agent: local.agent.current().name,
model: selectedModel,
parts: [
{
id: Identifier.ascending("part"),
type: "text",
text: inputText,
},
...nonTextParts.map((x) => ({
id: Identifier.ascending("part"),
...x,
})),
],
},
})
}
history.append(store.prompt)

View File

@@ -2,7 +2,7 @@ import { createMemo } from "solid-js"
import { useSync } from "@tui/context/sync"
import { Keybind } from "@/util/keybind"
import { pipe, mapValues } from "remeda"
import type { KeybindsConfig } from "@opencode-ai/sdk/v2"
import type { KeybindsConfig } from "@opencode-ai/sdk"
import type { ParsedKey, Renderable } from "@opentui/core"
import { createStore } from "solid-js/store"
import { useKeyboard, useRenderer } from "@opentui/solid"

View File

@@ -1,4 +1,4 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2"
import { createOpencodeClient, type Event } from "@opencode-ai/sdk"
import { createSimpleContext } from "./helper"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { batch, onCleanup, onMount } from "solid-js"
@@ -20,12 +20,9 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
onMount(async () => {
while (true) {
if (abort.signal.aborted) break
const events = await sdk.event.subscribe(
{},
{
signal: abort.signal,
},
)
const events = await sdk.event.subscribe({
signal: abort.signal,
})
let queue: Event[] = []
let timer: Timer | undefined
let last = 0

View File

@@ -15,7 +15,7 @@ import type {
ProviderListResponse,
ProviderAuthMethod,
VcsInfo,
} from "@opencode-ai/sdk/v2"
} from "@opencode-ai/sdk"
import { createStore, produce, reconcile } from "solid-js/store"
import { useSDK } from "@tui/context/sdk"
import { Binary } from "@opencode-ai/util/binary"
@@ -255,19 +255,19 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
async function bootstrap() {
// blocking
await Promise.all([
sdk.client.config.providers({}, { throwOnError: true }).then((x) => {
sdk.client.config.providers({ throwOnError: true }).then((x) => {
batch(() => {
setStore("provider", x.data!.providers)
setStore("provider_default", x.data!.default)
})
}),
sdk.client.provider.list({}, { throwOnError: true }).then((x) => {
sdk.client.provider.list({ throwOnError: true }).then((x) => {
batch(() => {
setStore("provider_next", x.data!)
})
}),
sdk.client.app.agents({}, { throwOnError: true }).then((x) => setStore("agent", x.data ?? [])),
sdk.client.config.get({}, { throwOnError: true }).then((x) => setStore("config", x.data!)),
sdk.client.app.agents({ throwOnError: true }).then((x) => setStore("agent", x.data ?? [])),
sdk.client.config.get({ throwOnError: true }).then((x) => setStore("config", x.data!)),
])
.then(() => {
if (store.status !== "complete") setStore("status", "partial")
@@ -333,10 +333,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
async sync(sessionID: string) {
if (fullSyncedSessions.has(sessionID)) return
const [session, messages, todo, diff] = await Promise.all([
sdk.client.session.get({ sessionID }, { throwOnError: true }),
sdk.client.session.messages({ sessionID, limit: 100 }),
sdk.client.session.todo({ sessionID }),
sdk.client.session.diff({ sessionID }),
sdk.client.session.get({ path: { id: sessionID }, throwOnError: true }),
sdk.client.session.messages({ path: { id: sessionID }, query: { limit: 100 } }),
sdk.client.session.todo({ path: { id: sessionID } }),
sdk.client.session.diff({ path: { id: sessionID } }),
])
setStore(
produce((draft) => {

View File

@@ -29,8 +29,12 @@ export function DialogMessage(props: {
if (!msg) return
sdk.client.session.revert({
sessionID: props.sessionID,
messageID: msg.id,
path: {
id: props.sessionID,
},
body: {
messageID: msg.id,
},
})
if (props.setPrompt) {
@@ -77,8 +81,12 @@ export function DialogMessage(props: {
description: "create a new session",
onSelect: async (dialog) => {
const result = await sdk.client.session.fork({
sessionID: props.sessionID,
messageID: props.messageID,
path: {
id: props.sessionID,
},
body: {
messageID: props.messageID,
},
})
route.navigate({
sessionID: result.data!.id,

View File

@@ -1,7 +1,7 @@
import { createMemo, onMount } from "solid-js"
import { useSync } from "@tui/context/sync"
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
import type { TextPart } from "@opencode-ai/sdk/v2"
import type { TextPart } from "@opencode-ai/sdk"
import { Locale } from "@/util/locale"
import { DialogMessage } from "./dialog-message"
import { useDialog } from "../../ui/dialog"

View File

@@ -4,7 +4,7 @@ import { useSync } from "@tui/context/sync"
import { pipe, sumBy } from "remeda"
import { useTheme } from "@tui/context/theme"
import { SplitBorder, EmptyBorder } from "@tui/component/border"
import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2"
import type { AssistantMessage, Session } from "@opencode-ai/sdk"
import { useDirectory } from "../../context/directory"
import { useKeybind } from "../../context/keybind"

View File

@@ -25,7 +25,7 @@ import {
type ScrollAcceleration,
} from "@opentui/core"
import { Prompt, type PromptRef } from "@tui/component/prompt"
import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk/v2"
import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk"
import { useLocal } from "@tui/context/local"
import { Locale } from "@/util/locale"
import type { Tool } from "@/tool/tool"
@@ -150,8 +150,7 @@ export function Session() {
.then(() => {
if (scroll) scroll.scrollBy(100_000)
})
.catch((e) => {
console.error(e)
.catch(() => {
toast.show({
message: `Session not found: ${route.sessionID}`,
variant: "error",
@@ -203,10 +202,14 @@ export function Session() {
return
})
if (response) {
sdk.client.permission.respond({
permissionID: first.id,
sessionID: route.sessionID,
response: response,
sdk.client.postSessionIdPermissionsPermissionId({
path: {
permissionID: first.id,
id: route.sessionID,
},
body: {
response: response,
},
})
}
}
@@ -251,7 +254,9 @@ export function Session() {
onSelect: async (dialog: any) => {
await sdk.client.session
.share({
sessionID: route.sessionID,
path: {
id: route.sessionID,
},
})
.then((res) =>
Clipboard.copy(res.data!.share!.url).catch(() =>
@@ -309,9 +314,13 @@ export function Session() {
return
}
sdk.client.session.summarize({
sessionID: route.sessionID,
modelID: selectedModel.modelID,
providerID: selectedModel.providerID,
path: {
id: route.sessionID,
},
body: {
modelID: selectedModel.modelID,
providerID: selectedModel.providerID,
},
})
dialog.clear()
},
@@ -324,7 +333,9 @@ export function Session() {
category: "Session",
onSelect: (dialog) => {
sdk.client.session.unshare({
sessionID: route.sessionID,
path: {
id: route.sessionID,
},
})
dialog.clear()
},
@@ -336,14 +347,18 @@ export function Session() {
category: "Session",
onSelect: async (dialog) => {
const status = sync.data.session_status[route.sessionID]
if (status?.type !== "idle") await sdk.client.session.abort({ sessionID: route.sessionID }).catch(() => {})
if (status?.type !== "idle") await sdk.client.session.abort({ path: { id: route.sessionID } }).catch(() => {})
const revert = session().revert?.messageID
const message = messages().findLast((x) => (!revert || x.id < revert) && x.role === "user")
if (!message) return
sdk.client.session
.revert({
sessionID: route.sessionID,
messageID: message.id,
path: {
id: route.sessionID,
},
body: {
messageID: message.id,
},
})
.then(() => {
toBottom()
@@ -377,14 +392,20 @@ export function Session() {
const message = messages().find((x) => x.role === "user" && x.id > messageID)
if (!message) {
sdk.client.session.unrevert({
sessionID: route.sessionID,
path: {
id: route.sessionID,
},
})
prompt.set({ input: "", parts: [] })
return
}
sdk.client.session.revert({
sessionID: route.sessionID,
messageID: message.id,
path: {
id: route.sessionID,
},
body: {
messageID: message.id,
},
})
},
},
@@ -1045,7 +1066,7 @@ function UserMessage(props: {
</box>
</Show>
<text fg={theme.textMuted}>
{ctx.usernameVisible() ? `${sync.data.config.username ?? "You "}` : "You "}
{ctx.usernameVisible() ? `${sync.data.config.username ?? "You"}` : "You"}
<Show
when={queued()}
fallback={

View File

@@ -4,7 +4,7 @@ import { createStore } from "solid-js/store"
import { useTheme } from "../../context/theme"
import { Locale } from "@/util/locale"
import path from "path"
import type { AssistantMessage } from "@opencode-ai/sdk/v2"
import type { AssistantMessage } from "@opencode-ai/sdk"
import { Global } from "@/global"
import { Installation } from "@/installation"
import { useKeybind } from "../../context/keybind"
@@ -104,15 +104,11 @@ export function Sidebar(props: { sessionID: string }) {
<text
flexShrink={0}
style={{
fg: (
{
connected: theme.success,
failed: theme.error,
disabled: theme.textMuted,
needs_auth: theme.warning,
needs_client_registration: theme.error,
} as Record<string, typeof theme.success>
)[item.status],
fg: {
connected: theme.success,
failed: theme.error,
disabled: theme.textMuted,
}[item.status],
}}
>
@@ -120,14 +116,10 @@ export function Sidebar(props: { sessionID: string }) {
<text fg={theme.text} wrapMode="word">
{key}{" "}
<span style={{ fg: theme.textMuted }}>
<Switch fallback={item.status}>
<Switch>
<Match when={item.status === "connected"}>Connected</Match>
<Match when={item.status === "failed" && item}>{(val) => <i>{val().error}</i>}</Match>
<Match when={item.status === "disabled"}>Disabled</Match>
<Match when={(item.status as string) === "needs_auth"}>Needs auth</Match>
<Match when={(item.status as string) === "needs_client_registration"}>
Needs client ID
</Match>
<Match when={item.status === "disabled"}>Disabled in configuration</Match>
</Switch>
</span>
</text>

View File

@@ -1,344 +0,0 @@
import type { Argv } from "yargs"
import { UI } from "../ui"
import * as prompts from "@clack/prompts"
import { Installation } from "../../installation"
import { Global } from "../../global"
import { $ } from "bun"
import fs from "fs/promises"
import path from "path"
import os from "os"
interface UninstallArgs {
keepConfig: boolean
keepData: boolean
dryRun: boolean
force: boolean
}
interface RemovalTargets {
directories: Array<{ path: string; label: string; keep: boolean }>
shellConfig: string | null
binary: string | null
}
export const UninstallCommand = {
command: "uninstall",
describe: "uninstall opencode and remove all related files",
builder: (yargs: Argv) =>
yargs
.option("keep-config", {
alias: "c",
type: "boolean",
describe: "keep configuration files",
default: false,
})
.option("keep-data", {
alias: "d",
type: "boolean",
describe: "keep session data and snapshots",
default: false,
})
.option("dry-run", {
type: "boolean",
describe: "show what would be removed without removing",
default: false,
})
.option("force", {
alias: "f",
type: "boolean",
describe: "skip confirmation prompts",
default: false,
}),
handler: async (args: UninstallArgs) => {
UI.empty()
UI.println(UI.logo(" "))
UI.empty()
prompts.intro("Uninstall OpenCode")
const method = await Installation.method()
prompts.log.info(`Installation method: ${method}`)
const targets = await collectRemovalTargets(args, method)
await showRemovalSummary(targets, method)
if (!args.force && !args.dryRun) {
const confirm = await prompts.confirm({
message: "Are you sure you want to uninstall?",
initialValue: false,
})
if (!confirm || prompts.isCancel(confirm)) {
prompts.outro("Cancelled")
return
}
}
if (args.dryRun) {
prompts.log.warn("Dry run - no changes made")
prompts.outro("Done")
return
}
await executeUninstall(method, targets)
prompts.outro("Done")
},
}
async function collectRemovalTargets(args: UninstallArgs, method: Installation.Method): Promise<RemovalTargets> {
const directories: RemovalTargets["directories"] = [
{ path: Global.Path.data, label: "Data", keep: args.keepData },
{ path: Global.Path.cache, label: "Cache", keep: false },
{ path: Global.Path.config, label: "Config", keep: args.keepConfig },
{ path: Global.Path.state, label: "State", keep: false },
]
const shellConfig = method === "curl" ? await getShellConfigFile() : null
const binary = method === "curl" ? process.execPath : null
return { directories, shellConfig, binary }
}
async function showRemovalSummary(targets: RemovalTargets, method: Installation.Method) {
prompts.log.message("The following will be removed:")
for (const dir of targets.directories) {
const exists = await fs
.access(dir.path)
.then(() => true)
.catch(() => false)
if (!exists) continue
const size = await getDirectorySize(dir.path)
const sizeStr = formatSize(size)
const status = dir.keep ? UI.Style.TEXT_DIM + "(keeping)" : ""
const prefix = dir.keep ? "○" : "✓"
prompts.log.info(` ${prefix} ${dir.label}: ${shortenPath(dir.path)} ${UI.Style.TEXT_DIM}(${sizeStr})${status}`)
}
if (targets.binary) {
prompts.log.info(` ✓ Binary: ${shortenPath(targets.binary)}`)
}
if (targets.shellConfig) {
prompts.log.info(` ✓ Shell PATH in ${shortenPath(targets.shellConfig)}`)
}
if (method !== "curl" && method !== "unknown") {
const cmds: Record<string, string> = {
npm: "npm uninstall -g opencode-ai",
pnpm: "pnpm uninstall -g opencode-ai",
bun: "bun remove -g opencode-ai",
yarn: "yarn global remove opencode-ai",
brew: "brew uninstall opencode",
}
prompts.log.info(` ✓ Package: ${cmds[method] || method}`)
}
}
async function executeUninstall(method: Installation.Method, targets: RemovalTargets) {
const spinner = prompts.spinner()
const errors: string[] = []
for (const dir of targets.directories) {
if (dir.keep) {
prompts.log.step(`Skipping ${dir.label} (--keep-${dir.label.toLowerCase()})`)
continue
}
const exists = await fs
.access(dir.path)
.then(() => true)
.catch(() => false)
if (!exists) continue
spinner.start(`Removing ${dir.label}...`)
const err = await fs.rm(dir.path, { recursive: true, force: true }).catch((e) => e)
if (err) {
spinner.stop(`Failed to remove ${dir.label}`, 1)
errors.push(`${dir.label}: ${err.message}`)
continue
}
spinner.stop(`Removed ${dir.label}`)
}
if (targets.shellConfig) {
spinner.start("Cleaning shell config...")
const err = await cleanShellConfig(targets.shellConfig).catch((e) => e)
if (err) {
spinner.stop("Failed to clean shell config", 1)
errors.push(`Shell config: ${err.message}`)
} else {
spinner.stop("Cleaned shell config")
}
}
if (method !== "curl" && method !== "unknown") {
const cmds: Record<string, string[]> = {
npm: ["npm", "uninstall", "-g", "opencode-ai"],
pnpm: ["pnpm", "uninstall", "-g", "opencode-ai"],
bun: ["bun", "remove", "-g", "opencode-ai"],
yarn: ["yarn", "global", "remove", "opencode-ai"],
brew: ["brew", "uninstall", "opencode"],
}
const cmd = cmds[method]
if (cmd) {
spinner.start(`Running ${cmd.join(" ")}...`)
const result = await $`${cmd}`.quiet().nothrow()
if (result.exitCode !== 0) {
spinner.stop(`Package manager uninstall failed`, 1)
prompts.log.warn(`You may need to run manually: ${cmd.join(" ")}`)
errors.push(`Package manager: exit code ${result.exitCode}`)
} else {
spinner.stop("Package removed")
}
}
}
if (method === "curl" && targets.binary) {
UI.empty()
prompts.log.message("To finish removing the binary, run:")
prompts.log.info(` rm "${targets.binary}"`)
const binDir = path.dirname(targets.binary)
if (binDir.includes(".opencode")) {
prompts.log.info(` rmdir "${binDir}" 2>/dev/null`)
}
}
if (errors.length > 0) {
UI.empty()
prompts.log.warn("Some operations failed:")
for (const err of errors) {
prompts.log.error(` ${err}`)
}
}
UI.empty()
prompts.log.success("Thank you for using OpenCode!")
}
async function getShellConfigFile(): Promise<string | null> {
const shell = path.basename(process.env.SHELL || "bash")
const home = os.homedir()
const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(home, ".config")
const configFiles: Record<string, string[]> = {
fish: [path.join(xdgConfig, "fish", "config.fish")],
zsh: [
path.join(home, ".zshrc"),
path.join(home, ".zshenv"),
path.join(xdgConfig, "zsh", ".zshrc"),
path.join(xdgConfig, "zsh", ".zshenv"),
],
bash: [
path.join(home, ".bashrc"),
path.join(home, ".bash_profile"),
path.join(home, ".profile"),
path.join(xdgConfig, "bash", ".bashrc"),
path.join(xdgConfig, "bash", ".bash_profile"),
],
ash: [path.join(home, ".ashrc"), path.join(home, ".profile")],
sh: [path.join(home, ".profile")],
}
const candidates = configFiles[shell] || configFiles.bash
for (const file of candidates) {
const exists = await fs
.access(file)
.then(() => true)
.catch(() => false)
if (!exists) continue
const content = await Bun.file(file)
.text()
.catch(() => "")
if (content.includes("# opencode") || content.includes(".opencode/bin")) {
return file
}
}
return null
}
async function cleanShellConfig(file: string) {
const content = await Bun.file(file).text()
const lines = content.split("\n")
const filtered: string[] = []
let skip = false
for (const line of lines) {
const trimmed = line.trim()
if (trimmed === "# opencode") {
skip = true
continue
}
if (skip) {
skip = false
if (trimmed.includes(".opencode/bin") || trimmed.includes("fish_add_path")) {
continue
}
}
if (
(trimmed.startsWith("export PATH=") && trimmed.includes(".opencode/bin")) ||
(trimmed.startsWith("fish_add_path") && trimmed.includes(".opencode"))
) {
continue
}
filtered.push(line)
}
while (filtered.length > 0 && filtered[filtered.length - 1].trim() === "") {
filtered.pop()
}
const output = filtered.join("\n") + "\n"
await Bun.write(file, output)
}
async function getDirectorySize(dir: string): Promise<number> {
let total = 0
const walk = async (current: string) => {
const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => [])
for (const entry of entries) {
const full = path.join(current, entry.name)
if (entry.isDirectory()) {
await walk(full)
continue
}
if (entry.isFile()) {
const stat = await fs.stat(full).catch(() => null)
if (stat) total += stat.size
}
}
}
await walk(dir)
return total
}
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
}
function shortenPath(p: string): string {
const home = os.homedir()
if (p.startsWith(home)) {
return p.replace(home, "~")
}
return p
}

View File

@@ -325,33 +325,12 @@ export namespace Config {
ref: "McpLocalConfig",
})
export const McpOAuth = z
.object({
clientId: z
.string()
.optional()
.describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."),
clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"),
scope: z.string().optional().describe("OAuth scopes to request during authorization"),
})
.strict()
.meta({
ref: "McpOAuthConfig",
})
export type McpOAuth = z.infer<typeof McpOAuth>
export const McpRemote = z
.object({
type: z.literal("remote").describe("Type of MCP server connection"),
url: z.string().describe("URL of the remote MCP server"),
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
oauth: z
.union([McpOAuth, z.literal(false)])
.optional()
.describe(
"OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.",
),
timeout: z
.number()
.int()

View File

@@ -6,7 +6,6 @@ import { Log } from "./util/log"
import { AuthCommand } from "./cli/cmd/auth"
import { AgentCommand } from "./cli/cmd/agent"
import { UpgradeCommand } from "./cli/cmd/upgrade"
import { UninstallCommand } from "./cli/cmd/uninstall"
import { ModelsCommand } from "./cli/cmd/models"
import { UI } from "./cli/ui"
import { Installation } from "./installation"
@@ -87,7 +86,6 @@ const cli = yargs(hideBin(process.argv))
.command(AuthCommand)
.command(AgentCommand)
.command(UpgradeCommand)
.command(UninstallCommand)
.command(ServeCommand)
.command(WebCommand)
.command(ModelsCommand)

View File

@@ -1,82 +0,0 @@
import path from "path"
import fs from "fs/promises"
import z from "zod"
import { Global } from "../global"
export namespace McpAuth {
export const Tokens = z.object({
accessToken: z.string(),
refreshToken: z.string().optional(),
expiresAt: z.number().optional(),
scope: z.string().optional(),
})
export type Tokens = z.infer<typeof Tokens>
export const ClientInfo = z.object({
clientId: z.string(),
clientSecret: z.string().optional(),
clientIdIssuedAt: z.number().optional(),
clientSecretExpiresAt: z.number().optional(),
})
export type ClientInfo = z.infer<typeof ClientInfo>
export const Entry = z.object({
tokens: Tokens.optional(),
clientInfo: ClientInfo.optional(),
codeVerifier: z.string().optional(),
})
export type Entry = z.infer<typeof Entry>
const filepath = path.join(Global.Path.data, "mcp-auth.json")
export async function get(mcpName: string): Promise<Entry | undefined> {
const data = await all()
return data[mcpName]
}
export async function all(): Promise<Record<string, Entry>> {
const file = Bun.file(filepath)
return file.json().catch(() => ({}))
}
export async function set(mcpName: string, entry: Entry): Promise<void> {
const file = Bun.file(filepath)
const data = await all()
await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2))
await fs.chmod(file.name!, 0o600)
}
export async function remove(mcpName: string): Promise<void> {
const file = Bun.file(filepath)
const data = await all()
delete data[mcpName]
await Bun.write(file, JSON.stringify(data, null, 2))
await fs.chmod(file.name!, 0o600)
}
export async function updateTokens(mcpName: string, tokens: Tokens): Promise<void> {
const entry = (await get(mcpName)) ?? {}
entry.tokens = tokens
await set(mcpName, entry)
}
export async function updateClientInfo(mcpName: string, clientInfo: ClientInfo): Promise<void> {
const entry = (await get(mcpName)) ?? {}
entry.clientInfo = clientInfo
await set(mcpName, entry)
}
export async function updateCodeVerifier(mcpName: string, codeVerifier: string): Promise<void> {
const entry = (await get(mcpName)) ?? {}
entry.codeVerifier = codeVerifier
await set(mcpName, entry)
}
export async function clearCodeVerifier(mcpName: string): Promise<void> {
const entry = await get(mcpName)
if (entry) {
delete entry.codeVerifier
await set(mcpName, entry)
}
}
}

View File

@@ -3,17 +3,12 @@ import { experimental_createMCPClient } from "@ai-sdk/mcp"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
import { Config } from "../config/config"
import { Log } from "../util/log"
import { NamedError } from "@opencode-ai/util/error"
import z from "zod/v4"
import { Instance } from "../project/instance"
import { withTimeout } from "@/util/timeout"
import { McpOAuthProvider } from "./oauth-provider"
import { McpOAuthCallback } from "./oauth-callback"
import { McpAuth } from "./auth"
import open from "open"
export namespace MCP {
const log = Log.create({ service: "mcp" })
@@ -51,21 +46,6 @@ export namespace MCP {
.meta({
ref: "MCPStatusFailed",
}),
z
.object({
status: z.literal("needs_auth"),
})
.meta({
ref: "MCPStatusNeedsAuth",
}),
z
.object({
status: z.literal("needs_client_registration"),
error: z.string(),
})
.meta({
ref: "MCPStatusNeedsClientRegistration",
}),
])
.meta({
ref: "MCPStatus",
@@ -73,10 +53,6 @@ export namespace MCP {
export type Status = z.infer<typeof Status>
type MCPClient = Awaited<ReturnType<typeof experimental_createMCPClient>>
// Store transports for OAuth servers to allow finishing auth
type TransportWithAuth = StreamableHTTPClientTransport | SSEClientTransport
const pendingOAuthTransports = new Map<string, TransportWithAuth>()
const state = Instance.state(
async () => {
const cfg = await Config.get()
@@ -111,7 +87,6 @@ export namespace MCP {
}),
),
)
pendingOAuthTransports.clear()
},
)
@@ -145,98 +120,58 @@ export namespace MCP {
async function create(key: string, mcp: Config.Mcp) {
if (mcp.enabled === false) {
log.info("mcp server disabled", { key })
return {
mcpClient: undefined,
status: { status: "disabled" as const },
}
return
}
log.info("found", { key, type: mcp.type })
let mcpClient: MCPClient | undefined
let status: Status | undefined = undefined
if (mcp.type === "remote") {
// OAuth is enabled by default for remote servers unless explicitly disabled with oauth: false
const oauthDisabled = mcp.oauth === false
const oauthConfig = typeof mcp.oauth === "object" ? mcp.oauth : undefined
let authProvider: McpOAuthProvider | undefined
if (!oauthDisabled) {
authProvider = new McpOAuthProvider(
key,
mcp.url,
{
clientId: oauthConfig?.clientId,
clientSecret: oauthConfig?.clientSecret,
scope: oauthConfig?.scope,
},
{
onRedirect: async (url) => {
log.info("oauth redirect requested", { key, url: url.toString() })
// Store the URL - actual browser opening is handled by startAuth
},
},
)
}
const transports: Array<{ name: string; transport: TransportWithAuth }> = [
const transports = [
{
name: "StreamableHTTP",
transport: new StreamableHTTPClientTransport(new URL(mcp.url), {
authProvider,
requestInit: oauthDisabled && mcp.headers ? { headers: mcp.headers } : undefined,
requestInit: {
headers: mcp.headers,
},
}),
},
{
name: "SSE",
transport: new SSEClientTransport(new URL(mcp.url), {
authProvider,
requestInit: oauthDisabled && mcp.headers ? { headers: mcp.headers } : undefined,
requestInit: {
headers: mcp.headers,
},
}),
},
]
let lastError: Error | undefined
for (const { name, transport } of transports) {
try {
mcpClient = await experimental_createMCPClient({
name: "opencode",
transport,
const result = await experimental_createMCPClient({
name: "opencode",
transport,
})
.then((client) => {
log.info("connected", { key, transport: name })
mcpClient = client
status = { status: "connected" }
return true
})
log.info("connected", { key, transport: name })
status = { status: "connected" }
break
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error))
// Handle OAuth-specific errors
if (error instanceof UnauthorizedError) {
log.info("mcp server requires authentication", { key, transport: name })
// Check if this is a "needs registration" error
if (lastError.message.includes("registration") || lastError.message.includes("client_id")) {
status = {
status: "needs_client_registration" as const,
error: "Server does not support dynamic client registration. Please provide clientId in config.",
}
} else {
// Store transport for later finishAuth call
pendingOAuthTransports.set(key, transport)
status = { status: "needs_auth" as const }
.catch((error) => {
lastError = error instanceof Error ? error : new Error(String(error))
log.debug("transport connection failed", {
key,
transport: name,
url: mcp.url,
error: lastError.message,
})
status = {
status: "failed" as const,
error: lastError.message,
}
break
}
log.debug("transport connection failed", {
key,
transport: name,
url: mcp.url,
error: lastError.message,
return false
})
status = {
status: "failed" as const,
error: lastError.message,
}
}
if (result) break
}
}
@@ -351,165 +286,4 @@ export namespace MCP {
}
return result
}
/**
* Start OAuth authentication flow for an MCP server.
* Returns the authorization URL that should be opened in a browser.
*/
export async function startAuth(mcpName: string): Promise<{ authorizationUrl: string }> {
const cfg = await Config.get()
const mcpConfig = cfg.mcp?.[mcpName]
if (!mcpConfig) {
throw new Error(`MCP server not found: ${mcpName}`)
}
if (mcpConfig.type !== "remote") {
throw new Error(`MCP server ${mcpName} is not a remote server`)
}
if (mcpConfig.oauth === false) {
throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`)
}
// Start the callback server
await McpOAuthCallback.ensureRunning()
// Create a new auth provider for this flow
// OAuth config is optional - if not provided, we'll use auto-discovery
const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined
let capturedUrl: URL | undefined
const authProvider = new McpOAuthProvider(
mcpName,
mcpConfig.url,
{
clientId: oauthConfig?.clientId,
clientSecret: oauthConfig?.clientSecret,
scope: oauthConfig?.scope,
},
{
onRedirect: async (url) => {
capturedUrl = url
},
},
)
// Create transport with auth provider
const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), {
authProvider,
})
// Try to connect - this will trigger the OAuth flow
try {
await experimental_createMCPClient({
name: "opencode",
transport,
})
// If we get here, we're already authenticated
return { authorizationUrl: "" }
} catch (error) {
if (error instanceof UnauthorizedError && capturedUrl) {
// Store transport for finishAuth
pendingOAuthTransports.set(mcpName, transport)
return { authorizationUrl: capturedUrl.toString() }
}
throw error
}
}
/**
* Complete OAuth authentication after user authorizes in browser.
* Opens the browser and waits for callback.
*/
export async function authenticate(mcpName: string): Promise<Status> {
const { authorizationUrl } = await startAuth(mcpName)
if (!authorizationUrl) {
// Already authenticated
const s = await state()
return s.status[mcpName] ?? { status: "connected" }
}
// Extract state from authorization URL to use as callback key
// If no state parameter, use mcpName as fallback
const authUrl = new URL(authorizationUrl)
const oauthState = authUrl.searchParams.get("state") ?? mcpName
// Open browser
log.info("opening browser for oauth", { mcpName, url: authorizationUrl, state: oauthState })
await open(authorizationUrl)
// Wait for callback using the OAuth state parameter (or mcpName as fallback)
const code = await McpOAuthCallback.waitForCallback(oauthState)
// Finish auth
return finishAuth(mcpName, code)
}
/**
* Complete OAuth authentication with the authorization code.
*/
export async function finishAuth(mcpName: string, authorizationCode: string): Promise<Status> {
const transport = pendingOAuthTransports.get(mcpName)
if (!transport) {
throw new Error(`No pending OAuth flow for MCP server: ${mcpName}`)
}
try {
// Call finishAuth on the transport
await transport.finishAuth(authorizationCode)
// Clear the code verifier after successful auth
await McpAuth.clearCodeVerifier(mcpName)
// Now try to reconnect
const cfg = await Config.get()
const mcpConfig = cfg.mcp?.[mcpName]
if (!mcpConfig) {
throw new Error(`MCP server not found: ${mcpName}`)
}
// Re-add the MCP server to establish connection
pendingOAuthTransports.delete(mcpName)
const result = await add(mcpName, mcpConfig)
const statusRecord = result.status as Record<string, Status>
return statusRecord[mcpName] ?? { status: "failed", error: "Unknown error after auth" }
} catch (error) {
log.error("failed to finish oauth", { mcpName, error })
return {
status: "failed",
error: error instanceof Error ? error.message : String(error),
}
}
}
/**
* Remove OAuth credentials for an MCP server.
*/
export async function removeAuth(mcpName: string): Promise<void> {
await McpAuth.remove(mcpName)
McpOAuthCallback.cancelPending(mcpName)
pendingOAuthTransports.delete(mcpName)
log.info("removed oauth credentials", { mcpName })
}
/**
* Check if an MCP server supports OAuth (remote servers support OAuth by default unless explicitly disabled).
*/
export async function supportsOAuth(mcpName: string): Promise<boolean> {
const cfg = await Config.get()
const mcpConfig = cfg.mcp?.[mcpName]
return mcpConfig?.type === "remote" && mcpConfig.oauth !== false
}
/**
* Check if an MCP server has stored OAuth tokens.
*/
export async function hasStoredTokens(mcpName: string): Promise<boolean> {
const entry = await McpAuth.get(mcpName)
return !!entry?.tokens
}
}

View File

@@ -1,203 +0,0 @@
import { Log } from "../util/log"
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider"
const log = Log.create({ service: "mcp.oauth-callback" })
const HTML_SUCCESS = `<!DOCTYPE html>
<html>
<head>
<title>OpenCode - Authorization Successful</title>
<style>
body { font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee; }
.container { text-align: center; padding: 2rem; }
h1 { color: #4ade80; margin-bottom: 1rem; }
p { color: #aaa; }
</style>
</head>
<body>
<div class="container">
<h1>Authorization Successful</h1>
<p>You can close this window and return to OpenCode.</p>
</div>
<script>setTimeout(() => window.close(), 2000);</script>
</body>
</html>`
const HTML_ERROR = (error: string) => `<!DOCTYPE html>
<html>
<head>
<title>OpenCode - Authorization Failed</title>
<style>
body { font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee; }
.container { text-align: center; padding: 2rem; }
h1 { color: #f87171; margin-bottom: 1rem; }
p { color: #aaa; }
.error { color: #fca5a5; font-family: monospace; margin-top: 1rem; padding: 1rem; background: rgba(248,113,113,0.1); border-radius: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<h1>Authorization Failed</h1>
<p>An error occurred during authorization.</p>
<div class="error">${error}</div>
</div>
</body>
</html>`
interface PendingAuth {
resolve: (code: string) => void
reject: (error: Error) => void
timeout: ReturnType<typeof setTimeout>
}
export namespace McpOAuthCallback {
let server: ReturnType<typeof Bun.serve> | undefined
const pendingAuths = new Map<string, PendingAuth>()
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
export async function ensureRunning(): Promise<void> {
if (server) return
const running = await isPortInUse()
if (running) {
log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
return
}
server = Bun.serve({
port: OAUTH_CALLBACK_PORT,
fetch(req) {
const url = new URL(req.url)
if (url.pathname !== OAUTH_CALLBACK_PATH) {
return new Response("Not found", { status: 404 })
}
const code = url.searchParams.get("code")
const state = url.searchParams.get("state")
const error = url.searchParams.get("error")
const errorDescription = url.searchParams.get("error_description")
log.info("received oauth callback", { hasCode: !!code, state, error })
if (error) {
const errorMsg = errorDescription || error
if (state && pendingAuths.has(state)) {
const pending = pendingAuths.get(state)!
clearTimeout(pending.timeout)
pendingAuths.delete(state)
pending.reject(new Error(errorMsg))
}
return new Response(HTML_ERROR(errorMsg), {
headers: { "Content-Type": "text/html" },
})
}
if (!code) {
return new Response(HTML_ERROR("No authorization code provided"), {
status: 400,
headers: { "Content-Type": "text/html" },
})
}
// Try to find the pending auth by state parameter, or if no state, use the single pending auth
let pending: PendingAuth | undefined
let pendingKey: string | undefined
if (state && pendingAuths.has(state)) {
pending = pendingAuths.get(state)!
pendingKey = state
} else if (!state && pendingAuths.size === 1) {
// No state parameter but only one pending auth - use it
const [key, value] = pendingAuths.entries().next().value as [string, PendingAuth]
pending = value
pendingKey = key
log.info("no state parameter, using single pending auth", { key })
}
if (!pending || !pendingKey) {
const errorMsg = !state
? "No state parameter provided and multiple pending authorizations"
: "Unknown or expired authorization request"
return new Response(HTML_ERROR(errorMsg), {
status: 400,
headers: { "Content-Type": "text/html" },
})
}
clearTimeout(pending.timeout)
pendingAuths.delete(pendingKey)
pending.resolve(code)
return new Response(HTML_SUCCESS, {
headers: { "Content-Type": "text/html" },
})
},
})
log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
}
export function waitForCallback(mcpName: string): Promise<string> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
if (pendingAuths.has(mcpName)) {
pendingAuths.delete(mcpName)
reject(new Error("OAuth callback timeout - authorization took too long"))
}
}, CALLBACK_TIMEOUT_MS)
pendingAuths.set(mcpName, { resolve, reject, timeout })
})
}
export function cancelPending(mcpName: string): void {
const pending = pendingAuths.get(mcpName)
if (pending) {
clearTimeout(pending.timeout)
pendingAuths.delete(mcpName)
pending.reject(new Error("Authorization cancelled"))
}
}
export async function isPortInUse(): Promise<boolean> {
return new Promise((resolve) => {
Bun.connect({
hostname: "127.0.0.1",
port: OAUTH_CALLBACK_PORT,
socket: {
open(socket) {
socket.end()
resolve(true)
},
error() {
resolve(false)
},
data() {},
close() {},
},
}).catch(() => {
resolve(false)
})
})
}
export async function stop(): Promise<void> {
if (server) {
server.stop()
server = undefined
log.info("oauth callback server stopped")
}
for (const [name, pending] of pendingAuths) {
clearTimeout(pending.timeout)
pending.reject(new Error("OAuth callback server stopped"))
}
pendingAuths.clear()
}
export function isRunning(): boolean {
return server !== undefined
}
}

View File

@@ -1,132 +0,0 @@
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
import type {
OAuthClientMetadata,
OAuthTokens,
OAuthClientInformation,
OAuthClientInformationFull,
} from "@modelcontextprotocol/sdk/shared/auth.js"
import { McpAuth } from "./auth"
import { Log } from "../util/log"
const log = Log.create({ service: "mcp.oauth" })
const OAUTH_CALLBACK_PORT = 19876
const OAUTH_CALLBACK_PATH = "/mcp/oauth/callback"
export interface McpOAuthConfig {
clientId?: string
clientSecret?: string
scope?: string
}
export interface McpOAuthCallbacks {
onRedirect: (url: URL) => void | Promise<void>
}
export class McpOAuthProvider implements OAuthClientProvider {
constructor(
private mcpName: string,
private serverUrl: string,
private config: McpOAuthConfig,
private callbacks: McpOAuthCallbacks,
) {}
get redirectUrl(): string {
return `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`
}
get clientMetadata(): OAuthClientMetadata {
return {
redirect_uris: [this.redirectUrl],
client_name: "OpenCode",
client_uri: "https://opencode.ai",
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
token_endpoint_auth_method: this.config.clientSecret ? "client_secret_post" : "none",
}
}
async clientInformation(): Promise<OAuthClientInformation | undefined> {
// Check config first (pre-registered client)
if (this.config.clientId) {
return {
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
}
}
// Check stored client info (from dynamic registration)
const entry = await McpAuth.get(this.mcpName)
if (entry?.clientInfo) {
// Check if client secret has expired
if (entry.clientInfo.clientSecretExpiresAt && entry.clientInfo.clientSecretExpiresAt < Date.now() / 1000) {
log.info("client secret expired, need to re-register", { mcpName: this.mcpName })
return undefined
}
return {
client_id: entry.clientInfo.clientId,
client_secret: entry.clientInfo.clientSecret,
}
}
// No client info - will trigger dynamic registration
return undefined
}
async saveClientInformation(info: OAuthClientInformationFull): Promise<void> {
await McpAuth.updateClientInfo(this.mcpName, {
clientId: info.client_id,
clientSecret: info.client_secret,
clientIdIssuedAt: info.client_id_issued_at,
clientSecretExpiresAt: info.client_secret_expires_at,
})
log.info("saved dynamically registered client", {
mcpName: this.mcpName,
clientId: info.client_id,
})
}
async tokens(): Promise<OAuthTokens | undefined> {
const entry = await McpAuth.get(this.mcpName)
if (!entry?.tokens) return undefined
return {
access_token: entry.tokens.accessToken,
token_type: "Bearer",
refresh_token: entry.tokens.refreshToken,
expires_in: entry.tokens.expiresAt
? Math.max(0, Math.floor(entry.tokens.expiresAt - Date.now() / 1000))
: undefined,
scope: entry.tokens.scope,
}
}
async saveTokens(tokens: OAuthTokens): Promise<void> {
await McpAuth.updateTokens(this.mcpName, {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: tokens.expires_in ? Date.now() / 1000 + tokens.expires_in : undefined,
scope: tokens.scope,
})
log.info("saved oauth tokens", { mcpName: this.mcpName })
}
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
log.info("redirecting to authorization", { mcpName: this.mcpName, url: authorizationUrl.toString() })
await this.callbacks.onRedirect(authorizationUrl)
}
async saveCodeVerifier(codeVerifier: string): Promise<void> {
await McpAuth.updateCodeVerifier(this.mcpName, codeVerifier)
}
async codeVerifier(): Promise<string> {
const entry = await McpAuth.get(this.mcpName)
if (!entry?.codeVerifier) {
throw new Error(`No code verifier saved for MCP server: ${this.mcpName}`)
}
return entry.codeVerifier
}
}
export { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH }

View File

@@ -61,14 +61,10 @@ export namespace Plugin {
for (const hook of await state().then((x) => x.hooks)) {
const fn = hook[name]
if (!fn) continue
try {
// @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you
// give up.
// try-counter: 2
await fn(input, output)
} catch (e) {
log.error("failed to trigger hook", { name, error: e })
}
// @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you
// give up.
// try-counter: 2
await fn(input, output)
}
return output
}

View File

@@ -535,7 +535,7 @@ export namespace Provider {
id: model.id ?? existingModel?.api.id ?? modelID,
npm:
model.provider?.npm ?? provider.npm ?? existingModel?.api.npm ?? modelsDev[providerID]?.npm ?? providerID,
url: provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api,
url: provider?.api ?? existingModel?.api.url,
},
status: model.status ?? existingModel?.status ?? "active",
name,

View File

@@ -209,7 +209,7 @@ export namespace Server {
},
)
.get(
"/pty/:ptyID",
"/pty/:id",
describeRoute({
description: "Get PTY session info",
operationId: "pty.get",
@@ -225,9 +225,9 @@ export namespace Server {
...errors(404),
},
}),
validator("param", z.object({ ptyID: z.string() })),
validator("param", z.object({ id: z.string() })),
async (c) => {
const info = Pty.get(c.req.valid("param").ptyID)
const info = Pty.get(c.req.valid("param").id)
if (!info) {
throw new Storage.NotFoundError({ message: "Session not found" })
}
@@ -235,7 +235,7 @@ export namespace Server {
},
)
.put(
"/pty/:ptyID",
"/pty/:id",
describeRoute({
description: "Update PTY session",
operationId: "pty.update",
@@ -251,15 +251,15 @@ export namespace Server {
...errors(400),
},
}),
validator("param", z.object({ ptyID: z.string() })),
validator("param", z.object({ id: z.string() })),
validator("json", Pty.UpdateInput),
async (c) => {
const info = await Pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
const info = await Pty.update(c.req.valid("param").id, c.req.valid("json"))
return c.json(info)
},
)
.delete(
"/pty/:ptyID",
"/pty/:id",
describeRoute({
description: "Remove a PTY session",
operationId: "pty.remove",
@@ -275,14 +275,14 @@ export namespace Server {
...errors(404),
},
}),
validator("param", z.object({ ptyID: z.string() })),
validator("param", z.object({ id: z.string() })),
async (c) => {
await Pty.remove(c.req.valid("param").ptyID)
await Pty.remove(c.req.valid("param").id)
return c.json(true)
},
)
.get(
"/pty/:ptyID/connect",
"/pty/:id/connect",
describeRoute({
description: "Connect to a PTY session",
operationId: "pty.connect",
@@ -298,9 +298,9 @@ export namespace Server {
...errors(404),
},
}),
validator("param", z.object({ ptyID: z.string() })),
validator("param", z.object({ id: z.string() })),
upgradeWebSocket((c) => {
const id = c.req.param("ptyID")
const id = c.req.param("id")
let handler: ReturnType<typeof Pty.connect>
if (!Pty.get(id)) throw new Error("Session not found")
return {
@@ -557,7 +557,7 @@ export namespace Server {
},
)
.get(
"/session/:sessionID",
"/session/:id",
describeRoute({
description: "Get session",
operationId: "session.get",
@@ -576,18 +576,17 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: Session.get.schema,
id: Session.get.schema,
}),
),
async (c) => {
const sessionID = c.req.valid("param").sessionID
log.info("SEARCH", { url: c.req.url })
const sessionID = c.req.valid("param").id
const session = await Session.get(sessionID)
return c.json(session)
},
)
.get(
"/session/:sessionID/children",
"/session/:id/children",
describeRoute({
description: "Get a session's children",
operationId: "session.children",
@@ -606,17 +605,17 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: Session.children.schema,
id: Session.children.schema,
}),
),
async (c) => {
const sessionID = c.req.valid("param").sessionID
const sessionID = c.req.valid("param").id
const session = await Session.children(sessionID)
return c.json(session)
},
)
.get(
"/session/:sessionID/todo",
"/session/:id/todo",
describeRoute({
description: "Get the todo list for a session",
operationId: "session.todo",
@@ -635,11 +634,11 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string().meta({ description: "Session ID" }),
id: z.string().meta({ description: "Session ID" }),
}),
),
async (c) => {
const sessionID = c.req.valid("param").sessionID
const sessionID = c.req.valid("param").id
const todos = await Todo.get(sessionID)
return c.json(todos)
},
@@ -669,7 +668,7 @@ export namespace Server {
},
)
.delete(
"/session/:sessionID",
"/session/:id",
describeRoute({
description: "Delete a session and all its data",
operationId: "session.delete",
@@ -688,11 +687,11 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: Session.remove.schema,
id: Session.remove.schema,
}),
),
async (c) => {
const sessionID = c.req.valid("param").sessionID
const sessionID = c.req.valid("param").id
await Session.remove(sessionID)
await Bus.publish(TuiEvent.CommandExecute, {
command: "session.list",
@@ -701,7 +700,7 @@ export namespace Server {
},
)
.patch(
"/session/:sessionID",
"/session/:id",
describeRoute({
description: "Update session properties",
operationId: "session.update",
@@ -720,7 +719,7 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string(),
id: z.string(),
}),
),
validator(
@@ -730,7 +729,7 @@ export namespace Server {
}),
),
async (c) => {
const sessionID = c.req.valid("param").sessionID
const sessionID = c.req.valid("param").id
const updates = c.req.valid("json")
const updatedSession = await Session.update(sessionID, (session) => {
@@ -743,7 +742,7 @@ export namespace Server {
},
)
.post(
"/session/:sessionID/init",
"/session/:id/init",
describeRoute({
description: "Analyze the app and create an AGENTS.md file",
operationId: "session.init",
@@ -762,19 +761,19 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string().meta({ description: "Session ID" }),
id: z.string().meta({ description: "Session ID" }),
}),
),
validator("json", Session.initialize.schema.omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").sessionID
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
await Session.initialize({ ...body, sessionID })
return c.json(true)
},
)
.post(
"/session/:sessionID/fork",
"/session/:id/fork",
describeRoute({
description: "Fork an existing session at a specific message",
operationId: "session.fork",
@@ -792,19 +791,19 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: Session.fork.schema.shape.sessionID,
id: Session.fork.schema.shape.sessionID,
}),
),
validator("json", Session.fork.schema.omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").sessionID
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
const result = await Session.fork({ ...body, sessionID })
return c.json(result)
},
)
.post(
"/session/:sessionID/abort",
"/session/:id/abort",
describeRoute({
description: "Abort a session",
operationId: "session.abort",
@@ -823,16 +822,16 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string(),
id: z.string(),
}),
),
async (c) => {
SessionPrompt.cancel(c.req.valid("param").sessionID)
SessionPrompt.cancel(c.req.valid("param").id)
return c.json(true)
},
)
.post(
"/session/:sessionID/share",
"/session/:id/share",
describeRoute({
description: "Share a session",
operationId: "session.share",
@@ -851,18 +850,18 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string(),
id: z.string(),
}),
),
async (c) => {
const sessionID = c.req.valid("param").sessionID
await Session.share(sessionID)
const session = await Session.get(sessionID)
const id = c.req.valid("param").id
await Session.share(id)
const session = await Session.get(id)
return c.json(session)
},
)
.get(
"/session/:sessionID/diff",
"/session/:id/diff",
describeRoute({
description: "Get the diff that resulted from this user message",
operationId: "session.diff",
@@ -880,7 +879,7 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: SessionSummary.diff.schema.shape.sessionID,
id: SessionSummary.diff.schema.shape.sessionID,
}),
),
validator(
@@ -893,14 +892,14 @@ export namespace Server {
const query = c.req.valid("query")
const params = c.req.valid("param")
const result = await SessionSummary.diff({
sessionID: params.sessionID,
sessionID: params.id,
messageID: query.messageID,
})
return c.json(result)
},
)
.delete(
"/session/:sessionID/share",
"/session/:id/share",
describeRoute({
description: "Unshare the session",
operationId: "session.unshare",
@@ -919,18 +918,18 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: Session.unshare.schema,
id: Session.unshare.schema,
}),
),
async (c) => {
const sessionID = c.req.valid("param").sessionID
await Session.unshare(sessionID)
const session = await Session.get(sessionID)
const id = c.req.valid("param").id
await Session.unshare(id)
const session = await Session.get(id)
return c.json(session)
},
)
.post(
"/session/:sessionID/summarize",
"/session/:id/summarize",
describeRoute({
description: "Summarize the session",
operationId: "session.summarize",
@@ -949,7 +948,7 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string().meta({ description: "Session ID" }),
id: z.string().meta({ description: "Session ID" }),
}),
),
validator(
@@ -960,9 +959,9 @@ export namespace Server {
}),
),
async (c) => {
const sessionID = c.req.valid("param").sessionID
const id = c.req.valid("param").id
const body = c.req.valid("json")
const msgs = await Session.messages({ sessionID })
const msgs = await Session.messages({ sessionID: id })
let currentAgent = "build"
for (let i = msgs.length - 1; i >= 0; i--) {
const info = msgs[i].info
@@ -972,7 +971,7 @@ export namespace Server {
}
}
await SessionCompaction.create({
sessionID,
sessionID: id,
agent: currentAgent,
model: {
providerID: body.providerID,
@@ -980,12 +979,12 @@ export namespace Server {
},
auto: false,
})
await SessionPrompt.loop(sessionID)
await SessionPrompt.loop(id)
return c.json(true)
},
)
.get(
"/session/:sessionID/message",
"/session/:id/message",
describeRoute({
description: "List messages for a session",
operationId: "session.messages",
@@ -1004,7 +1003,7 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string().meta({ description: "Session ID" }),
id: z.string().meta({ description: "Session ID" }),
}),
),
validator(
@@ -1016,14 +1015,14 @@ export namespace Server {
async (c) => {
const query = c.req.valid("query")
const messages = await Session.messages({
sessionID: c.req.valid("param").sessionID,
sessionID: c.req.valid("param").id,
limit: query.limit,
})
return c.json(messages)
},
)
.get(
"/session/:sessionID/diff",
"/session/:id/diff",
describeRoute({
description: "Get the diff for this session",
operationId: "session.diff",
@@ -1042,16 +1041,16 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string().meta({ description: "Session ID" }),
id: z.string().meta({ description: "Session ID" }),
}),
),
async (c) => {
const diff = await Session.diff(c.req.valid("param").sessionID)
const diff = await Session.diff(c.req.valid("param").id)
return c.json(diff)
},
)
.get(
"/session/:sessionID/message/:messageID",
"/session/:id/message/:messageID",
describeRoute({
description: "Get a message from a session",
operationId: "session.message",
@@ -1075,21 +1074,21 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string().meta({ description: "Session ID" }),
id: z.string().meta({ description: "Session ID" }),
messageID: z.string().meta({ description: "Message ID" }),
}),
),
async (c) => {
const params = c.req.valid("param")
const message = await MessageV2.get({
sessionID: params.sessionID,
sessionID: params.id,
messageID: params.messageID,
})
return c.json(message)
},
)
.post(
"/session/:sessionID/message",
"/session/:id/message",
describeRoute({
description: "Create and send a new message to a session",
operationId: "session.prompt",
@@ -1113,7 +1112,7 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string().meta({ description: "Session ID" }),
id: z.string().meta({ description: "Session ID" }),
}),
),
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
@@ -1121,7 +1120,7 @@ export namespace Server {
c.status(200)
c.header("Content-Type", "application/json")
return stream(c, async (stream) => {
const sessionID = c.req.valid("param").sessionID
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
const msg = await SessionPrompt.prompt({ ...body, sessionID })
stream.write(JSON.stringify(msg))
@@ -1129,7 +1128,7 @@ export namespace Server {
},
)
.post(
"/session/:sessionID/prompt_async",
"/session/:id/prompt_async",
describeRoute({
description: "Create and send a new message to a session, start if needed and return immediately",
operationId: "session.prompt_async",
@@ -1143,7 +1142,7 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string().meta({ description: "Session ID" }),
id: z.string().meta({ description: "Session ID" }),
}),
),
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
@@ -1151,14 +1150,14 @@ export namespace Server {
c.status(204)
c.header("Content-Type", "application/json")
return stream(c, async () => {
const sessionID = c.req.valid("param").sessionID
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
SessionPrompt.prompt({ ...body, sessionID })
})
},
)
.post(
"/session/:sessionID/command",
"/session/:id/command",
describeRoute({
description: "Send a new command to a session",
operationId: "session.command",
@@ -1182,19 +1181,19 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string().meta({ description: "Session ID" }),
id: z.string().meta({ description: "Session ID" }),
}),
),
validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").sessionID
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
const msg = await SessionPrompt.command({ ...body, sessionID })
return c.json(msg)
},
)
.post(
"/session/:sessionID/shell",
"/session/:id/shell",
describeRoute({
description: "Run a shell command",
operationId: "session.shell",
@@ -1213,19 +1212,19 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string().meta({ description: "Session ID" }),
id: z.string().meta({ description: "Session ID" }),
}),
),
validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").sessionID
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
const msg = await SessionPrompt.shell({ ...body, sessionID })
return c.json(msg)
},
)
.post(
"/session/:sessionID/revert",
"/session/:id/revert",
describeRoute({
description: "Revert a message",
operationId: "session.revert",
@@ -1244,22 +1243,22 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string(),
id: z.string(),
}),
),
validator("json", SessionRevert.RevertInput.omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").sessionID
const id = c.req.valid("param").id
log.info("revert", c.req.valid("json"))
const session = await SessionRevert.revert({
sessionID,
sessionID: id,
...c.req.valid("json"),
})
return c.json(session)
},
)
.post(
"/session/:sessionID/unrevert",
"/session/:id/unrevert",
describeRoute({
description: "Restore all reverted messages",
operationId: "session.unrevert",
@@ -1278,20 +1277,19 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string(),
id: z.string(),
}),
),
async (c) => {
const sessionID = c.req.valid("param").sessionID
const session = await SessionRevert.unrevert({ sessionID })
const id = c.req.valid("param").id
const session = await SessionRevert.unrevert({ sessionID: id })
return c.json(session)
},
)
.post(
"/session/:sessionID/permissions/:permissionID",
"/session/:id/permissions/:permissionID",
describeRoute({
description: "Respond to a permission request",
operationId: "permission.respond",
responses: {
200: {
description: "Permission processed successfully",
@@ -1307,17 +1305,17 @@ export namespace Server {
validator(
"param",
z.object({
sessionID: z.string(),
id: z.string(),
permissionID: z.string(),
}),
),
validator("json", z.object({ response: Permission.Response })),
async (c) => {
const params = c.req.valid("param")
const sessionID = params.sessionID
const id = params.id
const permissionID = params.permissionID
Permission.respond({
sessionID,
sessionID: id,
permissionID,
response: c.req.valid("json").response,
})
@@ -1431,7 +1429,7 @@ export namespace Server {
},
)
.post(
"/provider/:providerID/oauth/authorize",
"/provider/:id/oauth/authorize",
describeRoute({
description: "Authorize a provider using OAuth",
operationId: "provider.oauth.authorize",
@@ -1450,7 +1448,7 @@ export namespace Server {
validator(
"param",
z.object({
providerID: z.string().meta({ description: "Provider ID" }),
id: z.string().meta({ description: "Provider ID" }),
}),
),
validator(
@@ -1460,17 +1458,17 @@ export namespace Server {
}),
),
async (c) => {
const providerID = c.req.valid("param").providerID
const id = c.req.valid("param").id
const { method } = c.req.valid("json")
const result = await ProviderAuth.authorize({
providerID,
providerID: id,
method,
})
return c.json(result)
},
)
.post(
"/provider/:providerID/oauth/callback",
"/provider/:id/oauth/callback",
describeRoute({
description: "Handle OAuth callback for a provider",
operationId: "provider.oauth.callback",
@@ -1489,7 +1487,7 @@ export namespace Server {
validator(
"param",
z.object({
providerID: z.string().meta({ description: "Provider ID" }),
id: z.string().meta({ description: "Provider ID" }),
}),
),
validator(
@@ -1500,10 +1498,10 @@ export namespace Server {
}),
),
async (c) => {
const providerID = c.req.valid("param").providerID
const id = c.req.valid("param").id
const { method, code } = c.req.valid("json")
await ProviderAuth.callback({
providerID,
providerID: id,
method,
code,
})
@@ -1806,117 +1804,6 @@ export namespace Server {
return c.json(result.status)
},
)
.post(
"/mcp/:name/auth",
describeRoute({
description: "Start OAuth authentication flow for an MCP server",
operationId: "mcp.auth.start",
responses: {
200: {
description: "OAuth flow started",
content: {
"application/json": {
schema: resolver(
z.object({
authorizationUrl: z.string().describe("URL to open in browser for authorization"),
}),
),
},
},
},
...errors(400, 404),
},
}),
async (c) => {
const name = c.req.param("name")
const supportsOAuth = await MCP.supportsOAuth(name)
if (!supportsOAuth) {
return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
}
const result = await MCP.startAuth(name)
return c.json(result)
},
)
.post(
"/mcp/:name/auth/callback",
describeRoute({
description: "Complete OAuth authentication with authorization code",
operationId: "mcp.auth.callback",
responses: {
200: {
description: "OAuth authentication completed",
content: {
"application/json": {
schema: resolver(MCP.Status),
},
},
},
...errors(400, 404),
},
}),
validator(
"json",
z.object({
code: z.string().describe("Authorization code from OAuth callback"),
}),
),
async (c) => {
const name = c.req.param("name")
const { code } = c.req.valid("json")
const status = await MCP.finishAuth(name, code)
return c.json(status)
},
)
.post(
"/mcp/:name/auth/authenticate",
describeRoute({
description: "Start OAuth flow and wait for callback (opens browser)",
operationId: "mcp.auth.authenticate",
responses: {
200: {
description: "OAuth authentication completed",
content: {
"application/json": {
schema: resolver(MCP.Status),
},
},
},
...errors(400, 404),
},
}),
async (c) => {
const name = c.req.param("name")
const supportsOAuth = await MCP.supportsOAuth(name)
if (!supportsOAuth) {
return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
}
const status = await MCP.authenticate(name)
return c.json(status)
},
)
.delete(
"/mcp/:name/auth",
describeRoute({
description: "Remove OAuth credentials for an MCP server",
operationId: "mcp.auth.remove",
responses: {
200: {
description: "OAuth credentials removed",
content: {
"application/json": {
schema: resolver(z.object({ success: z.literal(true) })),
},
},
},
...errors(404),
},
}),
async (c) => {
const name = c.req.param("name")
await MCP.removeAuth(name)
return c.json({ success: true as const })
},
)
.get(
"/lsp",
describeRoute({
@@ -2217,7 +2104,7 @@ export namespace Server {
)
.route("/tui/control", TuiRoute)
.put(
"/auth/:providerID",
"/auth/:id",
describeRoute({
description: "Set authentication credentials",
operationId: "auth.set",
@@ -2236,14 +2123,14 @@ export namespace Server {
validator(
"param",
z.object({
providerID: z.string(),
id: z.string(),
}),
),
validator("json", Auth.Info),
async (c) => {
const providerID = c.req.valid("param").providerID
const id = c.req.valid("param").id
const info = c.req.valid("json")
await Auth.set(providerID, info)
await Auth.set(id, info)
return c.json(true)
},
)

View File

@@ -6,7 +6,7 @@ import { Session } from "@/session"
import { MessageV2 } from "@/session/message-v2"
import { Storage } from "@/storage/storage"
import { Log } from "@/util/log"
import type * as SDK from "@opencode-ai/sdk/v2"
import type * as SDK from "@opencode-ai/sdk"
export namespace ShareNext {
const log = Log.create({ service: "share-next" })

View File

@@ -60,8 +60,7 @@ export const BashTool = Tool.define("bash", async () => {
const shell = iife(() => {
const s = process.env.SHELL
if (s) {
const basename = path.basename(s)
if (!new Set(["fish", "nu"]).has(basename)) {
if (!new Set(["/bin/fish", "/bin/nu", "/usr/bin/fish", "/usr/bin/nu"]).has(s)) {
return s
}
}

View File

@@ -1763,47 +1763,3 @@ test("custom model inherits npm package from models.dev provider config", async
},
})
})
test("custom model inherits api.url from models.dev provider", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
openrouter: {
models: {
"prime-intellect/intellect-3": {},
"deepseek/deepseek-r1-0528": {
name: "DeepSeek R1",
},
},
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
init: async () => {
Env.set("OPENROUTER_API_KEY", "test-api-key")
},
fn: async () => {
const providers = await Provider.list()
expect(providers["openrouter"]).toBeDefined()
// New model not in database should inherit api.url from provider
const intellect = providers["openrouter"].models["prime-intellect/intellect-3"]
expect(intellect).toBeDefined()
expect(intellect.api.url).toBe("https://openrouter.ai/api/v1")
// Another new model should also inherit api.url
const deepseek = providers["openrouter"].models["deepseek/deepseek-r1-0528"]
expect(deepseek).toBeDefined()
expect(deepseek.api.url).toBe("https://openrouter.ai/api/v1")
expect(deepseek.name).toBe("DeepSeek R1")
},
})
})

View File

@@ -0,0 +1,7 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/debian
{
"name": "Development",
"image": "mcr.microsoft.com/devcontainers/go:1.23-bookworm",
"postCreateCommand": "go mod tidy"
}

View File

@@ -0,0 +1,49 @@
name: CI
on:
push:
branches-ignore:
- "generated"
- "codegen/**"
- "integrated/**"
- "stl-preview-head/**"
- "stl-preview-base/**"
pull_request:
branches-ignore:
- "stl-preview-head/**"
- "stl-preview-base/**"
jobs:
lint:
timeout-minutes: 10
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/opencode-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
- name: Setup go
uses: actions/setup-go@v5
with:
go-version-file: ./go.mod
- name: Run lints
run: ./scripts/lint
test:
timeout-minutes: 10
name: test
runs-on: ${{ github.repository == 'stainless-sdks/opencode-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
- name: Setup go
uses: actions/setup-go@v5
with:
go-version-file: ./go.mod
- name: Bootstrap
run: ./scripts/bootstrap
- name: Run tests
run: ./scripts/test

4
packages/sdk/go/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.prism.log
codegen.log
Brewfile.lock.json
.idea/

View File

@@ -0,0 +1,3 @@
{
".": "0.18.0"
}

View File

@@ -0,0 +1,4 @@
configured_endpoints: 43
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-92f9d0f8daee2ea7458f8b9f1d7a7f941ff932442ad944bc7576254d5978b6d5.yml
openapi_spec_hash: 5b785c4ff6fb69039915f0e746abdaf9
config_hash: 026ef000d34bf2f930e7b41e77d2d3ff

1
packages/sdk/go/Brewfile Normal file
View File

@@ -0,0 +1 @@
brew "go"

View File

@@ -0,0 +1,222 @@
# Changelog
## 0.18.0 (2025-10-10)
Full Changelog: [v0.17.0...v0.18.0](https://github.com/sst/opencode-sdk-go/compare/v0.17.0...v0.18.0)
### Features
- **api:** api update ([0a7f5e7](https://github.com/sst/opencode-sdk-go/commit/0a7f5e710911506512a132ba39e0593c412beb77))
## 0.17.0 (2025-10-07)
Full Changelog: [v0.16.2...v0.17.0](https://github.com/sst/opencode-sdk-go/compare/v0.16.2...v0.17.0)
### Features
- **api:** api update ([84a3df5](https://github.com/sst/opencode-sdk-go/commit/84a3df50a7ff3d87e5593e4f29dfb5d561f71cc3))
## 0.16.2 (2025-09-26)
Full Changelog: [v0.16.1...v0.16.2](https://github.com/sst/opencode-sdk-go/compare/v0.16.1...v0.16.2)
### Bug Fixes
- bugfix for setting JSON keys with special characters ([ac9a36f](https://github.com/sst/opencode-sdk-go/commit/ac9a36feb1c185ebf766d76909d0b86ac805e8a6))
## 0.16.1 (2025-09-20)
Full Changelog: [v0.16.0...v0.16.1](https://github.com/sst/opencode-sdk-go/compare/v0.16.0...v0.16.1)
### Bug Fixes
- use slices.Concat instead of sometimes modifying r.Options ([12e8b40](https://github.com/sst/opencode-sdk-go/commit/12e8b40809071095b0abb9b8031686353c8ac149))
### Chores
- bump minimum go version to 1.22 ([1a61c5c](https://github.com/sst/opencode-sdk-go/commit/1a61c5cc7e8f68cc1b0c219738cab530cb6aa3a2))
- do not install brew dependencies in ./scripts/bootstrap by default ([f6d3eaf](https://github.com/sst/opencode-sdk-go/commit/f6d3eafffc20e124bbfae6ac5ddc1b1122ad3e27))
- update more docs for 1.22 ([a3d0b0f](https://github.com/sst/opencode-sdk-go/commit/a3d0b0f26ed92ce1a6433f5bcf37a6436d268ba5))
## 0.16.0 (2025-09-17)
Full Changelog: [v0.15.0...v0.16.0](https://github.com/sst/opencode-sdk-go/compare/v0.15.0...v0.16.0)
### Features
- **api:** api update ([46e978e](https://github.com/sst/opencode-sdk-go/commit/46e978e43aee733d5c1c09dc5be6d8ac2a752427))
## 0.15.0 (2025-09-16)
Full Changelog: [v0.14.0...v0.15.0](https://github.com/sst/opencode-sdk-go/compare/v0.14.0...v0.15.0)
### Features
- **api:** api update ([397048f](https://github.com/sst/opencode-sdk-go/commit/397048faca7a1de7a028edd2254a0ad7797b151f))
## 0.14.0 (2025-09-14)
Full Changelog: [v0.13.0...v0.14.0](https://github.com/sst/opencode-sdk-go/compare/v0.13.0...v0.14.0)
### Features
- **api:** api update ([dad0bc3](https://github.com/sst/opencode-sdk-go/commit/dad0bc3da99f20a0d002a6b94e049fb70f8e6a77))
## 0.13.0 (2025-09-14)
Full Changelog: [v0.12.0...v0.13.0](https://github.com/sst/opencode-sdk-go/compare/v0.12.0...v0.13.0)
### Features
- **api:** api update ([80da4fb](https://github.com/sst/opencode-sdk-go/commit/80da4fb4ea9c6afb51a7e7135d9f5560ce6f2a6c))
## 0.12.0 (2025-09-14)
Full Changelog: [v0.11.0...v0.12.0](https://github.com/sst/opencode-sdk-go/compare/v0.11.0...v0.12.0)
### Features
- **api:** api update ([7e3808b](https://github.com/sst/opencode-sdk-go/commit/7e3808ba349dc653174b32b48a1120c18d2975c2))
## 0.11.0 (2025-09-14)
Full Changelog: [v0.10.0...v0.11.0](https://github.com/sst/opencode-sdk-go/compare/v0.10.0...v0.11.0)
### Features
- **api:** api update ([a3d37f5](https://github.com/sst/opencode-sdk-go/commit/a3d37f5671545866547d351fc21b49809cc8b3c2))
## 0.10.0 (2025-09-11)
Full Changelog: [v0.9.0...v0.10.0](https://github.com/sst/opencode-sdk-go/compare/v0.9.0...v0.10.0)
### Features
- **api:** api update ([0dc01f6](https://github.com/sst/opencode-sdk-go/commit/0dc01f6695c9b8400a4dc92166c5002bb120cf50))
## 0.9.0 (2025-09-10)
Full Changelog: [v0.8.0...v0.9.0](https://github.com/sst/opencode-sdk-go/compare/v0.8.0...v0.9.0)
### Features
- **api:** api update ([2d3a28d](https://github.com/sst/opencode-sdk-go/commit/2d3a28df5657845aa4d73087e1737d1fc8c3ce1c))
## 0.8.0 (2025-09-01)
Full Changelog: [v0.7.0...v0.8.0](https://github.com/sst/opencode-sdk-go/compare/v0.7.0...v0.8.0)
### Features
- **api:** api update ([ae87a71](https://github.com/sst/opencode-sdk-go/commit/ae87a71949994590ace8285a39f0991ef34b664d))
## 0.7.0 (2025-09-01)
Full Changelog: [v0.6.0...v0.7.0](https://github.com/sst/opencode-sdk-go/compare/v0.6.0...v0.7.0)
### Features
- **api:** api update ([64bb1b1](https://github.com/sst/opencode-sdk-go/commit/64bb1b1ee0cbe153abc6fb7bd9703b47911724d4))
## 0.6.0 (2025-09-01)
Full Changelog: [v0.5.0...v0.6.0](https://github.com/sst/opencode-sdk-go/compare/v0.5.0...v0.6.0)
### Features
- **api:** api update ([928e384](https://github.com/sst/opencode-sdk-go/commit/928e3843355f96899f046f002b84372281dad0c8))
## 0.5.0 (2025-08-31)
Full Changelog: [v0.4.0...v0.5.0](https://github.com/sst/opencode-sdk-go/compare/v0.4.0...v0.5.0)
### Features
- **api:** api update ([44b281d](https://github.com/sst/opencode-sdk-go/commit/44b281d0bb39c5022a984ac9d0fca1529ccc0604))
## 0.4.0 (2025-08-31)
Full Changelog: [v0.3.0...v0.4.0](https://github.com/sst/opencode-sdk-go/compare/v0.3.0...v0.4.0)
### Features
- **api:** api update ([fa9d6ec](https://github.com/sst/opencode-sdk-go/commit/fa9d6ec6472e62f4f6605d0a71a7aa8bf8a24559))
## 0.3.0 (2025-08-31)
Full Changelog: [v0.2.0...v0.3.0](https://github.com/sst/opencode-sdk-go/compare/v0.2.0...v0.3.0)
### Features
- **api:** api update ([aae1c06](https://github.com/sst/opencode-sdk-go/commit/aae1c06bb5a93a1cd9c589846a84b3f16246f5da))
## 0.2.0 (2025-08-31)
Full Changelog: [v0.1.0...v0.2.0](https://github.com/sst/opencode-sdk-go/compare/v0.1.0...v0.2.0)
### Features
- **api:** api update ([1472790](https://github.com/sst/opencode-sdk-go/commit/1472790542515f47bd46e2a9e28d8afea024cf9c))
## 0.1.0 (2025-08-31)
Full Changelog: [v0.0.1...v0.1.0](https://github.com/sst/opencode-sdk-go/compare/v0.0.1...v0.1.0)
### Features
- **api:** api update ([3f03ddd](https://github.com/sst/opencode-sdk-go/commit/3f03dddd5ec0de98f99ce48679077dcae9ceffd6))
- **api:** api update ([e9f79c4](https://github.com/sst/opencode-sdk-go/commit/e9f79c4792b21ef64ab0431ffd76f5a71e04d182))
- **api:** api update ([139a686](https://github.com/sst/opencode-sdk-go/commit/139a6862d2f0ab0c8ea791663d736868be3e96e6))
- **api:** api update ([2ed0800](https://github.com/sst/opencode-sdk-go/commit/2ed0800b2c5b99877e9f7fde669a6c005fad6b77))
- **api:** api update ([88a87a4](https://github.com/sst/opencode-sdk-go/commit/88a87a458f56ce0c18b502c73da933f614f56e8b))
- **api:** api update ([0e5d65b](https://github.com/sst/opencode-sdk-go/commit/0e5d65b571e7b30dc6347e6730098878ebba3a42))
- **api:** api update ([ba381f1](https://github.com/sst/opencode-sdk-go/commit/ba381f1e07aad24e9824df7d53befae2a644f69f))
- **api:** api update ([3f429f5](https://github.com/sst/opencode-sdk-go/commit/3f429f5b4be5607433ef5fdc0d5bf67fe590d039))
- **api:** api update ([9f34787](https://github.com/sst/opencode-sdk-go/commit/9f347876b35b7f898060c1a5f71c322e95978e3e))
- **api:** api update ([379c8e0](https://github.com/sst/opencode-sdk-go/commit/379c8e00197e13aebaf2f2d61277b125f1f90011))
- **api:** api update ([550511c](https://github.com/sst/opencode-sdk-go/commit/550511c4c5b5055ac8ff22b7b11731331bd9d088))
- **api:** api update ([547f0c2](https://github.com/sst/opencode-sdk-go/commit/547f0c262f2df1ce83eaa7267d68be64bb29b841))
- **api:** api update ([b7b0720](https://github.com/sst/opencode-sdk-go/commit/b7b07204bff314da24b1819c128835a43ef64065))
- **api:** api update ([7250ffc](https://github.com/sst/opencode-sdk-go/commit/7250ffcba262b916c958ddecc2a42927982db39f))
- **api:** api update ([17fbab7](https://github.com/sst/opencode-sdk-go/commit/17fbab73111a3eae488737c69b12370bc69c65f7))
- **api:** api update ([1270b5c](https://github.com/sst/opencode-sdk-go/commit/1270b5cd81e6ac769dcd92ade6d877891bf51bd5))
- **api:** api update ([a238d4a](https://github.com/sst/opencode-sdk-go/commit/a238d4abd6ed7d15f3547d27a4b6ecf4aec8431e))
- **api:** api update ([7475655](https://github.com/sst/opencode-sdk-go/commit/7475655aca577fe4f807c2f02f92171f6a358e9c))
- **api:** api update ([429d258](https://github.com/sst/opencode-sdk-go/commit/429d258bb56e9cdeb1528be3944bf5537ac26a96))
- **api:** api update ([f250915](https://github.com/sst/opencode-sdk-go/commit/f2509157eaf1b453e741ee9482127cad2e3ace25))
- **api:** api update ([5efc987](https://github.com/sst/opencode-sdk-go/commit/5efc987353801d1e772c20edf162b1c75da32743))
- **api:** api update ([98a8350](https://github.com/sst/opencode-sdk-go/commit/98a83504f7cfc361e83314c3e79a4e9ff53f0560))
- **api:** api update ([6da8bf8](https://github.com/sst/opencode-sdk-go/commit/6da8bf8bfe91d45991fb580753d77c5534fc0b1b))
- **api:** api update ([f8c7148](https://github.com/sst/opencode-sdk-go/commit/f8c7148ae56143823186e2675a78e82676154956))
- **api:** manual updates ([7cf038f](https://github.com/sst/opencode-sdk-go/commit/7cf038ffae5da1b77e1cef11b5fa166a53b467f2))
- **api:** update via SDK Studio ([068a0eb](https://github.com/sst/opencode-sdk-go/commit/068a0eb025010da0c8d86fa1bb496a39dbedcef9))
- **api:** update via SDK Studio ([ca651ed](https://github.com/sst/opencode-sdk-go/commit/ca651edaf71d1f3678f929287474f5bc4f1aad10))
- **api:** update via SDK Studio ([13550a5](https://github.com/sst/opencode-sdk-go/commit/13550a5c65d77325e945ed99fe0799cd1107b775))
- **api:** update via SDK Studio ([7b73730](https://github.com/sst/opencode-sdk-go/commit/7b73730c7fa62ba966dda3541c3e97b49be8d2bf))
- **api:** update via SDK Studio ([9e39a59](https://github.com/sst/opencode-sdk-go/commit/9e39a59b3d5d1bd5e64633732521fb28362cc70e))
- **api:** update via SDK Studio ([9609d1b](https://github.com/sst/opencode-sdk-go/commit/9609d1b1db7806d00cb846c9914cb4935cdedf52))
- **api:** update via SDK Studio ([51315fa](https://github.com/sst/opencode-sdk-go/commit/51315fa2eae424743ea79701e67d44447c44144d))
- **api:** update via SDK Studio ([af07955](https://github.com/sst/opencode-sdk-go/commit/af0795543240aefaf04fc7663a348825541c79ed))
- **api:** update via SDK Studio ([5e3468a](https://github.com/sst/opencode-sdk-go/commit/5e3468a0aaa5ed3b13e019c3a24e0ba9147d1675))
- **api:** update via SDK Studio ([0a73e04](https://github.com/sst/opencode-sdk-go/commit/0a73e04c23c90b2061611edaa8fd6282dc0ce397))
- **api:** update via SDK Studio ([9b7883a](https://github.com/sst/opencode-sdk-go/commit/9b7883a144eeac526d9d04538e0876a9d18bb844))
- **client:** expand max streaming buffer size ([76303e5](https://github.com/sst/opencode-sdk-go/commit/76303e51067e78e732af26ced9d83b8bad7655c3))
- **client:** support optional json html escaping ([449748f](https://github.com/sst/opencode-sdk-go/commit/449748f35a1d8cb6f91dc36d25bf9489f4f371bd))
### Bug Fixes
- **client:** process custom base url ahead of time ([9b360d6](https://github.com/sst/opencode-sdk-go/commit/9b360d642cf6f302104308af5622e17099899e5f))
- **client:** resolve lint errors in streaming tests ([4d36cb0](https://github.com/sst/opencode-sdk-go/commit/4d36cb09fc9d436734d5dab1c499acaa88568ff7))
- close body before retrying ([4da3f7f](https://github.com/sst/opencode-sdk-go/commit/4da3f7f372bad222a189ba3eabcfde3373166ae5))
- don't try to deserialize as json when ResponseBodyInto is []byte ([595291f](https://github.com/sst/opencode-sdk-go/commit/595291f6dba6af472f160b9f8e3d145002f43a4a))
### Chores
- **ci:** only run for pushes and fork pull requests ([bea59b8](https://github.com/sst/opencode-sdk-go/commit/bea59b886800ef555f89c47a9256d6392ed2e53d))
- **internal:** codegen related update ([6a22ce6](https://github.com/sst/opencode-sdk-go/commit/6a22ce6df155f5003e80b8a75686a9e513a5568a))
- **internal:** fix lint script for tests ([391c482](https://github.com/sst/opencode-sdk-go/commit/391c482148ed0a77c4ad52807abeb2d540b56797))
- **internal:** update comment in script ([b7f1c3e](https://github.com/sst/opencode-sdk-go/commit/b7f1c3e16935c71e243004b8f321d661cd8e9474))
- lint tests ([616796b](https://github.com/sst/opencode-sdk-go/commit/616796b761704bde6be5c6c2428f28c79c7f05ff))
- lint tests in subpackages ([50c82ff](https://github.com/sst/opencode-sdk-go/commit/50c82ff0757c973834b68adc22566b70f767b611))
- sync repo ([2f34d5d](https://github.com/sst/opencode-sdk-go/commit/2f34d5d53e56e9cdc3df99be7ee7efc83dd977a3))
- update @stainless-api/prism-cli to v5.15.0 ([2f24852](https://github.com/sst/opencode-sdk-go/commit/2f2485216d4f4891d1fbfbc23ff8410c2f35152a))

View File

@@ -0,0 +1,66 @@
## Setting up the environment
To set up the repository, run:
```sh
$ ./scripts/bootstrap
$ ./scripts/build
```
This will install all the required dependencies and build the SDK.
You can also [install go 1.22+ manually](https://go.dev/doc/install).
## Modifying/Adding code
Most of the SDK is generated code. Modifications to code will be persisted between generations, but may
result in merge conflicts between manual patches and changes from the generator. The generator will never
modify the contents of the `lib/` and `examples/` directories.
## Adding and running examples
All files in the `examples/` directory are not modified by the generator and can be freely edited or added to.
```go
# add an example to examples/<your-example>/main.go
package main
func main() {
// ...
}
```
```sh
$ go run ./examples/<your-example>
```
## Using the repository from source
To use a local version of this library from source in another project, edit the `go.mod` with a replace
directive. This can be done through the CLI with the following:
```sh
$ go mod edit -replace github.com/sst/opencode-sdk-go=/path/to/opencode-sdk-go
```
## Running tests
Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests.
```sh
# you will need npm installed
$ npx prism mock path/to/your/openapi.yml
```
```sh
$ ./scripts/test
```
## Formatting
This library uses the standard gofmt code formatter:
```sh
$ ./scripts/format
```

7
packages/sdk/go/LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2025 opencode
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

363
packages/sdk/go/README.md Normal file
View File

@@ -0,0 +1,363 @@
# Opencode Go API Library
<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go"><img src="https://pkg.go.dev/badge/github.com/sst/opencode-sdk-go.svg" alt="Go Reference"></a>
The Opencode Go library provides convenient access to the [Opencode REST API](https://opencode.ai/docs)
from applications written in Go.
It is generated with [Stainless](https://www.stainless.com/).
## Installation
<!-- x-release-please-start-version -->
```go
import (
"github.com/sst/opencode-sdk-go" // imported as opencode
)
```
<!-- x-release-please-end -->
Or to pin the version:
<!-- x-release-please-start-version -->
```sh
go get -u 'github.com/sst/opencode-sdk-go@v0.18.0'
```
<!-- x-release-please-end -->
## Requirements
This library requires Go 1.22+.
## Usage
The full API of this library can be found in [api.md](api.md).
```go
package main
import (
"context"
"fmt"
"github.com/sst/opencode-sdk-go"
)
func main() {
client := opencode.NewClient()
sessions, err := client.Session.List(context.TODO(), opencode.SessionListParams{})
if err != nil {
panic(err.Error())
}
fmt.Printf("%+v\n", sessions)
}
```
### Request fields
All request parameters are wrapped in a generic `Field` type,
which we use to distinguish zero values from null or omitted fields.
This prevents accidentally sending a zero value if you forget a required parameter,
and enables explicitly sending `null`, `false`, `''`, or `0` on optional parameters.
Any field not specified is not sent.
To construct fields with values, use the helpers `String()`, `Int()`, `Float()`, or most commonly, the generic `F[T]()`.
To send a null, use `Null[T]()`, and to send a nonconforming value, use `Raw[T](any)`. For example:
```go
params := FooParams{
Name: opencode.F("hello"),
// Explicitly send `"description": null`
Description: opencode.Null[string](),
Point: opencode.F(opencode.Point{
X: opencode.Int(0),
Y: opencode.Int(1),
// In cases where the API specifies a given type,
// but you want to send something else, use `Raw`:
Z: opencode.Raw[int64](0.01), // sends a float
}),
}
```
### Response objects
All fields in response structs are value types (not pointers or wrappers).
If a given field is `null`, not present, or invalid, the corresponding field
will simply be its zero value.
All response structs also include a special `JSON` field, containing more detailed
information about each property, which you can use like so:
```go
if res.Name == "" {
// true if `"name"` is either not present or explicitly null
res.JSON.Name.IsNull()
// true if the `"name"` key was not present in the response JSON at all
res.JSON.Name.IsMissing()
// When the API returns data that cannot be coerced to the expected type:
if res.JSON.Name.IsInvalid() {
raw := res.JSON.Name.Raw()
legacyName := struct{
First string `json:"first"`
Last string `json:"last"`
}{}
json.Unmarshal([]byte(raw), &legacyName)
name = legacyName.First + " " + legacyName.Last
}
}
```
These `.JSON` structs also include an `Extras` map containing
any properties in the json response that were not specified
in the struct. This can be useful for API features not yet
present in the SDK.
```go
body := res.JSON.ExtraFields["my_unexpected_field"].Raw()
```
### RequestOptions
This library uses the functional options pattern. Functions defined in the
`option` package return a `RequestOption`, which is a closure that mutates a
`RequestConfig`. These options can be supplied to the client or at individual
requests. For example:
```go
client := opencode.NewClient(
// Adds a header to every request made by the client
option.WithHeader("X-Some-Header", "custom_header_info"),
)
client.Session.List(context.TODO(), ...,
// Override the header
option.WithHeader("X-Some-Header", "some_other_custom_header_info"),
// Add an undocumented field to the request body, using sjson syntax
option.WithJSONSet("some.json.path", map[string]string{"my": "object"}),
)
```
See the [full list of request options](https://pkg.go.dev/github.com/sst/opencode-sdk-go/option).
### Pagination
This library provides some conveniences for working with paginated list endpoints.
You can use `.ListAutoPaging()` methods to iterate through items across all pages:
Or you can use simple `.List()` methods to fetch a single page and receive a standard response object
with additional helper methods like `.GetNextPage()`, e.g.:
### Errors
When the API returns a non-success status code, we return an error with type
`*opencode.Error`. This contains the `StatusCode`, `*http.Request`, and
`*http.Response` values of the request, as well as the JSON of the error body
(much like other response objects in the SDK).
To handle errors, we recommend that you use the `errors.As` pattern:
```go
_, err := client.Session.List(context.TODO(), opencode.SessionListParams{})
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
println(string(apierr.DumpRequest(true))) // Prints the serialized HTTP request
println(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response
}
panic(err.Error()) // GET "/session": 400 Bad Request { ... }
}
```
When other errors occur, they are returned unwrapped; for example,
if HTTP transport fails, you might receive `*url.Error` wrapping `*net.OpError`.
### Timeouts
Requests do not time out by default; use context to configure a timeout for a request lifecycle.
Note that if a request is [retried](#retries), the context timeout does not start over.
To set a per-retry timeout, use `option.WithRequestTimeout()`.
```go
// This sets the timeout for the request, including all the retries.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
client.Session.List(
ctx,
opencode.SessionListParams{},
// This sets the per-retry timeout
option.WithRequestTimeout(20*time.Second),
)
```
### File uploads
Request parameters that correspond to file uploads in multipart requests are typed as
`param.Field[io.Reader]`. The contents of the `io.Reader` will by default be sent as a multipart form
part with the file name of "anonymous_file" and content-type of "application/octet-stream".
The file name and content-type can be customized by implementing `Name() string` or `ContentType()
string` on the run-time type of `io.Reader`. Note that `os.File` implements `Name() string`, so a
file returned by `os.Open` will be sent with the file name on disk.
We also provide a helper `opencode.FileParam(reader io.Reader, filename string, contentType string)`
which can be used to wrap any `io.Reader` with the appropriate file name and content type.
### Retries
Certain errors will be automatically retried 2 times by default, with a short exponential backoff.
We retry by default all connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit,
and >=500 Internal errors.
You can use the `WithMaxRetries` option to configure or disable this:
```go
// Configure the default for all requests:
client := opencode.NewClient(
option.WithMaxRetries(0), // default is 2
)
// Override per-request:
client.Session.List(
context.TODO(),
opencode.SessionListParams{},
option.WithMaxRetries(5),
)
```
### Accessing raw response data (e.g. response headers)
You can access the raw HTTP response data by using the `option.WithResponseInto()` request option. This is useful when
you need to examine response headers, status codes, or other details.
```go
// Create a variable to store the HTTP response
var response *http.Response
sessions, err := client.Session.List(
context.TODO(),
opencode.SessionListParams{},
option.WithResponseInto(&response),
)
if err != nil {
// handle error
}
fmt.Printf("%+v\n", sessions)
fmt.Printf("Status Code: %d\n", response.StatusCode)
fmt.Printf("Headers: %+#v\n", response.Header)
```
### Making custom/undocumented requests
This library is typed for convenient access to the documented API. If you need to access undocumented
endpoints, params, or response properties, the library can still be used.
#### Undocumented endpoints
To make requests to undocumented endpoints, you can use `client.Get`, `client.Post`, and other HTTP verbs.
`RequestOptions` on the client, such as retries, will be respected when making these requests.
```go
var (
// params can be an io.Reader, a []byte, an encoding/json serializable object,
// or a "…Params" struct defined in this library.
params map[string]interface{}
// result can be an []byte, *http.Response, a encoding/json deserializable object,
// or a model defined in this library.
result *http.Response
)
err := client.Post(context.Background(), "/unspecified", params, &result)
if err != nil {
}
```
#### Undocumented request params
To make requests using undocumented parameters, you may use either the `option.WithQuerySet()`
or the `option.WithJSONSet()` methods.
```go
params := FooNewParams{
ID: opencode.F("id_xxxx"),
Data: opencode.F(FooNewParamsData{
FirstName: opencode.F("John"),
}),
}
client.Foo.New(context.Background(), params, option.WithJSONSet("data.last_name", "Doe"))
```
#### Undocumented response properties
To access undocumented response properties, you may either access the raw JSON of the response as a string
with `result.JSON.RawJSON()`, or get the raw JSON of a particular field on the result with
`result.JSON.Foo.Raw()`.
Any fields that are not present on the response struct will be saved and can be accessed by `result.JSON.ExtraFields()` which returns the extra fields as a `map[string]Field`.
### Middleware
We provide `option.WithMiddleware` which applies the given
middleware to requests.
```go
func Logger(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) {
// Before the request
start := time.Now()
LogReq(req)
// Forward the request to the next handler
res, err = next(req)
// Handle stuff after the request
end := time.Now()
LogRes(res, err, start - end)
return res, err
}
client := opencode.NewClient(
option.WithMiddleware(Logger),
)
```
When multiple middlewares are provided as variadic arguments, the middlewares
are applied left to right. If `option.WithMiddleware` is given
multiple times, for example first in the client then the method, the
middleware in the client will run first and the middleware given in the method
will run next.
You may also replace the default `http.Client` with
`option.WithHTTPClient(client)`. Only one http client is
accepted (this overwrites any previous client) and receives requests after any
middleware has been applied.
## Semantic versioning
This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_
2. Changes that we do not expect to impact the vast majority of users in practice.
We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
We are keen for your feedback; please open an [issue](https://www.github.com/sst/opencode-sdk-go/issues) with questions, bugs, or suggestions.
## Contributing
See [the contributing documentation](./CONTRIBUTING.md).

View File

@@ -0,0 +1,27 @@
# Security Policy
## Reporting Security Issues
This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
To report a security issue, please contact the Stainless team at security@stainless.com.
## Responsible Disclosure
We appreciate the efforts of security researchers and individuals who help us maintain the security of
SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible
disclosure practices by allowing us a reasonable amount of time to investigate and address the issue
before making any information public.
## Reporting Non-SDK Related Security Issues
If you encounter security issues that are not directly related to SDKs but pertain to the services
or products provided by Opencode, please follow the respective company's security reporting guidelines.
### Opencode Terms and Policies
Please contact support@sst.dev for any questions or concerns regarding the security of our services.
---
Thank you for helping us keep the SDKs and systems they interact with secure.

207
packages/sdk/go/agent.go Normal file
View File

@@ -0,0 +1,207 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode
import (
"context"
"net/http"
"net/url"
"slices"
"github.com/sst/opencode-sdk-go/internal/apijson"
"github.com/sst/opencode-sdk-go/internal/apiquery"
"github.com/sst/opencode-sdk-go/internal/param"
"github.com/sst/opencode-sdk-go/internal/requestconfig"
"github.com/sst/opencode-sdk-go/option"
)
// AgentService contains methods and other services that help with interacting with
// the opencode API.
//
// Note, unlike clients, this service does not read variables from the environment
// automatically. You should not instantiate this service directly, and instead use
// the [NewAgentService] method instead.
type AgentService struct {
Options []option.RequestOption
}
// NewAgentService generates a new service that applies the given options to each
// request. These options are applied after the parent client's options (if there
// is one), and before any request-specific options.
func NewAgentService(opts ...option.RequestOption) (r *AgentService) {
r = &AgentService{}
r.Options = opts
return
}
// List all agents
func (r *AgentService) List(ctx context.Context, query AgentListParams, opts ...option.RequestOption) (res *[]Agent, err error) {
opts = slices.Concat(r.Options, opts)
path := "agent"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
return
}
type Agent struct {
BuiltIn bool `json:"builtIn,required"`
Mode AgentMode `json:"mode,required"`
Name string `json:"name,required"`
Options map[string]interface{} `json:"options,required"`
Permission AgentPermission `json:"permission,required"`
Tools map[string]bool `json:"tools,required"`
Color string `json:"color"`
Description string `json:"description"`
Model AgentModel `json:"model"`
Prompt string `json:"prompt"`
Temperature float64 `json:"temperature"`
TopP float64 `json:"topP"`
JSON agentJSON `json:"-"`
}
// agentJSON contains the JSON metadata for the struct [Agent]
type agentJSON struct {
BuiltIn apijson.Field
Mode apijson.Field
Name apijson.Field
Options apijson.Field
Permission apijson.Field
Tools apijson.Field
Color apijson.Field
Description apijson.Field
Model apijson.Field
Prompt apijson.Field
Temperature apijson.Field
TopP apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *Agent) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r agentJSON) RawJSON() string {
return r.raw
}
type AgentMode string
const (
AgentModeSubagent AgentMode = "subagent"
AgentModePrimary AgentMode = "primary"
AgentModeAll AgentMode = "all"
)
func (r AgentMode) IsKnown() bool {
switch r {
case AgentModeSubagent, AgentModePrimary, AgentModeAll:
return true
}
return false
}
type AgentPermission struct {
Bash map[string]AgentPermissionBash `json:"bash,required"`
Edit AgentPermissionEdit `json:"edit,required"`
Webfetch AgentPermissionWebfetch `json:"webfetch"`
JSON agentPermissionJSON `json:"-"`
}
// agentPermissionJSON contains the JSON metadata for the struct [AgentPermission]
type agentPermissionJSON struct {
Bash apijson.Field
Edit apijson.Field
Webfetch apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *AgentPermission) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r agentPermissionJSON) RawJSON() string {
return r.raw
}
type AgentPermissionBash string
const (
AgentPermissionBashAsk AgentPermissionBash = "ask"
AgentPermissionBashAllow AgentPermissionBash = "allow"
AgentPermissionBashDeny AgentPermissionBash = "deny"
)
func (r AgentPermissionBash) IsKnown() bool {
switch r {
case AgentPermissionBashAsk, AgentPermissionBashAllow, AgentPermissionBashDeny:
return true
}
return false
}
type AgentPermissionEdit string
const (
AgentPermissionEditAsk AgentPermissionEdit = "ask"
AgentPermissionEditAllow AgentPermissionEdit = "allow"
AgentPermissionEditDeny AgentPermissionEdit = "deny"
)
func (r AgentPermissionEdit) IsKnown() bool {
switch r {
case AgentPermissionEditAsk, AgentPermissionEditAllow, AgentPermissionEditDeny:
return true
}
return false
}
type AgentPermissionWebfetch string
const (
AgentPermissionWebfetchAsk AgentPermissionWebfetch = "ask"
AgentPermissionWebfetchAllow AgentPermissionWebfetch = "allow"
AgentPermissionWebfetchDeny AgentPermissionWebfetch = "deny"
)
func (r AgentPermissionWebfetch) IsKnown() bool {
switch r {
case AgentPermissionWebfetchAsk, AgentPermissionWebfetchAllow, AgentPermissionWebfetchDeny:
return true
}
return false
}
type AgentModel struct {
ModelID string `json:"modelID,required"`
ProviderID string `json:"providerID,required"`
JSON agentModelJSON `json:"-"`
}
// agentModelJSON contains the JSON metadata for the struct [AgentModel]
type agentModelJSON struct {
ModelID apijson.Field
ProviderID apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *AgentModel) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r agentModelJSON) RawJSON() string {
return r.raw
}
type AgentListParams struct {
Directory param.Field[string] `query:"directory"`
}
// URLQuery serializes [AgentListParams]'s query parameters as `url.Values`.
func (r AgentListParams) URLQuery() (v url.Values) {
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
ArrayFormat: apiquery.ArrayQueryFormatComma,
NestedFormat: apiquery.NestedQueryFormatBrackets,
})
}

View File

@@ -0,0 +1,38 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode_test
import (
"context"
"errors"
"os"
"testing"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode-sdk-go/internal/testutil"
"github.com/sst/opencode-sdk-go/option"
)
func TestAgentListWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
baseURL = envURL
}
if !testutil.CheckTestServer(t, baseURL) {
return
}
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
_, err := client.Agent.List(context.TODO(), opencode.AgentListParams{
Directory: opencode.F("directory"),
})
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
t.Log(string(apierr.DumpRequest(true)))
}
t.Fatalf("err should be nil: %s", err.Error())
}
}

View File

@@ -0,0 +1,46 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode
import (
"github.com/sst/opencode-sdk-go/internal/apierror"
"github.com/sst/opencode-sdk-go/shared"
)
type Error = apierror.Error
// This is an alias to an internal type.
type MessageAbortedError = shared.MessageAbortedError
// This is an alias to an internal type.
type MessageAbortedErrorData = shared.MessageAbortedErrorData
// This is an alias to an internal type.
type MessageAbortedErrorName = shared.MessageAbortedErrorName
// This is an alias to an internal value.
const MessageAbortedErrorNameMessageAbortedError = shared.MessageAbortedErrorNameMessageAbortedError
// This is an alias to an internal type.
type ProviderAuthError = shared.ProviderAuthError
// This is an alias to an internal type.
type ProviderAuthErrorData = shared.ProviderAuthErrorData
// This is an alias to an internal type.
type ProviderAuthErrorName = shared.ProviderAuthErrorName
// This is an alias to an internal value.
const ProviderAuthErrorNameProviderAuthError = shared.ProviderAuthErrorNameProviderAuthError
// This is an alias to an internal type.
type UnknownError = shared.UnknownError
// This is an alias to an internal type.
type UnknownErrorData = shared.UnknownErrorData
// This is an alias to an internal type.
type UnknownErrorName = shared.UnknownErrorName
// This is an alias to an internal value.
const UnknownErrorNameUnknownError = shared.UnknownErrorNameUnknownError

194
packages/sdk/go/api.md Normal file
View File

@@ -0,0 +1,194 @@
# Shared Response Types
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared">shared</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared#MessageAbortedError">MessageAbortedError</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared">shared</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared#ProviderAuthError">ProviderAuthError</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared">shared</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared#UnknownError">UnknownError</a>
# Event
Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#EventListResponse">EventListResponse</a>
Methods:
- <code title="get /event">client.Event.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#EventService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#EventListParams">EventListParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#EventListResponse">EventListResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# Path
Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Path">Path</a>
Methods:
- <code title="get /path">client.Path.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#PathService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#PathGetParams">PathGetParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Path">Path</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# App
Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Model">Model</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Provider">Provider</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppProvidersResponse">AppProvidersResponse</a>
Methods:
- <code title="post /log">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Log">Log</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppLogParams">AppLogParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /config/providers">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Providers">Providers</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppProvidersParams">AppProvidersParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppProvidersResponse">AppProvidersResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# Agent
Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Agent">Agent</a>
Methods:
- <code title="get /agent">client.Agent.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AgentService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AgentListParams">AgentListParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Agent">Agent</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# Find
Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Symbol">Symbol</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextResponse">FindTextResponse</a>
Methods:
- <code title="get /find/file">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Files">Files</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindFilesParams">FindFilesParams</a>) ([]<a href="https://pkg.go.dev/builtin#string">string</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /find/symbol">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Symbols">Symbols</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindSymbolsParams">FindSymbolsParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Symbol">Symbol</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /find">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Text">Text</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextParams">FindTextParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextResponse">FindTextResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# File
Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#File">File</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileNode">FileNode</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadResponse">FileReadResponse</a>
Methods:
- <code title="get /file">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileListParams">FileListParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileNode">FileNode</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /file/content">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.Read">Read</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadParams">FileReadParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadResponse">FileReadResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /file/status">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.Status">Status</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileStatusParams">FileStatusParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#File">File</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# Config
Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Config">Config</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#KeybindsConfig">KeybindsConfig</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#McpLocalConfig">McpLocalConfig</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#McpRemoteConfig">McpRemoteConfig</a>
Methods:
- <code title="get /config">client.Config.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigGetParams">ConfigGetParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Config">Config</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# Command
Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Command">Command</a>
Methods:
- <code title="get /command">client.Command.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#CommandService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#CommandListParams">CommandListParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Command">Command</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# Project
Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Project">Project</a>
Methods:
- <code title="get /project">client.Project.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ProjectService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ProjectListParams">ProjectListParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Project">Project</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /project/current">client.Project.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ProjectService.Current">Current</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ProjectCurrentParams">ProjectCurrentParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Project">Project</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# Session
Params Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AgentPartInputParam">AgentPartInputParam</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartInputParam">FilePartInputParam</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSourceUnionParam">FilePartSourceUnionParam</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSourceTextParam">FilePartSourceTextParam</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileSourceParam">FileSourceParam</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SymbolSourceParam">SymbolSourceParam</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPartInputParam">TextPartInputParam</a>
Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AgentPart">AgentPart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePart">FilePart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSource">FilePartSource</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSourceText">FilePartSourceText</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileSource">FileSource</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Message">Message</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Part">Part</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ReasoningPart">ReasoningPart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SnapshotPart">SnapshotPart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepFinishPart">StepFinishPart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepStartPart">StepStartPart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SymbolSource">SymbolSource</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPart">TextPart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolPart">ToolPart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateCompleted">ToolStateCompleted</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateError">ToolStateError</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStatePending">ToolStatePending</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateRunning">ToolStateRunning</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessage">UserMessage</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandResponse">SessionCommandResponse</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionPromptResponse">SessionPromptResponse</a>
Methods:
- <code title="post /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.New">New</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionNewParams">SessionNewParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="patch /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Update">Update</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionUpdateParams">SessionUpdateParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionListParams">SessionListParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="delete /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionDeleteParams">SessionDeleteParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionAbortParams">SessionAbortParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session/{id}/children">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Children">Children</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChildrenParams">SessionChildrenParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/command">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Command">Command</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandParams">SessionCommandParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandResponse">SessionCommandResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionGetParams">SessionGetParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session/{id}/message/{messageID}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Message">Message</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, messageID <a href="https://pkg.go.dev/builtin#string">string</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageParams">SessionMessageParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesParams">SessionMessagesParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Prompt">Prompt</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionPromptParams">SessionPromptParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionPromptResponse">SessionPromptResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/revert">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Revert">Revert</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionRevertParams">SessionRevertParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionShareParams">SessionShareParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/shell">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Shell">Shell</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionShellParams">SessionShellParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/summarize">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Summarize">Summarize</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionSummarizeParams">SessionSummarizeParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/unrevert">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unrevert">Unrevert</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionUnrevertParams">SessionUnrevertParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="delete /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unshare">Unshare</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionUnshareParams">SessionUnshareParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
## Permissions
Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Permission">Permission</a>
Methods:
- <code title="post /session/{id}/permissions/{permissionID}">client.Session.Permissions.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionPermissionService.Respond">Respond</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, permissionID <a href="https://pkg.go.dev/builtin#string">string</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionPermissionRespondParams">SessionPermissionRespondParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# Tui
Methods:
- <code title="post /tui/append-prompt">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.AppendPrompt">AppendPrompt</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiAppendPromptParams">TuiAppendPromptParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /tui/clear-prompt">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.ClearPrompt">ClearPrompt</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiClearPromptParams">TuiClearPromptParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /tui/execute-command">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.ExecuteCommand">ExecuteCommand</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiExecuteCommandParams">TuiExecuteCommandParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /tui/open-help">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenHelp">OpenHelp</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiOpenHelpParams">TuiOpenHelpParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /tui/open-models">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenModels">OpenModels</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiOpenModelsParams">TuiOpenModelsParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /tui/open-sessions">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenSessions">OpenSessions</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiOpenSessionsParams">TuiOpenSessionsParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /tui/open-themes">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenThemes">OpenThemes</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiOpenThemesParams">TuiOpenThemesParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /tui/show-toast">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.ShowToast">ShowToast</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, params <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiShowToastParams">TuiShowToastParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /tui/submit-prompt">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.SubmitPrompt">SubmitPrompt</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiSubmitPromptParams">TuiSubmitPromptParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>

345
packages/sdk/go/app.go Normal file
View File

@@ -0,0 +1,345 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode
import (
"context"
"net/http"
"net/url"
"slices"
"github.com/sst/opencode-sdk-go/internal/apijson"
"github.com/sst/opencode-sdk-go/internal/apiquery"
"github.com/sst/opencode-sdk-go/internal/param"
"github.com/sst/opencode-sdk-go/internal/requestconfig"
"github.com/sst/opencode-sdk-go/option"
)
// AppService contains methods and other services that help with interacting with
// the opencode API.
//
// Note, unlike clients, this service does not read variables from the environment
// automatically. You should not instantiate this service directly, and instead use
// the [NewAppService] method instead.
type AppService struct {
Options []option.RequestOption
}
// NewAppService generates a new service that applies the given options to each
// request. These options are applied after the parent client's options (if there
// is one), and before any request-specific options.
func NewAppService(opts ...option.RequestOption) (r *AppService) {
r = &AppService{}
r.Options = opts
return
}
// Write a log entry to the server logs
func (r *AppService) Log(ctx context.Context, params AppLogParams, opts ...option.RequestOption) (res *bool, err error) {
opts = slices.Concat(r.Options, opts)
path := "log"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...)
return
}
// List all providers
func (r *AppService) Providers(ctx context.Context, query AppProvidersParams, opts ...option.RequestOption) (res *AppProvidersResponse, err error) {
opts = slices.Concat(r.Options, opts)
path := "config/providers"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
return
}
type Model struct {
ID string `json:"id,required"`
Attachment bool `json:"attachment,required"`
Cost ModelCost `json:"cost,required"`
Limit ModelLimit `json:"limit,required"`
Name string `json:"name,required"`
Options map[string]interface{} `json:"options,required"`
Reasoning bool `json:"reasoning,required"`
ReleaseDate string `json:"release_date,required"`
Temperature bool `json:"temperature,required"`
ToolCall bool `json:"tool_call,required"`
Experimental bool `json:"experimental"`
Modalities ModelModalities `json:"modalities"`
Provider ModelProvider `json:"provider"`
Status ModelStatus `json:"status"`
JSON modelJSON `json:"-"`
}
// modelJSON contains the JSON metadata for the struct [Model]
type modelJSON struct {
ID apijson.Field
Attachment apijson.Field
Cost apijson.Field
Limit apijson.Field
Name apijson.Field
Options apijson.Field
Reasoning apijson.Field
ReleaseDate apijson.Field
Temperature apijson.Field
ToolCall apijson.Field
Experimental apijson.Field
Modalities apijson.Field
Provider apijson.Field
Status apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *Model) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r modelJSON) RawJSON() string {
return r.raw
}
type ModelCost struct {
Input float64 `json:"input,required"`
Output float64 `json:"output,required"`
CacheRead float64 `json:"cache_read"`
CacheWrite float64 `json:"cache_write"`
JSON modelCostJSON `json:"-"`
}
// modelCostJSON contains the JSON metadata for the struct [ModelCost]
type modelCostJSON struct {
Input apijson.Field
Output apijson.Field
CacheRead apijson.Field
CacheWrite apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ModelCost) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r modelCostJSON) RawJSON() string {
return r.raw
}
type ModelLimit struct {
Context float64 `json:"context,required"`
Output float64 `json:"output,required"`
JSON modelLimitJSON `json:"-"`
}
// modelLimitJSON contains the JSON metadata for the struct [ModelLimit]
type modelLimitJSON struct {
Context apijson.Field
Output apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ModelLimit) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r modelLimitJSON) RawJSON() string {
return r.raw
}
type ModelModalities struct {
Input []ModelModalitiesInput `json:"input,required"`
Output []ModelModalitiesOutput `json:"output,required"`
JSON modelModalitiesJSON `json:"-"`
}
// modelModalitiesJSON contains the JSON metadata for the struct [ModelModalities]
type modelModalitiesJSON struct {
Input apijson.Field
Output apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ModelModalities) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r modelModalitiesJSON) RawJSON() string {
return r.raw
}
type ModelModalitiesInput string
const (
ModelModalitiesInputText ModelModalitiesInput = "text"
ModelModalitiesInputAudio ModelModalitiesInput = "audio"
ModelModalitiesInputImage ModelModalitiesInput = "image"
ModelModalitiesInputVideo ModelModalitiesInput = "video"
ModelModalitiesInputPdf ModelModalitiesInput = "pdf"
)
func (r ModelModalitiesInput) IsKnown() bool {
switch r {
case ModelModalitiesInputText, ModelModalitiesInputAudio, ModelModalitiesInputImage, ModelModalitiesInputVideo, ModelModalitiesInputPdf:
return true
}
return false
}
type ModelModalitiesOutput string
const (
ModelModalitiesOutputText ModelModalitiesOutput = "text"
ModelModalitiesOutputAudio ModelModalitiesOutput = "audio"
ModelModalitiesOutputImage ModelModalitiesOutput = "image"
ModelModalitiesOutputVideo ModelModalitiesOutput = "video"
ModelModalitiesOutputPdf ModelModalitiesOutput = "pdf"
)
func (r ModelModalitiesOutput) IsKnown() bool {
switch r {
case ModelModalitiesOutputText, ModelModalitiesOutputAudio, ModelModalitiesOutputImage, ModelModalitiesOutputVideo, ModelModalitiesOutputPdf:
return true
}
return false
}
type ModelProvider struct {
Npm string `json:"npm,required"`
JSON modelProviderJSON `json:"-"`
}
// modelProviderJSON contains the JSON metadata for the struct [ModelProvider]
type modelProviderJSON struct {
Npm apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ModelProvider) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r modelProviderJSON) RawJSON() string {
return r.raw
}
type ModelStatus string
const (
ModelStatusAlpha ModelStatus = "alpha"
ModelStatusBeta ModelStatus = "beta"
)
func (r ModelStatus) IsKnown() bool {
switch r {
case ModelStatusAlpha, ModelStatusBeta:
return true
}
return false
}
type Provider struct {
ID string `json:"id,required"`
Env []string `json:"env,required"`
Models map[string]Model `json:"models,required"`
Name string `json:"name,required"`
API string `json:"api"`
Npm string `json:"npm"`
JSON providerJSON `json:"-"`
}
// providerJSON contains the JSON metadata for the struct [Provider]
type providerJSON struct {
ID apijson.Field
Env apijson.Field
Models apijson.Field
Name apijson.Field
API apijson.Field
Npm apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *Provider) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r providerJSON) RawJSON() string {
return r.raw
}
type AppProvidersResponse struct {
Default map[string]string `json:"default,required"`
Providers []Provider `json:"providers,required"`
JSON appProvidersResponseJSON `json:"-"`
}
// appProvidersResponseJSON contains the JSON metadata for the struct
// [AppProvidersResponse]
type appProvidersResponseJSON struct {
Default apijson.Field
Providers apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *AppProvidersResponse) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r appProvidersResponseJSON) RawJSON() string {
return r.raw
}
type AppLogParams struct {
// Log level
Level param.Field[AppLogParamsLevel] `json:"level,required"`
// Log message
Message param.Field[string] `json:"message,required"`
// Service name for the log entry
Service param.Field[string] `json:"service,required"`
Directory param.Field[string] `query:"directory"`
// Additional metadata for the log entry
Extra param.Field[map[string]interface{}] `json:"extra"`
}
func (r AppLogParams) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
// URLQuery serializes [AppLogParams]'s query parameters as `url.Values`.
func (r AppLogParams) URLQuery() (v url.Values) {
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
ArrayFormat: apiquery.ArrayQueryFormatComma,
NestedFormat: apiquery.NestedQueryFormatBrackets,
})
}
// Log level
type AppLogParamsLevel string
const (
AppLogParamsLevelDebug AppLogParamsLevel = "debug"
AppLogParamsLevelInfo AppLogParamsLevel = "info"
AppLogParamsLevelError AppLogParamsLevel = "error"
AppLogParamsLevelWarn AppLogParamsLevel = "warn"
)
func (r AppLogParamsLevel) IsKnown() bool {
switch r {
case AppLogParamsLevelDebug, AppLogParamsLevelInfo, AppLogParamsLevelError, AppLogParamsLevelWarn:
return true
}
return false
}
type AppProvidersParams struct {
Directory param.Field[string] `query:"directory"`
}
// URLQuery serializes [AppProvidersParams]'s query parameters as `url.Values`.
func (r AppProvidersParams) URLQuery() (v url.Values) {
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
ArrayFormat: apiquery.ArrayQueryFormatComma,
NestedFormat: apiquery.NestedQueryFormatBrackets,
})
}

View File

@@ -0,0 +1,68 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode_test
import (
"context"
"errors"
"os"
"testing"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode-sdk-go/internal/testutil"
"github.com/sst/opencode-sdk-go/option"
)
func TestAppLogWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
baseURL = envURL
}
if !testutil.CheckTestServer(t, baseURL) {
return
}
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
_, err := client.App.Log(context.TODO(), opencode.AppLogParams{
Level: opencode.F(opencode.AppLogParamsLevelDebug),
Message: opencode.F("message"),
Service: opencode.F("service"),
Directory: opencode.F("directory"),
Extra: opencode.F(map[string]interface{}{
"foo": "bar",
}),
})
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
t.Log(string(apierr.DumpRequest(true)))
}
t.Fatalf("err should be nil: %s", err.Error())
}
}
func TestAppProvidersWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
baseURL = envURL
}
if !testutil.CheckTestServer(t, baseURL) {
return
}
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
_, err := client.App.Providers(context.TODO(), opencode.AppProvidersParams{
Directory: opencode.F("directory"),
})
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
t.Log(string(apierr.DumpRequest(true)))
}
t.Fatalf("err should be nil: %s", err.Error())
}
}

134
packages/sdk/go/client.go Normal file
View File

@@ -0,0 +1,134 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode
import (
"context"
"net/http"
"os"
"slices"
"github.com/sst/opencode-sdk-go/internal/requestconfig"
"github.com/sst/opencode-sdk-go/option"
)
// Client creates a struct with services and top level methods that help with
// interacting with the opencode API. You should not instantiate this client
// directly, and instead use the [NewClient] method instead.
type Client struct {
Options []option.RequestOption
Event *EventService
Path *PathService
App *AppService
Agent *AgentService
Find *FindService
File *FileService
Config *ConfigService
Command *CommandService
Project *ProjectService
Session *SessionService
Tui *TuiService
}
// DefaultClientOptions read from the environment (OPENCODE_BASE_URL). This should
// be used to initialize new clients.
func DefaultClientOptions() []option.RequestOption {
defaults := []option.RequestOption{option.WithEnvironmentProduction()}
if o, ok := os.LookupEnv("OPENCODE_BASE_URL"); ok {
defaults = append(defaults, option.WithBaseURL(o))
}
return defaults
}
// NewClient generates a new client with the default option read from the
// environment (OPENCODE_BASE_URL). The option passed in as arguments are applied
// after these default arguments, and all option will be passed down to the
// services and requests that this client makes.
func NewClient(opts ...option.RequestOption) (r *Client) {
opts = append(DefaultClientOptions(), opts...)
r = &Client{Options: opts}
r.Event = NewEventService(opts...)
r.Path = NewPathService(opts...)
r.App = NewAppService(opts...)
r.Agent = NewAgentService(opts...)
r.Find = NewFindService(opts...)
r.File = NewFileService(opts...)
r.Config = NewConfigService(opts...)
r.Command = NewCommandService(opts...)
r.Project = NewProjectService(opts...)
r.Session = NewSessionService(opts...)
r.Tui = NewTuiService(opts...)
return
}
// Execute makes a request with the given context, method, URL, request params,
// response, and request options. This is useful for hitting undocumented endpoints
// while retaining the base URL, auth, retries, and other options from the client.
//
// If a byte slice or an [io.Reader] is supplied to params, it will be used as-is
// for the request body.
//
// The params is by default serialized into the body using [encoding/json]. If your
// type implements a MarshalJSON function, it will be used instead to serialize the
// request. If a URLQuery method is implemented, the returned [url.Values] will be
// used as query strings to the url.
//
// If your params struct uses [param.Field], you must provide either [MarshalJSON],
// [URLQuery], and/or [MarshalForm] functions. It is undefined behavior to use a
// struct uses [param.Field] without specifying how it is serialized.
//
// Any "…Params" object defined in this library can be used as the request
// argument. Note that 'path' arguments will not be forwarded into the url.
//
// The response body will be deserialized into the res variable, depending on its
// type:
//
// - A pointer to a [*http.Response] is populated by the raw response.
// - A pointer to a byte array will be populated with the contents of the request
// body.
// - A pointer to any other type uses this library's default JSON decoding, which
// respects UnmarshalJSON if it is defined on the type.
// - A nil value will not read the response body.
//
// For even greater flexibility, see [option.WithResponseInto] and
// [option.WithResponseBodyInto].
func (r *Client) Execute(ctx context.Context, method string, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
opts = slices.Concat(r.Options, opts)
return requestconfig.ExecuteNewRequest(ctx, method, path, params, res, opts...)
}
// Get makes a GET request with the given URL, params, and optionally deserializes
// to a response. See [Execute] documentation on the params and response.
func (r *Client) Get(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
return r.Execute(ctx, http.MethodGet, path, params, res, opts...)
}
// Post makes a POST request with the given URL, params, and optionally
// deserializes to a response. See [Execute] documentation on the params and
// response.
func (r *Client) Post(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
return r.Execute(ctx, http.MethodPost, path, params, res, opts...)
}
// Put makes a PUT request with the given URL, params, and optionally deserializes
// to a response. See [Execute] documentation on the params and response.
func (r *Client) Put(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
return r.Execute(ctx, http.MethodPut, path, params, res, opts...)
}
// Patch makes a PATCH request with the given URL, params, and optionally
// deserializes to a response. See [Execute] documentation on the params and
// response.
func (r *Client) Patch(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
return r.Execute(ctx, http.MethodPatch, path, params, res, opts...)
}
// Delete makes a DELETE request with the given URL, params, and optionally
// deserializes to a response. See [Execute] documentation on the params and
// response.
func (r *Client) Delete(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
return r.Execute(ctx, http.MethodDelete, path, params, res, opts...)
}

View File

@@ -0,0 +1,336 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode_test
import (
"context"
"fmt"
"io"
"net/http"
"reflect"
"testing"
"time"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode-sdk-go/internal"
"github.com/sst/opencode-sdk-go/option"
)
type closureTransport struct {
fn func(req *http.Request) (*http.Response, error)
}
func (t *closureTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return t.fn(req)
}
func TestUserAgentHeader(t *testing.T) {
var userAgent string
client := opencode.NewClient(
option.WithHTTPClient(&http.Client{
Transport: &closureTransport{
fn: func(req *http.Request) (*http.Response, error) {
userAgent = req.Header.Get("User-Agent")
return &http.Response{
StatusCode: http.StatusOK,
}, nil
},
},
}),
)
client.Session.List(context.Background(), opencode.SessionListParams{})
if userAgent != fmt.Sprintf("Opencode/Go %s", internal.PackageVersion) {
t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent)
}
}
func TestRetryAfter(t *testing.T) {
retryCountHeaders := make([]string, 0)
client := opencode.NewClient(
option.WithHTTPClient(&http.Client{
Transport: &closureTransport{
fn: func(req *http.Request) (*http.Response, error) {
retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count"))
return &http.Response{
StatusCode: http.StatusTooManyRequests,
Header: http.Header{
http.CanonicalHeaderKey("Retry-After"): []string{"0.1"},
},
}, nil
},
},
}),
)
_, err := client.Session.List(context.Background(), opencode.SessionListParams{})
if err == nil {
t.Error("Expected there to be a cancel error")
}
attempts := len(retryCountHeaders)
if attempts != 3 {
t.Errorf("Expected %d attempts, got %d", 3, attempts)
}
expectedRetryCountHeaders := []string{"0", "1", "2"}
if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) {
t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders)
}
}
func TestDeleteRetryCountHeader(t *testing.T) {
retryCountHeaders := make([]string, 0)
client := opencode.NewClient(
option.WithHTTPClient(&http.Client{
Transport: &closureTransport{
fn: func(req *http.Request) (*http.Response, error) {
retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count"))
return &http.Response{
StatusCode: http.StatusTooManyRequests,
Header: http.Header{
http.CanonicalHeaderKey("Retry-After"): []string{"0.1"},
},
}, nil
},
},
}),
option.WithHeaderDel("X-Stainless-Retry-Count"),
)
_, err := client.Session.List(context.Background(), opencode.SessionListParams{})
if err == nil {
t.Error("Expected there to be a cancel error")
}
expectedRetryCountHeaders := []string{"", "", ""}
if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) {
t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders)
}
}
func TestOverwriteRetryCountHeader(t *testing.T) {
retryCountHeaders := make([]string, 0)
client := opencode.NewClient(
option.WithHTTPClient(&http.Client{
Transport: &closureTransport{
fn: func(req *http.Request) (*http.Response, error) {
retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count"))
return &http.Response{
StatusCode: http.StatusTooManyRequests,
Header: http.Header{
http.CanonicalHeaderKey("Retry-After"): []string{"0.1"},
},
}, nil
},
},
}),
option.WithHeader("X-Stainless-Retry-Count", "42"),
)
_, err := client.Session.List(context.Background(), opencode.SessionListParams{})
if err == nil {
t.Error("Expected there to be a cancel error")
}
expectedRetryCountHeaders := []string{"42", "42", "42"}
if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) {
t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders)
}
}
func TestRetryAfterMs(t *testing.T) {
attempts := 0
client := opencode.NewClient(
option.WithHTTPClient(&http.Client{
Transport: &closureTransport{
fn: func(req *http.Request) (*http.Response, error) {
attempts++
return &http.Response{
StatusCode: http.StatusTooManyRequests,
Header: http.Header{
http.CanonicalHeaderKey("Retry-After-Ms"): []string{"100"},
},
}, nil
},
},
}),
)
_, err := client.Session.List(context.Background(), opencode.SessionListParams{})
if err == nil {
t.Error("Expected there to be a cancel error")
}
if want := 3; attempts != want {
t.Errorf("Expected %d attempts, got %d", want, attempts)
}
}
func TestContextCancel(t *testing.T) {
client := opencode.NewClient(
option.WithHTTPClient(&http.Client{
Transport: &closureTransport{
fn: func(req *http.Request) (*http.Response, error) {
<-req.Context().Done()
return nil, req.Context().Err()
},
},
}),
)
cancelCtx, cancel := context.WithCancel(context.Background())
cancel()
_, err := client.Session.List(cancelCtx, opencode.SessionListParams{})
if err == nil {
t.Error("Expected there to be a cancel error")
}
}
func TestContextCancelDelay(t *testing.T) {
client := opencode.NewClient(
option.WithHTTPClient(&http.Client{
Transport: &closureTransport{
fn: func(req *http.Request) (*http.Response, error) {
<-req.Context().Done()
return nil, req.Context().Err()
},
},
}),
)
cancelCtx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err := client.Session.List(cancelCtx, opencode.SessionListParams{})
if err == nil {
t.Error("expected there to be a cancel error")
}
}
func TestContextDeadline(t *testing.T) {
testTimeout := time.After(3 * time.Second)
testDone := make(chan struct{})
deadline := time.Now().Add(100 * time.Millisecond)
deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
go func() {
client := opencode.NewClient(
option.WithHTTPClient(&http.Client{
Transport: &closureTransport{
fn: func(req *http.Request) (*http.Response, error) {
<-req.Context().Done()
return nil, req.Context().Err()
},
},
}),
)
_, err := client.Session.List(deadlineCtx, opencode.SessionListParams{})
if err == nil {
t.Error("expected there to be a deadline error")
}
close(testDone)
}()
select {
case <-testTimeout:
t.Fatal("client didn't finish in time")
case <-testDone:
if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff {
t.Fatalf("client did not return within 30ms of context deadline, got %s", diff)
}
}
}
func TestContextDeadlineStreaming(t *testing.T) {
testTimeout := time.After(3 * time.Second)
testDone := make(chan struct{})
deadline := time.Now().Add(100 * time.Millisecond)
deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
go func() {
client := opencode.NewClient(
option.WithHTTPClient(&http.Client{
Transport: &closureTransport{
fn: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
Status: "200 OK",
Body: io.NopCloser(
io.Reader(readerFunc(func([]byte) (int, error) {
<-req.Context().Done()
return 0, req.Context().Err()
})),
),
}, nil
},
},
}),
)
stream := client.Event.ListStreaming(deadlineCtx, opencode.EventListParams{})
for stream.Next() {
_ = stream.Current()
}
if stream.Err() == nil {
t.Error("expected there to be a deadline error")
}
close(testDone)
}()
select {
case <-testTimeout:
t.Fatal("client didn't finish in time")
case <-testDone:
if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff {
t.Fatalf("client did not return within 30ms of context deadline, got %s", diff)
}
}
}
func TestContextDeadlineStreamingWithRequestTimeout(t *testing.T) {
testTimeout := time.After(3 * time.Second)
testDone := make(chan struct{})
deadline := time.Now().Add(100 * time.Millisecond)
go func() {
client := opencode.NewClient(
option.WithHTTPClient(&http.Client{
Transport: &closureTransport{
fn: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
Status: "200 OK",
Body: io.NopCloser(
io.Reader(readerFunc(func([]byte) (int, error) {
<-req.Context().Done()
return 0, req.Context().Err()
})),
),
}, nil
},
},
}),
)
stream := client.Event.ListStreaming(
context.Background(),
opencode.EventListParams{},
option.WithRequestTimeout((100 * time.Millisecond)),
)
for stream.Next() {
_ = stream.Current()
}
if stream.Err() == nil {
t.Error("expected there to be a deadline error")
}
close(testDone)
}()
select {
case <-testTimeout:
t.Fatal("client didn't finish in time")
case <-testDone:
if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff {
t.Fatalf("client did not return within 30ms of context deadline, got %s", diff)
}
}
}
type readerFunc func([]byte) (int, error)
func (f readerFunc) Read(p []byte) (int, error) { return f(p) }
func (f readerFunc) Close() error { return nil }

View File

@@ -0,0 +1,85 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode
import (
"context"
"net/http"
"net/url"
"slices"
"github.com/sst/opencode-sdk-go/internal/apijson"
"github.com/sst/opencode-sdk-go/internal/apiquery"
"github.com/sst/opencode-sdk-go/internal/param"
"github.com/sst/opencode-sdk-go/internal/requestconfig"
"github.com/sst/opencode-sdk-go/option"
)
// CommandService contains methods and other services that help with interacting
// with the opencode API.
//
// Note, unlike clients, this service does not read variables from the environment
// automatically. You should not instantiate this service directly, and instead use
// the [NewCommandService] method instead.
type CommandService struct {
Options []option.RequestOption
}
// NewCommandService generates a new service that applies the given options to each
// request. These options are applied after the parent client's options (if there
// is one), and before any request-specific options.
func NewCommandService(opts ...option.RequestOption) (r *CommandService) {
r = &CommandService{}
r.Options = opts
return
}
// List all commands
func (r *CommandService) List(ctx context.Context, query CommandListParams, opts ...option.RequestOption) (res *[]Command, err error) {
opts = slices.Concat(r.Options, opts)
path := "command"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
return
}
type Command struct {
Name string `json:"name,required"`
Template string `json:"template,required"`
Agent string `json:"agent"`
Description string `json:"description"`
Model string `json:"model"`
Subtask bool `json:"subtask"`
JSON commandJSON `json:"-"`
}
// commandJSON contains the JSON metadata for the struct [Command]
type commandJSON struct {
Name apijson.Field
Template apijson.Field
Agent apijson.Field
Description apijson.Field
Model apijson.Field
Subtask apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *Command) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r commandJSON) RawJSON() string {
return r.raw
}
type CommandListParams struct {
Directory param.Field[string] `query:"directory"`
}
// URLQuery serializes [CommandListParams]'s query parameters as `url.Values`.
func (r CommandListParams) URLQuery() (v url.Values) {
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
ArrayFormat: apiquery.ArrayQueryFormatComma,
NestedFormat: apiquery.NestedQueryFormatBrackets,
})
}

View File

@@ -0,0 +1,38 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode_test
import (
"context"
"errors"
"os"
"testing"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode-sdk-go/internal/testutil"
"github.com/sst/opencode-sdk-go/option"
)
func TestCommandListWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
baseURL = envURL
}
if !testutil.CheckTestServer(t, baseURL) {
return
}
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
_, err := client.Command.List(context.TODO(), opencode.CommandListParams{
Directory: opencode.F("directory"),
})
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
t.Log(string(apierr.DumpRequest(true)))
}
t.Fatalf("err should be nil: %s", err.Error())
}
}

2146
packages/sdk/go/config.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode_test
import (
"context"
"errors"
"os"
"testing"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode-sdk-go/internal/testutil"
"github.com/sst/opencode-sdk-go/option"
)
func TestConfigGetWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
baseURL = envURL
}
if !testutil.CheckTestServer(t, baseURL) {
return
}
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
_, err := client.Config.Get(context.TODO(), opencode.ConfigGetParams{
Directory: opencode.F("directory"),
})
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
t.Log(string(apierr.DumpRequest(true)))
}
t.Fatalf("err should be nil: %s", err.Error())
}
}

1638
packages/sdk/go/event.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
File generated from our OpenAPI spec by Stainless.
This directory can be used to store example files demonstrating usage of this SDK.
It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.

50
packages/sdk/go/field.go Normal file
View File

@@ -0,0 +1,50 @@
package opencode
import (
"github.com/sst/opencode-sdk-go/internal/param"
"io"
)
// F is a param field helper used to initialize a [param.Field] generic struct.
// This helps specify null, zero values, and overrides, as well as normal values.
// You can read more about this in our [README].
//
// [README]: https://pkg.go.dev/github.com/sst/opencode-sdk-go#readme-request-fields
func F[T any](value T) param.Field[T] { return param.Field[T]{Value: value, Present: true} }
// Null is a param field helper which explicitly sends null to the API.
func Null[T any]() param.Field[T] { return param.Field[T]{Null: true, Present: true} }
// Raw is a param field helper for specifying values for fields when the
// type you are looking to send is different from the type that is specified in
// the SDK. For example, if the type of the field is an integer, but you want
// to send a float, you could do that by setting the corresponding field with
// Raw[int](0.5).
func Raw[T any](value any) param.Field[T] { return param.Field[T]{Raw: value, Present: true} }
// Int is a param field helper which helps specify integers. This is
// particularly helpful when specifying integer constants for fields.
func Int(value int64) param.Field[int64] { return F(value) }
// String is a param field helper which helps specify strings.
func String(value string) param.Field[string] { return F(value) }
// Float is a param field helper which helps specify floats.
func Float(value float64) param.Field[float64] { return F(value) }
// Bool is a param field helper which helps specify bools.
func Bool(value bool) param.Field[bool] { return F(value) }
// FileParam is a param field helper which helps files with a mime content-type.
func FileParam(reader io.Reader, filename string, contentType string) param.Field[io.Reader] {
return F[io.Reader](&file{reader, filename, contentType})
}
type file struct {
io.Reader
name string
contentType string
}
func (f *file) ContentType() string { return f.contentType }
func (f *file) Filename() string { return f.name }

301
packages/sdk/go/file.go Normal file
View File

@@ -0,0 +1,301 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode
import (
"context"
"net/http"
"net/url"
"slices"
"github.com/sst/opencode-sdk-go/internal/apijson"
"github.com/sst/opencode-sdk-go/internal/apiquery"
"github.com/sst/opencode-sdk-go/internal/param"
"github.com/sst/opencode-sdk-go/internal/requestconfig"
"github.com/sst/opencode-sdk-go/option"
)
// FileService contains methods and other services that help with interacting with
// the opencode API.
//
// Note, unlike clients, this service does not read variables from the environment
// automatically. You should not instantiate this service directly, and instead use
// the [NewFileService] method instead.
type FileService struct {
Options []option.RequestOption
}
// NewFileService generates a new service that applies the given options to each
// request. These options are applied after the parent client's options (if there
// is one), and before any request-specific options.
func NewFileService(opts ...option.RequestOption) (r *FileService) {
r = &FileService{}
r.Options = opts
return
}
// List files and directories
func (r *FileService) List(ctx context.Context, query FileListParams, opts ...option.RequestOption) (res *[]FileNode, err error) {
opts = slices.Concat(r.Options, opts)
path := "file"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
return
}
// Read a file
func (r *FileService) Read(ctx context.Context, query FileReadParams, opts ...option.RequestOption) (res *FileReadResponse, err error) {
opts = slices.Concat(r.Options, opts)
path := "file/content"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
return
}
// Get file status
func (r *FileService) Status(ctx context.Context, query FileStatusParams, opts ...option.RequestOption) (res *[]File, err error) {
opts = slices.Concat(r.Options, opts)
path := "file/status"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
return
}
type File struct {
Added int64 `json:"added,required"`
Path string `json:"path,required"`
Removed int64 `json:"removed,required"`
Status FileStatus `json:"status,required"`
JSON fileJSON `json:"-"`
}
// fileJSON contains the JSON metadata for the struct [File]
type fileJSON struct {
Added apijson.Field
Path apijson.Field
Removed apijson.Field
Status apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *File) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r fileJSON) RawJSON() string {
return r.raw
}
type FileStatus string
const (
FileStatusAdded FileStatus = "added"
FileStatusDeleted FileStatus = "deleted"
FileStatusModified FileStatus = "modified"
)
func (r FileStatus) IsKnown() bool {
switch r {
case FileStatusAdded, FileStatusDeleted, FileStatusModified:
return true
}
return false
}
type FileNode struct {
Absolute string `json:"absolute,required"`
Ignored bool `json:"ignored,required"`
Name string `json:"name,required"`
Path string `json:"path,required"`
Type FileNodeType `json:"type,required"`
JSON fileNodeJSON `json:"-"`
}
// fileNodeJSON contains the JSON metadata for the struct [FileNode]
type fileNodeJSON struct {
Absolute apijson.Field
Ignored apijson.Field
Name apijson.Field
Path apijson.Field
Type apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *FileNode) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r fileNodeJSON) RawJSON() string {
return r.raw
}
type FileNodeType string
const (
FileNodeTypeFile FileNodeType = "file"
FileNodeTypeDirectory FileNodeType = "directory"
)
func (r FileNodeType) IsKnown() bool {
switch r {
case FileNodeTypeFile, FileNodeTypeDirectory:
return true
}
return false
}
type FileReadResponse struct {
Content string `json:"content,required"`
Type FileReadResponseType `json:"type,required"`
Diff string `json:"diff"`
Encoding FileReadResponseEncoding `json:"encoding"`
MimeType string `json:"mimeType"`
Patch FileReadResponsePatch `json:"patch"`
JSON fileReadResponseJSON `json:"-"`
}
// fileReadResponseJSON contains the JSON metadata for the struct
// [FileReadResponse]
type fileReadResponseJSON struct {
Content apijson.Field
Type apijson.Field
Diff apijson.Field
Encoding apijson.Field
MimeType apijson.Field
Patch apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *FileReadResponse) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r fileReadResponseJSON) RawJSON() string {
return r.raw
}
type FileReadResponseType string
const (
FileReadResponseTypeText FileReadResponseType = "text"
)
func (r FileReadResponseType) IsKnown() bool {
switch r {
case FileReadResponseTypeText:
return true
}
return false
}
type FileReadResponseEncoding string
const (
FileReadResponseEncodingBase64 FileReadResponseEncoding = "base64"
)
func (r FileReadResponseEncoding) IsKnown() bool {
switch r {
case FileReadResponseEncodingBase64:
return true
}
return false
}
type FileReadResponsePatch struct {
Hunks []FileReadResponsePatchHunk `json:"hunks,required"`
NewFileName string `json:"newFileName,required"`
OldFileName string `json:"oldFileName,required"`
Index string `json:"index"`
NewHeader string `json:"newHeader"`
OldHeader string `json:"oldHeader"`
JSON fileReadResponsePatchJSON `json:"-"`
}
// fileReadResponsePatchJSON contains the JSON metadata for the struct
// [FileReadResponsePatch]
type fileReadResponsePatchJSON struct {
Hunks apijson.Field
NewFileName apijson.Field
OldFileName apijson.Field
Index apijson.Field
NewHeader apijson.Field
OldHeader apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *FileReadResponsePatch) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r fileReadResponsePatchJSON) RawJSON() string {
return r.raw
}
type FileReadResponsePatchHunk struct {
Lines []string `json:"lines,required"`
NewLines float64 `json:"newLines,required"`
NewStart float64 `json:"newStart,required"`
OldLines float64 `json:"oldLines,required"`
OldStart float64 `json:"oldStart,required"`
JSON fileReadResponsePatchHunkJSON `json:"-"`
}
// fileReadResponsePatchHunkJSON contains the JSON metadata for the struct
// [FileReadResponsePatchHunk]
type fileReadResponsePatchHunkJSON struct {
Lines apijson.Field
NewLines apijson.Field
NewStart apijson.Field
OldLines apijson.Field
OldStart apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *FileReadResponsePatchHunk) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r fileReadResponsePatchHunkJSON) RawJSON() string {
return r.raw
}
type FileListParams struct {
Path param.Field[string] `query:"path,required"`
Directory param.Field[string] `query:"directory"`
}
// URLQuery serializes [FileListParams]'s query parameters as `url.Values`.
func (r FileListParams) URLQuery() (v url.Values) {
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
ArrayFormat: apiquery.ArrayQueryFormatComma,
NestedFormat: apiquery.NestedQueryFormatBrackets,
})
}
type FileReadParams struct {
Path param.Field[string] `query:"path,required"`
Directory param.Field[string] `query:"directory"`
}
// URLQuery serializes [FileReadParams]'s query parameters as `url.Values`.
func (r FileReadParams) URLQuery() (v url.Values) {
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
ArrayFormat: apiquery.ArrayQueryFormatComma,
NestedFormat: apiquery.NestedQueryFormatBrackets,
})
}
type FileStatusParams struct {
Directory param.Field[string] `query:"directory"`
}
// URLQuery serializes [FileStatusParams]'s query parameters as `url.Values`.
func (r FileStatusParams) URLQuery() (v url.Values) {
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
ArrayFormat: apiquery.ArrayQueryFormatComma,
NestedFormat: apiquery.NestedQueryFormatBrackets,
})
}

View File

@@ -0,0 +1,88 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode_test
import (
"context"
"errors"
"os"
"testing"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode-sdk-go/internal/testutil"
"github.com/sst/opencode-sdk-go/option"
)
func TestFileListWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
baseURL = envURL
}
if !testutil.CheckTestServer(t, baseURL) {
return
}
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
_, err := client.File.List(context.TODO(), opencode.FileListParams{
Path: opencode.F("path"),
Directory: opencode.F("directory"),
})
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
t.Log(string(apierr.DumpRequest(true)))
}
t.Fatalf("err should be nil: %s", err.Error())
}
}
func TestFileReadWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
baseURL = envURL
}
if !testutil.CheckTestServer(t, baseURL) {
return
}
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
_, err := client.File.Read(context.TODO(), opencode.FileReadParams{
Path: opencode.F("path"),
Directory: opencode.F("directory"),
})
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
t.Log(string(apierr.DumpRequest(true)))
}
t.Fatalf("err should be nil: %s", err.Error())
}
}
func TestFileStatusWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
baseURL = envURL
}
if !testutil.CheckTestServer(t, baseURL) {
return
}
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
_, err := client.File.Status(context.TODO(), opencode.FileStatusParams{
Directory: opencode.F("directory"),
})
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
t.Log(string(apierr.DumpRequest(true)))
}
t.Fatalf("err should be nil: %s", err.Error())
}
}

330
packages/sdk/go/find.go Normal file
View File

@@ -0,0 +1,330 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode
import (
"context"
"net/http"
"net/url"
"slices"
"github.com/sst/opencode-sdk-go/internal/apijson"
"github.com/sst/opencode-sdk-go/internal/apiquery"
"github.com/sst/opencode-sdk-go/internal/param"
"github.com/sst/opencode-sdk-go/internal/requestconfig"
"github.com/sst/opencode-sdk-go/option"
)
// FindService contains methods and other services that help with interacting with
// the opencode API.
//
// Note, unlike clients, this service does not read variables from the environment
// automatically. You should not instantiate this service directly, and instead use
// the [NewFindService] method instead.
type FindService struct {
Options []option.RequestOption
}
// NewFindService generates a new service that applies the given options to each
// request. These options are applied after the parent client's options (if there
// is one), and before any request-specific options.
func NewFindService(opts ...option.RequestOption) (r *FindService) {
r = &FindService{}
r.Options = opts
return
}
// Find files
func (r *FindService) Files(ctx context.Context, query FindFilesParams, opts ...option.RequestOption) (res *[]string, err error) {
opts = slices.Concat(r.Options, opts)
path := "find/file"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
return
}
// Find workspace symbols
func (r *FindService) Symbols(ctx context.Context, query FindSymbolsParams, opts ...option.RequestOption) (res *[]Symbol, err error) {
opts = slices.Concat(r.Options, opts)
path := "find/symbol"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
return
}
// Find text in files
func (r *FindService) Text(ctx context.Context, query FindTextParams, opts ...option.RequestOption) (res *[]FindTextResponse, err error) {
opts = slices.Concat(r.Options, opts)
path := "find"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
return
}
type Symbol struct {
Kind float64 `json:"kind,required"`
Location SymbolLocation `json:"location,required"`
Name string `json:"name,required"`
JSON symbolJSON `json:"-"`
}
// symbolJSON contains the JSON metadata for the struct [Symbol]
type symbolJSON struct {
Kind apijson.Field
Location apijson.Field
Name apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *Symbol) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r symbolJSON) RawJSON() string {
return r.raw
}
type SymbolLocation struct {
Range SymbolLocationRange `json:"range,required"`
Uri string `json:"uri,required"`
JSON symbolLocationJSON `json:"-"`
}
// symbolLocationJSON contains the JSON metadata for the struct [SymbolLocation]
type symbolLocationJSON struct {
Range apijson.Field
Uri apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *SymbolLocation) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r symbolLocationJSON) RawJSON() string {
return r.raw
}
type SymbolLocationRange struct {
End SymbolLocationRangeEnd `json:"end,required"`
Start SymbolLocationRangeStart `json:"start,required"`
JSON symbolLocationRangeJSON `json:"-"`
}
// symbolLocationRangeJSON contains the JSON metadata for the struct
// [SymbolLocationRange]
type symbolLocationRangeJSON struct {
End apijson.Field
Start apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *SymbolLocationRange) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r symbolLocationRangeJSON) RawJSON() string {
return r.raw
}
type SymbolLocationRangeEnd struct {
Character float64 `json:"character,required"`
Line float64 `json:"line,required"`
JSON symbolLocationRangeEndJSON `json:"-"`
}
// symbolLocationRangeEndJSON contains the JSON metadata for the struct
// [SymbolLocationRangeEnd]
type symbolLocationRangeEndJSON struct {
Character apijson.Field
Line apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *SymbolLocationRangeEnd) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r symbolLocationRangeEndJSON) RawJSON() string {
return r.raw
}
type SymbolLocationRangeStart struct {
Character float64 `json:"character,required"`
Line float64 `json:"line,required"`
JSON symbolLocationRangeStartJSON `json:"-"`
}
// symbolLocationRangeStartJSON contains the JSON metadata for the struct
// [SymbolLocationRangeStart]
type symbolLocationRangeStartJSON struct {
Character apijson.Field
Line apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *SymbolLocationRangeStart) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r symbolLocationRangeStartJSON) RawJSON() string {
return r.raw
}
type FindTextResponse struct {
AbsoluteOffset float64 `json:"absolute_offset,required"`
LineNumber float64 `json:"line_number,required"`
Lines FindTextResponseLines `json:"lines,required"`
Path FindTextResponsePath `json:"path,required"`
Submatches []FindTextResponseSubmatch `json:"submatches,required"`
JSON findTextResponseJSON `json:"-"`
}
// findTextResponseJSON contains the JSON metadata for the struct
// [FindTextResponse]
type findTextResponseJSON struct {
AbsoluteOffset apijson.Field
LineNumber apijson.Field
Lines apijson.Field
Path apijson.Field
Submatches apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *FindTextResponse) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r findTextResponseJSON) RawJSON() string {
return r.raw
}
type FindTextResponseLines struct {
Text string `json:"text,required"`
JSON findTextResponseLinesJSON `json:"-"`
}
// findTextResponseLinesJSON contains the JSON metadata for the struct
// [FindTextResponseLines]
type findTextResponseLinesJSON struct {
Text apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *FindTextResponseLines) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r findTextResponseLinesJSON) RawJSON() string {
return r.raw
}
type FindTextResponsePath struct {
Text string `json:"text,required"`
JSON findTextResponsePathJSON `json:"-"`
}
// findTextResponsePathJSON contains the JSON metadata for the struct
// [FindTextResponsePath]
type findTextResponsePathJSON struct {
Text apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *FindTextResponsePath) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r findTextResponsePathJSON) RawJSON() string {
return r.raw
}
type FindTextResponseSubmatch struct {
End float64 `json:"end,required"`
Match FindTextResponseSubmatchesMatch `json:"match,required"`
Start float64 `json:"start,required"`
JSON findTextResponseSubmatchJSON `json:"-"`
}
// findTextResponseSubmatchJSON contains the JSON metadata for the struct
// [FindTextResponseSubmatch]
type findTextResponseSubmatchJSON struct {
End apijson.Field
Match apijson.Field
Start apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *FindTextResponseSubmatch) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r findTextResponseSubmatchJSON) RawJSON() string {
return r.raw
}
type FindTextResponseSubmatchesMatch struct {
Text string `json:"text,required"`
JSON findTextResponseSubmatchesMatchJSON `json:"-"`
}
// findTextResponseSubmatchesMatchJSON contains the JSON metadata for the struct
// [FindTextResponseSubmatchesMatch]
type findTextResponseSubmatchesMatchJSON struct {
Text apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *FindTextResponseSubmatchesMatch) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r findTextResponseSubmatchesMatchJSON) RawJSON() string {
return r.raw
}
type FindFilesParams struct {
Query param.Field[string] `query:"query,required"`
Directory param.Field[string] `query:"directory"`
}
// URLQuery serializes [FindFilesParams]'s query parameters as `url.Values`.
func (r FindFilesParams) URLQuery() (v url.Values) {
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
ArrayFormat: apiquery.ArrayQueryFormatComma,
NestedFormat: apiquery.NestedQueryFormatBrackets,
})
}
type FindSymbolsParams struct {
Query param.Field[string] `query:"query,required"`
Directory param.Field[string] `query:"directory"`
}
// URLQuery serializes [FindSymbolsParams]'s query parameters as `url.Values`.
func (r FindSymbolsParams) URLQuery() (v url.Values) {
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
ArrayFormat: apiquery.ArrayQueryFormatComma,
NestedFormat: apiquery.NestedQueryFormatBrackets,
})
}
type FindTextParams struct {
Pattern param.Field[string] `query:"pattern,required"`
Directory param.Field[string] `query:"directory"`
}
// URLQuery serializes [FindTextParams]'s query parameters as `url.Values`.
func (r FindTextParams) URLQuery() (v url.Values) {
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
ArrayFormat: apiquery.ArrayQueryFormatComma,
NestedFormat: apiquery.NestedQueryFormatBrackets,
})
}

View File

@@ -0,0 +1,89 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode_test
import (
"context"
"errors"
"os"
"testing"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode-sdk-go/internal/testutil"
"github.com/sst/opencode-sdk-go/option"
)
func TestFindFilesWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
baseURL = envURL
}
if !testutil.CheckTestServer(t, baseURL) {
return
}
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
_, err := client.Find.Files(context.TODO(), opencode.FindFilesParams{
Query: opencode.F("query"),
Directory: opencode.F("directory"),
})
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
t.Log(string(apierr.DumpRequest(true)))
}
t.Fatalf("err should be nil: %s", err.Error())
}
}
func TestFindSymbolsWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
baseURL = envURL
}
if !testutil.CheckTestServer(t, baseURL) {
return
}
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
_, err := client.Find.Symbols(context.TODO(), opencode.FindSymbolsParams{
Query: opencode.F("query"),
Directory: opencode.F("directory"),
})
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
t.Log(string(apierr.DumpRequest(true)))
}
t.Fatalf("err should be nil: %s", err.Error())
}
}
func TestFindTextWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
baseURL = envURL
}
if !testutil.CheckTestServer(t, baseURL) {
return
}
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
_, err := client.Find.Text(context.TODO(), opencode.FindTextParams{
Pattern: opencode.F("pattern"),
Directory: opencode.F("directory"),
})
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
t.Log(string(apierr.DumpRequest(true)))
}
t.Fatalf("err should be nil: %s", err.Error())
}
}

13
packages/sdk/go/go.mod Normal file
View File

@@ -0,0 +1,13 @@
module github.com/sst/opencode-sdk-go
go 1.22
require (
github.com/tidwall/gjson v1.14.4
github.com/tidwall/sjson v1.2.5
)
require (
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
)

10
packages/sdk/go/go.sum Normal file
View File

@@ -0,0 +1,10 @@
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=

View File

@@ -0,0 +1,53 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package apierror
import (
"fmt"
"net/http"
"net/http/httputil"
"github.com/sst/opencode-sdk-go/internal/apijson"
)
// Error represents an error that originates from the API, i.e. when a request is
// made and the API returns a response with a HTTP status code. Other errors are
// not wrapped by this SDK.
type Error struct {
JSON errorJSON `json:"-"`
StatusCode int
Request *http.Request
Response *http.Response
}
// errorJSON contains the JSON metadata for the struct [Error]
type errorJSON struct {
raw string
ExtraFields map[string]apijson.Field
}
func (r *Error) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r errorJSON) RawJSON() string {
return r.raw
}
func (r *Error) Error() string {
// Attempt to re-populate the response body
return fmt.Sprintf("%s \"%s\": %d %s %s", r.Request.Method, r.Request.URL, r.Response.StatusCode, http.StatusText(r.Response.StatusCode), r.JSON.RawJSON())
}
func (r *Error) DumpRequest(body bool) []byte {
if r.Request.GetBody != nil {
r.Request.Body, _ = r.Request.GetBody()
}
out, _ := httputil.DumpRequestOut(r.Request, body)
return out
}
func (r *Error) DumpResponse(body bool) []byte {
out, _ := httputil.DumpResponse(r.Response, body)
return out
}

View File

@@ -0,0 +1,383 @@
package apiform
import (
"fmt"
"io"
"mime/multipart"
"net/textproto"
"path"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/sst/opencode-sdk-go/internal/param"
)
var encoders sync.Map // map[encoderEntry]encoderFunc
func Marshal(value interface{}, writer *multipart.Writer) error {
e := &encoder{dateFormat: time.RFC3339}
return e.marshal(value, writer)
}
func MarshalRoot(value interface{}, writer *multipart.Writer) error {
e := &encoder{root: true, dateFormat: time.RFC3339}
return e.marshal(value, writer)
}
type encoder struct {
dateFormat string
root bool
}
type encoderFunc func(key string, value reflect.Value, writer *multipart.Writer) error
type encoderField struct {
tag parsedStructTag
fn encoderFunc
idx []int
}
type encoderEntry struct {
reflect.Type
dateFormat string
root bool
}
func (e *encoder) marshal(value interface{}, writer *multipart.Writer) error {
val := reflect.ValueOf(value)
if !val.IsValid() {
return nil
}
typ := val.Type()
enc := e.typeEncoder(typ)
return enc("", val, writer)
}
func (e *encoder) typeEncoder(t reflect.Type) encoderFunc {
entry := encoderEntry{
Type: t,
dateFormat: e.dateFormat,
root: e.root,
}
if fi, ok := encoders.Load(entry); ok {
return fi.(encoderFunc)
}
// To deal with recursive types, populate the map with an
// indirect func before we build it. This type waits on the
// real func (f) to be ready and then calls it. This indirect
// func is only used for recursive types.
var (
wg sync.WaitGroup
f encoderFunc
)
wg.Add(1)
fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(key string, v reflect.Value, writer *multipart.Writer) error {
wg.Wait()
return f(key, v, writer)
}))
if loaded {
return fi.(encoderFunc)
}
// Compute the real encoder and replace the indirect func with it.
f = e.newTypeEncoder(t)
wg.Done()
encoders.Store(entry, f)
return f
}
func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc {
if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
return e.newTimeTypeEncoder()
}
if t.ConvertibleTo(reflect.TypeOf((*io.Reader)(nil)).Elem()) {
return e.newReaderTypeEncoder()
}
e.root = false
switch t.Kind() {
case reflect.Pointer:
inner := t.Elem()
innerEncoder := e.typeEncoder(inner)
return func(key string, v reflect.Value, writer *multipart.Writer) error {
if !v.IsValid() || v.IsNil() {
return nil
}
return innerEncoder(key, v.Elem(), writer)
}
case reflect.Struct:
return e.newStructTypeEncoder(t)
case reflect.Slice, reflect.Array:
return e.newArrayTypeEncoder(t)
case reflect.Map:
return e.newMapEncoder(t)
case reflect.Interface:
return e.newInterfaceEncoder()
default:
return e.newPrimitiveTypeEncoder(t)
}
}
func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc {
switch t.Kind() {
// Note that we could use `gjson` to encode these types but it would complicate our
// code more and this current code shouldn't cause any issues
case reflect.String:
return func(key string, v reflect.Value, writer *multipart.Writer) error {
return writer.WriteField(key, v.String())
}
case reflect.Bool:
return func(key string, v reflect.Value, writer *multipart.Writer) error {
if v.Bool() {
return writer.WriteField(key, "true")
}
return writer.WriteField(key, "false")
}
case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
return func(key string, v reflect.Value, writer *multipart.Writer) error {
return writer.WriteField(key, strconv.FormatInt(v.Int(), 10))
}
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return func(key string, v reflect.Value, writer *multipart.Writer) error {
return writer.WriteField(key, strconv.FormatUint(v.Uint(), 10))
}
case reflect.Float32:
return func(key string, v reflect.Value, writer *multipart.Writer) error {
return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 32))
}
case reflect.Float64:
return func(key string, v reflect.Value, writer *multipart.Writer) error {
return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 64))
}
default:
return func(key string, v reflect.Value, writer *multipart.Writer) error {
return fmt.Errorf("unknown type received at primitive encoder: %s", t.String())
}
}
}
func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc {
itemEncoder := e.typeEncoder(t.Elem())
return func(key string, v reflect.Value, writer *multipart.Writer) error {
if key != "" {
key = key + "."
}
for i := 0; i < v.Len(); i++ {
err := itemEncoder(key+strconv.Itoa(i), v.Index(i), writer)
if err != nil {
return err
}
}
return nil
}
}
func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) {
return e.newFieldTypeEncoder(t)
}
encoderFields := []encoderField{}
extraEncoder := (*encoderField)(nil)
// This helper allows us to recursively collect field encoders into a flat
// array. The parameter `index` keeps track of the access patterns necessary
// to get to some field.
var collectEncoderFields func(r reflect.Type, index []int)
collectEncoderFields = func(r reflect.Type, index []int) {
for i := 0; i < r.NumField(); i++ {
idx := append(index, i)
field := t.FieldByIndex(idx)
if !field.IsExported() {
continue
}
// If this is an embedded struct, traverse one level deeper to extract
// the field and get their encoders as well.
if field.Anonymous {
collectEncoderFields(field.Type, idx)
continue
}
// If json tag is not present, then we skip, which is intentionally
// different behavior from the stdlib.
ptag, ok := parseFormStructTag(field)
if !ok {
continue
}
// We only want to support unexported field if they're tagged with
// `extras` because that field shouldn't be part of the public API. We
// also want to only keep the top level extras
if ptag.extras && len(index) == 0 {
extraEncoder = &encoderField{ptag, e.typeEncoder(field.Type.Elem()), idx}
continue
}
if ptag.name == "-" {
continue
}
dateFormat, ok := parseFormatStructTag(field)
oldFormat := e.dateFormat
if ok {
switch dateFormat {
case "date-time":
e.dateFormat = time.RFC3339
case "date":
e.dateFormat = "2006-01-02"
}
}
encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx})
e.dateFormat = oldFormat
}
}
collectEncoderFields(t, []int{})
// Ensure deterministic output by sorting by lexicographic order
sort.Slice(encoderFields, func(i, j int) bool {
return encoderFields[i].tag.name < encoderFields[j].tag.name
})
return func(key string, value reflect.Value, writer *multipart.Writer) error {
if key != "" {
key = key + "."
}
for _, ef := range encoderFields {
field := value.FieldByIndex(ef.idx)
err := ef.fn(key+ef.tag.name, field, writer)
if err != nil {
return err
}
}
if extraEncoder != nil {
err := e.encodeMapEntries(key, value.FieldByIndex(extraEncoder.idx), writer)
if err != nil {
return err
}
}
return nil
}
}
func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc {
f, _ := t.FieldByName("Value")
enc := e.typeEncoder(f.Type)
return func(key string, value reflect.Value, writer *multipart.Writer) error {
present := value.FieldByName("Present")
if !present.Bool() {
return nil
}
null := value.FieldByName("Null")
if null.Bool() {
return nil
}
raw := value.FieldByName("Raw")
if !raw.IsNil() {
return e.typeEncoder(raw.Type())(key, raw, writer)
}
return enc(key, value.FieldByName("Value"), writer)
}
}
func (e *encoder) newTimeTypeEncoder() encoderFunc {
format := e.dateFormat
return func(key string, value reflect.Value, writer *multipart.Writer) error {
return writer.WriteField(key, value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format))
}
}
func (e encoder) newInterfaceEncoder() encoderFunc {
return func(key string, value reflect.Value, writer *multipart.Writer) error {
value = value.Elem()
if !value.IsValid() {
return nil
}
return e.typeEncoder(value.Type())(key, value, writer)
}
}
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
func escapeQuotes(s string) string {
return quoteEscaper.Replace(s)
}
func (e *encoder) newReaderTypeEncoder() encoderFunc {
return func(key string, value reflect.Value, writer *multipart.Writer) error {
reader := value.Convert(reflect.TypeOf((*io.Reader)(nil)).Elem()).Interface().(io.Reader)
filename := "anonymous_file"
contentType := "application/octet-stream"
if named, ok := reader.(interface{ Filename() string }); ok {
filename = named.Filename()
} else if named, ok := reader.(interface{ Name() string }); ok {
filename = path.Base(named.Name())
}
if typed, ok := reader.(interface{ ContentType() string }); ok {
contentType = typed.ContentType()
}
// Below is taken almost 1-for-1 from [multipart.CreateFormFile]
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(key), escapeQuotes(filename)))
h.Set("Content-Type", contentType)
filewriter, err := writer.CreatePart(h)
if err != nil {
return err
}
_, err = io.Copy(filewriter, reader)
return err
}
}
// Given a []byte of json (may either be an empty object or an object that already contains entries)
// encode all of the entries in the map to the json byte array.
func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipart.Writer) error {
type mapPair struct {
key string
value reflect.Value
}
if key != "" {
key = key + "."
}
pairs := []mapPair{}
iter := v.MapRange()
for iter.Next() {
if iter.Key().Type().Kind() == reflect.String {
pairs = append(pairs, mapPair{key: iter.Key().String(), value: iter.Value()})
} else {
return fmt.Errorf("cannot encode a map with a non string key")
}
}
// Ensure deterministic output
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].key < pairs[j].key
})
elementEncoder := e.typeEncoder(v.Type().Elem())
for _, p := range pairs {
err := elementEncoder(key+string(p.key), p.value, writer)
if err != nil {
return err
}
}
return nil
}
func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc {
return func(key string, value reflect.Value, writer *multipart.Writer) error {
return e.encodeMapEntries(key, value, writer)
}
}

View File

@@ -0,0 +1,5 @@
package apiform
type Marshaler interface {
MarshalMultipart() ([]byte, string, error)
}

View File

@@ -0,0 +1,440 @@
package apiform
import (
"bytes"
"mime/multipart"
"strings"
"testing"
"time"
)
func P[T any](v T) *T { return &v }
type Primitives struct {
A bool `form:"a"`
B int `form:"b"`
C uint `form:"c"`
D float64 `form:"d"`
E float32 `form:"e"`
F []int `form:"f"`
}
type PrimitivePointers struct {
A *bool `form:"a"`
B *int `form:"b"`
C *uint `form:"c"`
D *float64 `form:"d"`
E *float32 `form:"e"`
F *[]int `form:"f"`
}
type Slices struct {
Slice []Primitives `form:"slices"`
}
type DateTime struct {
Date time.Time `form:"date" format:"date"`
DateTime time.Time `form:"date-time" format:"date-time"`
}
type AdditionalProperties struct {
A bool `form:"a"`
Extras map[string]interface{} `form:"-,extras"`
}
type TypedAdditionalProperties struct {
A bool `form:"a"`
Extras map[string]int `form:"-,extras"`
}
type EmbeddedStructs struct {
AdditionalProperties
A *int `form:"number2"`
Extras map[string]interface{} `form:"-,extras"`
}
type Recursive struct {
Name string `form:"name"`
Child *Recursive `form:"child"`
}
type UnknownStruct struct {
Unknown interface{} `form:"unknown"`
}
type UnionStruct struct {
Union Union `form:"union" format:"date"`
}
type Union interface {
union()
}
type UnionInteger int64
func (UnionInteger) union() {}
type UnionStructA struct {
Type string `form:"type"`
A string `form:"a"`
B string `form:"b"`
}
func (UnionStructA) union() {}
type UnionStructB struct {
Type string `form:"type"`
A string `form:"a"`
}
func (UnionStructB) union() {}
type UnionTime time.Time
func (UnionTime) union() {}
type ReaderStruct struct {
}
var tests = map[string]struct {
buf string
val interface{}
}{
"map_string": {
`--xxx
Content-Disposition: form-data; name="foo"
bar
--xxx--
`,
map[string]string{"foo": "bar"},
},
"map_interface": {
`--xxx
Content-Disposition: form-data; name="a"
1
--xxx
Content-Disposition: form-data; name="b"
str
--xxx
Content-Disposition: form-data; name="c"
false
--xxx--
`,
map[string]interface{}{"a": float64(1), "b": "str", "c": false},
},
"primitive_struct": {
`--xxx
Content-Disposition: form-data; name="a"
false
--xxx
Content-Disposition: form-data; name="b"
237628372683
--xxx
Content-Disposition: form-data; name="c"
654
--xxx
Content-Disposition: form-data; name="d"
9999.43
--xxx
Content-Disposition: form-data; name="e"
43.76
--xxx
Content-Disposition: form-data; name="f.0"
1
--xxx
Content-Disposition: form-data; name="f.1"
2
--xxx
Content-Disposition: form-data; name="f.2"
3
--xxx
Content-Disposition: form-data; name="f.3"
4
--xxx--
`,
Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
},
"slices": {
`--xxx
Content-Disposition: form-data; name="slices.0.a"
false
--xxx
Content-Disposition: form-data; name="slices.0.b"
237628372683
--xxx
Content-Disposition: form-data; name="slices.0.c"
654
--xxx
Content-Disposition: form-data; name="slices.0.d"
9999.43
--xxx
Content-Disposition: form-data; name="slices.0.e"
43.76
--xxx
Content-Disposition: form-data; name="slices.0.f.0"
1
--xxx
Content-Disposition: form-data; name="slices.0.f.1"
2
--xxx
Content-Disposition: form-data; name="slices.0.f.2"
3
--xxx
Content-Disposition: form-data; name="slices.0.f.3"
4
--xxx--
`,
Slices{
Slice: []Primitives{{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}},
},
},
"primitive_pointer_struct": {
`--xxx
Content-Disposition: form-data; name="a"
false
--xxx
Content-Disposition: form-data; name="b"
237628372683
--xxx
Content-Disposition: form-data; name="c"
654
--xxx
Content-Disposition: form-data; name="d"
9999.43
--xxx
Content-Disposition: form-data; name="e"
43.76
--xxx
Content-Disposition: form-data; name="f.0"
1
--xxx
Content-Disposition: form-data; name="f.1"
2
--xxx
Content-Disposition: form-data; name="f.2"
3
--xxx
Content-Disposition: form-data; name="f.3"
4
--xxx
Content-Disposition: form-data; name="f.4"
5
--xxx--
`,
PrimitivePointers{
A: P(false),
B: P(237628372683),
C: P(uint(654)),
D: P(9999.43),
E: P(float32(43.76)),
F: &[]int{1, 2, 3, 4, 5},
},
},
"datetime_struct": {
`--xxx
Content-Disposition: form-data; name="date"
2006-01-02
--xxx
Content-Disposition: form-data; name="date-time"
2006-01-02T15:04:05Z
--xxx--
`,
DateTime{
Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC),
DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),
},
},
"additional_properties": {
`--xxx
Content-Disposition: form-data; name="a"
true
--xxx
Content-Disposition: form-data; name="bar"
value
--xxx
Content-Disposition: form-data; name="foo"
true
--xxx--
`,
AdditionalProperties{
A: true,
Extras: map[string]interface{}{
"bar": "value",
"foo": true,
},
},
},
"recursive_struct": {
`--xxx
Content-Disposition: form-data; name="child.name"
Alex
--xxx
Content-Disposition: form-data; name="name"
Robert
--xxx--
`,
Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
},
"unknown_struct_number": {
`--xxx
Content-Disposition: form-data; name="unknown"
12
--xxx--
`,
UnknownStruct{
Unknown: 12.,
},
},
"unknown_struct_map": {
`--xxx
Content-Disposition: form-data; name="unknown.foo"
bar
--xxx--
`,
UnknownStruct{
Unknown: map[string]interface{}{
"foo": "bar",
},
},
},
"union_integer": {
`--xxx
Content-Disposition: form-data; name="union"
12
--xxx--
`,
UnionStruct{
Union: UnionInteger(12),
},
},
"union_struct_discriminated_a": {
`--xxx
Content-Disposition: form-data; name="union.a"
foo
--xxx
Content-Disposition: form-data; name="union.b"
bar
--xxx
Content-Disposition: form-data; name="union.type"
typeA
--xxx--
`,
UnionStruct{
Union: UnionStructA{
Type: "typeA",
A: "foo",
B: "bar",
},
},
},
"union_struct_discriminated_b": {
`--xxx
Content-Disposition: form-data; name="union.a"
foo
--xxx
Content-Disposition: form-data; name="union.type"
typeB
--xxx--
`,
UnionStruct{
Union: UnionStructB{
Type: "typeB",
A: "foo",
},
},
},
"union_struct_time": {
`--xxx
Content-Disposition: form-data; name="union"
2010-05-23
--xxx--
`,
UnionStruct{
Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)),
},
},
}
func TestEncode(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
buf := bytes.NewBuffer(nil)
writer := multipart.NewWriter(buf)
writer.SetBoundary("xxx")
err := Marshal(test.val, writer)
if err != nil {
t.Errorf("serialization of %v failed with error %v", test.val, err)
}
err = writer.Close()
if err != nil {
t.Errorf("serialization of %v failed with error %v", test.val, err)
}
raw := buf.Bytes()
if string(raw) != strings.ReplaceAll(test.buf, "\n", "\r\n") {
t.Errorf("expected %+#v to serialize to '%s' but got '%s'", test.val, test.buf, string(raw))
}
})
}
}

View File

@@ -0,0 +1,48 @@
package apiform
import (
"reflect"
"strings"
)
const jsonStructTag = "json"
const formStructTag = "form"
const formatStructTag = "format"
type parsedStructTag struct {
name string
required bool
extras bool
metadata bool
}
func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) {
raw, ok := field.Tag.Lookup(formStructTag)
if !ok {
raw, ok = field.Tag.Lookup(jsonStructTag)
}
if !ok {
return
}
parts := strings.Split(raw, ",")
if len(parts) == 0 {
return tag, false
}
tag.name = parts[0]
for _, part := range parts[1:] {
switch part {
case "required":
tag.required = true
case "extras":
tag.extras = true
case "metadata":
tag.metadata = true
}
}
return
}
func parseFormatStructTag(field reflect.StructField) (format string, ok bool) {
format, ok = field.Tag.Lookup(formatStructTag)
return
}

View File

@@ -0,0 +1,670 @@
package apijson
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strconv"
"sync"
"time"
"unsafe"
"github.com/tidwall/gjson"
)
// decoders is a synchronized map with roughly the following type:
// map[reflect.Type]decoderFunc
var decoders sync.Map
// Unmarshal is similar to [encoding/json.Unmarshal] and parses the JSON-encoded
// data and stores it in the given pointer.
func Unmarshal(raw []byte, to any) error {
d := &decoderBuilder{dateFormat: time.RFC3339}
return d.unmarshal(raw, to)
}
// UnmarshalRoot is like Unmarshal, but doesn't try to call MarshalJSON on the
// root element. Useful if a struct's UnmarshalJSON is overrode to use the
// behavior of this encoder versus the standard library.
func UnmarshalRoot(raw []byte, to any) error {
d := &decoderBuilder{dateFormat: time.RFC3339, root: true}
return d.unmarshal(raw, to)
}
// decoderBuilder contains the 'compile-time' state of the decoder.
type decoderBuilder struct {
// Whether or not this is the first element and called by [UnmarshalRoot], see
// the documentation there to see why this is necessary.
root bool
// The dateFormat (a format string for [time.Format]) which is chosen by the
// last struct tag that was seen.
dateFormat string
}
// decoderState contains the 'run-time' state of the decoder.
type decoderState struct {
strict bool
exactness exactness
}
// Exactness refers to how close to the type the result was if deserialization
// was successful. This is useful in deserializing unions, where you want to try
// each entry, first with strict, then with looser validation, without actually
// having to do a lot of redundant work by marshalling twice (or maybe even more
// times).
type exactness int8
const (
// Some values had to fudged a bit, for example by converting a string to an
// int, or an enum with extra values.
loose exactness = iota
// There are some extra arguments, but other wise it matches the union.
extras
// Exactly right.
exact
)
type decoderFunc func(node gjson.Result, value reflect.Value, state *decoderState) error
type decoderField struct {
tag parsedStructTag
fn decoderFunc
idx []int
goname string
}
type decoderEntry struct {
reflect.Type
dateFormat string
root bool
}
func (d *decoderBuilder) unmarshal(raw []byte, to any) error {
value := reflect.ValueOf(to).Elem()
result := gjson.ParseBytes(raw)
if !value.IsValid() {
return fmt.Errorf("apijson: cannot marshal into invalid value")
}
return d.typeDecoder(value.Type())(result, value, &decoderState{strict: false, exactness: exact})
}
func (d *decoderBuilder) typeDecoder(t reflect.Type) decoderFunc {
entry := decoderEntry{
Type: t,
dateFormat: d.dateFormat,
root: d.root,
}
if fi, ok := decoders.Load(entry); ok {
return fi.(decoderFunc)
}
// To deal with recursive types, populate the map with an
// indirect func before we build it. This type waits on the
// real func (f) to be ready and then calls it. This indirect
// func is only used for recursive types.
var (
wg sync.WaitGroup
f decoderFunc
)
wg.Add(1)
fi, loaded := decoders.LoadOrStore(entry, decoderFunc(func(node gjson.Result, v reflect.Value, state *decoderState) error {
wg.Wait()
return f(node, v, state)
}))
if loaded {
return fi.(decoderFunc)
}
// Compute the real decoder and replace the indirect func with it.
f = d.newTypeDecoder(t)
wg.Done()
decoders.Store(entry, f)
return f
}
func indirectUnmarshalerDecoder(n gjson.Result, v reflect.Value, state *decoderState) error {
return v.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(n.Raw))
}
func unmarshalerDecoder(n gjson.Result, v reflect.Value, state *decoderState) error {
if v.Kind() == reflect.Pointer && v.CanSet() {
v.Set(reflect.New(v.Type().Elem()))
}
return v.Interface().(json.Unmarshaler).UnmarshalJSON([]byte(n.Raw))
}
func (d *decoderBuilder) newTypeDecoder(t reflect.Type) decoderFunc {
if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
return d.newTimeTypeDecoder(t)
}
if !d.root && t.Implements(reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()) {
return unmarshalerDecoder
}
if !d.root && reflect.PointerTo(t).Implements(reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()) {
if _, ok := unionVariants[t]; !ok {
return indirectUnmarshalerDecoder
}
}
d.root = false
if _, ok := unionRegistry[t]; ok {
return d.newUnionDecoder(t)
}
switch t.Kind() {
case reflect.Pointer:
inner := t.Elem()
innerDecoder := d.typeDecoder(inner)
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
if !v.IsValid() {
return fmt.Errorf("apijson: unexpected invalid reflection value %+#v", v)
}
newValue := reflect.New(inner).Elem()
err := innerDecoder(n, newValue, state)
if err != nil {
return err
}
v.Set(newValue.Addr())
return nil
}
case reflect.Struct:
return d.newStructTypeDecoder(t)
case reflect.Array:
fallthrough
case reflect.Slice:
return d.newArrayTypeDecoder(t)
case reflect.Map:
return d.newMapDecoder(t)
case reflect.Interface:
return func(node gjson.Result, value reflect.Value, state *decoderState) error {
if !value.IsValid() {
return fmt.Errorf("apijson: unexpected invalid value %+#v", value)
}
if node.Value() != nil && value.CanSet() {
value.Set(reflect.ValueOf(node.Value()))
}
return nil
}
default:
return d.newPrimitiveTypeDecoder(t)
}
}
// newUnionDecoder returns a decoderFunc that deserializes into a union using an
// algorithm roughly similar to Pydantic's [smart algorithm].
//
// Conceptually this is equivalent to choosing the best schema based on how 'exact'
// the deserialization is for each of the schemas.
//
// If there is a tie in the level of exactness, then the tie is broken
// left-to-right.
//
// [smart algorithm]: https://docs.pydantic.dev/latest/concepts/unions/#smart-mode
func (d *decoderBuilder) newUnionDecoder(t reflect.Type) decoderFunc {
unionEntry, ok := unionRegistry[t]
if !ok {
panic("apijson: couldn't find union of type " + t.String() + " in union registry")
}
decoders := []decoderFunc{}
for _, variant := range unionEntry.variants {
decoder := d.typeDecoder(variant.Type)
decoders = append(decoders, decoder)
}
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
// If there is a discriminator match, circumvent the exactness logic entirely
for idx, variant := range unionEntry.variants {
decoder := decoders[idx]
if variant.TypeFilter != n.Type {
continue
}
if len(unionEntry.discriminatorKey) != 0 {
discriminatorValue := n.Get(EscapeSJSONKey(unionEntry.discriminatorKey)).Value()
if discriminatorValue == variant.DiscriminatorValue {
inner := reflect.New(variant.Type).Elem()
err := decoder(n, inner, state)
v.Set(inner)
return err
}
}
}
// Set bestExactness to worse than loose
bestExactness := loose - 1
for idx, variant := range unionEntry.variants {
decoder := decoders[idx]
if variant.TypeFilter != n.Type {
continue
}
sub := decoderState{strict: state.strict, exactness: exact}
inner := reflect.New(variant.Type).Elem()
err := decoder(n, inner, &sub)
if err != nil {
continue
}
if sub.exactness == exact {
v.Set(inner)
return nil
}
if sub.exactness > bestExactness {
v.Set(inner)
bestExactness = sub.exactness
}
}
if bestExactness < loose {
return errors.New("apijson: was not able to coerce type as union")
}
if guardStrict(state, bestExactness != exact) {
return errors.New("apijson: was not able to coerce type as union strictly")
}
return nil
}
}
func (d *decoderBuilder) newMapDecoder(t reflect.Type) decoderFunc {
keyType := t.Key()
itemType := t.Elem()
itemDecoder := d.typeDecoder(itemType)
return func(node gjson.Result, value reflect.Value, state *decoderState) (err error) {
mapValue := reflect.MakeMapWithSize(t, len(node.Map()))
node.ForEach(func(key, value gjson.Result) bool {
// It's fine for us to just use `ValueOf` here because the key types will
// always be primitive types so we don't need to decode it using the standard pattern
keyValue := reflect.ValueOf(key.Value())
if !keyValue.IsValid() {
if err == nil {
err = fmt.Errorf("apijson: received invalid key type %v", keyValue.String())
}
return false
}
if keyValue.Type() != keyType {
if err == nil {
err = fmt.Errorf("apijson: expected key type %v but got %v", keyType, keyValue.Type())
}
return false
}
itemValue := reflect.New(itemType).Elem()
itemerr := itemDecoder(value, itemValue, state)
if itemerr != nil {
if err == nil {
err = itemerr
}
return false
}
mapValue.SetMapIndex(keyValue, itemValue)
return true
})
if err != nil {
return err
}
value.Set(mapValue)
return nil
}
}
func (d *decoderBuilder) newArrayTypeDecoder(t reflect.Type) decoderFunc {
itemDecoder := d.typeDecoder(t.Elem())
return func(node gjson.Result, value reflect.Value, state *decoderState) (err error) {
if !node.IsArray() {
return fmt.Errorf("apijson: could not deserialize to an array")
}
arrayNode := node.Array()
arrayValue := reflect.MakeSlice(reflect.SliceOf(t.Elem()), len(arrayNode), len(arrayNode))
for i, itemNode := range arrayNode {
err = itemDecoder(itemNode, arrayValue.Index(i), state)
if err != nil {
return err
}
}
value.Set(arrayValue)
return nil
}
}
func (d *decoderBuilder) newStructTypeDecoder(t reflect.Type) decoderFunc {
// map of json field name to struct field decoders
decoderFields := map[string]decoderField{}
anonymousDecoders := []decoderField{}
extraDecoder := (*decoderField)(nil)
inlineDecoder := (*decoderField)(nil)
for i := 0; i < t.NumField(); i++ {
idx := []int{i}
field := t.FieldByIndex(idx)
if !field.IsExported() {
continue
}
// If this is an embedded struct, traverse one level deeper to extract
// the fields and get their encoders as well.
if field.Anonymous {
anonymousDecoders = append(anonymousDecoders, decoderField{
fn: d.typeDecoder(field.Type),
idx: idx[:],
})
continue
}
// If json tag is not present, then we skip, which is intentionally
// different behavior from the stdlib.
ptag, ok := parseJSONStructTag(field)
if !ok {
continue
}
// We only want to support unexported fields if they're tagged with
// `extras` because that field shouldn't be part of the public API.
if ptag.extras {
extraDecoder = &decoderField{ptag, d.typeDecoder(field.Type.Elem()), idx, field.Name}
continue
}
if ptag.inline {
inlineDecoder = &decoderField{ptag, d.typeDecoder(field.Type), idx, field.Name}
continue
}
if ptag.metadata {
continue
}
oldFormat := d.dateFormat
dateFormat, ok := parseFormatStructTag(field)
if ok {
switch dateFormat {
case "date-time":
d.dateFormat = time.RFC3339
case "date":
d.dateFormat = "2006-01-02"
}
}
decoderFields[ptag.name] = decoderField{ptag, d.typeDecoder(field.Type), idx, field.Name}
d.dateFormat = oldFormat
}
return func(node gjson.Result, value reflect.Value, state *decoderState) (err error) {
if field := value.FieldByName("JSON"); field.IsValid() {
if raw := field.FieldByName("raw"); raw.IsValid() {
setUnexportedField(raw, node.Raw)
}
}
for _, decoder := range anonymousDecoders {
// ignore errors
decoder.fn(node, value.FieldByIndex(decoder.idx), state)
}
if inlineDecoder != nil {
var meta Field
dest := value.FieldByIndex(inlineDecoder.idx)
isValid := false
if dest.IsValid() && node.Type != gjson.Null {
err = inlineDecoder.fn(node, dest, state)
if err == nil {
isValid = true
}
}
if node.Type == gjson.Null {
meta = Field{
raw: node.Raw,
status: null,
}
} else if !isValid {
meta = Field{
raw: node.Raw,
status: invalid,
}
} else if isValid {
meta = Field{
raw: node.Raw,
status: valid,
}
}
if metadata := getSubField(value, inlineDecoder.idx, inlineDecoder.goname); metadata.IsValid() {
metadata.Set(reflect.ValueOf(meta))
}
return err
}
typedExtraType := reflect.Type(nil)
typedExtraFields := reflect.Value{}
if extraDecoder != nil {
typedExtraType = value.FieldByIndex(extraDecoder.idx).Type()
typedExtraFields = reflect.MakeMap(typedExtraType)
}
untypedExtraFields := map[string]Field{}
for fieldName, itemNode := range node.Map() {
df, explicit := decoderFields[fieldName]
var (
dest reflect.Value
fn decoderFunc
meta Field
)
if explicit {
fn = df.fn
dest = value.FieldByIndex(df.idx)
}
if !explicit && extraDecoder != nil {
dest = reflect.New(typedExtraType.Elem()).Elem()
fn = extraDecoder.fn
}
isValid := false
if dest.IsValid() && itemNode.Type != gjson.Null {
err = fn(itemNode, dest, state)
if err == nil {
isValid = true
}
}
if itemNode.Type == gjson.Null {
meta = Field{
raw: itemNode.Raw,
status: null,
}
} else if !isValid {
meta = Field{
raw: itemNode.Raw,
status: invalid,
}
} else if isValid {
meta = Field{
raw: itemNode.Raw,
status: valid,
}
}
if explicit {
if metadata := getSubField(value, df.idx, df.goname); metadata.IsValid() {
metadata.Set(reflect.ValueOf(meta))
}
}
if !explicit {
untypedExtraFields[fieldName] = meta
}
if !explicit && extraDecoder != nil {
typedExtraFields.SetMapIndex(reflect.ValueOf(fieldName), dest)
}
}
if extraDecoder != nil && typedExtraFields.Len() > 0 {
value.FieldByIndex(extraDecoder.idx).Set(typedExtraFields)
}
// Set exactness to 'extras' if there are untyped, extra fields.
if len(untypedExtraFields) > 0 && state.exactness > extras {
state.exactness = extras
}
if metadata := getSubField(value, []int{-1}, "ExtraFields"); metadata.IsValid() && len(untypedExtraFields) > 0 {
metadata.Set(reflect.ValueOf(untypedExtraFields))
}
return nil
}
}
func (d *decoderBuilder) newPrimitiveTypeDecoder(t reflect.Type) decoderFunc {
switch t.Kind() {
case reflect.String:
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
v.SetString(n.String())
if guardStrict(state, n.Type != gjson.String) {
return fmt.Errorf("apijson: failed to parse string strictly")
}
// Everything that is not an object can be loosely stringified.
if n.Type == gjson.JSON {
return fmt.Errorf("apijson: failed to parse string")
}
if guardUnknown(state, v) {
return fmt.Errorf("apijson: failed string enum validation")
}
return nil
}
case reflect.Bool:
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
v.SetBool(n.Bool())
if guardStrict(state, n.Type != gjson.True && n.Type != gjson.False) {
return fmt.Errorf("apijson: failed to parse bool strictly")
}
// Numbers and strings that are either 'true' or 'false' can be loosely
// deserialized as bool.
if n.Type == gjson.String && (n.Raw != "true" && n.Raw != "false") || n.Type == gjson.JSON {
return fmt.Errorf("apijson: failed to parse bool")
}
if guardUnknown(state, v) {
return fmt.Errorf("apijson: failed bool enum validation")
}
return nil
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
v.SetInt(n.Int())
if guardStrict(state, n.Type != gjson.Number || n.Num != float64(int(n.Num))) {
return fmt.Errorf("apijson: failed to parse int strictly")
}
// Numbers, booleans, and strings that maybe look like numbers can be
// loosely deserialized as numbers.
if n.Type == gjson.JSON || (n.Type == gjson.String && !canParseAsNumber(n.Str)) {
return fmt.Errorf("apijson: failed to parse int")
}
if guardUnknown(state, v) {
return fmt.Errorf("apijson: failed int enum validation")
}
return nil
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
v.SetUint(n.Uint())
if guardStrict(state, n.Type != gjson.Number || n.Num != float64(int(n.Num)) || n.Num < 0) {
return fmt.Errorf("apijson: failed to parse uint strictly")
}
// Numbers, booleans, and strings that maybe look like numbers can be
// loosely deserialized as uint.
if n.Type == gjson.JSON || (n.Type == gjson.String && !canParseAsNumber(n.Str)) {
return fmt.Errorf("apijson: failed to parse uint")
}
if guardUnknown(state, v) {
return fmt.Errorf("apijson: failed uint enum validation")
}
return nil
}
case reflect.Float32, reflect.Float64:
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
v.SetFloat(n.Float())
if guardStrict(state, n.Type != gjson.Number) {
return fmt.Errorf("apijson: failed to parse float strictly")
}
// Numbers, booleans, and strings that maybe look like numbers can be
// loosely deserialized as floats.
if n.Type == gjson.JSON || (n.Type == gjson.String && !canParseAsNumber(n.Str)) {
return fmt.Errorf("apijson: failed to parse float")
}
if guardUnknown(state, v) {
return fmt.Errorf("apijson: failed float enum validation")
}
return nil
}
default:
return func(node gjson.Result, v reflect.Value, state *decoderState) error {
return fmt.Errorf("unknown type received at primitive decoder: %s", t.String())
}
}
}
func (d *decoderBuilder) newTimeTypeDecoder(t reflect.Type) decoderFunc {
format := d.dateFormat
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
parsed, err := time.Parse(format, n.Str)
if err == nil {
v.Set(reflect.ValueOf(parsed).Convert(t))
return nil
}
if guardStrict(state, true) {
return err
}
layouts := []string{
"2006-01-02",
"2006-01-02T15:04:05Z07:00",
"2006-01-02T15:04:05Z0700",
"2006-01-02T15:04:05",
"2006-01-02 15:04:05Z07:00",
"2006-01-02 15:04:05Z0700",
"2006-01-02 15:04:05",
}
for _, layout := range layouts {
parsed, err := time.Parse(layout, n.Str)
if err == nil {
v.Set(reflect.ValueOf(parsed).Convert(t))
return nil
}
}
return fmt.Errorf("unable to leniently parse date-time string: %s", n.Str)
}
}
func setUnexportedField(field reflect.Value, value interface{}) {
reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Set(reflect.ValueOf(value))
}
func guardStrict(state *decoderState, cond bool) bool {
if !cond {
return false
}
if state.strict {
return true
}
state.exactness = loose
return false
}
func canParseAsNumber(str string) bool {
_, err := strconv.ParseFloat(str, 64)
return err == nil
}
func guardUnknown(state *decoderState, v reflect.Value) bool {
if have, ok := v.Interface().(interface{ IsKnown() bool }); guardStrict(state, ok && !have.IsKnown()) {
return true
}
return false
}

View File

@@ -0,0 +1,398 @@
package apijson
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/tidwall/sjson"
"github.com/sst/opencode-sdk-go/internal/param"
)
var encoders sync.Map // map[encoderEntry]encoderFunc
// If we want to set a literal key value into JSON using sjson, we need to make sure it doesn't have
// special characters that sjson interprets as a path.
var EscapeSJSONKey = strings.NewReplacer("\\", "\\\\", "|", "\\|", "#", "\\#", "@", "\\@", "*", "\\*", ".", "\\.", ":", "\\:", "?", "\\?").Replace
func Marshal(value interface{}) ([]byte, error) {
e := &encoder{dateFormat: time.RFC3339}
return e.marshal(value)
}
func MarshalRoot(value interface{}) ([]byte, error) {
e := &encoder{root: true, dateFormat: time.RFC3339}
return e.marshal(value)
}
type encoder struct {
dateFormat string
root bool
}
type encoderFunc func(value reflect.Value) ([]byte, error)
type encoderField struct {
tag parsedStructTag
fn encoderFunc
idx []int
}
type encoderEntry struct {
reflect.Type
dateFormat string
root bool
}
func (e *encoder) marshal(value interface{}) ([]byte, error) {
val := reflect.ValueOf(value)
if !val.IsValid() {
return nil, nil
}
typ := val.Type()
enc := e.typeEncoder(typ)
return enc(val)
}
func (e *encoder) typeEncoder(t reflect.Type) encoderFunc {
entry := encoderEntry{
Type: t,
dateFormat: e.dateFormat,
root: e.root,
}
if fi, ok := encoders.Load(entry); ok {
return fi.(encoderFunc)
}
// To deal with recursive types, populate the map with an
// indirect func before we build it. This type waits on the
// real func (f) to be ready and then calls it. This indirect
// func is only used for recursive types.
var (
wg sync.WaitGroup
f encoderFunc
)
wg.Add(1)
fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(v reflect.Value) ([]byte, error) {
wg.Wait()
return f(v)
}))
if loaded {
return fi.(encoderFunc)
}
// Compute the real encoder and replace the indirect func with it.
f = e.newTypeEncoder(t)
wg.Done()
encoders.Store(entry, f)
return f
}
func marshalerEncoder(v reflect.Value) ([]byte, error) {
return v.Interface().(json.Marshaler).MarshalJSON()
}
func indirectMarshalerEncoder(v reflect.Value) ([]byte, error) {
return v.Addr().Interface().(json.Marshaler).MarshalJSON()
}
func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc {
if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
return e.newTimeTypeEncoder()
}
if !e.root && t.Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) {
return marshalerEncoder
}
if !e.root && reflect.PointerTo(t).Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) {
return indirectMarshalerEncoder
}
e.root = false
switch t.Kind() {
case reflect.Pointer:
inner := t.Elem()
innerEncoder := e.typeEncoder(inner)
return func(v reflect.Value) ([]byte, error) {
if !v.IsValid() || v.IsNil() {
return nil, nil
}
return innerEncoder(v.Elem())
}
case reflect.Struct:
return e.newStructTypeEncoder(t)
case reflect.Array:
fallthrough
case reflect.Slice:
return e.newArrayTypeEncoder(t)
case reflect.Map:
return e.newMapEncoder(t)
case reflect.Interface:
return e.newInterfaceEncoder()
default:
return e.newPrimitiveTypeEncoder(t)
}
}
func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc {
switch t.Kind() {
// Note that we could use `gjson` to encode these types but it would complicate our
// code more and this current code shouldn't cause any issues
case reflect.String:
return func(v reflect.Value) ([]byte, error) {
return json.Marshal(v.Interface())
}
case reflect.Bool:
return func(v reflect.Value) ([]byte, error) {
if v.Bool() {
return []byte("true"), nil
}
return []byte("false"), nil
}
case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
return func(v reflect.Value) ([]byte, error) {
return []byte(strconv.FormatInt(v.Int(), 10)), nil
}
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return func(v reflect.Value) ([]byte, error) {
return []byte(strconv.FormatUint(v.Uint(), 10)), nil
}
case reflect.Float32:
return func(v reflect.Value) ([]byte, error) {
return []byte(strconv.FormatFloat(v.Float(), 'f', -1, 32)), nil
}
case reflect.Float64:
return func(v reflect.Value) ([]byte, error) {
return []byte(strconv.FormatFloat(v.Float(), 'f', -1, 64)), nil
}
default:
return func(v reflect.Value) ([]byte, error) {
return nil, fmt.Errorf("unknown type received at primitive encoder: %s", t.String())
}
}
}
func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc {
itemEncoder := e.typeEncoder(t.Elem())
return func(value reflect.Value) ([]byte, error) {
json := []byte("[]")
for i := 0; i < value.Len(); i++ {
var value, err = itemEncoder(value.Index(i))
if err != nil {
return nil, err
}
if value == nil {
// Assume that empty items should be inserted as `null` so that the output array
// will be the same length as the input array
value = []byte("null")
}
json, err = sjson.SetRawBytes(json, "-1", value)
if err != nil {
return nil, err
}
}
return json, nil
}
}
func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) {
return e.newFieldTypeEncoder(t)
}
encoderFields := []encoderField{}
extraEncoder := (*encoderField)(nil)
// This helper allows us to recursively collect field encoders into a flat
// array. The parameter `index` keeps track of the access patterns necessary
// to get to some field.
var collectEncoderFields func(r reflect.Type, index []int)
collectEncoderFields = func(r reflect.Type, index []int) {
for i := 0; i < r.NumField(); i++ {
idx := append(index, i)
field := t.FieldByIndex(idx)
if !field.IsExported() {
continue
}
// If this is an embedded struct, traverse one level deeper to extract
// the field and get their encoders as well.
if field.Anonymous {
collectEncoderFields(field.Type, idx)
continue
}
// If json tag is not present, then we skip, which is intentionally
// different behavior from the stdlib.
ptag, ok := parseJSONStructTag(field)
if !ok {
continue
}
// We only want to support unexported field if they're tagged with
// `extras` because that field shouldn't be part of the public API. We
// also want to only keep the top level extras
if ptag.extras && len(index) == 0 {
extraEncoder = &encoderField{ptag, e.typeEncoder(field.Type.Elem()), idx}
continue
}
if ptag.name == "-" {
continue
}
dateFormat, ok := parseFormatStructTag(field)
oldFormat := e.dateFormat
if ok {
switch dateFormat {
case "date-time":
e.dateFormat = time.RFC3339
case "date":
e.dateFormat = "2006-01-02"
}
}
encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx})
e.dateFormat = oldFormat
}
}
collectEncoderFields(t, []int{})
// Ensure deterministic output by sorting by lexicographic order
sort.Slice(encoderFields, func(i, j int) bool {
return encoderFields[i].tag.name < encoderFields[j].tag.name
})
return func(value reflect.Value) (json []byte, err error) {
json = []byte("{}")
for _, ef := range encoderFields {
field := value.FieldByIndex(ef.idx)
encoded, err := ef.fn(field)
if err != nil {
return nil, err
}
if encoded == nil {
continue
}
json, err = sjson.SetRawBytes(json, EscapeSJSONKey(ef.tag.name), encoded)
if err != nil {
return nil, err
}
}
if extraEncoder != nil {
json, err = e.encodeMapEntries(json, value.FieldByIndex(extraEncoder.idx))
if err != nil {
return nil, err
}
}
return
}
}
func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc {
f, _ := t.FieldByName("Value")
enc := e.typeEncoder(f.Type)
return func(value reflect.Value) (json []byte, err error) {
present := value.FieldByName("Present")
if !present.Bool() {
return nil, nil
}
null := value.FieldByName("Null")
if null.Bool() {
return []byte("null"), nil
}
raw := value.FieldByName("Raw")
if !raw.IsNil() {
return e.typeEncoder(raw.Type())(raw)
}
return enc(value.FieldByName("Value"))
}
}
func (e *encoder) newTimeTypeEncoder() encoderFunc {
format := e.dateFormat
return func(value reflect.Value) (json []byte, err error) {
return []byte(`"` + value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format) + `"`), nil
}
}
func (e encoder) newInterfaceEncoder() encoderFunc {
return func(value reflect.Value) ([]byte, error) {
value = value.Elem()
if !value.IsValid() {
return nil, nil
}
return e.typeEncoder(value.Type())(value)
}
}
// Given a []byte of json (may either be an empty object or an object that already contains entries)
// encode all of the entries in the map to the json byte array.
func (e *encoder) encodeMapEntries(json []byte, v reflect.Value) ([]byte, error) {
type mapPair struct {
key []byte
value reflect.Value
}
pairs := []mapPair{}
keyEncoder := e.typeEncoder(v.Type().Key())
iter := v.MapRange()
for iter.Next() {
var encodedKeyString string
if iter.Key().Type().Kind() == reflect.String {
encodedKeyString = iter.Key().String()
} else {
var err error
encodedKeyBytes, err := keyEncoder(iter.Key())
if err != nil {
return nil, err
}
encodedKeyString = string(encodedKeyBytes)
}
encodedKey := []byte(encodedKeyString)
pairs = append(pairs, mapPair{key: encodedKey, value: iter.Value()})
}
// Ensure deterministic output
sort.Slice(pairs, func(i, j int) bool {
return bytes.Compare(pairs[i].key, pairs[j].key) < 0
})
elementEncoder := e.typeEncoder(v.Type().Elem())
for _, p := range pairs {
encodedValue, err := elementEncoder(p.value)
if err != nil {
return nil, err
}
if len(encodedValue) == 0 {
continue
}
json, err = sjson.SetRawBytes(json, EscapeSJSONKey(string(p.key)), encodedValue)
if err != nil {
return nil, err
}
}
return json, nil
}
func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc {
return func(value reflect.Value) ([]byte, error) {
json := []byte("{}")
var err error
json, err = e.encodeMapEntries(json, value)
if err != nil {
return nil, err
}
return json, nil
}
}

View File

@@ -0,0 +1,41 @@
package apijson
import "reflect"
type status uint8
const (
missing status = iota
null
invalid
valid
)
type Field struct {
raw string
status status
}
// Returns true if the field is explicitly `null` _or_ if it is not present at all (ie, missing).
// To check if the field's key is present in the JSON with an explicit null value,
// you must check `f.IsNull() && !f.IsMissing()`.
func (j Field) IsNull() bool { return j.status <= null }
func (j Field) IsMissing() bool { return j.status == missing }
func (j Field) IsInvalid() bool { return j.status == invalid }
func (j Field) Raw() string { return j.raw }
func getSubField(root reflect.Value, index []int, name string) reflect.Value {
strct := root.FieldByIndex(index[:len(index)-1])
if !strct.IsValid() {
panic("couldn't find encapsulating struct for field " + name)
}
meta := strct.FieldByName("JSON")
if !meta.IsValid() {
return reflect.Value{}
}
field := meta.FieldByName(name)
if !field.IsValid() {
return reflect.Value{}
}
return field
}

View File

@@ -0,0 +1,66 @@
package apijson
import (
"testing"
"time"
"github.com/sst/opencode-sdk-go/internal/param"
)
type Struct struct {
A string `json:"a"`
B int64 `json:"b"`
}
type FieldStruct struct {
A param.Field[string] `json:"a"`
B param.Field[int64] `json:"b"`
C param.Field[Struct] `json:"c"`
D param.Field[time.Time] `json:"d" format:"date"`
E param.Field[time.Time] `json:"e" format:"date-time"`
F param.Field[int64] `json:"f"`
}
func TestFieldMarshal(t *testing.T) {
tests := map[string]struct {
value interface{}
expected string
}{
"null_string": {param.Field[string]{Present: true, Null: true}, "null"},
"null_int": {param.Field[int]{Present: true, Null: true}, "null"},
"null_int64": {param.Field[int64]{Present: true, Null: true}, "null"},
"null_struct": {param.Field[Struct]{Present: true, Null: true}, "null"},
"string": {param.Field[string]{Present: true, Value: "string"}, `"string"`},
"int": {param.Field[int]{Present: true, Value: 123}, "123"},
"int64": {param.Field[int64]{Present: true, Value: int64(123456789123456789)}, "123456789123456789"},
"struct": {param.Field[Struct]{Present: true, Value: Struct{A: "yo", B: 123}}, `{"a":"yo","b":123}`},
"string_raw": {param.Field[int]{Present: true, Raw: "string"}, `"string"`},
"int_raw": {param.Field[int]{Present: true, Raw: 123}, "123"},
"int64_raw": {param.Field[int]{Present: true, Raw: int64(123456789123456789)}, "123456789123456789"},
"struct_raw": {param.Field[int]{Present: true, Raw: Struct{A: "yo", B: 123}}, `{"a":"yo","b":123}`},
"param_struct": {
FieldStruct{
A: param.Field[string]{Present: true, Value: "hello"},
B: param.Field[int64]{Present: true, Value: int64(12)},
D: param.Field[time.Time]{Present: true, Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)},
E: param.Field[time.Time]{Present: true, Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)},
},
`{"a":"hello","b":12,"d":"2023-03-18","e":"2023-03-18T14:47:38Z"}`,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
b, err := Marshal(test.value)
if err != nil {
t.Fatalf("didn't expect error %v", err)
}
if string(b) != test.expected {
t.Fatalf("expected %s, received %s", test.expected, string(b))
}
})
}
}

View File

@@ -0,0 +1,617 @@
package apijson
import (
"reflect"
"strings"
"testing"
"time"
"github.com/tidwall/gjson"
)
func P[T any](v T) *T { return &v }
type Primitives struct {
A bool `json:"a"`
B int `json:"b"`
C uint `json:"c"`
D float64 `json:"d"`
E float32 `json:"e"`
F []int `json:"f"`
}
type PrimitivePointers struct {
A *bool `json:"a"`
B *int `json:"b"`
C *uint `json:"c"`
D *float64 `json:"d"`
E *float32 `json:"e"`
F *[]int `json:"f"`
}
type Slices struct {
Slice []Primitives `json:"slices"`
}
type DateTime struct {
Date time.Time `json:"date" format:"date"`
DateTime time.Time `json:"date-time" format:"date-time"`
}
type AdditionalProperties struct {
A bool `json:"a"`
ExtraFields map[string]interface{} `json:"-,extras"`
}
type TypedAdditionalProperties struct {
A bool `json:"a"`
ExtraFields map[string]int `json:"-,extras"`
}
type EmbeddedStruct struct {
A bool `json:"a"`
B string `json:"b"`
JSON EmbeddedStructJSON
}
type EmbeddedStructJSON struct {
A Field
B Field
ExtraFields map[string]Field
raw string
}
type EmbeddedStructs struct {
EmbeddedStruct
A *int `json:"a"`
ExtraFields map[string]interface{} `json:"-,extras"`
JSON EmbeddedStructsJSON
}
type EmbeddedStructsJSON struct {
A Field
ExtraFields map[string]Field
raw string
}
type Recursive struct {
Name string `json:"name"`
Child *Recursive `json:"child"`
}
type JSONFieldStruct struct {
A bool `json:"a"`
B int64 `json:"b"`
C string `json:"c"`
D string `json:"d"`
ExtraFields map[string]int64 `json:"-,extras"`
JSON JSONFieldStructJSON `json:"-,metadata"`
}
type JSONFieldStructJSON struct {
A Field
B Field
C Field
D Field
ExtraFields map[string]Field
raw string
}
type UnknownStruct struct {
Unknown interface{} `json:"unknown"`
}
type UnionStruct struct {
Union Union `json:"union" format:"date"`
}
type Union interface {
union()
}
type Inline struct {
InlineField Primitives `json:"-,inline"`
JSON InlineJSON `json:"-,metadata"`
}
type InlineArray struct {
InlineField []string `json:"-,inline"`
JSON InlineJSON `json:"-,metadata"`
}
type InlineJSON struct {
InlineField Field
raw string
}
type UnionInteger int64
func (UnionInteger) union() {}
type UnionStructA struct {
Type string `json:"type"`
A string `json:"a"`
B string `json:"b"`
}
func (UnionStructA) union() {}
type UnionStructB struct {
Type string `json:"type"`
A string `json:"a"`
}
func (UnionStructB) union() {}
type UnionTime time.Time
func (UnionTime) union() {}
func init() {
RegisterUnion(reflect.TypeOf((*Union)(nil)).Elem(), "type",
UnionVariant{
TypeFilter: gjson.String,
Type: reflect.TypeOf(UnionTime{}),
},
UnionVariant{
TypeFilter: gjson.Number,
Type: reflect.TypeOf(UnionInteger(0)),
},
UnionVariant{
TypeFilter: gjson.JSON,
DiscriminatorValue: "typeA",
Type: reflect.TypeOf(UnionStructA{}),
},
UnionVariant{
TypeFilter: gjson.JSON,
DiscriminatorValue: "typeB",
Type: reflect.TypeOf(UnionStructB{}),
},
)
}
type ComplexUnionStruct struct {
Union ComplexUnion `json:"union"`
}
type ComplexUnion interface {
complexUnion()
}
type ComplexUnionA struct {
Boo string `json:"boo"`
Foo bool `json:"foo"`
}
func (ComplexUnionA) complexUnion() {}
type ComplexUnionB struct {
Boo bool `json:"boo"`
Foo string `json:"foo"`
}
func (ComplexUnionB) complexUnion() {}
type ComplexUnionC struct {
Boo int64 `json:"boo"`
}
func (ComplexUnionC) complexUnion() {}
type ComplexUnionTypeA struct {
Baz int64 `json:"baz"`
Type TypeA `json:"type"`
}
func (ComplexUnionTypeA) complexUnion() {}
type TypeA string
func (t TypeA) IsKnown() bool {
return t == "a"
}
type ComplexUnionTypeB struct {
Baz int64 `json:"baz"`
Type TypeB `json:"type"`
}
type TypeB string
func (t TypeB) IsKnown() bool {
return t == "b"
}
type UnmarshalStruct struct {
Foo string `json:"foo"`
prop bool `json:"-"`
}
func (r *UnmarshalStruct) UnmarshalJSON(json []byte) error {
r.prop = true
return UnmarshalRoot(json, r)
}
func (ComplexUnionTypeB) complexUnion() {}
func init() {
RegisterUnion(reflect.TypeOf((*ComplexUnion)(nil)).Elem(), "",
UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(ComplexUnionA{}),
},
UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(ComplexUnionB{}),
},
UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(ComplexUnionC{}),
},
UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(ComplexUnionTypeA{}),
},
UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(ComplexUnionTypeB{}),
},
)
}
type MarshallingUnionStruct struct {
Union MarshallingUnion
}
func (r *MarshallingUnionStruct) UnmarshalJSON(data []byte) (err error) {
*r = MarshallingUnionStruct{}
err = UnmarshalRoot(data, &r.Union)
return
}
func (r MarshallingUnionStruct) MarshalJSON() (data []byte, err error) {
return MarshalRoot(r.Union)
}
type MarshallingUnion interface {
marshallingUnion()
}
type MarshallingUnionA struct {
Boo string `json:"boo"`
}
func (MarshallingUnionA) marshallingUnion() {}
func (r *MarshallingUnionA) UnmarshalJSON(data []byte) (err error) {
return UnmarshalRoot(data, r)
}
type MarshallingUnionB struct {
Foo string `json:"foo"`
}
func (MarshallingUnionB) marshallingUnion() {}
func (r *MarshallingUnionB) UnmarshalJSON(data []byte) (err error) {
return UnmarshalRoot(data, r)
}
func init() {
RegisterUnion(
reflect.TypeOf((*MarshallingUnion)(nil)).Elem(),
"",
UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(MarshallingUnionA{}),
},
UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(MarshallingUnionB{}),
},
)
}
var tests = map[string]struct {
buf string
val interface{}
}{
"true": {"true", true},
"false": {"false", false},
"int": {"1", 1},
"int_bigger": {"12324", 12324},
"int_string_coerce": {`"65"`, 65},
"int_boolean_coerce": {"true", 1},
"int64": {"1", int64(1)},
"int64_huge": {"123456789123456789", int64(123456789123456789)},
"uint": {"1", uint(1)},
"uint_bigger": {"12324", uint(12324)},
"uint_coerce": {`"65"`, uint(65)},
"float_1.54": {"1.54", float32(1.54)},
"float_1.89": {"1.89", float64(1.89)},
"string": {`"str"`, "str"},
"string_int_coerce": {`12`, "12"},
"array_string": {`["foo","bar"]`, []string{"foo", "bar"}},
"array_int": {`[1,2]`, []int{1, 2}},
"array_int_coerce": {`["1",2]`, []int{1, 2}},
"ptr_true": {"true", P(true)},
"ptr_false": {"false", P(false)},
"ptr_int": {"1", P(1)},
"ptr_int_bigger": {"12324", P(12324)},
"ptr_int_string_coerce": {`"65"`, P(65)},
"ptr_int_boolean_coerce": {"true", P(1)},
"ptr_int64": {"1", P(int64(1))},
"ptr_int64_huge": {"123456789123456789", P(int64(123456789123456789))},
"ptr_uint": {"1", P(uint(1))},
"ptr_uint_bigger": {"12324", P(uint(12324))},
"ptr_uint_coerce": {`"65"`, P(uint(65))},
"ptr_float_1.54": {"1.54", P(float32(1.54))},
"ptr_float_1.89": {"1.89", P(float64(1.89))},
"date_time": {`"2007-03-01T13:00:00Z"`, time.Date(2007, time.March, 1, 13, 0, 0, 0, time.UTC)},
"date_time_nano_coerce": {`"2007-03-01T13:03:05.123456789Z"`, time.Date(2007, time.March, 1, 13, 3, 5, 123456789, time.UTC)},
"date_time_missing_t_coerce": {`"2007-03-01 13:03:05Z"`, time.Date(2007, time.March, 1, 13, 3, 5, 0, time.UTC)},
"date_time_missing_timezone_coerce": {`"2007-03-01T13:03:05"`, time.Date(2007, time.March, 1, 13, 3, 5, 0, time.UTC)},
// note: using -1200 to minimize probability of conflicting with the local timezone of the test runner
// see https://en.wikipedia.org/wiki/UTC%E2%88%9212:00
"date_time_missing_timezone_colon_coerce": {`"2007-03-01T13:03:05-1200"`, time.Date(2007, time.March, 1, 13, 3, 5, 0, time.FixedZone("", -12*60*60))},
"date_time_nano_missing_t_coerce": {`"2007-03-01 13:03:05.123456789Z"`, time.Date(2007, time.March, 1, 13, 3, 5, 123456789, time.UTC)},
"map_string": {`{"foo":"bar"}`, map[string]string{"foo": "bar"}},
"map_string_with_sjson_path_chars": {`{":a.b.c*:d*-1e.f@g?h":"bar"}`, map[string]string{":a.b.c*:d*-1e.f@g?h": "bar"}},
"map_interface": {`{"a":1,"b":"str","c":false}`, map[string]interface{}{"a": float64(1), "b": "str", "c": false}},
"primitive_struct": {
`{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4]}`,
Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
},
"slices": {
`{"slices":[{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4]}]}`,
Slices{
Slice: []Primitives{{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}},
},
},
"primitive_pointer_struct": {
`{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4,5]}`,
PrimitivePointers{
A: P(false),
B: P(237628372683),
C: P(uint(654)),
D: P(9999.43),
E: P(float32(43.76)),
F: &[]int{1, 2, 3, 4, 5},
},
},
"datetime_struct": {
`{"date":"2006-01-02","date-time":"2006-01-02T15:04:05Z"}`,
DateTime{
Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC),
DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),
},
},
"additional_properties": {
`{"a":true,"bar":"value","foo":true}`,
AdditionalProperties{
A: true,
ExtraFields: map[string]interface{}{
"bar": "value",
"foo": true,
},
},
},
"embedded_struct": {
`{"a":1,"b":"bar"}`,
EmbeddedStructs{
EmbeddedStruct: EmbeddedStruct{
A: true,
B: "bar",
JSON: EmbeddedStructJSON{
A: Field{raw: `1`, status: valid},
B: Field{raw: `"bar"`, status: valid},
raw: `{"a":1,"b":"bar"}`,
},
},
A: P(1),
ExtraFields: map[string]interface{}{"b": "bar"},
JSON: EmbeddedStructsJSON{
A: Field{raw: `1`, status: valid},
ExtraFields: map[string]Field{
"b": {raw: `"bar"`, status: valid},
},
raw: `{"a":1,"b":"bar"}`,
},
},
},
"recursive_struct": {
`{"child":{"name":"Alex"},"name":"Robert"}`,
Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
},
"metadata_coerce": {
`{"a":"12","b":"12","c":null,"extra_typed":12,"extra_untyped":{"foo":"bar"}}`,
JSONFieldStruct{
A: false,
B: 12,
C: "",
JSON: JSONFieldStructJSON{
raw: `{"a":"12","b":"12","c":null,"extra_typed":12,"extra_untyped":{"foo":"bar"}}`,
A: Field{raw: `"12"`, status: invalid},
B: Field{raw: `"12"`, status: valid},
C: Field{raw: "null", status: null},
D: Field{raw: "", status: missing},
ExtraFields: map[string]Field{
"extra_typed": {
raw: "12",
status: valid,
},
"extra_untyped": {
raw: `{"foo":"bar"}`,
status: invalid,
},
},
},
ExtraFields: map[string]int64{
"extra_typed": 12,
"extra_untyped": 0,
},
},
},
"unknown_struct_number": {
`{"unknown":12}`,
UnknownStruct{
Unknown: 12.,
},
},
"unknown_struct_map": {
`{"unknown":{"foo":"bar"}}`,
UnknownStruct{
Unknown: map[string]interface{}{
"foo": "bar",
},
},
},
"union_integer": {
`{"union":12}`,
UnionStruct{
Union: UnionInteger(12),
},
},
"union_struct_discriminated_a": {
`{"union":{"a":"foo","b":"bar","type":"typeA"}}`,
UnionStruct{
Union: UnionStructA{
Type: "typeA",
A: "foo",
B: "bar",
},
},
},
"union_struct_discriminated_b": {
`{"union":{"a":"foo","type":"typeB"}}`,
UnionStruct{
Union: UnionStructB{
Type: "typeB",
A: "foo",
},
},
},
"union_struct_time": {
`{"union":"2010-05-23"}`,
UnionStruct{
Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)),
},
},
"complex_union_a": {
`{"union":{"boo":"12","foo":true}}`,
ComplexUnionStruct{Union: ComplexUnionA{Boo: "12", Foo: true}},
},
"complex_union_b": {
`{"union":{"boo":true,"foo":"12"}}`,
ComplexUnionStruct{Union: ComplexUnionB{Boo: true, Foo: "12"}},
},
"complex_union_c": {
`{"union":{"boo":12}}`,
ComplexUnionStruct{Union: ComplexUnionC{Boo: 12}},
},
"complex_union_type_a": {
`{"union":{"baz":12,"type":"a"}}`,
ComplexUnionStruct{Union: ComplexUnionTypeA{Baz: 12, Type: TypeA("a")}},
},
"complex_union_type_b": {
`{"union":{"baz":12,"type":"b"}}`,
ComplexUnionStruct{Union: ComplexUnionTypeB{Baz: 12, Type: TypeB("b")}},
},
"marshalling_union_a": {
`{"boo":"hello"}`,
MarshallingUnionStruct{Union: MarshallingUnionA{Boo: "hello"}},
},
"marshalling_union_b": {
`{"foo":"hi"}`,
MarshallingUnionStruct{Union: MarshallingUnionB{Foo: "hi"}},
},
"unmarshal": {
`{"foo":"hello"}`,
&UnmarshalStruct{Foo: "hello", prop: true},
},
"array_of_unmarshal": {
`[{"foo":"hello"}]`,
[]UnmarshalStruct{{Foo: "hello", prop: true}},
},
"inline_coerce": {
`{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4]}`,
Inline{
InlineField: Primitives{A: false, B: 237628372683, C: 0x28e, D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
JSON: InlineJSON{
InlineField: Field{raw: "{\"a\":false,\"b\":237628372683,\"c\":654,\"d\":9999.43,\"e\":43.76,\"f\":[1,2,3,4]}", status: 3},
raw: "{\"a\":false,\"b\":237628372683,\"c\":654,\"d\":9999.43,\"e\":43.76,\"f\":[1,2,3,4]}",
},
},
},
"inline_array_coerce": {
`["Hello","foo","bar"]`,
InlineArray{
InlineField: []string{"Hello", "foo", "bar"},
JSON: InlineJSON{
InlineField: Field{raw: `["Hello","foo","bar"]`, status: 3},
raw: `["Hello","foo","bar"]`,
},
},
},
}
func TestDecode(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
result := reflect.New(reflect.TypeOf(test.val))
if err := Unmarshal([]byte(test.buf), result.Interface()); err != nil {
t.Fatalf("deserialization of %v failed with error %v", result, err)
}
if !reflect.DeepEqual(result.Elem().Interface(), test.val) {
t.Fatalf("expected '%s' to deserialize to \n%#v\nbut got\n%#v", test.buf, test.val, result.Elem().Interface())
}
})
}
}
func TestEncode(t *testing.T) {
for name, test := range tests {
if strings.HasSuffix(name, "_coerce") {
continue
}
t.Run(name, func(t *testing.T) {
raw, err := Marshal(test.val)
if err != nil {
t.Fatalf("serialization of %v failed with error %v", test.val, err)
}
if string(raw) != test.buf {
t.Fatalf("expected %+#v to serialize to %s but got %s", test.val, test.buf, string(raw))
}
})
}
}

View File

@@ -0,0 +1,120 @@
package apijson
import (
"fmt"
"reflect"
)
// Port copies over values from one struct to another struct.
func Port(from any, to any) error {
toVal := reflect.ValueOf(to)
fromVal := reflect.ValueOf(from)
if toVal.Kind() != reflect.Ptr || toVal.IsNil() {
return fmt.Errorf("destination must be a non-nil pointer")
}
for toVal.Kind() == reflect.Ptr {
toVal = toVal.Elem()
}
toType := toVal.Type()
for fromVal.Kind() == reflect.Ptr {
fromVal = fromVal.Elem()
}
fromType := fromVal.Type()
if toType.Kind() != reflect.Struct {
return fmt.Errorf("destination must be a non-nil pointer to a struct (%v %v)", toType, toType.Kind())
}
values := map[string]reflect.Value{}
fields := map[string]reflect.Value{}
fromJSON := fromVal.FieldByName("JSON")
toJSON := toVal.FieldByName("JSON")
// Iterate through the fields of v and load all the "normal" fields in the struct to the map of
// string to reflect.Value, as well as their raw .JSON.Foo counterpart indicated by j.
var getFields func(t reflect.Type, v reflect.Value)
getFields = func(t reflect.Type, v reflect.Value) {
j := v.FieldByName("JSON")
// Recurse into anonymous fields first, since the fields on the object should win over the fields in the
// embedded object.
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.Anonymous {
getFields(field.Type, v.Field(i))
continue
}
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
ptag, ok := parseJSONStructTag(field)
if !ok || ptag.name == "-" {
continue
}
values[ptag.name] = v.Field(i)
if j.IsValid() {
fields[ptag.name] = j.FieldByName(field.Name)
}
}
}
getFields(fromType, fromVal)
// Use the values from the previous step to populate the 'to' struct.
for i := 0; i < toType.NumField(); i++ {
field := toType.Field(i)
ptag, ok := parseJSONStructTag(field)
if !ok {
continue
}
if ptag.name == "-" {
continue
}
if value, ok := values[ptag.name]; ok {
delete(values, ptag.name)
if field.Type.Kind() == reflect.Interface {
toVal.Field(i).Set(value)
} else {
switch value.Kind() {
case reflect.String:
toVal.Field(i).SetString(value.String())
case reflect.Bool:
toVal.Field(i).SetBool(value.Bool())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
toVal.Field(i).SetInt(value.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
toVal.Field(i).SetUint(value.Uint())
case reflect.Float32, reflect.Float64:
toVal.Field(i).SetFloat(value.Float())
default:
toVal.Field(i).Set(value)
}
}
}
if fromJSONField, ok := fields[ptag.name]; ok {
if toJSONField := toJSON.FieldByName(field.Name); toJSONField.IsValid() {
toJSONField.Set(fromJSONField)
}
}
}
// Finally, copy over the .JSON.raw and .JSON.ExtraFields
if toJSON.IsValid() {
if raw := toJSON.FieldByName("raw"); raw.IsValid() {
setUnexportedField(raw, fromJSON.Interface().(interface{ RawJSON() string }).RawJSON())
}
if toExtraFields := toJSON.FieldByName("ExtraFields"); toExtraFields.IsValid() {
if fromExtraFields := fromJSON.FieldByName("ExtraFields"); fromExtraFields.IsValid() {
setUnexportedField(toExtraFields, fromExtraFields.Interface())
}
}
}
return nil
}

View File

@@ -0,0 +1,257 @@
package apijson
import (
"reflect"
"testing"
)
type Metadata struct {
CreatedAt string `json:"created_at"`
}
// Card is the "combined" type of CardVisa and CardMastercard
type Card struct {
Processor CardProcessor `json:"processor"`
Data any `json:"data"`
IsFoo bool `json:"is_foo"`
IsBar bool `json:"is_bar"`
Metadata Metadata `json:"metadata"`
Value interface{} `json:"value"`
JSON cardJSON
}
type cardJSON struct {
Processor Field
Data Field
IsFoo Field
IsBar Field
Metadata Field
Value Field
ExtraFields map[string]Field
raw string
}
func (r cardJSON) RawJSON() string { return r.raw }
type CardProcessor string
// CardVisa
type CardVisa struct {
Processor CardVisaProcessor `json:"processor"`
Data CardVisaData `json:"data"`
IsFoo bool `json:"is_foo"`
Metadata Metadata `json:"metadata"`
Value string `json:"value"`
JSON cardVisaJSON
}
type cardVisaJSON struct {
Processor Field
Data Field
IsFoo Field
Metadata Field
Value Field
ExtraFields map[string]Field
raw string
}
func (r cardVisaJSON) RawJSON() string { return r.raw }
type CardVisaProcessor string
type CardVisaData struct {
Foo string `json:"foo"`
}
// CardMastercard
type CardMastercard struct {
Processor CardMastercardProcessor `json:"processor"`
Data CardMastercardData `json:"data"`
IsBar bool `json:"is_bar"`
Metadata Metadata `json:"metadata"`
Value bool `json:"value"`
JSON cardMastercardJSON
}
type cardMastercardJSON struct {
Processor Field
Data Field
IsBar Field
Metadata Field
Value Field
ExtraFields map[string]Field
raw string
}
func (r cardMastercardJSON) RawJSON() string { return r.raw }
type CardMastercardProcessor string
type CardMastercardData struct {
Bar int64 `json:"bar"`
}
type CommonFields struct {
Metadata Metadata `json:"metadata"`
Value string `json:"value"`
JSON commonFieldsJSON
}
type commonFieldsJSON struct {
Metadata Field
Value Field
ExtraFields map[string]Field
raw string
}
type CardEmbedded struct {
CommonFields
Processor CardVisaProcessor `json:"processor"`
Data CardVisaData `json:"data"`
IsFoo bool `json:"is_foo"`
JSON cardEmbeddedJSON
}
type cardEmbeddedJSON struct {
Processor Field
Data Field
IsFoo Field
ExtraFields map[string]Field
raw string
}
func (r cardEmbeddedJSON) RawJSON() string { return r.raw }
var portTests = map[string]struct {
from any
to any
}{
"visa to card": {
CardVisa{
Processor: "visa",
IsFoo: true,
Data: CardVisaData{
Foo: "foo",
},
Metadata: Metadata{
CreatedAt: "Mar 29 2024",
},
Value: "value",
JSON: cardVisaJSON{
raw: `{"processor":"visa","is_foo":true,"data":{"foo":"foo"}}`,
Processor: Field{raw: `"visa"`, status: valid},
IsFoo: Field{raw: `true`, status: valid},
Data: Field{raw: `{"foo":"foo"}`, status: valid},
Value: Field{raw: `"value"`, status: valid},
ExtraFields: map[string]Field{"extra": {raw: `"yo"`, status: valid}},
},
},
Card{
Processor: "visa",
IsFoo: true,
IsBar: false,
Data: CardVisaData{
Foo: "foo",
},
Metadata: Metadata{
CreatedAt: "Mar 29 2024",
},
Value: "value",
JSON: cardJSON{
raw: `{"processor":"visa","is_foo":true,"data":{"foo":"foo"}}`,
Processor: Field{raw: `"visa"`, status: valid},
IsFoo: Field{raw: `true`, status: valid},
Data: Field{raw: `{"foo":"foo"}`, status: valid},
Value: Field{raw: `"value"`, status: valid},
ExtraFields: map[string]Field{"extra": {raw: `"yo"`, status: valid}},
},
},
},
"mastercard to card": {
CardMastercard{
Processor: "mastercard",
IsBar: true,
Data: CardMastercardData{
Bar: 13,
},
Value: false,
},
Card{
Processor: "mastercard",
IsFoo: false,
IsBar: true,
Data: CardMastercardData{
Bar: 13,
},
Value: false,
},
},
"embedded to card": {
CardEmbedded{
CommonFields: CommonFields{
Metadata: Metadata{
CreatedAt: "Mar 29 2024",
},
Value: "embedded_value",
JSON: commonFieldsJSON{
Metadata: Field{raw: `{"created_at":"Mar 29 2024"}`, status: valid},
Value: Field{raw: `"embedded_value"`, status: valid},
raw: `should not matter`,
},
},
Processor: "visa",
IsFoo: true,
Data: CardVisaData{
Foo: "embedded_foo",
},
JSON: cardEmbeddedJSON{
raw: `{"processor":"visa","is_foo":true,"data":{"foo":"embedded_foo"},"metadata":{"created_at":"Mar 29 2024"},"value":"embedded_value"}`,
Processor: Field{raw: `"visa"`, status: valid},
IsFoo: Field{raw: `true`, status: valid},
Data: Field{raw: `{"foo":"embedded_foo"}`, status: valid},
},
},
Card{
Processor: "visa",
IsFoo: true,
IsBar: false,
Data: CardVisaData{
Foo: "embedded_foo",
},
Metadata: Metadata{
CreatedAt: "Mar 29 2024",
},
Value: "embedded_value",
JSON: cardJSON{
raw: `{"processor":"visa","is_foo":true,"data":{"foo":"embedded_foo"},"metadata":{"created_at":"Mar 29 2024"},"value":"embedded_value"}`,
Processor: Field{raw: `"visa"`, status: 0x3},
IsFoo: Field{raw: "true", status: 0x3},
Data: Field{raw: `{"foo":"embedded_foo"}`, status: 0x3},
Metadata: Field{raw: `{"created_at":"Mar 29 2024"}`, status: 0x3},
Value: Field{raw: `"embedded_value"`, status: 0x3},
},
},
},
}
func TestPort(t *testing.T) {
for name, test := range portTests {
t.Run(name, func(t *testing.T) {
toVal := reflect.New(reflect.TypeOf(test.to))
err := Port(test.from, toVal.Interface())
if err != nil {
t.Fatalf("port of %v failed with error %v", test.from, err)
}
if !reflect.DeepEqual(toVal.Elem().Interface(), test.to) {
t.Fatalf("expected:\n%+#v\n\nto port to:\n%+#v\n\nbut got:\n%+#v", test.from, test.to, toVal.Elem().Interface())
}
})
}
}

View File

@@ -0,0 +1,41 @@
package apijson
import (
"reflect"
"github.com/tidwall/gjson"
)
type UnionVariant struct {
TypeFilter gjson.Type
DiscriminatorValue interface{}
Type reflect.Type
}
var unionRegistry = map[reflect.Type]unionEntry{}
var unionVariants = map[reflect.Type]interface{}{}
type unionEntry struct {
discriminatorKey string
variants []UnionVariant
}
func RegisterUnion(typ reflect.Type, discriminator string, variants ...UnionVariant) {
unionRegistry[typ] = unionEntry{
discriminatorKey: discriminator,
variants: variants,
}
for _, variant := range variants {
unionVariants[variant.Type] = typ
}
}
// Useful to wrap a union type to force it to use [apijson.UnmarshalJSON] since you cannot define an
// UnmarshalJSON function on the interface itself.
type UnionUnmarshaler[T any] struct {
Value T
}
func (c *UnionUnmarshaler[T]) UnmarshalJSON(buf []byte) error {
return UnmarshalRoot(buf, &c.Value)
}

View File

@@ -0,0 +1,47 @@
package apijson
import (
"reflect"
"strings"
)
const jsonStructTag = "json"
const formatStructTag = "format"
type parsedStructTag struct {
name string
required bool
extras bool
metadata bool
inline bool
}
func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) {
raw, ok := field.Tag.Lookup(jsonStructTag)
if !ok {
return
}
parts := strings.Split(raw, ",")
if len(parts) == 0 {
return tag, false
}
tag.name = parts[0]
for _, part := range parts[1:] {
switch part {
case "required":
tag.required = true
case "extras":
tag.extras = true
case "metadata":
tag.metadata = true
case "inline":
tag.inline = true
}
}
return
}
func parseFormatStructTag(field reflect.StructField) (format string, ok bool) {
format, ok = field.Tag.Lookup(formatStructTag)
return
}

View File

@@ -0,0 +1,341 @@
package apiquery
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/sst/opencode-sdk-go/internal/param"
)
var encoders sync.Map // map[reflect.Type]encoderFunc
type encoder struct {
dateFormat string
root bool
settings QuerySettings
}
type encoderFunc func(key string, value reflect.Value) []Pair
type encoderField struct {
tag parsedStructTag
fn encoderFunc
idx []int
}
type encoderEntry struct {
reflect.Type
dateFormat string
root bool
settings QuerySettings
}
type Pair struct {
key string
value string
}
func (e *encoder) typeEncoder(t reflect.Type) encoderFunc {
entry := encoderEntry{
Type: t,
dateFormat: e.dateFormat,
root: e.root,
settings: e.settings,
}
if fi, ok := encoders.Load(entry); ok {
return fi.(encoderFunc)
}
// To deal with recursive types, populate the map with an
// indirect func before we build it. This type waits on the
// real func (f) to be ready and then calls it. This indirect
// func is only used for recursive types.
var (
wg sync.WaitGroup
f encoderFunc
)
wg.Add(1)
fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(key string, v reflect.Value) []Pair {
wg.Wait()
return f(key, v)
}))
if loaded {
return fi.(encoderFunc)
}
// Compute the real encoder and replace the indirect func with it.
f = e.newTypeEncoder(t)
wg.Done()
encoders.Store(entry, f)
return f
}
func marshalerEncoder(key string, value reflect.Value) []Pair {
s, _ := value.Interface().(json.Marshaler).MarshalJSON()
return []Pair{{key, string(s)}}
}
func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc {
if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
return e.newTimeTypeEncoder(t)
}
if !e.root && t.Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) {
return marshalerEncoder
}
e.root = false
switch t.Kind() {
case reflect.Pointer:
encoder := e.typeEncoder(t.Elem())
return func(key string, value reflect.Value) (pairs []Pair) {
if !value.IsValid() || value.IsNil() {
return
}
pairs = encoder(key, value.Elem())
return
}
case reflect.Struct:
return e.newStructTypeEncoder(t)
case reflect.Array:
fallthrough
case reflect.Slice:
return e.newArrayTypeEncoder(t)
case reflect.Map:
return e.newMapEncoder(t)
case reflect.Interface:
return e.newInterfaceEncoder()
default:
return e.newPrimitiveTypeEncoder(t)
}
}
func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) {
return e.newFieldTypeEncoder(t)
}
encoderFields := []encoderField{}
// This helper allows us to recursively collect field encoders into a flat
// array. The parameter `index` keeps track of the access patterns necessary
// to get to some field.
var collectEncoderFields func(r reflect.Type, index []int)
collectEncoderFields = func(r reflect.Type, index []int) {
for i := 0; i < r.NumField(); i++ {
idx := append(index, i)
field := t.FieldByIndex(idx)
if !field.IsExported() {
continue
}
// If this is an embedded struct, traverse one level deeper to extract
// the field and get their encoders as well.
if field.Anonymous {
collectEncoderFields(field.Type, idx)
continue
}
// If query tag is not present, then we skip, which is intentionally
// different behavior from the stdlib.
ptag, ok := parseQueryStructTag(field)
if !ok {
continue
}
if ptag.name == "-" && !ptag.inline {
continue
}
dateFormat, ok := parseFormatStructTag(field)
oldFormat := e.dateFormat
if ok {
switch dateFormat {
case "date-time":
e.dateFormat = time.RFC3339
case "date":
e.dateFormat = "2006-01-02"
}
}
encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx})
e.dateFormat = oldFormat
}
}
collectEncoderFields(t, []int{})
return func(key string, value reflect.Value) (pairs []Pair) {
for _, ef := range encoderFields {
var subkey string = e.renderKeyPath(key, ef.tag.name)
if ef.tag.inline {
subkey = key
}
field := value.FieldByIndex(ef.idx)
pairs = append(pairs, ef.fn(subkey, field)...)
}
return
}
}
func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc {
keyEncoder := e.typeEncoder(t.Key())
elementEncoder := e.typeEncoder(t.Elem())
return func(key string, value reflect.Value) (pairs []Pair) {
iter := value.MapRange()
for iter.Next() {
encodedKey := keyEncoder("", iter.Key())
if len(encodedKey) != 1 {
panic("Unexpected number of parts for encoded map key. Are you using a non-primitive for this map?")
}
subkey := encodedKey[0].value
keyPath := e.renderKeyPath(key, subkey)
pairs = append(pairs, elementEncoder(keyPath, iter.Value())...)
}
return
}
}
func (e *encoder) renderKeyPath(key string, subkey string) string {
if len(key) == 0 {
return subkey
}
if e.settings.NestedFormat == NestedQueryFormatDots {
return fmt.Sprintf("%s.%s", key, subkey)
}
return fmt.Sprintf("%s[%s]", key, subkey)
}
func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc {
switch e.settings.ArrayFormat {
case ArrayQueryFormatComma:
innerEncoder := e.typeEncoder(t.Elem())
return func(key string, v reflect.Value) []Pair {
elements := []string{}
for i := 0; i < v.Len(); i++ {
for _, pair := range innerEncoder("", v.Index(i)) {
elements = append(elements, pair.value)
}
}
if len(elements) == 0 {
return []Pair{}
}
return []Pair{{key, strings.Join(elements, ",")}}
}
case ArrayQueryFormatRepeat:
innerEncoder := e.typeEncoder(t.Elem())
return func(key string, value reflect.Value) (pairs []Pair) {
for i := 0; i < value.Len(); i++ {
pairs = append(pairs, innerEncoder(key, value.Index(i))...)
}
return pairs
}
case ArrayQueryFormatIndices:
panic("The array indices format is not supported yet")
case ArrayQueryFormatBrackets:
innerEncoder := e.typeEncoder(t.Elem())
return func(key string, value reflect.Value) []Pair {
pairs := []Pair{}
for i := 0; i < value.Len(); i++ {
pairs = append(pairs, innerEncoder(key+"[]", value.Index(i))...)
}
return pairs
}
default:
panic(fmt.Sprintf("Unknown ArrayFormat value: %d", e.settings.ArrayFormat))
}
}
func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc {
switch t.Kind() {
case reflect.Pointer:
inner := t.Elem()
innerEncoder := e.newPrimitiveTypeEncoder(inner)
return func(key string, v reflect.Value) []Pair {
if !v.IsValid() || v.IsNil() {
return nil
}
return innerEncoder(key, v.Elem())
}
case reflect.String:
return func(key string, v reflect.Value) []Pair {
return []Pair{{key, v.String()}}
}
case reflect.Bool:
return func(key string, v reflect.Value) []Pair {
if v.Bool() {
return []Pair{{key, "true"}}
}
return []Pair{{key, "false"}}
}
case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
return func(key string, v reflect.Value) []Pair {
return []Pair{{key, strconv.FormatInt(v.Int(), 10)}}
}
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return func(key string, v reflect.Value) []Pair {
return []Pair{{key, strconv.FormatUint(v.Uint(), 10)}}
}
case reflect.Float32, reflect.Float64:
return func(key string, v reflect.Value) []Pair {
return []Pair{{key, strconv.FormatFloat(v.Float(), 'f', -1, 64)}}
}
case reflect.Complex64, reflect.Complex128:
bitSize := 64
if t.Kind() == reflect.Complex128 {
bitSize = 128
}
return func(key string, v reflect.Value) []Pair {
return []Pair{{key, strconv.FormatComplex(v.Complex(), 'f', -1, bitSize)}}
}
default:
return func(key string, v reflect.Value) []Pair {
return nil
}
}
}
func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc {
f, _ := t.FieldByName("Value")
enc := e.typeEncoder(f.Type)
return func(key string, value reflect.Value) []Pair {
present := value.FieldByName("Present")
if !present.Bool() {
return nil
}
null := value.FieldByName("Null")
if null.Bool() {
// TODO: Error?
return nil
}
raw := value.FieldByName("Raw")
if !raw.IsNil() {
return e.typeEncoder(raw.Type())(key, raw)
}
return enc(key, value.FieldByName("Value"))
}
}
func (e *encoder) newTimeTypeEncoder(t reflect.Type) encoderFunc {
format := e.dateFormat
return func(key string, value reflect.Value) []Pair {
return []Pair{{
key,
value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format),
}}
}
}
func (e encoder) newInterfaceEncoder() encoderFunc {
return func(key string, value reflect.Value) []Pair {
value = value.Elem()
if !value.IsValid() {
return nil
}
return e.typeEncoder(value.Type())(key, value)
}
}

View File

@@ -0,0 +1,50 @@
package apiquery
import (
"net/url"
"reflect"
"time"
)
func MarshalWithSettings(value interface{}, settings QuerySettings) url.Values {
e := encoder{time.RFC3339, true, settings}
kv := url.Values{}
val := reflect.ValueOf(value)
if !val.IsValid() {
return nil
}
typ := val.Type()
for _, pair := range e.typeEncoder(typ)("", val) {
kv.Add(pair.key, pair.value)
}
return kv
}
func Marshal(value interface{}) url.Values {
return MarshalWithSettings(value, QuerySettings{})
}
type Queryer interface {
URLQuery() url.Values
}
type QuerySettings struct {
NestedFormat NestedQueryFormat
ArrayFormat ArrayQueryFormat
}
type NestedQueryFormat int
const (
NestedQueryFormatBrackets NestedQueryFormat = iota
NestedQueryFormatDots
)
type ArrayQueryFormat int
const (
ArrayQueryFormatComma ArrayQueryFormat = iota
ArrayQueryFormatRepeat
ArrayQueryFormatIndices
ArrayQueryFormatBrackets
)

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