diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 00000000..5fa4a0a7 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"ede80fa1-4747-4ca0-bea8-10696e73b91c","pid":10849,"acquiredAt":1774569361869} \ No newline at end of file diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..47653a1a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,37 @@ +{ + "permissions": { + "allow": [ + "Bash(bash /home/linuxdev/Desktop/workshop/studio/hustle/occ/.tickets/scripts/verify_tickets.sh)", + "Bash(git add:*)", + "Bash(git config:*)", + "Bash(git -C /home/linuxdev/Desktop/workshop/studio/hustle/occ add docker/docker-compose.full.yml launch-editor.sh)", + "Bash(git -C /home/linuxdev/Desktop/workshop/studio/hustle/occ status)", + "Bash(GIT_CONFIG_NOSYSTEM=1 HOME=/tmp git -C /home/linuxdev/Desktop/workshop/studio/hustle/occ status)", + "Read(//root/**)", + "Bash(cp /root/.gitconfig /tmp/.gitconfig-bak)", + "Bash(GIT_CONFIG_GLOBAL=/tmp/.gitconfig-bak git config --global --add safe.directory /home/linuxdev/Desktop/workshop/studio/hustle/occ)", + "Bash(GIT_CONFIG_GLOBAL=/tmp/.gitconfig-bak git -C /home/linuxdev/Desktop/workshop/studio/hustle/occ status)", + "Bash(GIT_CONFIG_GLOBAL=/tmp/.gitconfig-bak git -C /home/linuxdev/Desktop/workshop/studio/hustle/occ add docker/docker-compose.full.yml launch-editor.sh)", + "Bash(GIT_CONFIG_GLOBAL=/tmp/.gitconfig-bak git -C /home/linuxdev/Desktop/workshop/studio/hustle/occ commit -m \"$\\(cat <<'EOF'\nfeat\\(ticket-021\\): add docker compose full stack + fix launch-editor.sh for Linux\n\nCo-Authored-By: Claude Sonnet 4.6 \nEOF\n\\)\")", + "Bash(GIT_DIR=/home/linuxdev/Desktop/workshop/studio/hustle/occ/.git GIT_WORK_TREE=/home/linuxdev/Desktop/workshop/studio/hustle/occ git -C /home/linuxdev/Desktop/workshop/studio/hustle/occ status --short)", + "Bash(git -c safe.directory=/home/linuxdev/Desktop/workshop/studio/hustle/occ -C /home/linuxdev/Desktop/workshop/studio/hustle/occ status --short)", + "Bash(git -c safe.directory=/home/linuxdev/Desktop/workshop/studio/hustle/occ -C /home/linuxdev/Desktop/workshop/studio/hustle/occ add apps/editor/extensions/openclaw/src/panels/home.ts)", + "Bash(git -c:*)", + "Bash(make container-build-linux:*)", + "Bash(python3 -m json.tool)", + "Bash(docker compose:*)", + "WebFetch(domain:docs.github.com)", + "WebSearch", + "WebFetch(domain:github.blog)", + "WebFetch(domain:github.com)", + "Bash(git pull:*)", + "Bash(npx commit-and-tag-version:*)", + "Bash(git commit:*)", + "Bash(find /home/linuxdev/Desktop/workshop/studio/hustle/occ -path *extensions/openclaw/src/panels/home.ts -o -path *extensions/*/panels/home.ts)", + "Bash(grep -E \"\\\\.\\(ts|tsx\\)$\")", + "mcp__chrome-devtools__list_pages", + "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\)[0]; print\\('Ports:', d['NetworkSettings']['Ports']\\); print\\('State:', d['State']['Status']\\)\")", + "Bash(curl -s --max-time 3 http://localhost:9888/)" + ] + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..7d3a9e51 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +node_modules +npm-debug.log +.git +.gitignore +.env +.env.local +.env.*.local +out/ +dist/ +build/ +.next +.nvm +.fnm +.DS_Store +*.swp +*.swo +*~ +.vscode +.tickets +.claude diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 83e3671f..1fd90c71 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -10,7 +10,49 @@ on: jobs: build-macos: - runs-on: macos-latest + # Available runners as of March 2026: + # Retired: macos-11 (2024), macos-12 (Jan 2025), macos-13 (Dec 2025) + # Active: macos-14 (arm64), macos-15 (arm64), macos-26 (arm64, GA Feb 2026) + # macos-15-intel / macos-26-intel for x64 (replaces retired macos-13) + # macos-14 deprecation begins July 2026 — remove that entry then. + strategy: + fail-fast: false + matrix: + include: + # x64 (Intel) builds — macos-13 retired; use -intel variants + - runner: macos-15-intel + arch: x64 + make_target: build-macos-x64 + vscode_arch: x64 + output_dir: VSCode-darwin-x64 + artifact_name: darwin-x64-macos15 + - runner: macos-26-intel + arch: x64 + make_target: build-macos-x64 + vscode_arch: x64 + output_dir: VSCode-darwin-x64 + artifact_name: darwin-x64-macos26 + # arm64 (Apple Silicon) builds + - runner: macos-14 + arch: arm64 + make_target: build-macos-arm64 + vscode_arch: arm64 + output_dir: VSCode-darwin-arm64 + artifact_name: darwin-arm64-macos14 + - runner: macos-15 + arch: arm64 + make_target: build-macos-arm64 + vscode_arch: arm64 + output_dir: VSCode-darwin-arm64 + artifact_name: darwin-arm64-macos15 + - runner: macos-26 + arch: arm64 + make_target: build-macos-arm64 + vscode_arch: arm64 + output_dir: VSCode-darwin-arm64 + artifact_name: darwin-arm64-macos26 + + runs-on: ${{ matrix.runner }} timeout-minutes: 120 permissions: contents: write @@ -25,6 +67,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: '20.18.2' + cache: 'npm' + cache-dependency-path: apps/editor/package-lock.json - name: Stamp release version in product.json if: startsWith(github.ref, 'refs/tags/') @@ -43,110 +87,12 @@ jobs: console.log('Stamped version =>', tag, 'commit =>', sha); " - - name: Install editor dependencies - working-directory: apps/editor - run: npm ci --ignore-scripts - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Rebuild native modules for Electron (arm64) - working-directory: apps/editor - run: npx --yes @electron/rebuild -v 34.3.2 -a arm64 + - name: Build macOS (${{ matrix.arch }}) + run: make ${{ matrix.make_target }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Install build dependencies - working-directory: apps/editor/build - run: npm ci - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Patch compilation to emit despite pre-existing TS errors - working-directory: apps/editor - run: | - node -e " - const fs = require('fs'); - const path = 'build/lib/compilation.js'; - let c = fs.readFileSync(path, 'utf8'); - c = c.replace( - 'createCompile(src, { build, emitError: true, transpileOnly: false', - 'createCompile(src, { build, emitError: false, transpileOnly: false' - ); - fs.writeFileSync(path, c); - console.log('Patched compilation.js: emitError -> false'); - " - - - name: Compile to out-build (tsc, no type-check errors) - working-directory: apps/editor - run: node_modules/.bin/gulp compile-build-without-mangling - env: - NODE_OPTIONS: --max-old-space-size=7168 - - - name: Install extension dependencies - working-directory: apps/editor - run: | - # Install deps for all extensions and their nested subdirs (e.g. server/) - find extensions -name "package.json" -not -path "*/node_modules/*" | while read pkg; do - dir=$(dirname "$pkg") - echo "Installing deps in $dir" - (cd "$dir" && npm install --ignore-scripts 2>/dev/null || true) - done - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Compile OpenClaw extension (tsc) - working-directory: apps/editor/extensions/openclaw - run: node_modules/.bin/tsc -p tsconfig.json - - - name: Compile non-native extensions - working-directory: apps/editor - run: node_modules/.bin/gulp compile-non-native-extensions-build - env: - NODE_OPTIONS: --max-old-space-size=7168 - - - name: Compile extension media - working-directory: apps/editor - run: node_modules/.bin/gulp compile-extension-media-build - env: - NODE_OPTIONS: --max-old-space-size=7168 - - - name: Build React bundles (Void UI) - working-directory: apps/editor/src/vs/workbench/contrib/void/browser/react - run: | - npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-" - npx tsup - - - name: Copy React bundles into out-build - working-directory: apps/editor - run: | - mkdir -p out-build/vs/workbench/contrib/void/browser/react - cp -r src/vs/workbench/contrib/void/browser/react/out out-build/vs/workbench/contrib/void/browser/react/ - - - name: Bundle (out-build -> out-vscode) - working-directory: apps/editor - run: node_modules/.bin/gulp bundle-vscode - env: - NODE_OPTIONS: --max-old-space-size=7168 - OCC_INFERENCE_ENDPOINT: ${{ secrets.OCC_INFERENCE_ENDPOINT }} - OCC_INFERENCE_API_KEY: ${{ secrets.OCC_INFERENCE_API_KEY }} - - - name: Minify (out-vscode -> out-vscode-min) - working-directory: apps/editor - run: node_modules/.bin/gulp minify-vscode - env: NODE_OPTIONS: --max-old-space-size=7168 - - name: Download Electron - working-directory: apps/editor - run: node build/lib/electron.js - continue-on-error: true - - - name: Package app (arm64) - working-directory: apps/editor - run: node_modules/.bin/gulp vscode-darwin-arm64-min-ci - env: - VSCODE_ARCH: arm64 - - name: Import certificate to keychain env: P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }} @@ -168,18 +114,16 @@ jobs: - name: Sign app env: CODESIGN_IDENTITY: "Developer ID Application: HITL, Inc (SQZ9VHYXJ3)" - # gulp outputs to path.dirname(root) = $GITHUB_WORKSPACE/apps/VSCode-darwin-arm64 AGENT_BUILDDIRECTORY: ${{ github.workspace }}/apps AGENT_TEMPDIRECTORY: ${{ runner.temp }} - VSCODE_ARCH: arm64 + VSCODE_ARCH: ${{ matrix.vscode_arch }} working-directory: apps/editor run: node build/darwin/sign.js "$GITHUB_WORKSPACE/apps" - name: Zip signed app run: | - APP_DIR="$GITHUB_WORKSPACE/apps/VSCode-darwin-arm64" - cd "$APP_DIR" - zip -Xry "$RUNNER_TEMP/OCcode-darwin-arm64-${GITHUB_REF_NAME}.zip" "OCcode.app" + cd "$GITHUB_WORKSPACE/apps/${{ matrix.output_dir }}" + zip -Xry "$RUNNER_TEMP/OCcode-${{ matrix.artifact_name }}-${GITHUB_REF_NAME}.zip" "OCcode.app" - name: Notarize env: @@ -189,11 +133,10 @@ jobs: APPLE_API_KEY_ISSUER: ${{ secrets.APPLE_API_KEY_ISSUER }} APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }} run: | - # Write the .p8 key file mkdir -p ~/.appstoreconnect/private_keys echo "$APPLE_API_KEY_P8" > ~/.appstoreconnect/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8 - xcrun notarytool submit "$RUNNER_TEMP/OCcode-darwin-arm64-${GITHUB_REF_NAME}.zip" \ + xcrun notarytool submit "$RUNNER_TEMP/OCcode-${{ matrix.artifact_name }}-${GITHUB_REF_NAME}.zip" \ --key ~/.appstoreconnect/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8 \ --key-id "$APPLE_API_KEY_ID" \ --issuer "$APPLE_API_KEY_ISSUER" \ @@ -202,250 +145,33 @@ jobs: - name: Staple notarization ticket run: | - xcrun stapler staple "$GITHUB_WORKSPACE/apps/VSCode-darwin-arm64/OCcode.app" + xcrun stapler staple "$GITHUB_WORKSPACE/apps/${{ matrix.output_dir }}/OCcode.app" - name: Re-zip stapled app run: | - cd "$GITHUB_WORKSPACE/apps/VSCode-darwin-arm64" - zip -Xry "$RUNNER_TEMP/OCcode-darwin-arm64-${GITHUB_REF_NAME}-signed.zip" "OCcode.app" + cd "$GITHUB_WORKSPACE/apps/${{ matrix.output_dir }}" + zip -Xry "$RUNNER_TEMP/OCcode-${{ matrix.artifact_name }}-${GITHUB_REF_NAME}-signed.zip" "OCcode.app" - name: Verify signature run: | - codesign -dv --deep --verbose=4 "$GITHUB_WORKSPACE/apps/VSCode-darwin-arm64/OCcode.app" 2>&1 - spctl -a -vvv -t install "$GITHUB_WORKSPACE/apps/VSCode-darwin-arm64/OCcode.app" 2>&1 + codesign -dv --deep --verbose=4 "$GITHUB_WORKSPACE/apps/${{ matrix.output_dir }}/OCcode.app" 2>&1 + spctl -a -vvv -t install "$GITHUB_WORKSPACE/apps/${{ matrix.output_dir }}/OCcode.app" 2>&1 - name: Upload signed app artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 continue-on-error: true with: - name: OCcode-darwin-arm64-${{ github.ref_name }}-signed - path: ${{ runner.temp }}/OCcode-darwin-arm64-${{ github.ref_name }}-signed.zip + name: OCcode-${{ matrix.artifact_name }}-${{ github.ref_name }}-signed + path: ${{ runner.temp }}/OCcode-${{ matrix.artifact_name }}-${{ github.ref_name }}-signed.zip retention-days: 30 - name: Create GitHub Release (on tag) if: startsWith(github.ref, 'refs/tags/') uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b with: - files: ${{ runner.temp }}/OCcode-darwin-arm64-${{ github.ref_name }}-signed.zip - name: OCcode ${{ github.ref_name }} - draft: false - prerelease: false - - build-macos-x64: - runs-on: macos-13 # last Intel-based GitHub Actions runner - timeout-minutes: 120 - permissions: - contents: write - - steps: - - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - with: - submodules: recursive - - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '20.18.2' - - - name: Stamp release version in product.json - if: startsWith(github.ref, 'refs/tags/') - working-directory: apps/editor - run: | - node -e " - const fs = require('fs'); - const p = JSON.parse(fs.readFileSync('product.json', 'utf8')); - const tag = process.env.GITHUB_REF_NAME; - const sha = process.env.GITHUB_SHA ? process.env.GITHUB_SHA.slice(0, 7) : tag; - p.occRelease = tag; - p.voidVersion = tag; - p.commit = sha; - p.date = new Date().toISOString(); - fs.writeFileSync('product.json', JSON.stringify(p, null, '\t')); - console.log('Stamped version =>', tag, 'commit =>', sha); - " - - - name: Install editor dependencies - working-directory: apps/editor - run: npm ci --ignore-scripts - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Rebuild native modules for Electron (x64) - working-directory: apps/editor - run: npx --yes @electron/rebuild -v 34.3.2 -a x64 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Install build dependencies - working-directory: apps/editor/build - run: npm ci - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Patch compilation to emit despite pre-existing TS errors - working-directory: apps/editor - run: | - node -e " - const fs = require('fs'); - const path = 'build/lib/compilation.js'; - let c = fs.readFileSync(path, 'utf8'); - c = c.replace( - 'createCompile(src, { build, emitError: true, transpileOnly: false', - 'createCompile(src, { build, emitError: false, transpileOnly: false' - ); - fs.writeFileSync(path, c); - console.log('Patched compilation.js: emitError -> false'); - " - - - name: Compile to out-build (tsc, no type-check errors) - working-directory: apps/editor - run: node_modules/.bin/gulp compile-build-without-mangling - env: - NODE_OPTIONS: --max-old-space-size=7168 - - - name: Install extension dependencies - working-directory: apps/editor - run: | - find extensions -name "package.json" -not -path "*/node_modules/*" | while read pkg; do - dir=$(dirname "$pkg") - echo "Installing deps in $dir" - (cd "$dir" && npm install --ignore-scripts 2>/dev/null || true) - done - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Compile OpenClaw extension (tsc) - working-directory: apps/editor/extensions/openclaw - run: node_modules/.bin/tsc -p tsconfig.json - - - name: Compile non-native extensions - working-directory: apps/editor - run: node_modules/.bin/gulp compile-non-native-extensions-build - env: - NODE_OPTIONS: --max-old-space-size=7168 - - - name: Compile extension media - working-directory: apps/editor - run: node_modules/.bin/gulp compile-extension-media-build - env: - NODE_OPTIONS: --max-old-space-size=7168 - - - name: Build React bundles (Void UI) - working-directory: apps/editor/src/vs/workbench/contrib/void/browser/react - run: | - npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-" - npx tsup - - - name: Copy React bundles into out-build - working-directory: apps/editor - run: | - mkdir -p out-build/vs/workbench/contrib/void/browser/react - cp -r src/vs/workbench/contrib/void/browser/react/out out-build/vs/workbench/contrib/void/browser/react/ - - - name: Bundle (out-build -> out-vscode) - working-directory: apps/editor - run: node_modules/.bin/gulp bundle-vscode - env: - NODE_OPTIONS: --max-old-space-size=7168 - OCC_INFERENCE_ENDPOINT: ${{ secrets.OCC_INFERENCE_ENDPOINT }} - OCC_INFERENCE_API_KEY: ${{ secrets.OCC_INFERENCE_API_KEY }} - - - name: Minify (out-vscode -> out-vscode-min) - working-directory: apps/editor - run: node_modules/.bin/gulp minify-vscode - env: - NODE_OPTIONS: --max-old-space-size=7168 - - - name: Download Electron - working-directory: apps/editor - run: node build/lib/electron.js - continue-on-error: true - - - name: Package app (x64) - working-directory: apps/editor - run: node_modules/.bin/gulp vscode-darwin-x64-min-ci - env: - VSCODE_ARCH: x64 - - - name: Import certificate to keychain - env: - P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }} - P12_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - run: | - KEYCHAIN_PATH=$RUNNER_TEMP/buildagent.keychain - CERT_PATH=$RUNNER_TEMP/developer-id.p12 - - echo "$P12_BASE64" | base64 --decode -o "$CERT_PATH" - - security create-keychain -p "keychain-password" "$KEYCHAIN_PATH" - security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" - security unlock-keychain -p "keychain-password" "$KEYCHAIN_PATH" - security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$P12_PASSWORD" -T /usr/bin/codesign - security list-keychain -d user -s "$KEYCHAIN_PATH" - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "keychain-password" "$KEYCHAIN_PATH" - - - name: Sign app - env: - CODESIGN_IDENTITY: "Developer ID Application: HITL, Inc (SQZ9VHYXJ3)" - AGENT_BUILDDIRECTORY: ${{ github.workspace }}/apps - AGENT_TEMPDIRECTORY: ${{ runner.temp }} - VSCODE_ARCH: x64 - working-directory: apps/editor - run: node build/darwin/sign.js "$GITHUB_WORKSPACE/apps" - - - name: Zip signed app - run: | - APP_DIR="$GITHUB_WORKSPACE/apps/VSCode-darwin-x64" - cd "$APP_DIR" - zip -Xry "$RUNNER_TEMP/OCcode-darwin-x64-${GITHUB_REF_NAME}.zip" "OCcode.app" - - - name: Notarize - env: - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} - APPLE_API_KEY_ISSUER: ${{ secrets.APPLE_API_KEY_ISSUER }} - APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }} - run: | - mkdir -p ~/.appstoreconnect/private_keys - echo "$APPLE_API_KEY_P8" > ~/.appstoreconnect/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8 - - xcrun notarytool submit "$RUNNER_TEMP/OCcode-darwin-x64-${GITHUB_REF_NAME}.zip" \ - --key ~/.appstoreconnect/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8 \ - --key-id "$APPLE_API_KEY_ID" \ - --issuer "$APPLE_API_KEY_ISSUER" \ - --wait \ - --timeout 30m - - - name: Staple notarization ticket - run: | - xcrun stapler staple "$GITHUB_WORKSPACE/apps/VSCode-darwin-x64/OCcode.app" - - - name: Re-zip stapled app - run: | - cd "$GITHUB_WORKSPACE/apps/VSCode-darwin-x64" - zip -Xry "$RUNNER_TEMP/OCcode-darwin-x64-${GITHUB_REF_NAME}-signed.zip" "OCcode.app" - - - name: Verify signature - run: | - codesign -dv --deep --verbose=4 "$GITHUB_WORKSPACE/apps/VSCode-darwin-x64/OCcode.app" 2>&1 - spctl -a -vvv -t install "$GITHUB_WORKSPACE/apps/VSCode-darwin-x64/OCcode.app" 2>&1 - - - name: Upload signed app artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - continue-on-error: true - with: - name: OCcode-darwin-x64-${{ github.ref_name }}-signed - path: ${{ runner.temp }}/OCcode-darwin-x64-${{ github.ref_name }}-signed.zip - retention-days: 30 - - - name: Create GitHub Release (on tag) - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b - with: - files: ${{ runner.temp }}/OCcode-darwin-x64-${{ github.ref_name }}-signed.zip + files: ${{ runner.temp }}/OCcode-${{ matrix.artifact_name }}-${{ github.ref_name }}-signed.zip name: OCcode ${{ github.ref_name }} + body_path: CHANGELOG.md draft: false prerelease: false @@ -470,6 +196,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: '20.18.2' + cache: 'npm' + cache-dependency-path: apps/editor/package-lock.json - name: Stamp release version in product.json if: startsWith(github.ref, 'refs/tags/') @@ -488,127 +216,11 @@ jobs: console.log('Stamped version =>', tag, 'commit =>', sha); " - - name: Install editor dependencies - working-directory: apps/editor - run: npm ci --ignore-scripts - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Rebuild native modules for Electron (x64) - working-directory: apps/editor - run: npx --yes @electron/rebuild -v 34.3.2 -a x64 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Install build dependencies - working-directory: apps/editor/build - run: npm ci - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Patch compilation to emit despite pre-existing TS errors - working-directory: apps/editor - run: | - node -e " - const fs = require('fs'); - const path = 'build/lib/compilation.js'; - let c = fs.readFileSync(path, 'utf8'); - c = c.replace( - 'createCompile(src, { build, emitError: true, transpileOnly: false', - 'createCompile(src, { build, emitError: false, transpileOnly: false' - ); - fs.writeFileSync(path, c); - console.log('Patched compilation.js: emitError -> false'); - " - - - name: Compile to out-build (tsc, no type-check errors) - working-directory: apps/editor - run: node_modules/.bin/gulp compile-build-without-mangling - env: - NODE_OPTIONS: --max-old-space-size=7168 - - - name: Install extension dependencies - working-directory: apps/editor - run: | - find extensions -name "package.json" -not -path "*/node_modules/*" | while read pkg; do - dir=$(dirname "$pkg") - echo "Installing deps in $dir" - (cd "$dir" && npm install --ignore-scripts 2>/dev/null || true) - done + - name: Build Windows + run: make build-windows env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Compile OpenClaw extension (tsc) - working-directory: apps/editor/extensions/openclaw - run: node_modules/.bin/tsc -p tsconfig.json - - - name: Compile non-native extensions - working-directory: apps/editor - run: node_modules/.bin/gulp compile-non-native-extensions-build - env: - NODE_OPTIONS: --max-old-space-size=7168 - - - name: Compile extension media - working-directory: apps/editor - run: node_modules/.bin/gulp compile-extension-media-build - env: - NODE_OPTIONS: --max-old-space-size=7168 - - - name: Build React bundles (Void UI) - working-directory: apps/editor/src/vs/workbench/contrib/void/browser/react - run: | - npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-" - npx tsup - - - name: Copy React bundles into out-build - working-directory: apps/editor - run: | - mkdir -p out-build/vs/workbench/contrib/void/browser/react - cp -r src/vs/workbench/contrib/void/browser/react/out out-build/vs/workbench/contrib/void/browser/react/ - - - name: Bundle (out-build -> out-vscode) - working-directory: apps/editor - run: node_modules/.bin/gulp bundle-vscode - env: - NODE_OPTIONS: --max-old-space-size=7168 - OCC_INFERENCE_ENDPOINT: ${{ secrets.OCC_INFERENCE_ENDPOINT }} - OCC_INFERENCE_API_KEY: ${{ secrets.OCC_INFERENCE_API_KEY }} - - - name: Minify (out-vscode -> out-vscode-min) - working-directory: apps/editor - run: node_modules/.bin/gulp minify-vscode - env: - NODE_OPTIONS: --max-old-space-size=7168 - - - name: Download Electron - working-directory: apps/editor - run: node build/lib/electron.js - continue-on-error: true - - - name: Package app (x64) - working-directory: apps/editor - run: node_modules/.bin/gulp vscode-win32-x64-min-ci - env: - VSCODE_ARCH: x64 - - - name: Stamp app icon on main executable - working-directory: apps/editor - run: | - npx rcedit "$GITHUB_WORKSPACE/apps/VSCode-win32-x64/OCcode.exe" --set-icon resources/win32/code.ico - shell: bash - - - name: Copy inno_updater to build - working-directory: apps/editor - run: node_modules/.bin/gulp vscode-win32-x64-inno-updater - env: - VSCODE_ARCH: x64 - - - name: Build Windows installers (system + user) - working-directory: apps/editor - run: | - node_modules/.bin/gulp vscode-win32-x64-system-setup - node_modules/.bin/gulp vscode-win32-x64-user-setup - - name: Sign Windows installers (Azure Trusted Signing) if: ${{ env.AZURE_CLIENT_ID_CHECK != '' }} uses: azure/trusted-signing-action@87c2e83e6868da99d3380aa309851b32ed9a8346 @@ -667,6 +279,7 @@ jobs: ${{ runner.temp }}/OCcode-win32-x64-${{ github.ref_name }}-user-setup.exe ${{ runner.temp }}/OCcode-win32-x64-${{ github.ref_name }}.zip name: OCcode ${{ github.ref_name }} + body_path: CHANGELOG.md draft: false prerelease: false @@ -686,17 +299,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: '20.18.2' - - - name: Install system dependencies - run: | - sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends \ - fakeroot \ - rpm \ - libkrb5-dev \ - libsecret-1-dev \ - libx11-dev \ - libxkbfile-dev + cache: 'npm' + cache-dependency-path: apps/editor/package-lock.json - name: Stamp release version in product.json if: startsWith(github.ref, 'refs/tags/') @@ -715,134 +319,21 @@ jobs: console.log('Stamped version =>', tag, 'commit =>', sha); " - - name: Install editor dependencies - working-directory: apps/editor - run: npm ci --ignore-scripts - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Rebuild native modules for Electron (x64) - working-directory: apps/editor - run: npx --yes @electron/rebuild -v 34.3.2 -a x64 + - name: Build Linux (container) + run: make container-build-linux env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Install build dependencies - working-directory: apps/editor/build - run: npm ci - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Patch compilation to emit despite pre-existing TS errors - working-directory: apps/editor - run: | - node -e " - const fs = require('fs'); - const path = 'build/lib/compilation.js'; - let c = fs.readFileSync(path, 'utf8'); - c = c.replace( - 'createCompile(src, { build, emitError: true, transpileOnly: false', - 'createCompile(src, { build, emitError: false, transpileOnly: false' - ); - fs.writeFileSync(path, c); - console.log('Patched compilation.js: emitError -> false'); - " - - - name: Compile to out-build (tsc, no type-check errors) - working-directory: apps/editor - run: node_modules/.bin/gulp compile-build-without-mangling - env: - NODE_OPTIONS: --max-old-space-size=7168 - - - name: Install extension dependencies - working-directory: apps/editor - run: | - find extensions -name "package.json" -not -path "*/node_modules/*" | while read pkg; do - dir=$(dirname "$pkg") - echo "Installing deps in $dir" - (cd "$dir" && npm install --ignore-scripts 2>/dev/null || true) - done - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Compile OpenClaw extension (tsc) - working-directory: apps/editor/extensions/openclaw - run: node_modules/.bin/tsc -p tsconfig.json - - - name: Compile non-native extensions - working-directory: apps/editor - run: node_modules/.bin/gulp compile-non-native-extensions-build - env: - NODE_OPTIONS: --max-old-space-size=7168 - - - name: Compile extension media - working-directory: apps/editor - run: node_modules/.bin/gulp compile-extension-media-build - env: - NODE_OPTIONS: --max-old-space-size=7168 - - - name: Build React bundles (Void UI) - working-directory: apps/editor/src/vs/workbench/contrib/void/browser/react - run: | - npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-" - npx tsup - - - name: Copy React bundles into out-build - working-directory: apps/editor - run: | - mkdir -p out-build/vs/workbench/contrib/void/browser/react - cp -r src/vs/workbench/contrib/void/browser/react/out out-build/vs/workbench/contrib/void/browser/react/ - - - name: Bundle (out-build -> out-vscode) - working-directory: apps/editor - run: node_modules/.bin/gulp bundle-vscode - env: - NODE_OPTIONS: --max-old-space-size=7168 - OCC_INFERENCE_ENDPOINT: ${{ secrets.OCC_INFERENCE_ENDPOINT }} - OCC_INFERENCE_API_KEY: ${{ secrets.OCC_INFERENCE_API_KEY }} - - - name: Minify (out-vscode -> out-vscode-min) - working-directory: apps/editor - run: node_modules/.bin/gulp minify-vscode - env: - NODE_OPTIONS: --max-old-space-size=7168 - - - name: Download Electron - working-directory: apps/editor - run: node build/lib/electron.js - continue-on-error: true - - - name: Package app (linux-x64) - working-directory: apps/editor - run: node_modules/.bin/gulp vscode-linux-x64-min-ci - env: - VSCODE_ARCH: x64 - - - name: Remove musl watcher (not needed on glibc Ubuntu) - run: | - rm -rf "$GITHUB_WORKSPACE/apps/VSCode-linux-x64/resources/app/node_modules/@parcel/watcher-linux-x64-musl" - - - name: Build .deb package - working-directory: apps/editor + - name: Create .tar.gz (universal Linux) run: | - node_modules/.bin/gulp vscode-linux-x64-prepare-deb - node_modules/.bin/gulp vscode-linux-x64-build-deb - env: - VSCODE_ARCH: x64 + tar -czf "$RUNNER_TEMP/OCcode-linux-x64-${GITHUB_REF_NAME}.tar.gz" \ + -C apps VSCode-linux-x64 - - name: Collect .deb path - id: deb - working-directory: apps/editor + - name: Collect .deb and copy to temp run: | - DEB=$(ls .build/linux/deb/amd64/deb/*.deb) - echo "path=$DEB" >> $GITHUB_OUTPUT + DEB=$(ls apps/editor/.build/linux/deb/amd64/deb/*.deb) cp "$DEB" "$RUNNER_TEMP/OCcode-linux-x64-${GITHUB_REF_NAME}.deb" - - name: Create .tar.gz (universal Linux) - run: | - APP_DIR="$GITHUB_WORKSPACE/apps/VSCode-linux-x64" - tar -czf "$RUNNER_TEMP/OCcode-linux-x64-${GITHUB_REF_NAME}.tar.gz" -C "$(dirname "$APP_DIR")" "$(basename "$APP_DIR")" - - name: Upload Linux artifacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 continue-on-error: true @@ -861,5 +352,6 @@ jobs: ${{ runner.temp }}/OCcode-linux-x64-${{ github.ref_name }}.deb ${{ runner.temp }}/OCcode-linux-x64-${{ github.ref_name }}.tar.gz name: OCcode ${{ github.ref_name }} + body_path: CHANGELOG.md draft: false prerelease: false diff --git a/.gitignore b/.gitignore index 3c9c0071..05749026 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,21 @@ node_modules/ out/ dist/ +apps/editor/extensions/**/out/ +apps/editor/extensions/**/package-lock.json *.vsix .DS_Store apps/editor/.build/ +apps/editor/core.* +apps/VSCode-*/ +app/web/ + +# Temporary directories +.tmp/ +test-results/ +tmp/ +docker/openclaw_docker_data/ +docker/windows-storage/ # Internal infrastructure docs — keep private APPLE.md @@ -14,4 +26,10 @@ ISsues # Local secrets .env .env.local -occ-backend +occ-backend +.worktree +.worktrees/ + +package-lock.json + +core.* diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000..0708d782 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,18 @@ +{ + "mcpServers": { + "playwright-novnc": { + "command": "bash", + "args": [ + "-c", + "docker ps -q -f name=playwright-novnc || docker run -d --name playwright-novnc --network host ghcr.io/xtr-dev/mcp-playwright-novnc; docker exec -i playwright-novnc mcp-proxy http://localhost:3080/sse" + ] + }, + "chrome-devtools": { + "command": "bash", + "args": [ + "-c", + "exec npx chrome-devtools-mcp@latest --wsEndpoint \"$(curl -s -H 'Host: localhost' http://localhost:9222/json/version | awk '/Socket/{print $2}' | tr -d '\"' | sed 's|://localhost/|://localhost:9222/|')\"" + ] + } + } +} \ No newline at end of file diff --git a/.tickets/AGENTS.md b/.tickets/AGENTS.md new file mode 100644 index 00000000..7c7bda05 --- /dev/null +++ b/.tickets/AGENTS.md @@ -0,0 +1,448 @@ +# AGENTS.md - OpenClaw Code (OCC) Ticket Management + +> **OpenClaw Compatibility**: This framework is designed for OpenClaw agents working on the OCC project. Follow these guidelines for ticket-based task execution. + +--- + +## ⚠️ CRITICAL WORKFLOW COMPLIANCE WARNING ⚠️ + +**STRICT TICKET ORDERING MUST BE FOLLOWED RELIGIOUSLY!** + +- ✅ Work MUST proceed in ascending numerical order: ticket-001 → ticket-002 → ticket-003 → ... → ticket-020 +- ✅ Within each ticket, tasks MUST be completed in document order: Task 1 → Task 2 → Task 3 +- ❌ DEVIATIONS ARE A SERIOUS OFFENSE WITH SEVERE CONSEQUENCES +- ❌ SKIPPING, REORDERING, OR WORKING OUT OF SEQUENCE IS STRICTLY FORBIDDEN + +--- + +# Part 1: Ticket Management Framework + +## 1. Ticket Structure & Naming Convention + +Each ticket lives in `.tickets/ticket-XXX-name/` with: + +- `prd.md` — specification document (required) +- `agent-history.md` — logs of agent work (created when task begins) +- Optional: `diagrams/`, `research/`, `test-results/` +- **Formatting**: All `prd.md` specifications must be written in Gherkin BDD format (Given-When-Then). + +### Ignoring Tickets + +Add `` as first line of `prd.md` to skip. Agents MUST NOT work on ignored tickets. + +--- + +## 2. PRD.md Content Structure + +Each `prd.md` must include: + +### 2.1 Problem Statement +What is the issue/feature? Why does it matter? + +### 2.2 Proposed Solution +High-level approach and architecture. + +### 2.3 Acceptance Criteria +Measurable requirements for completion. +All acceptance criteria will be written as Gherkin BDD specifications (Given-When-Then). + +### 2.4 Technical Considerations +Constraints, performance, security, integration points. + +### 2.5 Dependencies +Related tickets (by number) that must be complete first. + +**Cross-Ticket Dependencies:** +```markdown +### Dependencies +- **Depends on ticket-007**: Auth must be complete before balance API +``` +Dependent tickets MUST have all tasks `[x]` before starting. + +--- + +## 3. Task & Subtask Specification + +### 3.1 Main Task Structure + +```markdown +- [ ] Task 1:
+ - **Problem**: + - **Test**: + - **Depends on**: + - **Subtasks**: + - [ ] Subtask 1.1: + - **Objective**: + - **Test**: + - **Depends on**: + - [ ] Subtask 1.2: + - **Objective**: ... + - **Test**: ... + - **Depends on**: Subtask 1.1 + +- [ ] Task 2: + - ... +``` + +Use nested subtasks (1.1.1) for complex breakdowns. + +### 3.2 Status Markers + +- `[ ]` — pending (not started) +- `[-]` — in progress (actively working) +- `[x]` — completed (verified) + +### 3.3 Workflow Rules + +1. Find tasks via `find_next_ticket.sh` +2. Set status to `[-]` when starting +3. Complete all subtasks and verification +4. Set status to `[x]` only after successful test +5. Never skip status transitions + +--- + +## 4. Task & Subtask Refinement + +Agents are ENCOURAGED to add missing subtasks before/during work: + +- If review reveals gaps, add new subtasks with `Objective` and `Test` +- Place them logically within the parent task +- Mark new subtasks as `[ ]` before starting +- You MAY NOT modify existing task descriptions, but CAN add new subtasks + +**Breakdown Guidelines:** + +- Each subtask should be a single focused effort (2-3 hours max) +- If a subtask is too large, break it down further +- Include testing as separate subtask when appropriate +- Declare dependencies to enable parallelism + +--- + +## 5. Graph Theory for Parallelization + +Tasks form a **Directed Acyclic Graph (DAG)**. Maximize parallelism: + +### 5.1 Concurrency Limits + +- **Direct agent work**: up to 3 parallel subtasks +- **With subagents**: up to 6 concurrent subagents + +### 5.2 Level-Based Execution + +- **Level 0**: Subtasks with `Depends on: None` — run all in parallel +- **Level 1**: Depends only on Level 0 — run after Level 0 finishes +- **Level N**: Depends on Level N-1 — run after that level + +### 5.3 Critical Path + +The longest dependency chain is the critical path. Prioritize these tasks as they determine overall completion time. + +### 5.4 Algorithm + +1. Build DAG from dependencies +2. Compute level = max(dep_levels) + 1 +3. Execute by level (all Level 0, then all Level 1, etc.) +4. Identify critical path to focus resources + +--- + +## 6. AI Agent Workflow + +### 6.1 Task Processing + +1. **Discover**: Run `scripts/find_next_ticket.sh` to get next pending task +2. **Analyze**: Review problem statement, acceptance criteria, dependencies +3. **Breakdown**: Ensure all needed subtasks exist; add if missing +4. **Implement**: Complete subtask objectives +5. **Test**: Execute verification tests +6. **Validate**: Confirm acceptance criteria satisfied +7. **Update**: Mark subtask `[x]`, then parent Task `[x]`, then acceptance criteria `[x]` +8. **Commit**: Create atomic commit referencing ticket and task number +9. **Report**: Log completion and proceed to next task + +### 6.2 Completion & Exit + +When no pending `[ ]` tasks exist across all tickets, agents MUST terminate gracefully. Log a completion message and exit. + +### 6.3 CRITICAL RULES + +**Ordering:** +- Tickets in ascending numerical order ONLY +- Tasks within a ticket in document order ONLY +- Violations = immediate termination + +**Dependencies:** +- All dependencies MUST be `[x]` before starting +- Verify by reading dependent ticket's task statuses + +**Status Transitions:** +- Sequence: `[ ]` → `[-]` → `[x]` only +- Skipping is PROHIBITED + +**Closeout Procedure (NEW):** +After completing all subtasks in a Task: +1. Verify ticket's acceptance criteria are satisfied +2. Mark parent Task checkbox `[x]` +3. Mark any newly fulfilled acceptance criteria checkboxes `[x]` +4. THEN commit and report + +--- + +## 7. Git Commit Process + +### 7.1 Timing + +✅ **Commit IMMEDIATELY after tests pass** — this is the official completion record. + +❌ Never batch multiple tasks +❌ Never delay commits +✅ Each task = one atomic commit + +### 7.2 Format + +``` +(): +``` + +Types: `feat`, `fix`, `docs`, `test`, `refactor`, `chore` + +Example: `feat(ticket-007): implement JWT authentication middleware` + +Always include ticket number. + +--- + +## 8. Verification & Testing + +- Each subtask must have defined **Test** criteria written in Gherkin BDD format (Given-When-Then) written in Gherkin BDD format (Given-When-Then) +- Follow TDD: write tests before or alongside implementation +- Include unit, edge case, and integration tests +- Verify acceptance criteria before marking `[x]` + +--- + +## 9. Code Quality Standards + +- Follow existing code conventions +- Maintain consistent style +- Include error handling +- Add meaningful comments +- Self-review before completion + +--- + +## 10. Subagent Execution Framework + +### When to Use Subagents + +Spawn subagents when: +- A task has multiple independent subtasks (parallel work) +- Volume of work exceeds single-agent efficiency +- Need to coordinate parallel execution + +### Limits + +- Max **6 concurrent subagents** per main agent +- Poll all subagents every **2 minutes** +- If a subagent fails or stalls, respawn immediately + +### Subagent Requirements + +- **Naming**: `sub-agent-histories/agent-history-ticket-001-task-1.2-subtask-name.md` +- **Creation**: Must create agent-history file at START +- **Scope**: Only work on assigned task/subtask +- **Compliance**: Follow all AGENTS.md policies +- **Reporting**: Create git commit, update prd.md, report back to main agent + +### Main Agent Responsibilities + +- **Spawn**: Use Task tool with specific assignment +- **Monitor**: Poll every 2 mins (read agent-history) +- **Respawn**: On failure/stall, create replacement +- **Coordinate**: Avoid file/resource conflicts +- **Merge**: Integrate completed work and update ticket + +### Agent-History File Structure + +```markdown +# Subagent History + +**Agent ID:** +**Ticket:** ticket-001 +**Task:** Task 1.2 - Implement login validation +**Started:** +**Status:** in_progress | completed | failed +**Completed:** + +## Work Log + +### - Start +- Assigned task +- Reviewed requirements + +### - Implementation +- Code changes +- Decisions made + +### - Testing +- Ran tests +- Verified acceptance criteria + +### - Completion +- Created git commit: feat(ticket-001): ... +- Updated prd.md status +- Reported to main agent + +## Errors/Issues Encountered +- None (or details) + +## Files Modified +- file1.ts +- file2.ts +``` + +### Lifecycle + +1. Main agent spawns subagent (Task tool) +2. Subagent creates agent-history with header +3. Work proceeds, logging chronologically +4. On completion: verify, commit, update prd, report +5. On failure: mark status failed, document reason, report +6. Main agent respawns if needed + +--- + +# Part 2: Project-Specific Configuration ⚙️ + +## OCC Project Settings + +### Technology Stack + +- **Editor**: Void editor fork (VS Code base) +- **Backend**: Node.js + Fastify + Drizzle ORM + Postgres +- **Frontend**: React/TypeScript (extension webviews) +- **Auth**: JWT + OAuth (Google/GitHub) +- **Payments**: Stripe Checkout +- **Inference**: OpenAI-compatible proxy to `https://inference.mba.sh/v1` + +### Development Commands + +```bash +# Install dependencies +npm ci +npm --prefix apps/editor ci + +# Build editor +npm --prefix apps/editor run compile + +# Run backend (when ready) +npm --prefix apps/backend run dev # or appropriate + +# Watch editor (dev) +npm --prefix apps/editor run watch + +# Package extension +npm --prefix apps/extension run ext:package +``` + +### Environment + +- Required Node version: **20.18.2** (enforced by editor) +- Backend port: `3001` (default) +- Database: PostgreSQL (use Docker for dev) + +### Docker Dev Setup + +```bash +docker compose -f docker-compose.dev.yml up -d +npx drizzle-kit migrate +npx ts-node src/db/seed.ts +``` + +### Key Paths + +- Editor source: `apps/editor/` +- Extension source: `apps/extension/` +- Backend source: `apps/backend/` or root `apps/` +- Tickets: `.tickets/` +- AGENTS.md: repo root and `.tickets/` + +--- + +## Available Scripts + +Run these from repository root: + +```bash +# Verify ticket statuses (count pending/in-progress per ticket) +bash .tickets/scripts/verify_tickets.sh + +# Find the next ticket with pending work +bash .tickets/scripts/find_next_ticket.sh + +# List tickets and backlog task counts +bash .tickets/scripts/list_backlog_tasks.sh + +# List tickets with completed tasks (all done) +bash .tickets/scripts/list_completed_tasks.sh +``` + +--- + +## Permission Management + +If Docker permissions cause issues: + +```bash +docker run --rm -v /path/to/worktree:/workspace --user root alpine chown -R 1000:1000 /workspace +``` + +--- + +## Commit Verification + +```bash +# Check last commit matches current task +git log -1 --oneline | grep -q "$(grep -A5 -B5 '\[-]' .tickets/*/prd.md | grep -E 'Task [0-9]+:' | tail -1 | sed 's/.*Task \([0-9]\+\):.*/ticket-\1/')" + +# Uncommitted changes count +git status --porcelain | wc -l +``` + +--- + +# Part 3: Parallel Execution Policy + +## Conflict Prevention + +- **File ownership**: Parallel tasks must work on distinct files/directories +- **Pre-flight scan**: Check for overlapping targets before spawning parallel work +- **Shared state**: Avoid mutable shared state; each task uses its own scratch space +- **Database schema changes**: Only one task at a time may modify schema (sequential) +- **If conflict arises**: Pause tasks, refactor for independence, or enforce dependency + +## Enforcement + +- **Parallelism is default** — maximize concurrency +- **Sequential only when dependencies block parallelism** — document why +- **Commit messages** should note parallel work: `feat(ticket-XXX): implement A and B in parallel (tasks 1.1, 1.2)` +- **Monitor progress** — ensure no task stalls +- **Subagent parallelism**: Up to 6 concurrent subagents; main agent polls every 2 min + +--- + +## OCC-Specific Notes + +- The extension uses `context.secrets` for JWT storage — never log tokens +- Backend balance updates must be atomic with usage logging +- Stripe webhook processing must be idempotent (use `stripe_events` table) +- All network calls should have error handling and retry logic +- Logging: use structured JSON (pino) with request IDs +- Status bar balance only visible in `authenticated` state (not BYOK) +- Deep-link URI handler: `occ-editor://auth?token=...&balance=...` + +--- + +*This AGENTS.md adapts the KitchenBookApp framework for OCC. Refer to `docs/` for additional technical specifications.* diff --git a/.tickets/scripts/find_next_ticket.sh b/.tickets/scripts/find_next_ticket.sh new file mode 100755 index 00000000..023df7f8 --- /dev/null +++ b/.tickets/scripts/find_next_ticket.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Find the next ticket with pending tasks, skipping any with ignore header +# Output: .tickets/ticket-XXX-name/prd.md:LINE_NUMBER + +tickets=$(ls -1d .tickets/ticket-[0-9]* 2>/dev/null | sort -V) + +for ticket in $tickets; do + prd="$ticket/prd.md" + [ -f "$prd" ] || continue + + # Skip ignored tickets + if grep -qE '^(|IGNORE:\s*true)' "$prd"; then + continue + fi + + # Find first pending [ ] + pending_line=$(grep -n '\[ \]' "$prd" | head -n1) + if [ -n "$pending_line" ]; then + line_num=$(echo "$pending_line" | cut -d: -f1) + echo "$prd:$line_num" + exit 0 + fi + + # If no pending, but has in-progress [-], that's next (shouldn't happen if workflow correct) + inprog_line=$(grep -n '\[-\]' "$prd" | head -n1) + if [ -n "$inprog_line" ]; then + line_num=$(echo "$inprog_line" | cut -d: -f1) + echo "$prd:$line_num" + exit 0 + fi +done + +echo "No pending tickets found" +exit 0 \ No newline at end of file diff --git a/.tickets/scripts/list_backlog_tasks.sh b/.tickets/scripts/list_backlog_tasks.sh new file mode 100755 index 00000000..5f495ff0 --- /dev/null +++ b/.tickets/scripts/list_backlog_tasks.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# List tickets and number of backlog (pending+in-progress) tasks +grep -rEc '\[(-| )\]' .tickets/*/prd.md | sort \ No newline at end of file diff --git a/.tickets/scripts/list_completed_tasks.sh b/.tickets/scripts/list_completed_tasks.sh new file mode 100755 index 00000000..3d15cd46 --- /dev/null +++ b/.tickets/scripts/list_completed_tasks.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# List tickets with completed task counts (tickets that have [x] tasks) +grep -rEc '\[(-| )\]' .tickets/*/prd.md | grep ':0' | sort \ No newline at end of file diff --git a/.tickets/scripts/verify_tickets.sh b/.tickets/scripts/verify_tickets.sh new file mode 100755 index 00000000..f1c40c81 --- /dev/null +++ b/.tickets/scripts/verify_tickets.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# Verify ticket status — counts pending/in-progress tasks per ticket +grep -rEc '\[(-| )\]' .tickets/*/prd.md | sort \ No newline at end of file diff --git a/.tickets/ticket-001-onboarding-copy-update/agent-history.md b/.tickets/ticket-001-onboarding-copy-update/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-001-onboarding-copy-update/prd.md b/.tickets/ticket-001-onboarding-copy-update/prd.md new file mode 100644 index 00000000..54531813 --- /dev/null +++ b/.tickets/ticket-001-onboarding-copy-update/prd.md @@ -0,0 +1,69 @@ +# PRD: Ticket 001 - Onboarding Copy Update + +## 1. Problem Statement + +The OCCode onboarding flow contains references to "MoltPilot" in user-facing copy that should be updated to reflect the current branding. The "$1 free tier" and "Lasts about a week" strings referenced in the original PRD no longer exist in the code — those were already updated in a previous pass. + +## 2. Current State Audit + +Strings found in `home.ts` that need updating: + +| Line | Current Text | Context | User-facing? | +|------|-------------|---------|-------------| +| 2186 | `Start Free` | Free tier CTA button | ✅ Yes | +| 2932 | `Start Free` | Wizard step 0 CTA button | ✅ Yes | +| 2225 | `Ask MoltPilot to fix this` | Error help button | ✅ Yes | +| 2320 | `Installing Inference for MoltPilot...` | Setup log message | ✅ Yes | +| 1618 | `handing off to MoltPilot for installation...` | Wizard log message | ✅ Yes | +| 518, 1477, 1478, 1615 | MoltPilot comments | Internal code comments | ❌ No | +| 1911, 2507-2508 | MoltPilot function names/IDs | Internal JS | ❌ No (but button text is user-facing) | + +## 3. Proposed Solution + +Update user-facing strings only (no logic changes): + +- **Free CTA button** (lines 2186, 2932): `Start Free` → `Create Account` +- **Error help button** (line 2225): `Ask MoltPilot to fix this` → `Ask AI to fix this` +- **Setup log** (line 2320): `Installing Inference for MoltPilot...\nInstalling Inference for your new OpenClaw...` → `Installing Inference for your new OpenClaw...` +- **Wizard log** (line 1618): `handing off to MoltPilot for installation...` → `handing off to AI assistant for installation...` + +Internal comments and function names (`askMoltPilot`, `molt-help` class, etc.) are NOT user-facing and should NOT be changed — renaming them would require changes across multiple files and provides no user benefit. + +## 4. Acceptance Criteria + +- [x] Free CTA buttons say "Create Account" instead of "Start Free" +- [x] Error help button says "Ask AI to fix this" instead of "Ask MoltPilot to fix this" +- [x] Setup log no longer references "MoltPilot" in user-visible messages +- [x] Wizard log no longer references "MoltPilot" in user-visible messages +- [x] No logic changes — only string replacements +- [x] Internal function names and CSS classes unchanged (non-user-facing) + +## 5. Dependencies + +- None (standalone UI text update) + +## 6. Subtask Checklist + +- [x] Task 1: Audit current strings in home.ts + - **Problem**: PRD references strings that don't exist; need to find what actually needs changing + - **Test**: Grep for "MoltPilot", "Start Free" in extension source + - **Subtasks**: + - [x] Subtask 1.1: Search and document all occurrences + - [x] Subtask 1.2: Classify as user-facing vs internal + +- [x] Task 2: Apply string replacements + - **Problem**: Update user-facing text correctly + - **Test**: Recompile extension and verify strings in compiled output + - **Subtasks**: + - [x] Subtask 2.1: Update "Start Free" → "Create Account" (2 locations) + - [x] Subtask 2.2: Update "Ask MoltPilot to fix this" → "Ask AI to fix this" + - [x] Subtask 2.3: Update "Installing Inference for MoltPilot..." log message + - [x] Subtask 2.4: Update "handing off to MoltPilot" wizard log message + +- [x] Task 3: Verify in compiled output + - **Problem**: Ensure changes are reflected in compiled JS + - **Test**: Recompile extension inside Docker container, grep compiled output + - **Subtasks**: + - [x] Subtask 3.1: Recompile extension (`npx tsc -p ./` inside container) + - [x] Subtask 3.2: Verify "Create Account" appears in compiled home.js + - [x] Subtask 3.3: Verify no user-facing "MoltPilot" remains in compiled output diff --git a/.tickets/ticket-002-onboarding-auth-waiting-state/agent-history.md b/.tickets/ticket-002-onboarding-auth-waiting-state/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-002-onboarding-auth-waiting-state/prd.md b/.tickets/ticket-002-onboarding-auth-waiting-state/prd.md new file mode 100644 index 00000000..ce4e99c9 --- /dev/null +++ b/.tickets/ticket-002-onboarding-auth-waiting-state/prd.md @@ -0,0 +1,83 @@ +# PRD: Ticket 002 - Onboarding Flow — Auth Waiting State + +## 1. Problem Statement + +The current onboarding flow does not handle the web-based authentication flow gracefully. When a user clicks "Sign in with OCC" (or "Create Account"), they are redirected to the browser to complete sign-up at MBA.sh. After successful authentication, the backend should redirect back to the editor via a deep-link (`occ-editor://auth?token=&balance=`). The editor must register a URI handler to receive this callback and transition from a "waiting" state to the authenticated state automatically. Without this, users must manually restart the editor or proceed without proper authentication. + +## 2. Proposed Solution + +Implement a waiting state in the onboarding flow with URI handler registration: + +1. `chooseFree()` opens `https://mba.sh/signup?ref=occ-editor` in system browser +2. Onboarding wizard transitions to a new **"Waiting for sign-in…"** step showing a spinner and a Cancel button +3. Extension registers an `occ-editor://` URI scheme handler on activation +4. When the system browser redirects to `occ-editor://auth?token=&balance=`: + - Store JWT in `context.secrets` (encrypted by VS Code, not in plain `globalState`) + - Dismiss the waiting state and proceed to gateway setup +5. Cancel button returns user to step 0 (onboarding start) + +## 3. Acceptance Criteria + +- Clicking "Create Account" opens the default browser to `https://mba.sh/signup?ref=occ-editor` +- Onboarding UI shows a "Waiting for sign-in…" step with spinner animation and Cancel button +- The extension successfully registers the `occ-editor://` URI handler +- After completing sign-up in browser, the editor window gains focus and the onboarding flow automatically advances to the next step +- JWT token is stored securely in `context.secrets` (not in plaintext global storage) +- Balance value from the callback is displayed correctly in the status bar after login +- Cancel button works at any time during waiting state and returns to step 0 + +## 4. Technical Considerations + +- **VS Code extension API:** Use `vscode.env.registerUriHandler` to handle deep-links +- **Security:** JWT must be stored in `context.secrets` (VS Code's encrypted secret storage) +- **State management:** The waiting state needs to be persisted across extension reactivations (in case user switches away and returns) +- **Race conditions:** The callback may arrive before the handler is registered; consider queuing or immediate state check on activation +- **Browser redirect:** Backend at MBA.sh must be configured to redirect to `occ-editor://auth` on successful signup + +## 5. Dependencies + +- **ticket-001-onboarding-copy-update:** Copy should already reflect MBA.sh sign-up +- Backend B1 (Auth — sign up & JWT issuance): The MBA.sh backend must exist and produce the redirect + +## 6. Subtask Checklist + +- [ ] Task 1: Implement URI handler registration + - **Problem:** Need to capture the deep-link callback + - **Test:** After sign-up, editor receives the `token` and `balance` query params + - **Subtasks:** + - [ ] Subtask 1.1: Add `vscode.env.registerUriHandler` in extension activation + - [ ] Subtask 1.2: Define `handleUri(uri: Uri)` method to parse JWT and balance + - [ ] Subtask 1.3: Validate token format and balance range before storing + +- [ ] Task 2: Store JWT securely and update global state + - **Problem:** Avoid plaintext storage; notify system of auth + - **Test:** `context.secrets.get('occ.sessionToken')` returns JWT; `globalState` reflects `authenticated` mode + - **Subtasks:** + - [ ] Subtask 2.1: `context.secrets.store('occ.sessionToken', token)` + - [ ] Subtask 2.2: `globalState.update('balance', parsedBalance)` + - [ ] Subtask 2.3: `globalState.update('sessionState', 'authenticated')` + - [ ] Subtask 2.4: Dispatch custom event to update UI panels + +- [ ] Task 3: Create "Waiting for sign-in…" onboarding step + - **Problem:** Provide visible feedback while user is in browser + - **Test:** Panel shows spinner + "Waiting for sign-in…" + Cancel button + - **Subtasks:** + - [ ] Subtask 3.1: Add new step to onboarding flow state machine + - [ ] Subtask 3.2: Design and render spinner UI (reuse existing loading indicator if available) + - [ ] Subtask 3.3: Wire Cancel button to reset state to step 0 + +- [ ] Task 4: Modify `chooseFree()` to open browser and set waiting state + - **Problem:** Current flow likely just opens browser and stays on same step + - **Test:** After clicking Create Account, browser opens and onboarding transitions to waiting step + - **Subtasks:** + - [ ] Subtask 4.1: `vscode.env.openExternal(vscode.Uri.parse('https://mba.sh/signup?ref=occ-editor'))` + - [ ] Subtask 4.2: Set onboarding step index to "waiting" state + - [ ] Subtask 4.3: Ensure Cancel button is prominent + +- [ ] Task 5: Edge case handling + - **Problem:** What if backend fails, or redirect never happens? + - **Test:** Cancellation or timeout returns user to start cleanly + - **Subtasks:** + - [ ] Subtask 5.1: On URI handler error, show error toast and reset + - [ ] Subtask 5.2: Consider a 5-minute timeout that auto-cancels (optional) + - [ ] Subtask 5.3: If token invalid (e.g., tampered), clear storage and show error diff --git a/.tickets/ticket-003-session-state-modes/agent-history.md b/.tickets/ticket-003-session-state-modes/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-003-session-state-modes/prd.md b/.tickets/ticket-003-session-state-modes/prd.md new file mode 100644 index 00000000..a4489052 --- /dev/null +++ b/.tickets/ticket-003-session-state-modes/prd.md @@ -0,0 +1,112 @@ +# PRD: Ticket 003 - Session State (Three Modes) + +## 1. Problem Statement + +The extension must correctly represent three mutually exclusive authentication/session states on every activation: + +| State | Condition | Behaviour | +|-------|-----------|-----------| +| `unauthenticated` | No JWT token, no BYOK config | Show onboarding flow | +| `authenticated` | Valid JWT present in `context.secrets` | Show balance bar, block inference if balance $0 | +| `byok` | BYOK provider configured (user supplies own API key) | Hide balance bar, allow unlimited inference | + +Currently the state handling is likely ad-hoc, leading to inconsistent UI and possible security issues (e.g., showing balance when not authenticated, or allowing inference without proper auth). + +## 2. Proposed Solution + +Implement a centralized session state manager (new `auth.ts` module) that: + +- On extension activation: + - Read JWT from `context.secrets.get('occ.sessionToken')` + - Read BYOK configuration from `globalState` (user settings) + - Determine current state: if JWT exists → `authenticated`; else if BYOK configured → `byok`; else → `unauthenticated` + - If `authenticated`, call `GET /api/v1/balance` (or stub) to fetch current balance and update UI + - If `byok`, hide balance-related UI elements + - If `unauthenticated`, auto-open Home panel to onboarding step 0 + +- Stub behavior during development: + - If token present but MBA.sh unreachable, use last-known balance from `globalState` as display fallback (clearly labelled "cached") + - If network error occurs, keep current state but show non-blocking warning + +- UI reactions: + - Status bar balance item visible only in `authenticated` state + - Home panel content differs by state (onboarding vs logged-in vs BYOK) + - Inference requests automatically attach `Authorization: Bearer ` when `authenticated`, or use BYOK key when `byok` + +## 3. Acceptance Criteria + +- On fresh launch with no token and no BYOK: Home panel shows onboarding step 0 automatically +- On launch with valid JWT: balance bar appears with correct balance (or cached value with label if offline) +- On launch with BYOK configured: no balance bar, MoltPilot inference works with user's key +- Launching with both JWT and BYOK: JWT takes precedence (authenticated state) +- When balance reaches $0 in `authenticated` state: inference calls are blocked and UI suggests top-up +- When user signs out (deletes token): state transitions to `unauthenticated` and onboarding appears +- State persists across editor restarts (JWT in secrets, BYOK in globalState) +- Network errors during balance fetch do not crash the extension; fallback balance is clearly labelled as stale + +## 4. Technical Considerations + +- **State machine:** Implement as a simple enum (`SessionState.UNAUTHENTICATED | AUTHENTICATED | BYOK`) with a `currentState` variable and `setState()` function that triggers UI updates +- **Storage:** `context.secrets` for JWT (encrypted), `globalState` for BYOK config and cached balance +- **Balance polling:** In `authenticated` state, poll `GET /api/v1/balance` every ~60 seconds to keep UI fresh +- **Staleness labeling:** When using cached balance due to network error, append "[cached]" and dim the value +- **Activation sequence:** Handle async initialization carefully; show loading spinner if state detection takes >~500ms +- **Extension lifecycle:** Re-run state detection on every activation (editor may stay open for days) + +## 5. Dependencies + +- **ticket-002-onboarding-auth-waiting-state:** JWT arrival via URI handler must integrate into state manager +- Backend B2 (Balance API): The real balance fetch endpoint (stubbed for now) + +## 6. Subtask Checklist + +- [ ] Task 1: Create `auth.ts` module + - **Problem:** Centralize all auth/session logic + - **Test:** Module exports `getSessionState()`, `setSessionState()`, `signOut()`, `fetchBalance()` + - **Subtasks:** + - [ ] Subtask 1.1: Define enum `SessionState` + - [ ] Subtask 1.2: Implement `detectSessionState()` that reads secrets + globalState + - [ ] Subtask 1.3: Implement `fetchBalance()` (calls `/api/v1/balance` or returns cached) + - [ ] Subtask 1.4: Implement `signOut()` that clears token and resets state + +- [ ] Task 2: Integrate state manager into extension activation + - **Problem:** Activation should immediately determine state and update UI + - **Test:** On every extension activation, the correct panel/status bar appears + - **Subtasks:** + - [ ] Subtask 2.1: In `extension.ts` `activate()`, call `auth.detectSessionState()` + - [ ] Subtask 2.2: State `unauthenticated` → `vscode.commands.executeCommand('occ.home.focus')` + - [ ] Subtask 2.3: State `authenticated` → start balance polling (every 60s) + - [ ] Subtask 2.4: State `byok` → ensure balance bar hidden, no polls + +- [ ] Task 3: Update Home panel to respect session state + - **Problem:** Home should show different content based on state + - **Test:** Panel renders onboarding when unauth, dashboard when auth, appropriate message when BYOK + - **Subtasks:** + - [ ] Subtask 3.1: Pass current `SessionState` to Home webview via `postMessage` + - [ ] Subtask 3.2: In `home.ts` (renderer), switch on state to show proper section + - [ ] Subtask 3.3: Ensure "Sign out" button appears only in authenticated/BYOK states + +- [ ] Task 4: Update Status bar (balance item) visibility + - **Problem:** Balance bar should only appear when authenticated + - **Test:** Status bar item creates/updates only in `authenticated` state, hides otherwise + - **Subtasks:** + - [ ] Subtask 4.1: In `extension.ts` status bar creation, check `sessionState === AUTHENTICATED` + - [ ] Subtask 4.2: When state changes to non-authenticated, `statusBarItem.hide()` + - [ ] Subtask 4.3: When state becomes authenticated, `statusBarItem.show()` and set text + +- [ ] Task 5: Balance fetch with fallback and stale labeling + - **Problem:** Network failures should not leave user in limbo + - **Test:** If `/balance` fails, last-known balance from `globalState` is used with "[cached]" suffix + - **Subtasks:** + - [ ] Subtask 5.1: In `fetchBalance()`, catch network errors and read `globalState.get('cachedBalance')` + - [ ] Subtask 5.2: Return object `{ balance, cached: true/false }` + - [ ] Subtask 5.3: Status bar format: `${balance.toFixed(2)} USD` (normal) or `${balance.toFixed(2)} USD [cached]` (stale) + - [ ] Subtask 5.4: Also postMessage to Home panel to show cached notice if needed + +- [ ] Task 6: Edge cases and polish + - **Problem:** Token expiry, malformed JWT, BYOK misconfiguration + - **Test:** Extension handles these gracefully without crashing + - **Subtasks:** + - [ ] Subtask 6.1: On balance fetch 401, auto-sign-out and show "Session expired, please sign in again" + - [ ] Subtask 6.2: On malformed JWT, clear secret and treat as unauthenticated + - [ ] Subtask 6.3: On BYOK configured but missing key, show warning in Home panel diff --git a/.tickets/ticket-004-status-bar-stub-mode/agent-history.md b/.tickets/ticket-004-status-bar-stub-mode/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-004-status-bar-stub-mode/prd.md b/.tickets/ticket-004-status-bar-stub-mode/prd.md new file mode 100644 index 00000000..5dba2bcd --- /dev/null +++ b/.tickets/ticket-004-status-bar-stub-mode/prd.md @@ -0,0 +1,106 @@ +# PRD: Ticket 004 - Status Bar — Stub Mode + +## 1. Problem Statement + +The status bar balance display needs to reflect the new OCC Credits model during the stub phase (before backend B2 is available). It should show a $5.00 free credit cap, animate smoothly when the balance changes, provide a tooltip with current value and "Get More Credits" link, and change colors based on remaining balance thresholds. The status bar item should be visible only when the session state is `authenticated` (not in BYOK or unauthenticated states). + +## 2. Proposed Solution + +Update the status bar implementation in `extensions/openclaw/src/extension.ts`: + +- Change `BALANCE_CAP` constant from `1.00` to `5.00` +- Update all `$1` user-facing copy to `$5` +- Add a `fetchBalance()` stub that returns the value from `globalState` (simulating backend response) +- Add smooth animation when balance decreases (use incremental steps or transition effect) +- Implement color thresholds: yellow/warning when balance < $0.20, red/error when near $0.00 (e.g., ≤ $0.05) +- Create a tooltip on hover that shows: "OCC Credits: $X.XX — Get More Credits" (link opens `https://mba.sh/credits`) +- Auto-refresh balance every ~60 seconds while editor is open (calls `fetchBalance()`) +- Ensure visibility tied to `sessionState === AUTHENTICATED` + +The stub should be easily swappable for real API calls when backend is ready. + +## 3. Acceptance Criteria + +- Status bar item appears in bottom-right when authenticated, with text `$5.00` initially (or actual balance) +- When user sends a chat message, balance decreases smoothly (animation duration ~500ms) to reflect inferred cost +- Balance turns yellow when below $0.20, red when ≤ $0.05 +- Hovering over balance shows tooltip: "OCC Credits: $X.XX — Get More Credits" where "Get More Credits" is a clickable link to `https://mba.sh/credits` +- Clicking the balance opens the credits page in browser (or executes command to open URL) +- Balance auto-updates every 60 seconds (network call simulated by stub) +- When user signs out or switches to BYOK state, status bar item hides +- If offline, status bar continues to show last-known balance without animation errors + +## 4. Technical Considerations + +- **Animation:** Use VS Code's `window.withProgress` or custom timer to step the displayed value from old→new +- **Colors:** Use `statusBarItem.backgroundColor` or icon theme colors; ensure they adapt to light/dark themes +- **Tooltips:** `statusBarItem.tooltip` can contain HTML-like text but not full links; consider `vscode.env.openExternal` on click instead +- **Click action:** Register `statusBarItem.command` to open credits URL +- **Polling:** Use `setInterval` in extension activation; clear on deactivation +- **Swap to real API:** Abstract `fetchBalance()` behind an interface; stub returns `globalState.get('balance')` while real version calls `GET /api/v1/balance` + +## 5. Dependencies + +- **ticket-003-session-state-modes:** Session state manager must exist and update status bar visibility +- Backend B2 (Balance API): Will replace stub later + +## 6. Subtask Checklist + +- [ ] Task 1: Update constants and UI defaults + - **Problem:** Stub needs correct cap and copy + - **Test:** Code contains `BALANCE_CAP = 5.00` and "$5" strings + - **Subtasks:** + - [ ] Subtask 1.1: Change hardcoded `1.00` to `5.00` in balance logic + - [ ] Subtask 1.2: Search/replace "$1" → "$5" in user-facing strings + +- [ ] Task 2: Implement `fetchBalance()` stub + - **Problem:** Simulate backend until B2 is ready + - **Test:** Function returns Promise from `globalState.get('balance')` + - **Subtasks:** + - [ ] Subtask 2.1: Add `fetchBalance(): Promise` that reads `globalState.get('balance')` (default 5.00) + - [ ] Subtable 2.2: Ensure it returns a Promise to match async real API shape + +- [ ] Task 3: Create/update status bar item with color thresholds + - **Problem:** Visual feedback for low balance + - **Test:** Status bar color changes as balance crosses <0.20 and ≤0.05 + - **Subtasks:** + - [ ] Subtask 3.1: Create `StatusBar` class or update existing with `updateBalance(amount: number)` method + - [ ] Subtask 3.2: Compute color: if amount ≤ 0.05 → red theme; else if amount < 0.20 → yellow; else default + - [ ] Subtask 3.3: Set `statusBarItem.backgroundColor` accordingly (or use `statusBarItem.name` with theme color identifiers) + +- [ ] Task 4: Add smooth balance decrease animation + - **Problem:** Balance jumps are jarring; need gradual update + - **Test:** When cost is deducted, display counts down from old→new over ~500ms + - **Subtasks:** + - [ ] Subtask 4.1: Implement `animateBalance(from: number, to: number, durationMs: number)` + - [ ] Subtask 4.2: Use `setInterval` or `requestAnimationFrame` style stepping + - [ ] Subtask 4.3: Cancel any running animation if a new update arrives mid-flight + +- [ ] Task 5: Implement tooltip and click-to-top-up + - **Problem:** Users need easy path to add credits + - **Test:** Hover shows formatted tooltip; click opens browser to `https://mba.sh/credits` + - **Subtasks:** + - [ ] Subtask 5.1: Set `statusBarItem.tooltip` to `OCC Credits: $${balance.toFixed(2)} — Get More Credits` + - [ ] Subtask 5.2: Register `statusBarItem.command = 'occ.openCreditsPage'` + - [ ] Subtask 5.3: In `extension.ts`, register command that calls `vscode.env.openExternal(vscode.Uri.parse('https://mba.sh/credits'))` + +- [ ] Task 6: Balance polling and state-based visibility + - **Problem:** Keep balance fresh; hide when not authenticated + - **Test:** Polls every 60s, updates if changed; hides on sign-out/BYOK + - **Subtasks:** + - [ ] Subtask 6.1: In session state manager, when state is `authenticated`, start polling: `setInterval(fetchAndUpdateBalance, 60000)` + - [ ] Subtask 6.2: On state change to non-authenticated, clear interval and `statusBarItem.hide()` + - [ ] Subtask 6.3: On state change to authenticated, `statusBarItem.show()` and trigger immediate fetch + +- [ ] Task 7: Integrate with chat inference cost deduction + - **Problem:** Balance must decrement when user sends a chat message + - **Test:** After chat completion, balance decreases by the cost reported in `x-litellm-response-cost` header (or simulated stub) + - **Subtasks:** + - [ ] Subtask 7.1: In the MoltPilot inference handler, read response header `x-litellm-response-cost` + - [ ] Subtask 7.2: `currentBalance -= cost`; save to `globalState.set('balance', currentBalance)` + - [ ] Subtask 7.3: Call `animateBalance(old, new)` to reflect change + +- [ ] Task 8: Offline/cached fallback + - **Problem:** Stub should never crash if network fails (future real API) + - **Test:** If fetchBalance() throws, continue showing last-known value without animation hiccup + - [ ] Subtask 8.1: Wrap fetch in try/catch; on error, keep previous balance and optionally show warning notification diff --git a/.tickets/ticket-005-settings-panel-occ-credits/agent-history.md b/.tickets/ticket-005-settings-panel-occ-credits/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-005-settings-panel-occ-credits/prd.md b/.tickets/ticket-005-settings-panel-occ-credits/prd.md new file mode 100644 index 00000000..69fa2ab5 --- /dev/null +++ b/.tickets/ticket-005-settings-panel-occ-credits/prd.md @@ -0,0 +1,109 @@ +# PRD: Ticket 005 - Settings Panel — OCC Credits Card + +## 1. Problem Statement + +The Settings UI (VS Code settings panel) currently has an "OCC Free Tier" card that needs updating to match the new OCC Credits model with MBA.sh integration. The card should now reflect sign-up, authentication, live balance, and top-up capabilities. When not authenticated, the card should offer a "Sign in to MBA.sh" button. When authenticated, it should show account email, current balance (with cached fallback), "Buy More Credits" link, and a "Sign Out" button. The BYOK card should clearly state "Always free. No account needed." The card titles and subtitles must be updated to new branding. + +## 2. Proposed Solution + +Modify the settings panel component (likely `src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx` or a similar path within the editor source) to implement the new OCC Credits card: + +- Rename card 1 from "OCC Free Tier" to "OCC Credits" +- Update subtitle text: change "Use the built-in model. No API key needed — $1 of free inference included." → "Powered by MBA.sh. $5 free on sign-up. Buy more at standard rates." +- Card 2 "Bring Your Own Key": add "Always free. No account." as subtext under the title +- When OCC Credits is selected but user is **not authenticated**: + - Do not show endpoint/key fields (use MBA.sh account) + - Instead show a prominent "Sign in to MBA.sh" button that opens `https://mba.sh/login?ref=occ-editor` in browser +- When OCC Credits is selected **and authenticated**: + - Show account email (from `GET /api/v1/me` or JWT decode) + - Show live balance (from cached/real) with color based on threshold + - Show "Buy More Credits" button linking to `https://mba.sh/billing` + - Show "Sign Out" button that clears credentials +- Ensure the card respects the session state from `ticket-003` + +## 3. Acceptance Criteria + +- Settings panel loads without crashing after changes +- The OCC Credits card displays correct title and subtitle +- BYOK card displays "Always free. No account needed." +- If user is not authenticated with OCC Credits mode, the card shows "Sign in to MBA.sh" button instead of API endpoint/key inputs +- Clicking "Sign in to MBA.sh" opens the browser to MBA.sh login with proper `ref=occ-editor` +- If user is authenticated with OCC Credits: + - Account email is displayed (truncated if long) + - Balance is shown with correct formatting ($X.XX) and color + - "Buy More Credits" opens `https://mba.sh/billing` + - "Sign Out" button logs user out (clears token, resets state) +- Switching between OCC Credits and BYOK correctly shows/hides relevant sections +- Settings changes are persisted to `globalState` appropriately + +## 4. Technical Considerations + +- **File location:** The settings panel lives within the `apps/editor` fork; exact path may be `src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx` or similar. Will need to locate the exact component responsible for the "OCC Free Tier" card. +- **State access:** The settings renderer likely does not have direct access to extension `context`. Must communicate via `postMessage` to the extension host, or read from `globalState` if it's a VS Code settings UI (which uses `ConfigurationTarget`). +- **Authentication state:** Use the `SessionState` from `ticket-003`; either expose via an event or query extension via `vscode.commands.executeCommand('occ.getSessionState')`. +- **Balance display:** Reuse the same balance formatting and color logic as the status bar (ticket-004) to stay consistent. +- **Sign out:** Should call the same `auth.signOut()` function used elsewhere. +- **Build:** The editor must be rebuilt after UI changes: `npm --prefix apps/editor run compile` or similar. + +## 5. Dependencies + +- **ticket-003-session-state-modes:** Need session state to drive UI variations +- **ticket-004-status-bar-stub-mode:** Balance formatting and color logic should be consistent +- Backend B1/B2: For real email and balance (stubbed for now) + +## 6. Subtask Checklist + +- [ ] Task 1: Locate settings panel component for OCC mode + - **Problem:** Need exact file path and component structure + - **Test:** Grep for "OCC Free Tier" or "Bring Your Own Key" in `apps/editor` source + - **Subtasks:** + - [ ] Subtask 1.1: Search `apps/editor` for "Free Tier" and "BYOK" + - [ ] Subtask 1.2: Identify the React component that renders the card(s) + - [ ] Subtask 1.3: Map the props/state that control which fields are shown + +- [ ] Task 2: Update copy text + - **Problem:** Replace outdated strings with new branding + - **Test:** Render shows "$5 free on sign-up", "Always free. No account needed." + - **Subtasks:** + - [ ] Subtask 2.1: Change title "OCC Free Tier" → "OCC Credits" + - [ ] Subtask 2.2: Update subtitle to "Powered by MBA.sh. $5 free on sign-up. Buy more at standard rates." + - [ ] Subtask 2.3: Add "Always free. No account needed." to BYOK card + +- [ ] Task 3: Implement conditional UI (authenticated vs not) + - **Problem:** Show "Sign in" button if not auth; show account/balance/action buttons if auth + - **Test:** UI switches correctly based on session state + - **Subtasks:** + - [ ] Subtask 3.1: Add state variable `isAuthenticated` (derived from session state) + - [ ] Subtask 3.2: If OCC Credits selected && !isAuthenticated → render "Sign in to MBA.sh" button + - [ ] Subtask 3.3: If OCC Credits selected && isAuthenticated → render email, balance, "Buy More Credits", "Sign Out" + - [ ] Subtask 3.4: Ensure BYOK section does not show these auth-specific elements + +- [ ] Task 4: Wire up "Sign in to MBA.sh" button + - **Problem:** Opens browser to correct sign-up/login flow with referral + - **Test:** Click opens `https://mba.sh/login?ref=occ-editor` (or signup if new user) + - **Subtasks:** + - [ ] Subtask 4.1: Button calls `vscode.env.openExternal` with the URL + - [ ] Subtask 4.2: Use `ref=occ-editor` query param for attribution + +- [ ] Task 5: Display account email and live balance + - **Problem:** Show user who is logged in and their credit amount + - **Test:** Email appears truncated (max ~30 chars); balance formatted with two decimals and appropriate color + - **Subtasks:** + - [ ] Subtask 5.1: Fetch email from JWT decode or `GET /api/v1/me` (stub returns cached) + - [ ] Subtable 5.2: Fetch balance using same stub as status bar, display `$${balance.toFixed(2)}` + - [ ] Subtask 5.3: Apply color: red if very low, yellow if low, default otherwise + +- [ ] Task 6: "Buy More Credits" and "Sign Out" buttons + - **Problem:** Provide clear next actions + - **Test:** "Buy More Credits" opens `https://mba.sh/billing`; "Sign Out" triggers sign-out flow and returns to unauth state + - **Subtasks:** + - [ ] Subtask 6.1: "Buy More Credits" button calls `vscode.env.openExternal(vscode.Uri.parse('https://mba.sh/billing'))` + - [ ] Subtask 6.2: "Sign Out" button calls `auth.signOut()` then resets settings view to OCC Credits mode (still selected) but now showing "Sign in" button + +- [ ] Task 7: Ensure settings persistence and rebuild + - **Problem:** Changes must survive rebuild and not regress + - **Test:** `npm --prefix apps/editor run compile` succeeds; editor launches; settings persist across reloads + - **Subtasks:** + - [ ] Subtask 7.1: Run editor build script; fix any TypeScript errors + - [ ] Subtask 7.2: Launch editor, verify settings UI appears correctly + - [ ] Subtask 7.3: Change OCC mode between OCC Credits and BYOK, restart editor, confirm selection persists diff --git a/.tickets/ticket-006-sign-out/agent-history.md b/.tickets/ticket-006-sign-out/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-006-sign-out/prd.md b/.tickets/ticket-006-sign-out/prd.md new file mode 100644 index 00000000..61b524e0 --- /dev/null +++ b/.tickets/ticket-006-sign-out/prd.md @@ -0,0 +1,104 @@ +# PRD: Ticket 006 - Sign Out + +## 1. Problem Statement + +Users need a way to sign out of their OCC session (authenticated mode) to switch accounts, revoke access, or return to BYOK/unauthenticated state. The sign-out flow must securely clear the JWT token and any cached balance, reset the session state to `unauthenticated`, and return the user to the onboarding start (step 0) in the Home panel. The session state change should also hide the status bar balance item. + +## 2. Proposed Solution + +Implement `signOut()` in the `auth.ts` module and wire it to UI elements: + +- `auth.signOut()`: + - `await context.secrets.delete('occ.sessionToken')` + - `globalState.update('balance', null)` (clear cached balance) + - `setSessionState(SessionState.UNAUTHENTICATED)` + - Optionally `vscode.commands.executeCommand('occ.home.focus')` to return to onboarding + +- UI triggers: + - "Sign Out" button in Settings panel (ticket-005) + - "Sign Out" button in Home panel (when authenticated) + - Possibly a command pallet command "OCC: Sign Out" + +- Ensure all components react to the state change: + - Status bar hides (already covered in ticket-003) + - Home panel switches to onboarding step 0 + - Settings panel OCC Credits card now shows "Sign in to MBA.sh" instead of account info + +## 3. Acceptance Criteria + +- User can trigger sign-out from Settings panel or Home panel +- After sign-out: + - JWT token is removed from `context.secrets` + - Cached balance is cleared from `globalState` + - Session state becomes `UNAUTHENTICATED` + - Home panel automatically opens and shows onboarding step 0 + - Status bar balance item disappears + - Settings OCC Credits card shows "Sign in to MBA.sh" button +- Attempting to use MoltPilot inference after sign-out should fail with clear "not authenticated" error and guide to sign in +- Sign-out is idempotent: calling it when already unauthenticated does nothing (no error) + +## 4. Technical Considerations + +- **Security:** Use `context.secrets.delete()` for JWT; ensure no residual copies in `globalState` or other storage +- **State propagation:** The `setSessionState()` method from ticket-003 should broadcast an event so all UI components update instantly +- **Race conditions:** If sign-out occurs during an in-flight API call, the call should be aborted or ignored when it returns +- **User experience:** Provide a confirmation dialog? Probably not for now; keep it simple but allow undo within 5 seconds via toast (optional, nice-to-have) +- **Cleanup:** Any open webviews (Home) should receive the state change via `postMessage` to re-render + +## 5. Dependencies + +- **ticket-003-session-state-modes:** `auth.signOut()` and state change propagation +- **ticket-005-settings-panel-occ-credits:** Settings UI button wiring +- Home panel updates (part of ticket-003 maybe) + +## 6. Subtask Checklist + +- [ ] Task 1: Implement `auth.signOut()` function + - **Problem:** Centralized sign-out logic + - **Test:** Calling `signOut()` clears token, balance, and sets state to UNAUTHENTICATED + - **Subtasks:** + - [ ] Subtask 1.1: `context.secrets.delete('occ.sessionToken')` + - [ ] Subtask 1.2: `globalState.update('balance', null)` + - [ ] Subtask 1.3: `setSessionState(SessionState.UNAUTHENTICATED)` + - [ ] Subtask 1.4: Emit custom event `'sessionStateChanged'` with new state + +- [ ] Task 2: Add UI buttons in Settings and Home + - **Problem:** Need entry points for user to trigger sign-out + - **Test:** Buttons visible when authenticated; clicking triggers sign-out + - **Subtasks:** + - [ ] Subtask 2.1: In Settings OCC Credits card (ticket-005), add "Sign Out" button that calls `auth.signOut()` + - [ ] Subtask 2.2: In Home panel authenticated view, add "Sign Out" button wired to extension command `occ.signOut` + - [ ] Subtask 2.3: In `extension.ts`, register command `'occ.signOut'` that calls `auth.signOut()` + +- [ ] Task 3: Ensure UI reacts to state change + - **Problem:** Components must update immediately after sign-out + - **Test:** After sign-out, Settings card shows "Sign in", Home shows onboarding, status bar hides + - **Subtasks:** + - [ ] Subtask 3.1: Settings panel listens to session state changes (via `globalState` or extension messages) and re-renders card conditions accordingly + - [ ] Subtask 3.2: Home panel listens for `'sessionStateChanged'` and navigates to step 0 if state is UNAUTHENTICATED + - [ ] Subtask 3.3: Status bar already listens to state (ticket-003); verify it hides on sign-out + +- [ ] Task 4: Guard inference requests after sign-out + - **Problem:** If a chat request is in flight during sign-out, or after, it should not use deleted token + - **Test:** Sending a message after sign-out yields "not authenticated" error; UI shows helpful message + - **Subtasks:** + - [ ] Subtask 4.1: In inference handler, check `sessionState === AUTHENTICATED` before attaching token + - [ ] Subtask 4.2: If not authenticated, return immediate error: "You are signed out. Please sign in to use chat." + - [ ] Subtask 4.3: Chat UI should display error and possibly offer "Sign In" button + +- [ ] Task 5: Optional: Undo toast + - **Problem:** Accidental sign-out should be recoverable + - **Test:** After sign-out, a toast appears: "Signed out. Undo?" with 5s timeout; clicking Undo re-signs in with previous token (if still valid) + - **Subtasks:** + - [ ] Subtask 5.1: In `signOut()`, show `vscode.window.showInformationMessage('Signed out. Undo?', 'Undo')` + - [ ] Subtask 5.2: If user clicks Undo within 5s, re-store the token from backup (copy token before delete) and restore state to AUTHENTICATED + - [ ] Subtask 5.3: Set a 5-second timeout to clear backup after sign-out + +- [ ] Task 6: Testing and polish + - **Problem:** Ensure flow works end-to-end + - **Test:** Manual test: sign in → verify UI → sign out → verify unauth UI; try to chat → blocked + - **Subtasks:** + - [ ] Subtask 6.1: Test sign-out from Settings + - [ ] Subtask 6.2: Test sign-out from Home + - [ ] Subtask 6.3: Test sign-out during chat request (should cancel) + - [ ] Subtask 6.4: Verify no console errors in developer console diff --git a/.tickets/ticket-007-backend-auth-jwt/agent-history.md b/.tickets/ticket-007-backend-auth-jwt/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-007-backend-auth-jwt/prd.md b/.tickets/ticket-007-backend-auth-jwt/prd.md new file mode 100644 index 00000000..3be50190 --- /dev/null +++ b/.tickets/ticket-007-backend-auth-jwt/prd.md @@ -0,0 +1,121 @@ +# PRD: Ticket 007 - Backend Auth & JWT Issuance + +## 1. Problem Statement + +The OCC backend at `https://occ.mba.sh` must handle user sign-up, authentication, and JWT issuance. Users create an account (email/password or OAuth) and receive a signed JWT with 7-day expiry, along with an initial $5.00 credit. After sign-up, the backend redirects to `occ-editor://auth?token=&balance=` to return the user to the editor. + +## 2. Proposed Solution + +Implement authentication endpoints using Node.js (Fastify) + Drizzle ORM + PostgreSQL: + +- `POST /api/v1/auth/signup`: Create user, grant $5 credit, issue JWT, return redirect URL (or directly redirect) +- `POST /api/v1/auth/refresh`: Refresh expiring token (optional, can use longer expiry) +- `GET /api/v1/me`: Validate token, return `{ email, balance }` + +The JWT should include user ID and email; sign with HS256 using `JWT_SECRET`. Use bcrypt to hash passwords for email accounts. For OAuth (Google, GitHub), use Passport.js strategies and create/link user records. + +After successful sign-up, respond with `302` redirect to `occ-editor://auth?token=&balance=5.00`. + +## 3. Acceptance Criteria + +- Sign-up with email/password creates a new user record with hashed password and `provider='email'` +- On sign-up, a corresponding row in `credits` table is created with `balance_usd=5.00` and `lifetime_usd=5.00` +- JWT is signed, includes `sub=userId`, `email`, and `exp` 7 days in future +- Sign-up responds with `302` redirect to `occ-editor://auth?token=...&balance=5.00` +- `/api/v1/me` with valid JWT returns `{ email, balance }` +- Refresh endpoint (if implemented) issues new JWT with fresh expiry +- Invalid/missing token returns `401 Unauthorized` +- OAuth flows produce same outcome (user + credits) without password + +## 4. Technical Considerations + +- **Database:** Use Drizzle to create `users` and `credits` tables; foreign key `user_id` references `users.id` +- **JWT library:** `jsonwebtoken`; store secret in `JWT_SECRET` env var (64 random chars) +- **Password hashing:** `bcrypt` with cost factor 10 +- **OAuth:** `passport` with strategies; session store not needed if using JWT directly; store `provider` and `provider_id` in `users` +- **Security:** + - Validate email format and uniqueness + - Rate limit sign-up per IP (e.g., 5/hour) to prevent abuse + - Set `Content-Security-Policy` and use HTTPS only +- **Testing:** Use Supertest to hit endpoints; mock DB with in-memory SQLite for unit tests, but integration tests should hit real Postgres +- **Environment:** `BASE_URL` should be `https://occ.mba.sh`; `EDITOR_CALLBACK_SCHEME=occ-editor` +- **Docker:** Provide `Dockerfile` and `docker-compose.yml` for local dev with Postgres + +## 5. Dependencies + +- None (backend foundation independent) + +## 6. Subtask Checklist + +- [ ] Task 1: Set up project skeleton + - **Problem:** Need Fastify + TS + Drizzle project + - **Test:** `npm init` with proper deps; `src/index.ts` starts server + - **Subtasks:** + - [ ] Subtask 1.1: Initialize Node.js project, install `fastify`, `@fastify/cors`, `@fastify/helmet`, `drizzle-orm`, `drizzle-kit`, `pg`, `jsonwebtoken`, `bcrypt`, `passport`, `passport-google-oauth20`, `passport-github2` + - [ ] Subtask 1.2: Set up TypeScript config (`tsconfig.json`) with `module: "NodeNext"` and `target: "ES2022"` + - [ ] Subtask 1.3: Create Fastify server with CORS + helmet + JSON body parser + - [ ] Subtask 1.4: Configure environment variable loading (`zod` + `dotenv`) + +- [ ] Task 2: Define database schema and migrations + - **Problem:** Need `users` and `credits` tables + - **Test:** `drizzle-kit migrate` creates tables correctly in Postgres + - **Subtasks:** + - [ ] Subtask 2.1: Write Drizzle schema in `src/db/schema.ts` with `users` and `credits` tables as per backend.md + - [ ] Subtask 2.2: Generate migrations: `drizzle-kit generate:pg` + - [ ] Subtask 2.3: Apply migrations to dev DB; verify with `psql` + - [ ] Subtask 2.4: Add Drizzle connection in `src/db/index.ts` (connect on startup, disconnect on SIGTERM) + +- [ ] Task 3: Implement `POST /api/v1/auth/signup` (email/password) + - **Problem:** Create user and grant credits + - **Test:** POST `{ email, password }` returns `302` redirect; DB has user+credit row + - **Subtasks:** + - [ ] Subtask 3.1: Validate email format; check email uniqueness via `db.select().from(users).where(eq(users.email, email))` + - [ ] Subtask 3.2: Hash password with `bcrypt.hash(password, 10)` + - [ ] Subtask 3.3: Insert user row: `db.insert(users).values({ email, password: hash, provider: 'email' })` + - [ ] Subtask 3.4: Insert credits row: `db.insert(credits).values({ user_id: newUser.id, balance_usd: 5.0, lifetime_usd: 5.0 })` + - [ ] Subtask 3.5: Issue JWT: `jwt.sign({ sub: newUser.id, email }, JWT_SECRET, { expiresIn: '7d' })` + - [ ] Subtask 3.6: Respond with `response.redirect(302, \`occ-editor://auth?token=${token}&balance=5.00\`)` + +- [ ] Task 4: Implement `GET /api/v1/me` (protected) + - **Problem:** Return authenticated user's profile and balance + - **Test:** GET with valid `Authorization: Bearer ` returns `{ email, balance }`; invalid returns 401 + - **Subtasks:** + - [ ] Subtask 4.1: Create Fastify `preHandler` decorator `authenticate` that verifies JWT via `jwt.verify(token, JWT_SECRET)`, attaches `decoded` to request + - [ ] Subtask 4.2: In handler, `const user = await db.query.users.findFirst({ where: eq(users.id, decoded.sub) })` + - [ ] Subtask 4.3: `const credit = await db.query.credits.findFirst({ where: eq(credits.user_id, user.id) })` + - [ ] Subtask 4.4: Return `{ email: user.email, balance: credit.balance_usd }` + +- [ ] Task 5: Implement OAuth routes (Google + GitHub) + - **Problem:** Users should be able to sign up/login via OAuth + - **Test:** `/auth/google` initiates OAuth flow; callback creates/link user and redirects to editor with JWT + - **Subtasks:** + - [ ] Subtask 5.1: Configure Passport with GoogleStrategy and GitHubStrategy using client IDs/secrets from env + - [ ] Subtask 5.2: Create routes `/api/v1/auth/google` and `/auth/github` that call `passport.authenticate('google', { scope: ['profile','email'] })` and similar for GitHub + - [ ] Subtask 5.3: Create callback routes `/api/v1/auth/google/callback` and `/github/callback` that verify profile, find or create user (provider+provider_id), ensure credits exist (create if missing), issue JWT, redirect to `occ-editor://auth?token=...&balance=...` + - [ ] Subtask 5.4: Use same credit granting logic: if user has no credits row, insert `balance_usd=5.00, lifetime_usd=5.00` + +- [ ] Task 6: Add optional refresh endpoint (if desired) + - **Problem:** Tokens expire after 7 days; could offer refresh + - **Test:** POST `/api/v1/auth/refresh` with valid token returns new token with fresh expiry + - **Subtasks:** + - [ ] Subtask 6.1: Define refresh handler that verifies current token, issues new token with same payload and new `exp` + - [ ] Subtask 6.2: Respond `{ token: newToken, expires_in: 604800 }` + +- [ ] Task 7: Security hardening and testing + - **Problem:** Ensure endpoints are robust and secure + - **Test:** Automated tests pass; manual curl tests succeed; penetration basics covered + - **Subtasks:** + - [ ] Subtask 7.1: Add rate limiting on signup (e.g., `fastify-rate-limit` 5 req/hour per IP) + - [ ] Subtask 7.2: Validate all inputs with `zod` or `fastest-validator` + - [ ] Subtask 7.3: Write integration tests using `supertest` and a separate test database + - [ ] Subtask 7.4: Ensure HTTPS only in production (set `NODE_ENV=production` rejects HTTP) + - [ ] Subtask 7.5: Verify JWT secret is at least 64 random chars; rotate script included + +- [ ] Task 8: Documentation and deployment readiness + - **Problem:** Developers and ops need to know how to run and deploy + - **Test:** README includes env vars, database setup, run instructions; Dockerfile builds and runs + - **Subtasks:** + - [ ] Subtask 8.1: Add `README.md` with setup steps, API endpoints, env vars table + - [ ] Subtask 8.2: Add `Dockerfile` using `node:20-alpine`, copy source, run `npm ci --only=production`, start `node dist/index.js` + - [ ] Subtask 8.3: Add `docker-compose.yml` for local dev with Postgres service + - [ ] Subtask 8.4: Configure Fly.io / Railway deployment scripts (if applicable) diff --git a/.tickets/ticket-008-balance-api/agent-history.md b/.tickets/ticket-008-balance-api/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-008-balance-api/prd.md b/.tickets/ticket-008-balance-api/prd.md new file mode 100644 index 00000000..21f8c098 --- /dev/null +++ b/.tickets/ticket-008-balance-api/prd.md @@ -0,0 +1,110 @@ +# PRD: Ticket 008 - Balance API + +## 1. Problem Statement + +The editor needs to fetch the current OCC Credits balance for the authenticated user to display in the status bar and settings panel. This requires a lightweight, secure endpoint that returns the user's remaining credit amount. The backend must provide `GET /api/v1/balance` that accepts an authenticated JWT and responds with `{ balance_usd: number, cap_usd: number }`. The editor will call this endpoint on startup and every ~60 seconds to keep the UI updated. + +## 2. Proposed Solution + +Add a new route in the Fastify backend: + +``` +GET /api/v1/balance +Headers: Authorization: Bearer +Response: { balance_usd: number, cap_usd: number } +``` + +Implementation: + +- Use the existing `authenticate` decorator to verify JWT (from ticket-007) +- Query the `credits` table for the user's current `balance_usd` +- Return JSON with `balance_usd` and `cap_usd` (read from env `CREDITS_CAP` or constant `5.00`) +- If user not found or no credits row, return 404 or 500 (shouldn't happen if signup creates it) + +Error handling: +- 401 if invalid/missing token +- 404 if user has no credits row (should create on demand, but safer to 500 alert) +- 503 if database down + +The endpoint should be fast (<50ms) and cacheable (but we want real-time so no caching). + +## 3. Acceptance Criteria + +- Authenticated request returns `{ "balance_usd": 4.25, "cap_usd": 5.00 }` with HTTP 200 +- If balance is exactly 0, still returns 200 with `0.00` +- If token is missing or invalid, returns 401 with `{ "error": "Unauthorized" }` +- If user has no credit row, returns 500 with error log (should not happen) +- Response time < 50ms for typical query +- CORS allows requests from `vscode://` and `localhost` (or editor's origin) + +## 4. Technical Considerations + +- **Integration with session state:** The editor's `fetchBalance()` stub will call this endpoint when backend is live; during stub phase, returns `globalState` value +- **Security:** Only accessible with valid JWT; no additional rate limiting beyond auth +- **Monitoring:** Log each call with user ID and response time (info level) +- **Error messages:** Keep generic for security; don't reveal if user exists or not +- **Cap:** The cap (`5.00`) may be configurable per deployment; read from env `CREDITS_CAP` (default `5.00`) to allow changes without code deploy + +## 5. Dependencies + +- **ticket-007-backend-auth-jwt:** Must have `authenticate` decorator and credits table + +## 6. Subtask Checklist + +- [ ] Task 1: Implement `GET /api/v1/balance` handler + - **Problem:** Create endpoint that returns balance + - **Test:** `curl -H "Authorization: Bearer " http://localhost:3001/api/v1/balance` returns JSON with balance + - **Subtasks:** + - [ ] Subtask 1.1: Define route in `src/routes/balance.ts` (or inline) + - [ ] Subtask 1.2: Use `authenticate` preHandler + - [ ] Subtask 1.3: Query: `db.select({ balance: credits.balance_usd }).from(credits).where(eq(credits.user_id, request.userId))` + - [ ] Subtask 1.4: If not found, return 500; else return `{ balance_usd, cap_usd: parseFloat(process.env.CREDITS_CAP || '5.00') }` + +- [ ] Task 2: Add CORS allowance for editor origin + - **Problem:** VS Code extension may be considered `vscode://` origin; need to allow it + - **Test:** Preflight `OPTIONS /api/v1/balance` returns `Access-Control-Allow-Origin: *` (or specific) + - **Subtasks:** + - [ ] Subtask 2.1: Configure `@fastify/cors` to allow `vscode://`, `null`, and `localhost` (editor webview origin can be `null` or `vscode://` depending on platform) + - [ ] Subtask 2.2: Allow headers `Authorization, Content-Type` + +- [ ] Task 3: Write unit/integration tests + - **Problem:** Ensure correctness and prevent regressions + - **Test:** Automated tests cover happy path, 401, 404, 500 + - **Subtasks:** + - [ ] Subtask 3.1: Set up test DB (SQLite in-memory for speed, or separate Postgres schema) + - [ ] Subtask 3.2: Seed test user with credits `5.00` + - [ ] Subtask 3.3: Test with valid JWT → 200 with correct balance + - [ ] Subtask 3.4: Test with invalid token → 401 + - [ ] Subtask 3.5: Test with no credits row → 500 (or create-on-demand if design changes) + +- [ ] Task 4: Add monitoring/logging + - **Problem:** Need to track usage and spot issues + - **Test:** Server logs each balance fetch with userId and duration + - **Subtasks:** + - [ ] Subtask 4.1: Add `fastify.log.info({ userId, durationMs })` in handler + - [ ] Subtask 4.2: Configure log aggregation (stdout fine; later will add Loki/Datadog) + +- [ ] Task 5: Documentation + - **Problem:** Editor team needs to know how to call it + - **Test:** API documented in README or OpenAPI spec + - **Subtasks:** + - [ ] Subtask 5.1: Add endpoint description to `README.md` with example request/response + - [ ] Subtask 5.2: Include sample curl command + - [ ] Subtask 5.3: Mention that editor should poll every ~60s + +- [ ] Task 6: Deploy to staging + - **Problem:** Verify in near-production environment + - **Test:** Staging instance (`https://occ-staging.mba.sh`) returns balance for test user + - **Subtasks:** + - [ ] Subtask 6.1: Push to staging branch; deploy to Railway/Render + - [ ] Subtask 6.2: Run smoke tests with test user + - [ ] Subtask 6.3: Verify editor stub can hit endpoint and receives expected JSON + +- [ ] Task 7: Enable in editor stub (real API toggle) + - **Problem:** Editor currently uses stub balance; need to call real endpoint when ready + - **Test:** When `globalState.useBackendBalance = true`, `fetchBalance()` makes real HTTP request to `/api/v1/balance` + - **Subtasks:** + - [ ] Subtask 7.1: Add feature flag in global settings "OCC: Use Backend Balance" (default false) + - [ ] Subtask 7.2: In `fetchBalance()`, check flag; if true, `fetch('https://occ.mba.sh/api/v1/balance', { headers: { Authorization: `Bearer ${token}` } })` + - [ ] Subtask 7.3: Parse response; handle 401 by showing "Session expired" and prompting re-auth + - [ ] Subtask 7.4: On success, update `globalState` cached balance for fallback diff --git a/.tickets/ticket-009-inference-proxy/agent-history.md b/.tickets/ticket-009-inference-proxy/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-009-inference-proxy/prd.md b/.tickets/ticket-009-inference-proxy/prd.md new file mode 100644 index 00000000..72deae3e --- /dev/null +++ b/.tickets/ticket-009-inference-proxy/prd.md @@ -0,0 +1,129 @@ +# PRD: Ticket 009 - Inference Proxy + +## 1. Problem Statement + +The OCC editor must be able to send chat completions requests to an inference endpoint while properly authenticating the user and deducting credits. The backend will provide an OpenAI-compatible streaming endpoint `POST /v1/chat/completions` that accepts a JWT in the `Authorization` header, checks the user's balance > 0, forwards the request to the upstream inference service (`https://inference.mba.sh/v1`), streams the response back to the client, and deducts the cost from the user's balance after completion (or incrementally if using streaming cost tracking). + +## 2. Proposed Solution + +Implement a proxy endpoint in the Fastify backend: + +``` +POST /v1/chat/completions +Headers: Authorization: Bearer +Body: OpenAI chat completion payload (stream: true/false) +Response: Streamed or non-streamed OpenAI-compatible response +``` + +Flow: + +1. Authenticate JWT via `authenticate` decorator +2. Fetch user's current balance from `credits` table +3. If `balance_usd <= 0`, immediately return `402 Payment Required` (or `429` with `error: "Insufficient credits"`) +4. Forward the request to upstream `INFERENCE_ENDPOINT` (e.g., `https://inference.mba.sh/v1/chat/completions`) using `fetch` with streaming +5. Pipe the upstream response back to the client **as it arrives** (stream passthrough) +6. Once the upstream response completes, calculate cost: + - If using non-stream, read `x-litellm-response-cost` header from upstream response + - If using stream, accumulate cost from stream headers as they arrive (requires parsing SSE stream for cost metadata if provided by upstream; alternatively estimate based on tokens in/out) +7. Deduct cost from user's balance: `UPDATE credits SET balance_usd = balance_usd - cost WHERE user_id = ?` +8. Log usage to `usage_log` table: `user_id, tokens_in, tokens_out, cost_usd, model, created_at` +9. If balance update fails, log error but do not roll back the stream (user may have already consumed response) + +## 3. Acceptance Criteria + +- Authenticated request with sufficient balance streams chat completion successfully +- If balance is $0 or negative, endpoint returns `402` with JSON `{ "error": "Insufficient credits" }` promptly (no upstream call) +- Upstream response headers (including `x-litellm-response-cost`) are preserved and visible to client +- Cost is deducted accurately and persisted to `credits.balance_usd` +- A usage record is inserted into `usage_log` with correct values +- The endpoint correctly handles both streaming (`stream: true`) and non-streaming requests +- Network errors from upstream result in appropriate `502` or `503` responses, no deduction +- The endpoint is performant: latency overhead < 200ms before first byte + +## 4. Technical Considerations + +- **Streaming passthrough:** Use `undici` or `node-fetch` with `response.body` pipe to `reply.stream()` in Fastify. Do not buffer entire response. +- **Cost extraction:** The upstream `inference.mba.sh` sets header `x-litellm-response-cost` on the final response. For streaming, it may send it in the last SSE `data:` event or as a trailing header. Implement both: capture final headers from upstream response and parse cost from there. If cost cannot be determined, log warning and skip deduction (or use a per-request estimated cost as fallback). +- **Race condition:** The stream may be long-running; the balance deduction should happen after the stream ends (client receives all data). Use `response.on('close')` or `finished` event to trigger deduction. Ensure idempotency: if deduction runs twice for same request, it should not double-charge. Could use a unique `request_id` logged in `usage_log` and check before deducting again. +- **Database update in streaming context:** Must not block the stream. Perform deduction in a `finally` block after upstream stream ends, using a new DB connection (pool). If DB fails, log but do not retry to avoid blocking. +- **Security:** Ensure JWT authentication is applied before checking balance (do not leak existence of user via timing differences). Balance check should be constant-time relative to valid/invalid token (difficult in DB, but avoid early returns that differ). +- **Rate limiting:** Consider per-user rate limiting to prevent abuse (e.g., max 100 requests/min). Could be added later. +- **Logging:** Log each request with `userId`, `model`, `tokens` (if available), `cost`, `durationMs`, `status`. +- **Testing:** Use `nock` to mock upstream responses, including streaming; test 402 response, 502 fallback, deduction accuracy. + +## 5. Dependencies + +- **ticket-007-backend-auth-jwt:** `authenticate` decorator +- **ticket-008-balance-api:** Balance fetching logic (can reuse) +- Upstream inference service (`https://inference.mba.sh/v1`) must be running and OpenAI-compatible + +## 6. Subtask Checklist + +- [ ] Task 1: Set up route handler skeleton + - **Problem:** Create `/v1/chat/completions` endpoint + - **Test:** `curl -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{"model":"gpt-4","messages":[{"role":"user","content":"Hello"}],"stream":false}' http://localhost:3001/v1/chat/completions` returns 200 with response body + - **Subtasks:** + - [ ] Subtask 1.1: Register route in Fastify: `fastify.post('/v1/chat/completions', { onRequest: [authenticate] }, handler)` + - [ ] Subtask 1.2: Declare reply content type: `application/json` or `text/event-stream` based on `stream` param + - [ ] Subtask 1.3: Set CORS headers for editor origin + +- [ ] Task 2: Balance check and 402 response + - **Problem:** Enforce credit requirement before upstream call + - **Test:** User with balance 0 receives `402` immediately; no upstream call made + - **Subtasks:** + - [ ] Subtask 2.1: After auth, fetch user balance: `select balance_usd from credits where user_id = ?` + - [ ] Subtask 2.2: If `balance_usd <= 0`, return `response.code(402).send({ error: 'Insufficient credits' })` + - [ ] Subtask 2.3: Log balance check result (debug level) + +- [ ] Task 3: Proxy non-streaming requests + - **Problem:** Forward request body and return response body + - **Test:** Non-stream request yields same response as upstream; `x-litellm-response-cost` present in response + - **Subtasks:** + - [ ] Subtask 3.1: Use `fetch(INFERENCE_ENDPOINT, { method: 'POST', headers: { ...incomingHeaders except host, 'Authorization': `Bearer ${INFERENCE_API_KEY}` }, body: JSON.stringify(payload) })` + - [ ] Subtask 3.2: Wait for upstream response; get `upstreamRes.headers.get('x-litellm-response-cost')` + - [ ] Subtask 3.3: Return upstream JSON body to client with same headers + - [ ] Subtask 3.4: Trigger deduction with parsed cost + +- [ ] Task 4: Proxy streaming requests + - **Problem:** Pipe SSE stream from upstream to client without buffering + - **Test:** Stream arrives line-by-line in client with minimal latency; cost captured from trailing headers + - **Subtasks:** + - [ ] Subtask 4.1: If payload has `stream: true`, set `reply.header('Content-Type', 'text/event-stream')` + - [ ] Subtask 4.2: Create `upstreamRes.body.pipe(reply.raw)` to forward bytes directly + - [ ] Subtask 4.3: Listen for `upstreamRes.on('close')` or `finished` event to trigger cost extraction and deduction + - [ ] Subtask 4.4: Capture trailing headers; extract `x-litellm-response-cost` (may be in last chunk) + - [ ] Subtask 4.5: If cost not found after 60s of stream end, log warning and skip deduction + +- [ ] Task 5: Deduct balance and log usage + - **Problem:** Update credits and record usage reliably + - **Test:** After successful proxy, `credits.balance_usd` decreases by exact cost; `usage_log` has new row + - **Subtasks:** + - [ ] Subtask 5.1: `db.update(credits).set({ balance_usd: sql\`balance_usd - ${cost}\` }).where(eq(credits.user_id, userId))` + - [ ] Subtask 5.2: `db.insert(usage_log).values({ user_id: userId, tokens_in: inputTokens, tokens_out: outputTokens, cost_usd: cost, model: payload.model, created_at: new Date() })` + - [ ] Subtask 5.3: Wrap in `try/catch`; on error, log stack trace but do not roll back stream + - [ ] Subtask 5.4: Emit metric event for monitoring + +- [ ] Task 6: Error handling and resilience + - **Problem:** Upstream failures should not break editor experience + - **Test:** If upstream returns 500, proxy returns same 502/503; no deduction; user sees error + - **Subtasks:** + - [ ] Subtask 6.1: On fetch error (network), return `502 Bad Gateway` with `{ "error": "Upstream unavailable" }` + - [ ] Subtask 6.2: On upstream 4xx/5xx, forward status and body as error response + - [ ] Subtask 6.3: Ensure no deduction occurs unless upstream returned successful completion (status 200) + - [ ] Subtask 6.4: Timeout handling: set `AbortController` with 5min timeout; on timeout, abort upstream and return `504 Gateway Timeout` + +- [ ] Task 7: Rate limiting (optional but recommended) + - **Problem:** Prevent single user from flooding upstream + - **Test:** User exceeding 100 req/min receives `429 Too Many Requests` + - **Subtasks:** + - [ ] Subtask 7.1: Install `fastify-rate-limit` + - [ ] Subtask 7.2: Apply to route with per-user key: `limit.max = 100`, `timeWindow: '1 minute'`, `keyGenerator: (req) => req.userId` + - [ ] Subtask 7.3: On limit exceed, return `429` with `{ "error": "Rate limit exceeded" }` + +- [ ] Task 8: Testing and performance validation + - **Problem:** Ensure endpoint works and is fast + - **Test:** Automated tests pass; manual `time` command shows <200ms overhead before first byte + - **Subtasks:** + - [ ] Subtask 8.1: Add unit tests with mocked upstream for both stream and non-stream + - [ ] Subtask 8.2: Add integration test against real upstream (or recorded VCR) to measure latency + - [ ] Subtask 8.3: Load test with `autocannon` to verify 100 RPS sustained without degradation diff --git a/.tickets/ticket-010-stripe-top-up/agent-history.md b/.tickets/ticket-010-stripe-top-up/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-010-stripe-top-up/prd.md b/.tickets/ticket-010-stripe-top-up/prd.md new file mode 100644 index 00000000..f291564a --- /dev/null +++ b/.tickets/ticket-010-stripe-top-up/prd.md @@ -0,0 +1,144 @@ +# PRD: Ticket 010 - Stripe Top-Up + +## 1. Problem Statement + +Users need to purchase additional OCC Credits when their balance runs low. The backend must integrate with Stripe Checkout to handle payments. The flow: + +1. Editor calls `GET /api/v1/billing/checkout?amount=10` (or predefined price tiers) +2. Backend creates a Stripe Checkout Session for the user (using stored Stripe customer ID or creating one on the fly) +3. Backend returns `{ sessionUrl }` (Stripe-hosted page URL) +4. Editor opens the URL in browser +5. User completes payment on Stripe +6. Stripe sends webhook `checkout.session.completed` to backend +7. Backend credits the user's account: increase `credits.balance_usd` and `lifetime_usd` by the purchased amount (e.g., $10 → add $5 credit, respecting margin) +8. User redirected to `https://occ.mba.sh/success?session_id={CHECKOUT_SESSION_ID}`; editor polls `GET /api/v1/balance` to see updated balance + +Pricing tiers (suggested): +- $10 → +$5 credits (50% margin) +- $25 → +$12.5 credits (50% margin) +- $50 → +$30 credits (40% margin) + +## 2. Proposed Solution + +Integrate Stripe Checkout: + +- Create `POST /api/v1/billing/checkout` endpoint + - Authenticated only (JWT) + - Body: `{ amount: number }` (must match allowed tiers) + - Lookup user's Stripe customer ID (store `stripe_customer_id` in `users` table) + - If no customer, create one via `stripe.customers.create({ email: user.email, metadata: { userId } })` and save + - Create Checkout Session: `stripe.checkout.sessions.create({ customer: stripeCustomerId, payment_method_types: ['card'], line_items: [{ price_data: { currency: 'usd', product_data: { name: 'OCC Credits' }, unit_amount: amount * 100, ... }, quantity: 1 }], mode: 'payment', success_url: `${BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${BASE_URL}/cancel` })` + - Return `{ sessionUrl: session.url }` +- Implement `POST /api/v1/stripe/webhook` + - Verify signature using `STRIPE_WEBHOOK_SECRET` + - On `checkout.session.completed` event: + - Extract `session.customer` (Stripe customer ID) and `session.amount_total` (cents) + - Lookup user by `stripe_customer_id` + - Compute credit amount: map `amount_total` to tier (e.g., 1000 cents → $5 credit) + - `UPDATE credits SET balance_usd = balance_usd + credit, lifetime_usd = lifetime_usd + credit WHERE user_id = userId` + - Log transaction in new `transactions` table (optional) +- Add `GET /api/v1/billing/history` (optional) to show user their purchase history + +## 3. Acceptance Criteria + +- Authenticated user can request a checkout session via `POST /api/v1/billing/checkout` with a supported amount (10, 25, 50) +- Endpoint creates/uses a Stripe customer ID linked to the user +- Returns a valid `sessionUrl` that opens Stripe Checkout +- Stripe webhook receives event and credits user's account correctly +- After payment, when user is redirected to success page, editor's balance poll reflects increased balance +- If webhook fails, idempotency: duplicate events do not over-credit (use Stripe event `id` uniqueness check) +- Amount tiers and corresponding credit amounts are clearly documented + +## 4. Technical Considerations + +- **Stripe library:** Use official `stripe` Node SDK +- **Idempotency:** Protect against duplicate webhook deliveries: store processed `event.id` in a table; skip if already processed +- **Webhook security:** Verify signature using raw request body and `STRIPE_WEBHOOK_SECRET`; reject if verification fails +- **Tier mapping:** Use a constant mapping like `{ 1000: 5.00, 2500: 12.50, 5000: 30.00 }` (cents → USD credit). Document in code. +- **Currency:** All amounts in USD cents; convert carefully to avoid floating rounding errors; store balances as NUMERIC(10,6) in DB +- **Error handling:** If Stripe API errors, return 500 with `{ error: 'payment_setup_failed' }` and log details; do not expose Stripe errors to client +- **Testing:** Use Stripe test mode with test keys and test cards (`4242 4242 4242 4242`). Write tests that hit Stripe test API or mock with `stripe-mock` +- **Environment:** `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `BASE_URL` env vars +- **Database:** Optional `transactions` table: `id`, `user_id`, `stripe_session_id`, `amount_usd`, `credit_added_usd`, `created_at` + +## 5. Dependencies + +- Backend foundation (ticket-007: DB, auth, secrets) +- Stripe account with test keys + +## 6. Subtask Checklist + +- [ ] Task 1: Install and configure Stripe SDK + - **Problem:** Need Stripe client with secret key + - **Test:** `stripe.customers.list()` works with test key + - **Subtasks:** + - [ ] Subtask 1.1: `npm install stripe` + - [ ] Subtask 1.2: `const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2024-06-20' })` + - [ ] Subtask 1.3: Verify `stripe` object in health check route (optional) + +- [ ] Task 2: Create `POST /api/v1/billing/checkout` + - **Problem:** Generate Stripe Checkout Session + - **Test:** Authenticated POST returns `{ sessionUrl }` that loads Stripe Checkout + - **Subtasks:** + - [ ] Subtask 2.1: Validate amount against allowed tiers (10,25,50); else return 400 + - [ ] Subtask 2.2: Get user (from `request.userId`) and ensure has email + - [ ] Subtask 2.3: Check if `users.stripe_customer_id` exists; if not, `stripe.customers.create({ email, metadata: { userId } })` and save + - [ ] Subtask 2.4: Map amount to credit amount using tier mapping + - [ ] Subtask 2.5: `stripe.checkout.sessions.create({ customer: stripeCustomerId, line_items: [{ price_data: { currency: 'usd', product_data: { name: 'OCC Credits' }, unit_amount: amount*100, ... }, quantity: 1 }], mode: 'payment', success_url: `${BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${BASE_URL}/cancel` })` + - [ ] Subtask 2.6: Return `{ sessionUrl: session.url }` + +- [ ] Task 3: Implement Stripe webhook endpoint + - **Problem:** Receive payment confirmation and credit account + - **Test:** Send test webhook from Stripe CLI; user's balance updates + - **Subtasks:** + - [ ] Subtask 3.1: Create `POST /api/v1/stripe/webhook` with raw body access (`fastify.addContentTypeParser('application/json', ...)` to get raw) + - [ ] Subtask 3.2: Get signature from `Stripe-Signature` header; `stripe.webhooks.constructEvent(rawBody, signature, STRIPE_WEBHOOK_SECRET)` + - [ ] Subtask 3.3: If event type `checkout.session.completed`: + - Extract `session.customer` and `session.amount_total` (cents) + - Lookup user by `stripe_customer_id = session.customer` + - Map amount to credit using tiers + - Begin transaction: `UPDATE credits SET balance_usd = balance_usd + credit WHERE user_id = userId` + - Optional insert `transactions` row: `stripe_session_id=session.id, amount_usd=session.amount_total/100, credit_added_usd=credit` + - Commit transaction + - [ ] Subtask 3.4: Respond `200 OK` to Stripe quickly (within seconds) + - [ ] Subtask 3.5: Log webhook processing (info) + +- [ ] Task 4: Idempotency and error handling + - **Problem:** Stripe may retry delivery; must not double-credit + - **Test:** Resend same webhook event; balance increases only once + - **Subtasks:** + - [ ] Subtask 4.1: Create `stripe_events` table: `event_id TEXT PRIMARY KEY`, `received_at TIMESTAMPTZ` + - [ ] Subtask 4.2: Before processing, check if `event.id` exists; if exists, respond 200 and skip + - [ ] Subtask 4.3: On processing success, insert `event.id` with `received_at` + - [ ] Subtask 4.4: If any DB/Stripe error, return 5xx to trigger Stripe retry (but ensure no partial updates) + +- [ ] Task 5: Tier mapping validation and documentation + - **Problem:** Be explicit about what each amount buys + - **Test:** Code clearly maps 10 → 5.00, 25 → 12.50, 50 → 30.00 + - **Subtasks:** + - [ ] Subtask 5.1: Create constant `TIER_MAP_CENTS_TO_CREDIT = new Map([[1000,5.00],[2500,12.50],[5000,30.00]])` + - [ ] Subtask 5.2: Document in code comment and README + - [ ] Subtask 5.3: Ensure any mismatch returns 400 before calling Stripe + +- [ ] Task 6: Testing (unit + integration) + - **Problem:** Confidence that flow works end-to-end + - **Test:** Automated tests cover checkout creation and webhook processing with mocked Stripe + - **Subtasks:** + - [ ] Subtask 6.1: Use `stripe-mock` or Jest mocks for Stripe SDK + - [ ] Subtask 6.2: Test `/billing/checkout` returns valid sessionUrl and creates customer if needed + - [ ] Subtask 6.3: Test webhook handler with sample event payload; verify balance update and idempotency + - [ ] Subtask 6.4: Add integration test that goes through real Stripe test mode (optional but valuable) + +- [ ] Task 7: Editor integration (frontend ticket) + - **Problem:** The editor needs to call this endpoint and open browser + - **Note:** Covered in frontend tickets (status bar "Buy More Credits" already opens `https://mba.sh/billing`; eventually should use backend to create session). Defer to frontend when backend is ready. + - **Verification:** This ticket only covers backend; frontend will call it later + +- [ ] Task 8: Deployment and monitoring + - **Problem:** Stripe keys must be present; webhook endpoint must be reachable + - **Test:** Production deployment has webhook URL configured in Stripe dashboard (`https://occ.mba.sh/api/v1/stripe/webhook`); logs show successful events + - **Subtasks:** + - [ ] Subtask 8.1: Set environment variables in hosting: `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET` + - [ ] Subtask 8.2: Register webhook endpoint in Stripe Dashboard for `checkout.session.completed` event (pointing to production URL) + - [ ] Subtask 8.3: Add monitoring: alert on webhook failures (non-2xx responses) + - [ ] Subtask 8.4: Add dashboard query to see recent top-ups and balances diff --git a/.tickets/ticket-011-database-schema/agent-history.md b/.tickets/ticket-011-database-schema/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-011-database-schema/prd.md b/.tickets/ticket-011-database-schema/prd.md new file mode 100644 index 00000000..890f5997 --- /dev/null +++ b/.tickets/ticket-011-database-schema/prd.md @@ -0,0 +1,171 @@ +# PRD: Ticket 011 - Database Schema + +## 1. Problem Statement + +The OCC backend requires a well-defined PostgreSQL database schema to store user accounts, credit balances, usage logs, and (optionally) transaction history. The schema must support: + +- User profiles (email, password hash, OAuth provider info) +- Credit balances (current balance and lifetime grants) +- Inference usage logs (token counts, cost, model, timestamp) +- Optional: Stripe customer mapping and transaction history for auditing + +The schema should be created and managed using Drizzle ORM migrations, with proper constraints, indexes, and relationships. + +## 2. Proposed Solution + +Define Drizzle schema files and generate SQL migrations: + +**Tables:** + +```sql +-- users +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email TEXT UNIQUE NOT NULL, + password TEXT, -- null for OAuth-only users + provider TEXT DEFAULT 'email', -- 'email' | 'google' | 'github' + provider_id TEXT, -- OAuth provider user ID + stripe_customer_id TEXT, -- Stripe customer ID (nullable) + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +-- credits +CREATE TABLE credits ( + user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + balance_usd NUMERIC(10,6) NOT NULL DEFAULT 5.000000, + lifetime_usd NUMERIC(10,6) NOT NULL DEFAULT 5.000000, + updated_at TIMESTAMPTZ DEFAULT now() +); + +-- usage_log +CREATE TABLE usage_log ( + id BIGSERIAL PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id), + tokens_in INT NOT NULL, + tokens_out INT NOT NULL, + cost_usd NUMERIC(10,6) NOT NULL, + model TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() +); + +-- optional: transactions (for Stripe top-ups) +CREATE TABLE transactions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id), + stripe_session_id TEXT UNIQUE NOT NULL, + amount_usd NUMERIC(10,2) NOT NULL, -- amount charged (e.g., 10.00) + credit_added_usd NUMERIC(10,6) NOT NULL, -- credit added (e.g., 5.00) + created_at TIMESTAMPTZ DEFAULT now() +); + +-- optional: stripe_events (idempotency for webhooks) +CREATE TABLE stripe_events ( + event_id TEXT PRIMARY KEY, + received_at TIMESTAMPTZ DEFAULT now() +); +``` + +Indexes: +- `CREATE INDEX idx_usage_log_user_created ON usage_log(user_id, created_at DESC);` +- `CREATE INDEX idx_users_email ON users(email);` +- `CREATE INDEX idx_users_stripe_customer ON users(stripe_customer_id) WHERE stripe_customer_id IS NOT NULL;` +- `CREATE INDEX idx_transactions_user ON transactions(user_id);` + +## 3. Acceptance Criteria + +- All tables exist in the PostgreSQL database with defined columns, types, and constraints +- Foreign key relationships enforce referential integrity +- Default values (`5.000000` for initial balance) work on insert without explicit values +- `gen_random_uuid()` generates version 4 UUIDs (requires pgcrypto extension) +- All indexes are present and used by queries (verify with `EXPLAIN`) +- Drizzle schema compiles and migrations apply without errors +- Test suite can seed a test database with sample data + +## 4. Technical Considerations + +- **Drizzle setup:** Install `drizzle-orm`, `drizzle-kit`, `pg`. Configure `drizzle.config.ts` with `schema` pointing to `src/db/schema.ts`, `out` to `drizzle`, and connection string from env `DATABASE_URL`. +- **Migrations:** Use `drizzle-kit generate:pg` to generate SQL files; review them; apply with `drizzle-kit migrate` or `psql`. +- **UUIDs:** Ensure `pgcrypto` extension is enabled: `CREATE EXTENSION IF NOT EXISTS "pgcrypto";` (can add to initial migration) +- **Numeric precision:** Use `NUMERIC(10,6)` for balances to support fractions of cents; display with `toFixed(2)`. +- **ON DELETE CASCADE:** When a user is deleted, their credits and usage logs should be removed automatically. +- **Partial index:** `users.stripe_customer_id` index only for non-null values to keep it small. +- **Test database:** Use separate DB or schema; run migrations on CI; seed with minimal data for tests. +- **Rollback strategy:** Drizzle does not auto-generate down migrations; write manual `DOWN` SQL or recreate DB from scratch in CI. + +## 5. Dependencies + +- None foundational; but will be used by tickets 007-010 + +## 6. Subtask Checklist + +- [ ] Task 1: Write Drizzle schema definitions + - **Problem:** Translate SQL tables to Drizzle `pgTable` definitions + - **Test:** `npx drizzle-kit generate:pg` produces valid SQL + - **Subtasks:** + - [ ] Subtask 1.1: Create `src/db/schema.ts` with `users`, `credits`, `usage_log`, `transactions`, `stripe_events` tables + - [ ] Subtask 1.2: Define columns with correct types: `uuid`, `text`, `numeric`, `timestamp`, `int` + - [ ] Subtask 1.3: Add primary keys, foreign keys (`references`), defaults (`$defaultFn(() => gen_random_uuid())`, `$defaultNow`) + - [ ] Subtask 1.4: Add `updatedAt` columns that automatically update on row change (using `$onUpdate` hook with `now()`) + +- [ ] Task 2: Generate initial migration + - **Problem:** Create SQL that builds the schema + - **Test:** Migration runs cleanly on fresh Postgres DB + - **Subtasks:** + - [ ] Subtask 2.1: Run `npx drizzle-kit generate:pg` to generate `drizzle/_initial.sql` + - [ ] Subtask 2.2: Review generated SQL; manually add `CREATE EXTENSION IF NOT EXISTS "pgcrypto";` at top if needed + - [ ] Subtask 2.3: Add index creation statements (Drizzle may auto-create PK indexes; need additional ones) + - [ ] Subtask 2.4: Test migration on local dev DB: `psql $DATABASE_URL -f drizzle/_initial.sql` + +- [ ] Task 3: Create additional migrations for future changes + - **Problem:** Schema evolves; need versioned migrations + - **Test:** New changes generate new migration files that apply cleanly on top of existing DB + - **Subtasks:** + - [ ] Subtask 3.1: After initial migration, record baseline version in DB (Drizzle stores a `drizzle_migrations` table automatically) + - [ ] Subtask 3.2: When modifying schema (e.g., adding `stripe_customer_id`), run `drizzle-kit generate:pg` again to produce new migration + - [ ] Subtask 3.3: Review and test the new migration on a DB that already has previous migrations applied + +- [ ] Task 4: Write DB utility module + - **Problem:** Provide convenient access to DB connection and queries + - **Test:** `src/db/index.ts` exports `db` connection and query helpers + - **Subtasks:** + - [ ] Subtask 4.1: `src/db/index.ts`: `import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; const pool = new Pool({ connectionString: process.env.DATABASE_URL }); export const db = drizzle(pool);` + - [ ] Subtask 4.2: Export `schema` from `./schema` + - [ ] Subtask 4.3: Add `pool.on('error', err => console.error('DB error', err))` + - [ ] Subtask 4.4: Ensure graceful shutdown: `pool.end()` on SIGTERM + +- [ ] Task 5: Integration tests with test database + - **Problem:** Tests need a clean DB + - **Test:** CI job migrates test DB and runs tests; tests pass + - **Subtasks:** + - [ ] Subtask 5.1: Set up separate `TEST_DATABASE_URL` (could be SQLite in-memory for speed, but Postgres is more accurate) + - [ ] Subtask 5.2: In test setup, run `drizzle-kit migrate` against test DB + - [ ] Subtask 5.3: Write a few integration tests: create user, insert credits, query back + - [ ] Subtask 5.4: In test teardown, truncate all tables or drop DB + +- [ ] Task 6: Documentation + - **Problem:** Developers need to set up local DB + - **Test:** README contains steps to provision Postgres, run migrations, seed dev data + - **Subtasks:** + - [ ] Subtask 6.1: Add `DATABASE_URL` env var instructions: `postgresql://user:pass@localhost:5432/occ` + - [ ] Subtask 6.2: Document `npx drizzle-kit generate:pg` and `npx drizzle-kit migrate` + - [ ] Subtask 6.3: Provide sample seed script (`src/db/seed.ts`) to create an initial admin user with $5 balance + - [ ] Subtask 6.4: Note that `pgcrypto` extension must be enabled (include in migration) + +- [ ] Task 7: Production deployment + - **Problem:** Migrations must run on production DB before starting server + - **Test:** Deploy script runs `drizzle-kit migrate` on production DB; no errors + - **Subtasks:** + - [ ] Subtask 7.1: Add "postdeploy" script in hosting (Railway/Render) that runs `npx drizzle-kit migrate` + - [ ] Subtask 7.2: Ensure `DATABASE_URL` is set in production environment + - [ ] Subtask 7.3: Verify production DB has all tables and indexes + - [ ] Subtask 7.4: Add health check endpoint `/health` that does a cheap `SELECT 1` to ensure DB is reachable + +- [ ] Task 8: Backup and disaster recovery plan + - **Problem:** Need to restore data if something goes wrong + - **Test:** Backup procedure documented and tested + - **Subtasks:** + - [ ] Subtask 8.1: Document how to take a PostgreSQL backup: `pg_dump -Fc -f occ.dump` + - [ ] Subtask 8.2: Document restore procedure: `pg_restore -d occ < occ.dump` + - [ ] Subtask 8.3: Recommend daily automated backups via hosting provider + - [ ] Subtask 8.4: Note that usage_log can be large; plan for partitioning or archiving (future) diff --git a/.tickets/ticket-012-extension-vsix-packaging/agent-history.md b/.tickets/ticket-012-extension-vsix-packaging/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-012-extension-vsix-packaging/prd.md b/.tickets/ticket-012-extension-vsix-packaging/prd.md new file mode 100644 index 00000000..0456f54d --- /dev/null +++ b/.tickets/ticket-012-extension-vsix-packaging/prd.md @@ -0,0 +1,105 @@ +# PRD: Ticket 012 - Extension VSIX Packaging + +## 1. Problem Statement + +The OCCode extension (`apps/extension/`) must be packaged as a `.vsix` file and included in the `apps/wrapper/extensions/` directory (or inside the editor bundle) so that the wrapper or the pre-bundled editor can install it automatically. Currently there is no pre-built `.vsix` in the repo, which causes the wrapper's `installExtension()` to fail silently if the file is missing. The build process must create a production-ready, signed (if needed) VSIX package. + +## 2. Proposed Solution + +Create a reproducible build pipeline that: + +- Compiles TypeScript to JavaScript +- Bundles all source files +- Packages extension manifest (`package.json` with `extensionKind`, `publisher`, `version`, `engines.vscode`) and assets into a `.vsix` (which is a ZIP with specific layout) +- Places the output into `apps/wrapper/extensions/` as `openclaw.vsix` (or `occ-openclaw.vsix`) +- Optionally signs the package with publisher certificate if required by marketplace + +The `package.json` scripts should include: + +```json +{ + "scripts": { + "ext:build": "...", + "ext:package": "vsce package -o ../wrapper/extensions/occ-openclaw.vsix" + } +} +``` + +The build should be deterministic (timestamps, etc. should not vary) to support caching. + +## 3. Acceptance Criteria + +- Running the package command produces a `.vsix` file in `apps/wrapper/extensions/` (or another agreed location) +- The `.vsix` installs successfully in VS Code / VSCodium via `code --install-extension path/to/file.vsix` +- The extension loads without errors (check `Developer: Show Running Extensions`) +- The wrapper's `installExtension()` finds the file and installs it silently during first-run flow +- The extension version in the VSIX matches the root `package.json` version +- The build works on all supported platforms (Linux, macOS, Windows) + +## 4. Technical Considerations + +- **Tooling:** Use `@vscode/vsce` (VS Code Extension CLI) to package. Ensure it's in `devDependencies` +- **Publisher:** The extension's `package.json` needs a `publisher` field (e.g., `"openclaw"`). If publishing to Marketplace, use a publisher ID and possibly a signing key; for internal packaging, a dummy publisher is fine but must be consistent +- **Manifest:** Ensure `engines.vscode` is compatible with the editor's VS Code version (Void editor fork based on 1.99.3) +- **Pre-publish checks:** `vsce package` will run a `prepublish` script if defined; ensure it compiles TypeScript and does any bundling (e.g., `esbuild` or `tsc`) +- **Resources:** `prepublish` script should copy any needed assets (icons, README, CHANGELOG) into the extension directory +- **Wrapper expectation:** The wrapper's `installExtension()` looks for `.vsix` files in the `extensions/` directory. Naming matters: use a predictable name like `occ-openclaw.vsix` + +## 5. Dependencies + +- `apps/extension` must be in a releasable state (compiles, works) before packaging + +## 6. Subtask Checklist + +- [ ] Task 1: Install and configure `vsce` + - **Problem:** Need the official packaging tool + - **Test:** `npx vsce --version` works + - **Subtasks:** + - [ ] Subtask 1.1: `npm install -D @vscode/vsce` in `apps/extension` + - [ ] Subtask 1.2: Verify `package.json` has required fields (`name`, `publisher`, `version`, `engines`, `activationEvents`, `contributes`, `main` points to compiled JS) + +- [ ] Task 2: Create a `prepublish` build script + - **Problem:** Package must contain compiled JS, not TS source + - **Test:** `npm run prepublish` produces `out/` or `dist/` directory with `.js` files + - **Subtasks:** + - [ ] Subtask 2.1: Add `"prepublish": "tsc -p tsconfig.json"` (or `npm run build` if using bundler) + - [ ] Subtask 2.2: Ensure `tsconfig.json` outputs to `out/` with `declmap` etc. appropriate for VS Code extensions + - [ ] Subtask 2.3: If using a bundler like `esbuild`, configure to produce a single file with external `vscode` module + +- [ ] Task 3: Add `ext:package` script + - **Problem:** Easy one-command packaging + - **Test:** `npm run ext:package` creates `../wrapper/extensions/openclaw.vsix` + - **Subtasks:** + - [ ] Subtask 3.1: In `apps/extension/package.json`, add `"ext:package": "vsce package -o ../../wrapper/extensions/occ-openclaw.vsix"` (adjust path from extension dir to wrapper's extensions) + - [ ] Subtask 3.2: Verify output path exists; create `extensions/.gitkeep` if needed + +- [ ] Task 4: Ensure extension package is lean + - **Problem:** VSIX should not contain dev files, tests, source maps unless needed + - **Test:** Inspect ZIP contents; no `src/*.ts`, `test/`, `.git/` + - **Subtasks:** + - [ ] Subtask 4.1: Add `.vscodeignore` file in extension root (like `.gitignore`) excluding `src`, `test`, `**/*.map`, `**/tsconfig.json`, etc. + - [ ] Subtask 4.2: Rebuild and verify ignored files are not in archive + +- [ ] Task 5: Integrate into wrapper build flow + - **Problem:** When wrapper is built or first-run, extension should be pre-installed + - **Test:** Wrapper's `installExtension()` finds `occ-openclaw.vsix` and installs it without errors + - **Subtasks:** + - [ ] Subtask 5.1: In wrapper source, confirm path to `extensions/` and filename + - [ ] Subtask 5.2: If wrapper expects specific naming, adjust package output name accordingly + - [ ] Subtask 5.3: Test full wrapper first-run flow: launch wrapper, it installs extension, editor starts with OCC Home panel + +- [ ] Task 6: CI/CD step to publish artifact (optional if automating) + - **Problem:** GitHub Releases should contain the built `.vsix` for manual install + - **Test:** Release asset includes `occ-openclaw-.vsix` + - **Subtasks:** + - [ ] Subtask 6.1: Create GitHub Actions workflow: on `release` or `push tag`, run `npm --prefix apps/extension run ext:package` + - [ ] Subtask 6.2: Upload artifact with `actions/upload-release-asset` + - [ ] Subtask 6.3: Ensure version in `package.json` matches tag + +- [ ] Task 7: Documentation + - **Problem:** Developers need to know how to build the extension + - **Test:** README in extension mentions `npm run ext:package` + - **Subtasks:** + - [ ] Subtask 7.1: Add section in `apps/extension/README.md`: "Building" → `npm install && npm run ext:package` + - [ ] Subtask 7.2: Note the output filename and location + - [ ] Subtask 7.3: Mention any required environment variables (if extension uses them) for testing diff --git a/.tickets/ticket-013-version-pinning-strategy/agent-history.md b/.tickets/ticket-013-version-pinning-strategy/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-013-version-pinning-strategy/prd.md b/.tickets/ticket-013-version-pinning-strategy/prd.md new file mode 100644 index 00000000..dfbe2b44 --- /dev/null +++ b/.tickets/ticket-013-version-pinning-strategy/prd.md @@ -0,0 +1,107 @@ +# PRD: Ticket 013 - Version Pinning Strategy + +## 1. Problem Statement + +The OCCode wrapper (or the editor build) currently hardcodes the VSCodium version in `apps/wrapper/src/download.js` (e.g., `'1.96.4.25027'`). This causes the wrapper to try downloading a non-existent version if not updated manually, leading to 404 errors and broken first-run experience. We need a strategy to automatically or easily keep the VSCodium version current without manual code changes every release. + +## 2. Proposed Solution + +Implement one of two approaches: + +**Approach A — GitHub API auto-discovery (preferred):** +- In `download.js`, when building the download URL, query GitHub releases API for the latest VSCodium release matching the current platform/arch +- Cache the result for a short period (e.g., 6 hours) to avoid hitting rate limits +- Fallback to a hardcoded "known good" version if API fails + +**Approach B — Config file with manual updates:** +- Move the version string to a JSON file (e.g., `vscodium-manifest.json`) that lists SHA-256 hashes and versions per platform +- Provide a script `npm run vscodium:update` that fetches the latest releases from GitHub, updates the manifest, and verifies hashes +- The download code reads the manifest and picks the correct entry for the current platform + +Both approaches require verifying download integrity (SHA-256) before extraction. + +## 3. Acceptance Criteria + +- The wrapper/editor automatically downloads the latest stable VSCodium release for the detected platform +- The download process verifies the SHA-256 checksum of the downloaded archive before extraction +- If the auto-discovery fails (network error, rate limit), the wrapper falls back to a known working version (hardcoded or last cached) +- The version update mechanism is documented and easy to run in CI or by a maintainer +- No manual code edits required to bump the version in normal operation +- All supported platforms (Linux x64, macOS x64/arm64, Windows x64) are covered + +## 4. Technical Considerations + +- **GitHub API:** `GET https://api.github.com/repos/VSCodium/vscodium/releases/latest` returns the latest release tag (e.g., `1.99.3.24750`). The tag format may differ per platform; need to extract version number from the release assets (e.g., `VSCodium-linux-x64-1.99.3.24750.tar.gz`) +- **Rate limiting:** Unauthenticated GitHub API is limited to 60 req/hour per IP. Use caching or a personal token for CI. For local wrapper runs, caching to disk is sufficient. +- **Checksum verification:** Need a source for SHA-256 hashes. The VSCodium releases page provides SHA-256SUMS file. Approach B can store hashes in manifest; Approach A can download the SHA-256SUMS file alongside the archive and verify. +- **Platform mapping:** The code already has a `PLATFORM_MAP` for mapping Node's `process.platform` and `process.arch` to VSCodium naming conventions. Ensure it covers `linux.x64`, `darwin.x64`, `darwin.arm64`, `win32.x64`. +- **Fallback:** Keep a minimal object in code with last-known-good versions and their hashes as a safety net. +- **Security:** Do not download from non-HTTPS sources; verify signatures if available (VSCodium provides GPG signatures but that may be overkill; SHA-256 is sufficient if fetched from official GitHub). + +## 5. Dependencies + +- None; this is an internal improvement to the wrapper/editor build + +## 6. Subtask Checklist + +- [ ] Task 1: Choose and design approach + - **Problem:** Decide between auto-discovery and manifest-based + - **Test:** Documented decision with rationale; issue comment approved + - **Subtasks:** + - [ ] Subtask 1.1: Evaluate auto-discovery pros/cons: simplicity vs. rate limits vs. offline usage + - [ ] Subtask 1.2: Evaluate manifest pros/cons: manual/scheduled updates vs. deterministic builds + - [ ] Subtask 1.3: Pick one (likely B for reproducibility) and create design doc + +- [ ] Task 2: Implement auto-discovery (if chosen) + - **Problem:** Query GitHub and parse latest release + - **Test:** Without pre-set version, wrapper downloads working VSCodium + - **Subtasks:** + - [ ] Subtask 2.1: Add `node-fetch` or native `fetch` (Node 20+) to query GitHub API + - [ ] Subtask 2.2: Parse releases; find asset matching current platform/arch; extract version from asset name + - [ ] Subtask 2.3: Cache response to `~/.occode/vscodium-latest.json` with timestamp; reuse if <6h old + - [ ] Subtask 2.4: On API error (rate limit, network), read cached file; if stale or missing, use hardcoded fallback + - [ ] Subtask 2.5: Download SHA-256SUMS file from same release; verify archive + +- [ ] Task 3: Implement manifest-based (if chosen) + - **Problem:** Maintain a manifest file with versions and hashes + - **Test:** Updating manifest with script updates hardcoded values in repo; wrapper uses manifest + - **Subtasks:** + - [ ] Subtask 3.1: Create `apps/wrapper/vscodium-manifest.json` structure: `{ "versions": { "1.99.3.24750": { "linux.x64": { "url": "...", "sha256": "..." }, ... } } }` + - [ ] Subtask 3.2: Write `scripts/update-vscodium.js` that: + - Fetches latest release from GitHub API + - For each platform, finds asset URL and downloads SHA-256SUMS to get hash + - Updates the manifest file with new entry (or updates a `latest` pointer) + - Commits changes (if run in CI) + - [ ] Subtask 3.3: Modify `download.js` to read manifest, pick entry for current platform, and download + - [ ] Subtask 3.4: If platform missing in manifest, fallback to previous entry or hardcoded + +- [ ] Task 4: Add SHA-256 verification + - **Problem:** Ensure downloaded archive integrity + - **Test:** Corrupting the file triggers verification error; re-download + - **Subtasks:** + - [ ] Subtask 4.1: In `download.js`, after download completes, compute SHA-256 hash (use Node `crypto.createHash('sha256')`) + - [ ] Subtask 4.2: Compare to expected hash from manifest or GitHub SHA-256SUMS + - [ ] Subtask 4.3: If mismatch, delete temp file, show error in UI, allow retry + +- [ ] Task 5: Handle platform coverage gaps + - **Problem:** Need to support arm64 Linux/macOS and future architectures + - **Test:** Wrapper attempts download on arm64; if no asset, gracefully error with helpful message + - **Subtasks:** + - [ ] Subtask 5.1: Extend `PLATFORM_MAP` to include `linux.arm64`, `darwin.arm64` + - [ ] Subtask 5.2: In manifest/discovery, map these correctly; if asset not found, attempt using closest version (e.g., x64 via Rosetta?) or show "unsupported" message + +- [ ] Task 6: Test across platforms + - **Problem:** Must work on all OSes we ship + - **Test:** Run wrapper on Linux x64, macOS x64/arm64, Windows x64; verify VSCodium downloads and launches + - **Subtasks:** + - [ ] Subtask 6.1: Set up CI matrix for at least Linux and macOS; run wrapper with `--headless` test mode if possible + - [ ] Subtask 6.2: Manually test on Windows if CI cannot cover + - [ ] Subtask 6.3: Simulate rate limit by mocking API error; ensure fallback works + +- [ ] Task 7: Documentation and monitoring + - **Problem:** If auto-update fails, we need to know + - **Test:** Wrapper logs version resolution steps; errors are captured optionally + - **Subtasks:** + - [ ] Subtask 7.1: Add debug logging: "Using cached manifest", "Fetching GitHub", "Selected version X", "Hash Y" + - [ ] Subtask 7.2: Document the fallback behavior and how to trigger a manual update + - [ ] Subtask 7.3: Add heartbeat check or ping to `https://api.github.com/rate_limit` to warn if接近 limit (optional) diff --git a/.tickets/ticket-014-linux-arm64-support/agent-history.md b/.tickets/ticket-014-linux-arm64-support/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-014-linux-arm64-support/prd.md b/.tickets/ticket-014-linux-arm64-support/prd.md new file mode 100644 index 00000000..2d420dfa --- /dev/null +++ b/.tickets/ticket-014-linux-arm64-support/prd.md @@ -0,0 +1,103 @@ +# PRD: Ticket 014 - Linux ARM64 Support + +## 1. Problem Statement + +The OCCode wrapper's `PLATFORM_MAP` currently lacks coverage for Linux on ARM64 architecture (e.g., Raspberry Pi 4/5, AWS Graviton, newer ARM laptops). Users on these systems cannot install VSCodium because the wrapper cannot construct a valid download URL. The project should support at least `linux.arm64` (and possibly `linux.armhf` if demand exists). This requires adding correct platform mappings, ensuring the build/test infrastructure includes ARM64, and verifying downloads and installation work. + +## 2. Proposed Solution + +Extend the platform detection and URL generation to handle `arm64` on Linux: + +- In `apps/wrapper/src/download.js` (or wherever `PLATFORM_MAP` lives), add entries: + - `'linux.arm64'`: { assetSuffix: 'linux-arm64', binDir: 'bin', binary: 'codium' } (verify exact naming) +- Research actual VSCodium release assets: they typically provide `VSCodium-linux-arm64-.tar.gz` or `...-armhf.tar.gz` for 32-bit. Use the official naming. +- Adjust any hardcoded checks that assume x64 only. +- Update CI/test configuration to include an ARM64 runner (e.g., GitHub Actions `runs-on: ubuntu-latest` with `ARCH=arm64` using QEMU or a ARM64 self-hosted runner if available). +- Update documentation (INSTALL.md) to mention ARM64 support. + +## 3. Acceptance Criteria + +- The wrapper correctly identifies a Linux ARM64 system (`process.platform === 'linux'` and `process.arch === 'arm64'`) +- The download URL is constructed to fetch the appropriate VSCodium ARM64 tarball (e.g., `https://github.com/VSCodium/vscodium/releases/download/1.99.3.24750/VSCodium-linux-arm64-1.99.3.24750.tar.gz`) +- The archive extracts to the expected directory structure with binaries in the right location +- The `codium` binary is marked executable and can be launched +- The wrapper's first-run flow works end-to-end on an ARM64 Linux machine (or in an ARM64 VM) +- If a user runs on an unsupported architecture (e.g., `armhf` not implemented), the wrapper shows a clear error message and possibly a link to manual install instructions + +## 4. Technical Considerations + +- **Asset naming:** VSCodium uses different naming conventions for ARM64 vs x64. Check latest release: `https://github.com/VSCodium/vscodium/releases/latest`. Identify exact asset names. They may also provide `VSCodium-linux-arm64.tar.gz` (without version in file name) in some releases; best to use versioned ones. +- **Extraction:** The tarball extraction code may assume a specific top-level folder name (e.g., `VSCodium-linux-x64-`). Ensure it works for arm64 (different folder name). +- **Binary permissions:** After extraction, set `chmod +x` on the `codium` binary. +- **Testing without ARM64 hardware:** Use QEMU-based emulation in CI: `docker run --privileged --platform linux/arm64 multiarch/ubuntu-core:22.04` or GitHub Actions `runs-on: ubuntu-latest` with `arch: arm64` if supported. Alternatively, rely on unit tests that validate URL construction and asset naming logic. +- **Performance:** Emulated ARM64 will be slow, but functional for smoke tests. + +## 5. Dependencies + +- None; this is an isolated change to platform detection and download URL logic + +## 6. Subtask Checklist + +- [ ] Task 1: Confirm VSCodium ARM64 asset naming + - **Problem:** Need accurate asset suffixes and folder structure + - **Test:** Browse GitHub releases; find a recent ARM64 asset name and its internal folder structure after extraction + - **Subtasks:** + - [ ] Subtask 1.1: `curl -s https://api.github.com/repos/VSCodium/vscodium/releases/latest | jq '.assets[].name' | grep -i arm64` + - [ ] Subtask 1.2: Download sample tarball, `tar -tzf` to see top-level directory name + - [ ] Subtask 1.3: Note URL pattern: `https://github.com/VSCodium/vscodium/releases/download/{tag}/VSCodium-linux-arm64-{version}.tar.gz` (or similar) + +- [ ] Task 2: Update `PLATFORM_MAP` with ARM64 entry + - **Problem:** Map `'linux.arm64'` to correct asset suffix and binary path + - **Test:** `getPlatform(process.platform, process.arch)` returns correct mapping; `getDownloadUrl(version, mapping)` produces valid URL + - **Subtasks:** + - [ ] Subtask 2.1: Add `'linux.arm64': { assetSuffix: 'linux-arm64', binDir: 'bin', binary: 'codium' }` (adjust `assetSuffix` after Task 1 confirmation) + - [ ] Subtask 2.2: Add `'linux.armhf'` if needed (future) + - [ ] Subtask 2.3: Add unit test for `'linux.arm64'` mapping to ensure URL format matches pattern + +- [ ] Task 3: Adjust extraction logic if needed + - **Problem:** Extraction code may rely on `process.platform === 'linux'` but also assume `assetSuffix` matches folder name + - **Test:** Extracting an ARM64 tarball results in expected `~/.occode/vscode/` layout + - **Subtasks:** + - [ ] Subtask 3.1: Check `extractTar(url, dest)` logic; ensure it does not hardcode `x64` in folder naming + - [ ] Subtask 3.2: If folder name includes architecture suffix, derive it from the asset name, not hardcoded + - [ ] Subtask 3.3: Add condition to handle case where top-level folder name includes architecture (e.g., `VSCodium-linux-arm64-1.99.3`) + +- [ ] Task 4: Update binary path detection and execution + - **Problem:** The binary path may differ slightly on ARM64 + - **Test:** After install, `~/.occode/vscode/bin/codium` exists and is executable + - **Subtasks:** + - [ ] Subtask 4.1: Verify `bin` directory name stays same (likely `bin`); + - [ ] Subtask 4.2: Ensure `chmod +x` is applied to `$INSTALL_DIR/bin/codium` + - [ ] Subtask 4.3: Test launch: `child_process.spawn('~/.occode/vscode/bin/codium', ['--version'])` returns version string + +- [ ] Task 5: CI/CD integration + - **Problem:** Need to test ARM64 build automatically + - **Test:** CI workflow runs on Linux ARM64 runner (or emulated) and passes + - **Subtasks:** + - [ ] Subtask 5.1: If using GitHub Actions with `ubuntu-latest` which is x64_64, add a job using `runs-on: ubuntu-latest` with `arch: arm64` setting (if supported) or a self-hosted ARM runner + - [ ] Subtask 5.2: Alternatively, use `docker run --rm --platform linux/arm64 node:20-alpine` to run wrapper unit tests inside container + - [ ] Subtask 5.3: Ensure CI job installs dependencies and runs `npm test` with platform set to `linux.arm64` + +- [ ] Task 6: Manual verification on real hardware (optional but ideal) + - **Problem:** Emulation may miss subtle issues + - **Test:** On a Raspberry Pi 4 (or other ARM64 Linux), run wrapper; VSCodium installs and launches + - **Subtasks:** + - [ ] Subtask 6.1: Build wrapper on ARM64 (or cross-compile if possible) + - [ ] Subtask 6.2: Run the downloader; verify binary launches + - [ ] Subtask 6.3: Report any issues and fix + +- [ ] Task 7: Update documentation + - **Problem:** Users need to know ARM64 is supported + - **Test:** `INSTALL.md` includes ARM64 in supported platforms table + - **Subtasks:** + - [ ] Subtask 7.1: Add row: `Linux ARM64 (Raspberry Pi 4/5, Graviton)` with any special notes + - [ ] Subtask 7.2: Mention that if auto-detect fails, users can manually install VSCodium and skip wrapper download + - [ ] Subtask 7.3: Update `CHANGELOG.md` or `README.md` to announce ARM64 support in next release + +- [ ] Task 8: Error handling for unknown arch + - **Problem:** If a new architecture appears (`riscv64`, `s390x`), wrapper should not crash + - **Test:** On unknown `process.platform/arch`, wrapper shows "Unsupported platform" and offers manual install link + - **Subtasks:** + - [ ] Subtask 8.1: In platform detection, if mapping not found, set `supported = false` + - [ ] Subtask 8.2: In UI, render error panel: "Your system architecture is not yet supported. You can manually install VSCodium from https://vscodium.com and then point OCCode to it." + - [ ] Subtask 8.3: Provide a "Skip install" button that proceeds to gateway setup without VSCodium diff --git a/.tickets/ticket-015-network-error-ui/agent-history.md b/.tickets/ticket-015-network-error-ui/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-015-network-error-ui/prd.md b/.tickets/ticket-015-network-error-ui/prd.md new file mode 100644 index 00000000..a7239d5a --- /dev/null +++ b/.tickets/ticket-015-network-error-ui/prd.md @@ -0,0 +1,113 @@ +# PRD: Ticket 015 - Network Error UI Improvements + +## 1. Problem Statement + +When the editor encounters network errors (e.g., failed download of VSCodium, inability to reach MBA.sh for balance or auth, Stripe checkout issues), the user currently sees a generic error or a blank screen with no clear recovery path. The UI should provide meaningful error messages, retry options, and offline fallbacks where possible. This ticket improves the error handling and user feedback across the OCCode extension, especially in the Home panel and any async operations. + +## 2. Proposed Solution + +Implement a unified error display component and enhance error handling in key flows: + +- **Home panel:** When an operation fails (install OpenClaw, fetch balance, start gateway), show a retryable error card with: + - Clear title ("Installation Failed" / "Network Error") + - Human-readable description (what went wrong, e.g., "Could not download VSCodium. Check your internet connection.") + - "Retry" button that re-invokes the failed operation + - "Skip" or "Continue Offline" where applicable (e.g., skip VSCodium install if user already has it) + - Optional "Copy error details" button to capture logs for support +- **Status bar:** If balance fetch fails repeatedly, show warning icon with tooltip "Unable to update balance (offline)". Keep last-known value but indicate staleness. +- **Settings panel:** When OCC Credits API calls fail, show non-blocking warning banner: "Using cached data. Some features may be limited." +- **General:** All `fetch()` calls should catch errors and surface via a user-friendly error handler rather than silently failing or showing stack traces. + +The goal is to make the editor feel resilient and informative even when the network is unreliable. + +## 3. Acceptance Criteria + +- Any network error during VSCodium download shows a retryable error in the Home panel with clear context +- Clicking "Retry" attempts the operation again +- Balance fetch failures do not crash the status bar; status bar shows a warning icon and/or `[offline]` suffix +- After multiple consecutive balance failures (e.g., 3), the UI suggests "Sign out and back in" or "Check network" +- Errors are logged to the developer console with full details for debugging +- The editor provides a "Copy error details" button that copies stack trace + error message to clipboard +- Offline mode: If the user is `byok` or has cached balance, core chat functionality still works despite network errors +- No uncaught promise rejections appear in console from OCC extension + +## 4. Technical Considerations + +- **Error boundary:** The Home webview should have an error boundary that catches render errors and shows a fallback UI with "Reload panel" button. +- **Standardized error format:** Define an `AppError` class with fields: `{ code: 'NETWORK'|'AUTH'|'INSTALL'|'UNKNOWN', message: string, details?: any, retryable: boolean }`. Convert all fetch rejections into `AppError`. +- **Retry logic:** Implement exponential backoff for retryable network errors (e.g., up to 3 attempts with jitter). Provide manual "Retry" button indefinitely. +- **User communication:** Avoid technical jargon. Use plain language: "Could not connect to server. Check your internet connection and try again." +- **Offline detection:** Use `navigator.onLine` in the webview to detect offline status and show distinct UI ("You appear to be offline"). +- **Logging:** In extension host, use `console.error` with full error objects; in production, consider sending to an error reporting service (e.g., Sentry) optionally. +- **Consistency:** Apply error handling consistently across all async operations: install, balance, gateway start/stop, Stripe checkout, sign-in. + +## 5. Dependencies + +- None; this is cross-cutting UI improvement + +## 6. Subtask Checklist + +- [ ] Task 1: Create error handling utilities + - **Problem:** Need a common way to wrap async calls and surface errors to UI + - **Test:** All important async calls are wrapped by error handler that posts `onError` to webview + - **Subtasks:** + - [ ] Subtask 1.1: Define `src/common/errors.ts` with `AppError` enum and constructor + - [ ] Subtask 1.2: Implement `withErrorHandling(fn: () => Promise): Promise` that catches and normalizes errors + - [ ] Subtask 1.3: Create `ErrorDisplay` React component (or webview component) that takes `AppError` and renders title, message, retry button + +- [ ] Task 2: Enhance Home panel error UI + - **Problem:** Current errors may be silent or show raw stack + - **Test:** When install fails, Home shows error card with "Retry" button; "Retry" works + - **Subtasks:** + - [ ] Subtask 2.1: In Home webview (`home.ts` renderer), add state for `currentError: AppError | null` + - [ ] Subtask 2.2: When an operation (install OpenClaw, start gateway, auth flow) fails, set `currentError` with appropriate code/message + - [ ] Subtask 2.3: Render `ErrorDisplay` component; on "Retry" click, clear error and re-run the failed operation (store the retry callback) + - [ ] Subtask 2.4: Add "Copy details" that copies `error.stack` to clipboard via `navigator.clipboard.writeText` + +- [ ] Task 3: Improve status bar error handling + - **Problem:** Balance fetch failures may leave status bar in unknown state or crash + - **Test:** Simulate network disconnect; status bar shows dimmed balance + warning icon, no crash + - **Subtasks:** + - [ ] Subtask 3.1: In `fetchBalance()` (ticket-004), catch network errors and return `null` or throw `AppError('NETWORK', 'Balance fetch failed')` + - [ ] Subtask 3.2: In status bar update logic, if balance fetch fails, set text to last-known balance + `[offline]` and set background to orange/yellow warning color + - [ ] Subtask 3.3: After 3 consecutive failures, show tooltip "Balance updates paused. Check your network." + +- [ ] Task 4: Settings panel network warnings + - **Problem:** User may not know settings are stale when offline + - **Test:** Disconnect network; OCC Credits card shows "Using cached data" banner + - **Subtasks:** + - [ ] Subtask 4.1: In settings renderer, listen for `balanceFetchFailed` events from extension host + - [ ] Subtask 4.2: Show a non-blocking warning banner within the card: "Updates paused (offline). Balance may be outdated." + - [ ] Subtask 4.3: When network returns, automatically hide banner and refresh balance + +- [ ] Task 5: Global error boundary for webview + - **Problem:** Unhandled exceptions in React renderer could blank the panel + - **Test:** Introduce test error (throw in componentDidMount); error boundary shows fallback UI with "Reload" + - **Subtasks:** + - [ ] Subtask 5.1: Wrap the root of Home webview in an error boundary (React 16+ `componentDidCatch` or `ErrorBoundary` component) + - [ ] Subtask 5.2: Fallback UI: "Something went wrong. [Reload] [Copy error]" buttons + - [ ] Subtask 5.3: "Reload" calls `window.location.reload()`; "Copy error" copies error info + +- [ ] Task 6: Offline detection and messaging + - **Problem:** User may be offline without realizing; auth flows will obviously fail + - **Test:** Turn off Wi-Fi; Home panel banner: "You are offline. Some features are unavailable." + - **Subtasks:** + - [ ] Subtask 6.1: In Home webview, add `window.addEventListener('online', ...)` and `'offline'` to toggle `isOnline` state + - [ ] Subtask 6.2: If offline, show persistent banner at top: "Offline — Please check your internet connection." + - [ ] Subtask 6.3: Hide network-dependent UI elements when offline (e.g., disable "Sign in" button, gray out "Install OpenClaw") + +- [ ] Task 7: Centralize fetch wrapper in extension host + - **Problem:** Multiple module fetch calls may not handle errors consistently + - **Test:** All HTTP calls go through `src/common/fetchWrapper.ts` that throws normalized `AppError` + - **Subtasks:** + - [ ] Subtask 7.1: Create `fetchJson(url, options)` that catches `fetch` errors, non-2xx status codes, and JSON parse errors + - [ ] Subtask 7.2: Map to `AppError`: network error → `code: 'NETWORK'`, 401 → `code: 'AUTH'`, 402 → `code: 'INSUFFICIENT_CREDITS'`, 5xx → `code: 'SERVER'`, etc. + - [ ] Subtask 7.3: Update all uses (balance, auth, inference) to use this wrapper + +- [ ] Task 8: Testing and polish + - **Problem:** Ensure error paths are covered + - **Test:** Simulate various failures (no network, 500 from server, invalid JSON) and verify UI response + - **Subtasks:** + - [ ] Subtask 8.1: Write unit tests for `withErrorHandling` wrapper + - [ ] Subtask 8.2: Write integration test for Home panel error rendering (using test webview) + - [ ] Subtask 8.3: Manual QA: kill network, break backend, 404 assets, verify recoverability diff --git a/.tickets/ticket-016-automated-e2e-tests/agent-history.md b/.tickets/ticket-016-automated-e2e-tests/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-016-automated-e2e-tests/prd.md b/.tickets/ticket-016-automated-e2e-tests/prd.md new file mode 100644 index 00000000..399d7d43 --- /dev/null +++ b/.tickets/ticket-016-automated-e2e-tests/prd.md @@ -0,0 +1,168 @@ +# PRD: Ticket 016 - Automated E2E Tests + +## 1. Problem Statement + +Currently the OCCode test suite (`test.md`) is a manual checklist that requires human interaction to verify every release. This is time-consuming, error-prone, and does not scale. We need an automated end-to-end (E2E) test suite that exercises core user flows: onboarding, authentication, OpenClaw installation, gateway start, and chat inference. The tests should run in CI on every push/PR and gate releases. + +## 2. Proposed Solution + +Implement Playwright-based E2E tests that drive the editor UI (Electron/Void) via the Playwright Electron driver or by launching the editor as a subprocess and connecting via CDP. Because the editor is a desktop app, we have two options: + +- **Option A:** Use `playwright-electron` to launch the editor directly and interact with its windows +- **Option B:** Launch the editor with `--remote-debugging-port` and use `@playwright/test` with `chromium` to connect to the existing Electron instance + +Given the complexity, start with **Option B** (connect to Electron via CDP) as it's more stable. The test suite should: + +- Launch the editor in a clean profile (`--user-data-dir` temp) +- Wait for the Home panel to load +- Interact with buttons: "Install OpenClaw", "Start Gateway", "Sign in" +- Fill settings, verify status bar updates +- Simulate chat messages (by injecting into MoltPilot or mocking the backend) + +Because the editor talks to real backend services (MBA.sh, Stripe), tests should use a dedicated test environment with mock servers or recorded HTTP interactions using `nock` on the backend side. For simplicity, run tests against a local test double of the backend that returns canned responses. + +Test cases (subset of `test.md`): + +1. Onboarding flow shows initial steps; "Create Account" opens browser (mock) +2. OpenClaw installation: when `openclaw` not present, installer runs and creates `~/.openclaw` structure +3. Gateway status: "Start" button triggers `openclaw gateway start`; status changes to "Running" +4. Authenticated state: after sign-in (mocked), status bar shows balance, Home shows account info +5. Sign out flow: clears credentials and returns to onboarding +6. Settings panel: switching between OCC Credits and BYOK updates UI correctly +7. Chat inference (simulated): send a message and verify mock backend receives request + +## 3. Acceptance Criteria + +- `npm test` (or `npm run test:e2e`) runs the full Playwright suite and exits with code 0 if all pass +- CI pipeline (GitHub Actions) runs the suite on every PR to `main` and on release branches +- The suite executes within 5 minutes on CI +- Tests are deterministic (no flaky waits) and isolated (clean user-data-dir each run) +- At least 80% of critical manual checklist items are covered by automated tests +- Test reports are uploaded as artifacts on CI failure (Playwright HTML report) + +## 4. Technical Considerations + +- **Playwright setup:** Install `@playwright/test`, `playwright`, and `playwright-electron` if needed. Configure `playwright.config.ts`. +- **Electron automation:** Use `electron` package to launch the editor binary, or launch `apps/editor` built output. Need to start with `--remote-debugging-port=9222` to let Playwright attach via CDP. +- **Test environment:** Use mocks rather than real network. Run a local mock server (e.g., Express) that implements the same endpoints as production backend (`/api/v1/auth/signup`, `/api/v1/balance`, `/api/v1/chat/completions`) with predictable responses. +- **File system isolation:** Use a temporary directory for editor's `~/.occode-editor` and `~/.openclaw` to avoid polluting developer machine. Clean up after tests. +- **Timing and waits:** Use Playwright's auto-waiting (`elementHandle.waitForElementState('visible')`, `expect(locator).toBeVisible()`). Avoid arbitrary sleeps. +- **CI resources:** Electron in headless mode may require Xvfb on Linux. GitHub Actions `ubuntu-latest` supports running Electron in headless with `xvfb-run`. +- **Mocking extension internals:** Possibly need to set environment variables to point extension to mock backend instead of production (`OCC_BACKEND_URL=http://localhost:3001`). +- **Test data:** Use fixed test user with known balance; mock JWT that extension accepts for auth. + +## 5. Dependencies + +- None; this is a new test suite added to repo + +## 6. Subtask Checklist + +- [ ] Task 1: Set up Playwright project + - **Problem:** Get basic test runner working with Electron + - **Test:** `npx playwright test` reports "0 tests" but runs without error; can take screenshot of editor window + - **Subtasks:** + - [ ] Subtask 1.1: `npm install -D @playwright/test playwright` + - [ ] Subtask 1.2: Create `playwright.config.ts` with `use: { headless: true, screenshot: 'only-on-failure', trace: 'on-first-retry' }` + - [ ] Subtask 1.3: Write a dummy test that launches editor: `const browser = await playwright.chromium.launch({ channel: 'chrome' });` actually need electron: `const { _electron: electron } = require('playwright');` maybe simpler to spawn editor process and connect via `browserType.connectOverCDP`; document whichever works + +- [ ] Task 2: Implement Electron launch fixture + - **Problem:** Need to start the editor with proper flags and attach Playwright + - **Test:** Fixture `editor` provides a `Page` object pointing to the main window; can take screenshot + - **Subtasks:** + - [ ] Subtask 2.1: Build the editor: `npm --prefix apps/editor run compile` (or use prebuilt) + - [ ] Subtask 2.2: Determine editor binary path: `apps/editor/out/...` or use `vscode` script to launch; likely need to use `node ./apps/editor/out/main.js` + - [ ] Subtask 2.3: Spawn child process with `--remote-debugging-port=9222 --user-data-dir=/tmp/occode-test-profile` + - [ ] Subtask 2.4: Use `playwright.chromium.connectOverCDP('http://localhost:9222')` to get browser and first page + - [ ] Subtask 2.5: Ensure cleanup: kill child process after tests (`afterAll` hook) + +- [ ] Task 3: Create mock backend server + - **Problem:** Editor needs endpoints to talk to + - **Test:** Running `node test/mock-backend.js` listens on `http://localhost:3001` and responds to `/api/v1/balance`, `/api/v1/auth/signup`, `/v1/chat/completions` + - **Subtasks:** + - [ ] Subtask 3.1: Write small Express server (or Fastify) with routes: + - `POST /api/v1/auth/signup` → `302` redirect to `occ-editor://auth?token=test.jwt&balance=5.00` + - `GET /api/v1/balance` → `{ balance_usd: 5.00 }` (or decrementable) + - `GET /api/v1/me` → `{ email: 'test@test.com' }` + - `POST /v1/chat/completions` → if `stream: false` return fake completion; if `stream: true` stream SSE chunks; include header `x-litellm-response-cost: 0.01` + - [ ] Subtask 3.2: Add environment variable `OCC_BACKEND_URL=http://localhost:3001` for the extension to use (instead of production) + - [ ] Subtask 3.3: Start mock server in Playwright fixture `beforeAll` and close in `afterAll` + +- [ ] Task 4: Write first test: Install OpenClaw flow + - **Problem:** Verify installer works + - **Test:** On fresh profile (no `~/.openclaw`), Home panel shows "Install OpenClaw"; click "Install"; progress log appears; after success, status shows installed version + - **Subtasks:** + - [ ] Subtask 4.1: Use `editor` fixture to get page; `await page.goto('home')` (maybe automatic) + - [ ] Subtask 4.2: `await page.click('text=Install OpenClaw')` + - [ ] Subtask 4.3: Wait for progress log to contain "OpenClaw installed successfully" + - [ ] Subtask 4.4: Check file system: `~/.openclaw/openclaw.json` exists in test temp dir + - [ ] Subtask 4.5: Verify status panel shows Gateway: Stopped (since not started yet) + +- [ ] Task 5: Write test: Gateway start + - **Problem:** Verify gateway can be started from Home + - **Test:** Click "Start Gateway"; status changes to "Running" after a few seconds + - **Subtasks:** + - [ ] Subtask 5.1: Ensure OpenClaw installed from previous test; now click "Start Gateway" + - [ ] Subtask 5.2: Wait for status to change: `await expect(page.locator('[data-testid="gateway-status"]')).toHaveText('Running')` + - [ ] Subtask 5.3: Verify `openclaw gateway status` would return running (check process or file? maybe just trust UI) + - [ ] Subtask 5.4: Click "Stop Gateway"; wait for "Stopped" + +- [ ] Task 6: Write test: Onboarding and auth flow + - **Problem:** Verify "Create Account" button opens browser and URI handler works + - **Test:** In fresh profile, Home shows onboarding steps; click "Create Account"; mock browser opens (we can hijack `openExternal` to just log); after timeout or simulated callback, home shows logged-in state with balance + - **Subtasks:** + - [ ] Subtask 6.1: Mock `vscode.env.openExternal` to capture URL and simulate user returning via URI (this may require extension host injection; might be complex). Alternative: Skip actual browser and simulate successful auth by directly calling the URI handler from test by sending a message to extension. + - [ ] Subtask 6.2: For feasibility, test the UI state transition: after installation, home shows "Sign in with OCC" card; clicking it triggers `openExternal`; we can assert `openExternal` was called with `https://mba.sh/signup?ref=occ-editor` + - [ ] Subtask 6.3: To test full flow, may need to wait for real auth; could be deferred to later priority + +- [ ] Task 7: Write test: Settings panel OCC Credits card + - **Problem:** Verify settings UI reflects state correctly + - **Test:** Open Settings (Cmd+,), search "OCC Credits"; card shows "Sign in" if unauth; after triggering sign-in simulation, card shows email and balance + - **Subtasks:** + - [ ] Subtask 7.1: Test sign-in flow via URI handler to set token in `context.secrets` + - [ ] Subtask 7.2: Refresh settings view; verify card title, balance text, "Buy More Credits" link present + - [ ] Subtask 7.3: Click "Sign Out"; verify card returns to "Sign in" state + +- [ ] Task 8: Write test: Balance polling and deduction + - **Problem:** Verify balance updates + - **Test:** Mock backend balance initially 5.00; after simulated chat deduction, backend reduces by 0.01; status bar updates accordingly + - **Subtasks:** + - [ ] Subtask 8.1: Initial balance fetch returns 5.00; status bar shows "$5.00" + - [ ] Subtask 8.2: Trigger chat inference (how? maybe call extension command directly that sends a request to mock `/chat/completions`) + - [ ] Subtask 8.3: Mock `/chat/completions` returns with header `x-litellm-response-cost: 0.01` + - [ ] Subtask 8.4: Wait for balance to update to 4.99; status bar shows "$4.99" + - [ ] Subtask 8.5: Also verify `usage_log` was inserted in mock backend (optional) + +- [ ] Task 9: CI integration + - **Problem:** Tests must run automatically + - **Test:** PR to main shows Playwright job; passes when tests succeed, fails when any test fails + - **Subtasks:** + - [ ] Subtask 9.1: Create `.github/workflows/e2e.yml` or similar + - [ ] Subtask 9.2: Set up job with `runs-on: ubuntu-latest` + - [ ] Subtask 9.3: Steps: checkout, setup Node, `npm ci`, `npm run build` (editor + extension), `npm run mock-backend &`, `npx playwright test` + - [ ] Subtask 9.4: If tests fail, upload Playwright HTML report as artifact: `actions/upload-artifact` + - [ ] Subtask 9.5: Add badge to README: `![E2E](https://github.com/.../badge.svg)` + +- [ ] Task 10: Flake mitigation and maintenance + - **Problem:** E2E tests can be flaky due to timing or external dependencies + - **Test:** Suite runs 10 times in a row without failure + - **Subtasks:** + - [ ] Subtask 10.1: Set global timeout to 30s per test, disable animations in editor (`"window.animationDisabled": true` flag) + - [ ] Subtask 10.2: Prefer `await expect(locator).toBeVisible({ timeout: 10000 })` over `sleep` + - [ ] Subtask 10.3: Use test isolation: each test gets a fresh user-data-dir and clean mock DB + - [ ] Subtask 10.4: If certain tests remain flaky, add `.skip` and document reason + +- [ ] Task 11: Documentation for running tests locally + - **Problem:** Developers need to run tests on their machines + - **Test:** `README.md` section "Running E2E tests" with steps + - **Subtasks:** + - [ ] Subtask 11.1: Document prerequisites: Node 20, Docker (if needed for DB), display (for Electron headless works without X) + - [ ] Subtask 11.2: `npm run build` (editor + extension) then `npm run test:e2e` + - [ ] Subtask 11.3: Explain how to debug a failing test: `npx playwright test --debug`, `npx playwright show-report` + +- [ ] Task 12: Coverage of remaining manual checklist items + - **Problem:** 80% coverage target + - **Test:** Map each manual item to an automated test; track in spreadsheet or comments + - **Subtasks:** + - [ ] Subtask 12.1: Review `test.md` checklist; mark items as covered or not + - [ ] Subtask 12.2: Prioritize covering critical paths: install, gateway, auth, balance, sign-out + - [ ] Subtask 12.3: Create additional tickets for outstanding items if needed (e.g., "test OAuth Google flow", "test Stripe webhook integration") diff --git a/.tickets/ticket-017-cicd-pipeline/agent-history.md b/.tickets/ticket-017-cicd-pipeline/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-017-cicd-pipeline/prd.md b/.tickets/ticket-017-cicd-pipeline/prd.md new file mode 100644 index 00000000..f7acfd27 --- /dev/null +++ b/.tickets/ticket-017-cicd-pipeline/prd.md @@ -0,0 +1,178 @@ +# PRD: Ticket 017 - CI/CD Pipeline + +## 1. Problem Statement + +The OCCode repository lacks a continuous integration and delivery pipeline. Every change currently requires manual building, testing, and deployment. We need automated CI that runs: + +- Linting and type-checking +- Unit/integration tests for backend and extension +- E2E tests (ticket-016) +- Build artifacts: editor compilation, extension VSIX packaging, Docker image +- (Optional) Automated deployment to staging/production + +A well-configured CI will catch bugs early, ensure code quality, and enable rapid, reliable releases. + +## 2. Proposed Solution + +Set up GitHub Actions workflows in `.github/workflows/`: + +**Workflows:** + +1. **PR Check** (`pr.yml`) — triggered on `pull_request` to any branch: + - Install dependencies (root, extension, web, packages) + - Run ESLint and TypeScript compiler (`npm run lint`, `npx tsc --noEmit`) + - Run unit tests (backend and extension) (`npm test --workspaces` or specific) + - Run backend integration tests (with test DB) + - If E2E tests are ready and not too flaky, also run them (maybe only on `main` PRs) + - Build the editor (`npm --prefix apps/editor run compile`) and extension package (`npm --prefix apps/extension run ext:package`) + - Upload build artifacts (editor binaries, extension VSIX) as PR artifacts for inspection + +2. **Release** (`release.yml`) — triggered on `push` to `main` tag `v*`: + - Same checks as PR, but all tests must pass + - Build all artifacts + - Create GitHub Release with assets: + - `occ-VERSION-linux-x64.tar.gz` (if built) + - `occ-VERSION-macos-x64.dmg` + - `occ-VERSION-macos-arm64.dmg` + - `occ-VERSION-windows-x64.exe` (or zip) + - `occ-openclaw-VERSION.vsix` + - `CHANGELOG.md` excerpt + - Optionally deploy backend to staging/production (if included in repo) + - Post message to Telegram/Discord about new release (via webhook) + +3. **Nightly Build** (`nightly.yml`) — scheduled `cron` to build and upload pre-release builds to identify breakages early + +Additional considerations: + +- Use actions/cache to speed up `npm ci` and build caches +- Set up `DOCKER_BUILDKIT=1` for Docker builds (if any) +- For E2E tests, use `xvfb-run` on Linux to provide display +- Secrets: `STRIPE_SECRET_KEY`, `JWT_SECRET` etc. should not be needed for CI since tests use mocks; but if real integration tests run against a test backend, need service secrets in GitHub Secrets + +## 3. Acceptance Criteria + +- Every PR shows a CI check that runs all required steps and reports success/failure +- No PR can be merged unless CI passes +- On merge to `main` with a new semver tag, GitHub Release is automatically created with all platform binaries +- Nightly builds run and upload pre-release assets to a draft release or storage +- All build artifacts are reproducible (same hash given same source) +- CI completes within 20 minutes for PR, within 40 minutes for release (including E2E) +- CI logs are clear and actionable when a step fails + +## 4. Technical Considerations + +- **Matrix strategy:** Build editor binaries per platform. Since GitHub Actions `ubuntu-latest` is Linux x64, macOS runner provides macOS builds, and Windows runner provides Windows builds. For ARM64 macOS, need macOS runner on ARM hardware (GitHub's macOS runners are Apple Silicon as of 2024, so both x64 and arm64 can be built via Rosetta or native). For Linux ARM64, may need self-hosted runner or QEMU. +- **Workspace installation:** Root `package.json` likely uses workspaces. Use `npm ci` at root to install all workspaces. +- **Build steps:** The editor build has a complex dependency on specific Node version (20.18.2). Use `actions/setup-node` with `node-version: 20.18.2` and `cache: npm`. +- **Electron E2E:** On Linux headless, need Xvfb. Use `xvfb-run --auto-servernum --server-args='-screen 0 1920x1080x24'` before `npx playwright test`. +- **Artifact upload:** Use `actions/upload-artifact` for PR artifacts to allow downloading the built VSIX for manual testing. Use `github-release-upload` or `softprops/action-gh-release` for release assets. +- **Security:** No secrets in PR builds. For release builds that might deploy backend, store service tokens in GitHub Secrets (`STRIPE_SECRET_KEY`, `INFERENCE_API_KEY`, etc.), and use them only when `github.event_name == 'push' && contains(github.ref, 'tags/')`. +- **Dotenv:** Load environment variables from `.env` file for local dev; in CI, use `env:` block in workflow. +- **Failure handling:** If any step fails, the workflow should fail immediately (no need to continue). + +## 5. Dependencies + +- None; this is a repo infrastructure task + +## 6. Subtask Checklist + +- [ ] Task 1: Create PR workflow (`pr.yml`) + - **Problem:** Run checks on every PR + - **Test:** Open a PR; GitHub checks appear; all succeed on clean repo + - **Subtasks:** + - [ ] Subtask 1.1: Define `on: pull_request` trigger; branches: `main`, `*` + - [ ] Subtask 1.2: Set up jobs: `lint`, `test`, `build`, `e2e` (optional) + - [ ] Subtask 1.3: In `lint` job: `npm ci`, `npm run lint` + - [ ] Subtask 1.4: In `test` job: `npm ci && npm test` (runs unit tests for backend and extension) + - [ ] Subtask 1.5: In `build` job: `npm run build` (editor and extension packaging) and upload editor build and VSIX as artifacts + - [ ] Subtask 1.6: In `e2e` job (if included): start mock backend, xvfb-run Playwright, upload HTML report on failure + +- [ ] Task 2: Create release workflow (`release.yml`) + - **Problem:** Automated release publishing on tag push + - **Test:** Merge to main and push tag `v0.1.0`; GitHub Release created with assets + - **Subtasks:** + - [ ] Subtask 2.1: `on: push` with `tags: ['v*']` + - [ ] Subtask 2.2: Checkout code, setup Node 20.18.2, cache npm, `npm ci` + - [ ] Subtask 2.3: Run full test suite (including E2E) – release must be green + - [ ] Subtask 2.4: Build all platform binaries. This may require matrix for os: + ```yaml + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + ``` + - [ ] Subtask 2.5: For each OS, run appropriate build script (e.g., `npm run package:linux`, `npm run package:mac`, `npm run package:win`). These scripts will produce `.tar.gz`, `.dmg`, `.exe` etc. + - [ ] Subtask 2.6: Also package extension: `npm --prefix apps/extension run ext:package` + - [ ] Subtask 2.7: Use `softprops/action-gh-release` to create release with tag name; upload all artifacts with appropriate MIME types + - [ ] Subtask 2.8: Include `CHANGELOG.md` content in release body (read file and pass to action) + +- [ ] Task 3: Nightly build workflow (`nightly.yml`) + - **Problem:** Detect breakages not caught by PRs (e.g., dependency updates) + - **Test:** Scheduled run at 2 AM UTC; builds and uploads draft release with timestamp + - **Subtasks:** + - [ ] Subtask 3.1: `on: schedule: - cron: '0 2 * * *'` + - [ ] Subtask 3.2: Build all platforms and extension + - [ ] Subtask 3.3: Create a draft release "Nightly " and upload assets (no notification) + - [ ] Subtask 3.4: Optionally post to Telegram channel if builds fail (alerting) + +- [ ] Task 4: Add build scripts to `package.json` + - **Problem:** CI needs simple commands to produce artifacts + - **Test:** `npm run package:linux` yields a `.tar.gz` with editor binary + - **Subtasks:** + - [ ] Subtask 4.1: Research packaging for the VSCodium-based editor. Likely using `electron-builder`? The repo may already have scripts; if not, create: + - `package:linux`: `npm --prefix apps/editor run package --linux --x64` (adjust) + - `package:mac`: `npm --prefix apps/editor run package --mac --x64` and `--arm64` in separate steps + - `package:win`: `npm --prefix apps/editor run package --win --x64` + - [ ] Subtask 4.2: Ensure build output directory (`apps/editor/dist` or `release/`) is known for artifact upload + - [ ] Subtask 4.3: Test each script locally before committing CI + +- [ ] Task 5: Configure caching to speed up CI + - **Problem:** CI runs take too long due to `npm ci` + - **Test:** Workflow runs with cache hit reduce time by ~50% + - **Subtasks:** + - [ ] Subtask 5.1: Use `actions/cache` with `key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}` and `path: | + ~/.npm + node_modules + .openclaw` (if any) + - [ ] Subtask 5.2: Set `restore-keys` to fallback + - [ ] Subtask 5.3: Cache Playwright browsers with `actions/cache` using `~/.cache/ms-playwright` (if using E2E) + +- [ ] Task 6: Handle E2E test environment + - **Problem:** E2E needs display and mock backend + - **Test:** E2E job runs without display errors; mock server starts on port 3001; tests pass + - **Subtasks:** + - [ ] Subtask 6.1: Use `xvfb-run` wrapper for Linux job: `run: xvfb-run --auto-servernum --server-args='-screen 0 1920x1080x24' npx playwright test` + - [ ] Subtask 6.2: Start mock server: `node test/mock-backend.js &`; wait for port + - [ ] Subtask 6.3: Set `OCC_BACKEND_URL=http://localhost:3001` env for editor process via `process.env` in fixture or wrapper script + - [ ] Subtask 6.4: Cleanup: kill mock server after tests + +- [ ] Task 7: Secrets management (if needed for integration tests) + - **Problem:** Some tests might require real Stripe or inference keys (unlikely) + - **Test:** If not needed, ignore. If needed, store in GitHub Secrets and use in `env:` of needed jobs (only for push to main?). + - **Subtasks:** + - [ ] Subtask 7.1: Add `STRIPE_SECRET_KEY`, `JWT_SECRET` as GitHub Secrets (encrypted) + - [ ] Subtask 7.2: In workflow, pass as `env: STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}` only for integration tests, not for PR builds from forks + - [ ] Subtask 7.3: Ensure secrets not logged (no `echo $STRIPE_SECRET_KEY`) + +- [ ] Task 8: Monitoring and alerts + - **Problem:** CI failures should be visible + - **Test:** If any workflow fails, a notification is sent to a Telegram/Discord channel + - **Subtasks:** + - [ ] Subtask 8.1: Use `actions/telegram` or `Ilshidur/action-notification` to send to OCCThings group on failure + - [ ] Subtask 8.2: Configure to send only on PR failure and nightly failure (not release, since release will be evident) + - [ ] Subtask 8.3: Include link to failed run in message + +- [ ] Task 9: Documentation + - **Problem:** Developers need to understand CI + - **Test:** `README.md` contains a "CI/CD" section explaining workflows and how to trigger release + - **Subtasks:** + - [ ] Subtask 9.1: Document the workflow files: `pr.yml` runs on PRs; `release.yml` on tags; `nightly.yml` on schedule + - [ ] Subtask 9.2: Explain how to create a release: `git tag v0.1.0 && git push origin v0.1.0` + - [ ] Subtask 9.3: Note about required Node version and environment variables for local builds + +- [ ] Task 10: Verify and iterate + - **Problem:** First CI may have issues; need to fix + - **Test:** After initial commit, monitor runs; fix any failures (path issues, missing env, timeouts) + - **Subtasks:** + - [ ] Subtask 10.1: Tune timeouts: if E2E takes long, increase job timeout (`timeout-minutes: 60`) + - [ ] Subtask 10.2: If caching causes corruption, clear caches manually in GitHub UI + - [ ] Subtask 10.3: If builds fail on Windows due to path length, enable long paths in runner (maybe add `core.longpaths` config) diff --git a/.tickets/ticket-018-backend-monitoring-logging/agent-history.md b/.tickets/ticket-018-backend-monitoring-logging/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-018-backend-monitoring-logging/prd.md b/.tickets/ticket-018-backend-monitoring-logging/prd.md new file mode 100644 index 00000000..b42e7c60 --- /dev/null +++ b/.tickets/ticket-018-backend-monitoring-logging/prd.md @@ -0,0 +1,161 @@ +# PRD: Ticket 018 - Backend Monitoring & Logging + +## 1. Problem Statement + +Once the OCC backend is deployed, we need visibility into its health, performance, and usage patterns. Without proper monitoring and logging, issues like downtime, slow inference, or abnormal usage will go undetected until users complain. We need to instrument the backend to emit structured logs, expose metrics endpoints, and optionally integrate with an external observability platform (e.g., Grafana Loki, Datadog, or even simple file-based logging with rotation). The solution should be lightweight and suitable for small-scale deployment (e.g., a single Railway/Render service), yet scalable. + +## 2. Proposed Solution + +Implement structured JSON logging and Prometheus-style metrics exposition: + +- **Logging:** Use `pino` or `winston` for JSON logs. Every request should be logged with: + - `timestamp`, `level`, `message` + - `method`, `path`, `statusCode`, `responseTimeMs` + - `userId` (if authenticated) + - `error` details (if error) +- Logs should be written to stdout (so hosting platform captures them) and optionally to rotating files if needed. +- **Metrics:** Expose `/metrics` endpoint (text format for Prometheus) with counters and histograms: + - `http_requests_total{method, path, status}` + - `http_request_duration_seconds{path}` bucket histogram + - `balance_fetches_total`, `inference_requests_total`, `inference_cost_usd_total`, `stripe_webhooks_total` + - `user_balance{user_id}` (maybe not for prom, but internal) +- **Health check:** Existing `/health` endpoint should return `{ status: 'ok', db: 'connected', timestamp }` or degrade gracefully +- **Dashboard (optional):** Set up Grafana Cloud or Datadog to ingest logs and metrics; create basic dashboards for request rate, error rate, latency, balance distribution, top users +- **Alerting:** Configure alerts for error rate > 5% or response time > 1s or downtime (service down). Notify via Telegram/Discord to OCCThings. + +## 3. Acceptance Criteria + +- All requests produce a structured log line in JSON to stdout +- `/metrics` endpoint returns Prometheus text exposition format; includes request count and duration metrics +- `/health` endpoint returns 200 with `{ "status": "ok" }` when DB is reachable; returns 503 otherwise +- Logs include request ID correlation (generate `requestId` per incoming request, pass through all logs) +- Metrics can be scraped by Prometheus (if deployed) or at least viewable via `curl http://localhost:3001/metrics` +- Alerts fire on defined conditions (error rate spike, downtime) +- Log retention is at least 7 days (depending on hosting provider) +- No sensitive data (JWT, passwords, API keys) appear in logs + +## 4. Technical Considerations + +- **Logging library:** `pino` is fast, produces JSON, works well with Node. Use `pino-http` for request logging middleware. Or `winston` with `winston-daily-rotate-file` if file logs needed. +- **Request ID:** Generate `uuidv4()` at request start via Fastify `onRequest` hook, store in `req.id`, and use `req.log` with child logger `req.log.child({ requestId: req.id })`. Include in all subsequent logs. +- **Sensitive data redaction:** Ensure `Authorization` headers are masked; never log full JWT. Use `pino` serializers to omit sensitive fields. +- **Metrics library:** Use `prom-client` to register metrics. Collect histogram for latency, counter for requests, gauge for active connections maybe. +- **Performance:** Structured logging and metrics add overhead; ensure they are non-blocking. `pino` is async; `prom-client` aggregates in memory. Keep heavy operations out of hot path. +- **Deployment:** Most hosts (Railway, Render) capture stdout logs automatically and provide log viewer. May need to configure log rotation if writing to files (not recommended). For metrics, either run Prometheus separately or just use logs-based monitoring (Gravatar) if metrics endpoint is too complex; but having `/metrics` is still useful. +- **Cost:** If using SaaS monitoring (Datadog, Grafana Cloud), may have costs; consider self-hosted Grafana + Prometheus on cheap VPS if budget constrained. However, for early stage, simple logs + health checks may suffice; implement metrics later. + +## 5. Dependencies + +- Backend must have basic structure (ticket-007) to integrate logging/monitoring + +## 6. Subtask Checklist + +- [ ] Task 1: Install and configure structured logger + - **Problem:** Replace `console.log` with proper JSON logging + - **Test:** `curl http://localhost:3001/health` produces a JSON log line on stdout with `level: 'info'` + - **Subtasks:** + - [ ] Subtask 1.1: `npm install pino pino-http pino-pretty` (pretty for dev) + - [ ] Subtask 1.2: Create `src/logger.ts`: `import pino from 'pino'; const logger = pino({ level: process.env.LOG_LEVEL || 'info', transport: process.env.NODE_ENV === 'production' ? undefined : { target: 'pino-pretty' } }); export default logger;` + - [ ] Subtask 1.3: In Fastify, use `fastify.use(require('pino-http')({ logger, genReqId: req => uuidv4() }))` or Fastify's built-in `requestId` and `logging` options + - [ ] Subtask 1.4: Ensure all subsequent `fastify.log.info()` calls attach request context automatically (via `pino-http`) + +- [ ] Task 2: Add request ID and context propagation + - **Problem:** Correlate logs across a single request + - **Test:** All log lines for a request contain same `requestId` field + - **Subtasks:** + - [ ] Subtask 2.1: In Fastify, enable `requestId: true` (or custom generator) + - [ ] Subtask 2.2: Verify `fastify.log` includes `requestId` in each log call + - [ ] Subtask 2.3: For async operations that span outside request (e.g., background deduction after streaming), pass `requestId` manually and create child logger `logger.child({ requestId })` + +- [ ] Task 3: Instrument request logging middleware + - **Problem:** Automatically log each HTTP request with method, path, status, response time + - **Test:** For every request, a log line like `{"method":"GET","path":"/api/v1/balance","statusCode":200,"responseTime":12}` appears + - **Subtasks:** + - [ ] Subtask 3.1: Use `fastify-pino-http` or `pino-http` as middleware; configure to log response time and status + - [ ] Subtask 3.2: Ensure error responses (4xx/5xx) log at `error` level + - [ ] Subtask 3.3: Sensitive headers: configure `pino-http` to redact `authorization`, `cookie` by using serializers: `headers: { filter: (hdrs) => hdrs.authorization ? '[REDACTED]' : hdrs.authorization }` + +- [ ] Task 4: Add detailed logs in key business logic + - **Problem:** Need context for auth, balance changes, webhook processing + - **Test:** When deduction occurs, a log line `deducted X USD from user Y, new balance Z` appears at info level + - **Subtasks:** + - [ ] Subtask 4.1: In balance deduction (ticket-009), after DB update: `logger.info({ userId, cost, newBalance }, 'Balance deducted')` + - [ ] Subtask 4.2: In Stripe webhook (ticket-010): `logger.info({ eventId, userId, creditAdded }, 'Stripe webhook processed')` + - [ ] Subtask 4.3: In auth sign-up: `logger.info({ email }, 'New user signup')` + - [ ] Subtask 4.4: On errors: `logger.error({ err, userId }, 'Operation failed')` + +- [ ] Task 5: Implement health check endpoint + - **Problem:** Load balancers and monitoring need a simple OK/fail signal + - **Test:** `curl http://localhost:3001/health` returns `{ "status": "ok", "db": "connected", "timestamp": "..." }` with 200; if DB down, returns 503 + - **Subtasks:** + - [ ] Subtask 5.1: Add `fastify.get('/health', async (req, reply) => { try { await db.$query`SELECT 1`; return { status: 'ok', db: 'connected', timestamp: new Date().toISOString() }; } catch (e) { reply.code(503); return { status: 'error', db: 'disconnected', error: e.message }; } })` + - [ ] Subtask 5.2: Ensure endpoint does not require authentication + - [ ] Subtask 5.3: Return proper `Content-Type: application/json` + - [ ] Subtask 5.4: Consider caching headers: `Cache-Control: no-cache` + +- [ ] Task 6: Expose Prometheus metrics endpoint + - **Problem:** Metrics needed for monitoring + - **Test:** `curl http://localhost:3001/metrics` returns text lines like `# TYPE http_requests_total counter\nhttp_requests_total{method="GET",path="/health",status="200"} 42\n...` + - **Subtasks:** + - [ ] Subtask 6.1: Install `prom-client` + - [ ] Subtask 6.2: Create `src/metrics.ts`: register counters, histograms + - `new client.Counter({ name: 'http_requests_total', help: 'Total HTTP requests', labelNames: ['method','path','status'] })` + - `new client.Histogram({ name: 'http_request_duration_seconds', help: 'Duration of HTTP requests in seconds', labelNames: ['path'], buckets: [0.05,0.1,0.25,0.5,1,2.5,5] })` + - `new client.Counter({ name: 'inference_requests_total', help: 'Total inference requests', labelNames: ['model'] })` + - `new client.Counter({ name: 'stripe_webhooks_total', help: 'Total Stripe webhooks processed', labelNames: ['type'] })` + - [ ] Subtask 6.3: In Fastify, `register(require('fastify-metrics')({ routeMetrics: { enabled: true }, endpoint: '/metrics' }))` or custom handler: `fastify.get('/metrics', async (req, reply) => { reply.type('text/plain'); return client.register.metrics(); })` + - [ ] Subtask 6.4: Instrument route handlers: increment `http_requests_total` counter automatically via Fastify plugin; time duration via histogram automatically if using `fastify-metrics`; verify with `curl` + - [ ] Subtask 6.5: Manually increment `inference_requests_total` in proxy handler (ticket-009) + - [ ] Subtask 6.6: Manually increment `stripe_webhooks_total` in webhook handler (ticket-010) + +- [ ] Task 7: Configurable log level and output + - **Problem:** Developers need verbose logs; production needs JSON only + - **Test:** `LOG_LEVEL=debug npm start` includes debug logs; production uses `info` + - **Subtasks:** + - [ ] Subtask 7.1: Read `LOG_LEVEL` env var; default `info`; allow `debug`, `warn`, `error` + - [ ] Subtask 7.2: In production, set `transport: undefined` so logs go to stdout as JSON only (no pretty) + - [ ] Subtask 7.3: Optionally add log rotation via external tool (not in code) + +- [ ] Task 8: Error tracking (optional but recommended) + - **Problem:** Crash reporting and unhandled rejections need visibility + - **Test:** Unhandled exception sends JSON log with `level: 'fatal'` and stack; optionally triggers alert + - **Subtasks:** + - [ ] Subtask 8.1: Add `process.on('unhandledRejection', (reason) => { logger.fatal({ reason }, 'Unhandled rejection'); process.exit(1); })` + - [ ] Subtask 8.2: Add `process.on('uncaughtException', (err) => { logger.fatal({ err }, 'Uncaught exception'); process.exit(1); })` + - [ ] Subtask 8.3: Consider sending these to Telegram/Discord alert via webhook (`fetch` to chat API) + +- [ ] Task 9: Deploy and verify monitoring in staging + - **Problem:** Ensure it works in real environment + - **Test:** Staging deployment (`https://occ-staging.mba.sh`) has working `/health` and `/metrics`; logs appear in hosting logs viewer + - **Subtasks:** + - [ ] Subtask 9.1: Deploy to Railway/Render with env vars + - [ ] Subtask 9.2: `curl https://occ-staging.mba.sh/health` → ok + - [ ] Subtask 9.3: `curl https://occ-staging.mba.sh/metrics` → prom text + - [ ] Subtask 9.4: Check hosting logs (e.g., Render logs) show JSON lines + - [ ] Subtask 9.5: Trigger an error (e.g., malformed request) and verify it logs at error level + +- [ ] Task 10: Alerting setup + - **Problem:** We need to know when things break + - **Test:** Simulate failure (stop backend) → alert arrives in OCCThings Telegram within 5 minutes + - **Subtasks:** + - [ ] Subtask 10.1: Choose alerting mechanism: UptimeRobot, healthchecks.io, or custom scheduler that pings `/health` and sends Telegram message on failure + - [ ] Subtask 10.2: Set up simple poller (could be OpenClaw itself) that runs every 5 minutes: `curl -fsS https://occ.mba.sh/health || send_telegram("Backend down!")` + - [ ] Subtask 10.3: Also monitor error rate: periodically fetch `/metrics` and parse `http_requests_total` vs 5xx counts; alert if >5% + - [ ] Subtask 10.4: Document alerts and incident response + +- [ ] Task 11: Documentation + - **Problem:** Operations team needs to understand logs and metrics + - **Test:** `docs/observability.md` explains log format, metrics names, how to debug + - **Subtasks:** + - [ ] Subtask 11.1: Create `docs/observability.md` with section "Logging": JSON fields, how to filter + - [ ] Subtask 11.2: Section "Metrics": list all metric names and labels, what they mean + - [ ] Subtask 11.3: Section "Health Check": endpoint and expected responses + - [ ] Subtask 11.4: Section "Alerting": what alerts exist, how to acknowledge, who to contact + +- [ ] Task 12: Performance baseline and tuning + - **Problem:** Too much logging can degrade throughput + - **Test:** With 100 RPS, CPU overhead of logging <5% + - **Subtasks:** + - [ ] Subtask 12.1: Benchmark: run `autocannon -c 100 -d 30 http://localhost:3001/health` while capturing logs; check CPU/latency + - [ ] Subtask 12.2: If needed, reduce log level in production (avoid `debug`); sample high-volume routes if necessary + - [ ] Subtask 12.3: Tune Prometheus histogram buckets based on actual latency distribution diff --git a/.tickets/ticket-019-stripe-webhook-retry/agent-history.md b/.tickets/ticket-019-stripe-webhook-retry/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-019-stripe-webhook-retry/prd.md b/.tickets/ticket-019-stripe-webhook-retry/prd.md new file mode 100644 index 00000000..43ba5586 --- /dev/null +++ b/.tickets/ticket-019-stripe-webhook-retry/prd.md @@ -0,0 +1,34 @@ +# PRD: Ticket 019 - Stripe Webhook Retry Logic + +## 1. Problem Statement +Stripe webhook (`POST /api/v1/stripe/webhook`) must be resilient to failures and ensure idempotent credit adjustments. If the backend crashes or returns an error after Stripe sends a webhook, Stripe will retry for up to 3 days. We need to ensure that retries do not over-credit users and that all events are eventually processed exactly once. Additionally, we need alerting on repeated webhook failures and a manual replay tool for emergency recovery. + +## 2. Proposed Solution +- **Idempotency:** Store processed Stripe event IDs in `stripe_events` table with unique constraint. Check before processing; if already exists, return 200 immediately. +- **Atomicity:** Process webhook inside a single database transaction: update `credits` balance and insert `stripe_events` record together. If either fails, rollback and return 5xx to trigger retry. +- **Error handling:** Catch all errors (signature, JSON, DB) and return appropriate non-2xx status to cause retry (except 400 for signature which Stripe won't retry). +- **Alerting:** After 3 consecutive failures, send alert to OCCThings Telegram. Include cooldown (1h) to avoid spam. +- **Metrics:** Increment `stripe_webhook_events_total{type,outcome}`. +- **Manual replay:** Provide `scripts/replay-stripe-event.js ` to force-reprocess an event from Stripe API. + +## 3. Acceptance Criteria +- Same event processed twice results in single credit adjustment (idempotent) +- If DB error occurs, webhook returns 503 (or 500) and Stripe retries +- After 3 consecutive failures, Telegram alert is sent +- Prometheus metrics expose success/error counts +- A script exists to manually replay an event safely + +## 4. Dependencies +- ticket-010-stripe-top-up (existing webhook) +- ticket-011-database-schema (stripe_events table) +- ticket-018-backend-monitoring-logging (metrics/logging) + +## 5. Subtasks +- [ ] Implement atomic transaction with `stripe_events` insert (ON CONFLICT handling) +- [ ] Add retry-appropriate error responses and validation +- [ ] Add consecutive failure counter and Telegram alert +- [ ] Add Prometheus metrics +- [ ] Write manual replay script +- [ ] Tests: idempotency, DB failure, signature failure +- [ ] Deploy, verify Stripe webhook health, set up alerting +- [ ] Documentation in `docs/webhooks.md` diff --git a/.tickets/ticket-020-developer-quickstart/agent-history.md b/.tickets/ticket-020-developer-quickstart/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-020-developer-quickstart/prd.md b/.tickets/ticket-020-developer-quickstart/prd.md new file mode 100644 index 00000000..293f92b3 --- /dev/null +++ b/.tickets/ticket-020-developer-quickstart/prd.md @@ -0,0 +1,186 @@ +# PRD: Ticket 020 - Developer Quickstart + +## 1. Problem Statement + +New contributors to OCCode face a steep learning curve when setting up the development environment. The project has multiple workspaces (editor, web, control-center), specific Node version requirements (exact 20.18.2), and several manual steps before they can start hacking. To lower the barrier to entry and accelerate onboarding, we need a developer-friendly quickstart guide and automation that gets a new dev from `git clone` to "hello world" in under 15 minutes. + +## 2. Proposed Solution + +Create a comprehensive `DEVELOPERS.md` guide and supporting scripts: + +- `DEVELOPERS.md` at repo root covering: + - Prerequisites (Node 20.18.2 via nvm, Git, possibly Docker for Postgres) + - One-command environment reset: `./scripts/setup-dev.sh` (Linux/macOS) and `scripts\setup-dev.ps1` (Windows) that: + - Checks Node version, installs correct version via nvm if missing + - Installs dependencies at root (`npm ci`) + - Installs editor dependencies (`npm --prefix apps/editor ci`) + - Sets up local PostgreSQL (Docker compose or local install) and runs migrations + - Seeds test data (initial user with $5) + - Builds the editor and extension + - Starts the backend API (or provides command) + - Launches the editor in dev mode + - Explanation of the monorepo structure + - How to run each part: `npm --prefix apps/web run dev` for marketing site, `npm --prefix apps/editor run watch` for editor compile/watch, etc. + - How to run tests: unit, integration, E2E + - How to debug, common pitfalls (Node version mismatch, port conflicts, etc.) + - Links to additional documentation (backend spec, AGENTS.md, etc.) + +Additionally: + +- Provide a `docker-compose.dev.yml` that spins up PostgreSQL, maybe the backend, and any other services (Redis if needed) with one command `docker compose -f docker-compose.dev.yml up -d` +- Add a `makefile` or `justfile` with common shortcuts (`make dev`, `make test`, `make build`, `make db-migrate`) +- Ensure `nvm` use is automatic: a `.nvmrc` file exists at repo root with `20.18.2` and optionally a `.bashrc` snippet that auto-runs `nvm use` when entering the repo (can be documented) + +The goal is to make setup as turnkey as possible, with clear error messages if something is missing. + +## 3. Acceptance Criteria + +- A new contributor can follow `DEVELOPERS.md` from scratch on a clean machine (Linux/macOS/Windows) and end up with a running editor connected to a local backend +- The `setup-dev.sh` script runs without user interaction (or prompts only for necessary inputs like Postgres password) and completes within 10 minutes on decent hardware +- All tests (unit + integration) pass in the fresh dev environment +- The script is idempotent: running it a second time does not break anything +- The guide explicitly states Node version requirement and provides `nvm` instructions; if wrong Node version is used, the script fails early with a clear message +- The guide includes a troubleshooting section with solutions to common errors (e.g., "npm install fails", "can't connect to Postgres", "editor won't launch") +- Docker is optional: if user doesn't have Docker, they can still set up Postgres manually; instructions provided + +## 4. Technical Considerations + +- **OS support:** Provide separate instructions for Windows (PowerShell) and Unix-like (bash). The script can be two versions or a cross-platform Node script. +- **Node version enforcement:** Use `engines` field in root `package.json` and also check in setup script: `node -v | grep -q 'v20.18.2' || { echo "Please install Node 20.18.2 via nvm"; exit 1; }` +- **nvm integration:** The script can call `nvm install` and `nvm use` automatically if nvm is present; if not, instruct user to install nvm first. +- **Database:** For local dev, provide a Docker Compose file that runs PostgreSQL 16 with default credentials (`postgres:postgres`). The backend should read `DATABASE_URL=postgresql://postgres:postgres@localhost:5432/occ` when `NODE_ENV=development`. The script can run `docker compose -f docker-compose.dev.yml up -d` and wait for DB to be ready (`pg_isready`). +- **Migrations:** After DB is up, script runs `npx drizzle-kit migrate` to apply schema. +- **Seeding:** Provide `src/db/seed.ts` that creates a demo user with known password and $5 balance. Script runs `node dist/db/seed.js` or `ts-node src/db/seed.ts`. +- **Backend start:** `npm --prefix apps/backend run dev` or similar (need to define script). The guide should explain how to start backend and editor separately, possibly concurrently with `npm-run-all` or `concurrently`. +- **Editor build:** Editor needs to compile (`npm --prefix apps/editor run compile`). The watch mode (`npm --prefix apps/editor run watch`) is useful for live editing. +- **Web app:** The marketing site (`apps/web`) is Next.js; dev: `npm --prefix apps/web run dev`. +- **Error handling:** Scripts should check each step and exit on failure with a helpful message. Use `set -e` in bash; in PowerShell, `$ErrorActionPreference = 'Stop'`. +- **Environment variables:** Document which are needed (`STRIPE_SECRET_KEY` not needed for dev if using mocks; but `JWT_SECRET` needed). Provide a `.env.example` that developers copy to `.env`. + +## 5. Dependencies + +- Backend must have basic structure (ticket-007) with migrations and seed script +- Database schema ready (ticket-011) +- Editor build scripts exist + +## 6. Subtask Checklist + +- [ ] Task 1: Write `DEVELOPERS.md` guide + - **Problem:** Authoritative onboarding doc + - **Test:** New contributor reads it and can set up environment + - **Subtasks:** + - [ ] Subtask 1.1: Introduction: what OCCode is, monorepo structure overview (diagram) + - [ ] Subtask 1.2: Prerequisites: Node 20.18.2 (with nvm), Git, Docker (optional), PostgreSQL (if not using Docker) + - [ ] Subtask 1.3: Step-by-step setup: + - `git clone ... && cd occ` + - `nvm install` (reads `.nvmrc`) and `nvm use` + - `./scripts/setup-dev.sh` (or manual steps) + - [ ] Subtask 1.4: Manual steps alternative if script not used: install deps, start DB, run migrations, seed, build editor, start backend, launch editor + - [ ] Subtask 1.5: Running dev: separate terminals for backend, editor watch, web + - [ ] Subtask 1.6: Testing: `npm test`, `npm run test:e2e` + - [ ] Subtask 1.7: Troubleshooting: common errors (Node version mismatch, port 3001 in use, Postgres not running, VS Code extension host errors) + - [ ] Subtask 1.8: Links to other docs: `AGENTS.md`, `backend.md`, `roadmap.md` + +- [ ] Task 2: Create `scripts/setup-dev.sh` (bash) and `setup-dev.ps1` (PowerShell) + - **Problem:** Automate the setup + - **Test:** On fresh Ubuntu VM, run script; it completes without manual intervention (except maybe sudo for Docker) + - **Subtasks:** + - [ ] Subtask 2.1: Check Node version: `node -v` should be `v20.18.2`; if not, try `nvm use` if `.nvmrc` exists; if fails, print "Please run `nvm install` then `nvm use` and re-run script" + - [ ] Subtask 2.2: `npm ci` at root + - [ ] Subtask 2.3: `npm --prefix apps/editor ci` + - [ ] Subtask 2.4: Start Postgres via Docker Compose: `docker compose -f docker-compose.dev.yml up -d` + - [ ] Subtask 2.5: Wait for DB: loop `docker exec occ-db pg_isready` or `nc -z localhost 5432` + - [ ] Subtask 2.6: Run migrations: `npx drizzle-kit migrate` + - [ ] Subtask 2.7: Run seed: `npx ts-node src/db/seed.ts` (or compiled dist) + - [ ] Subtask 2.8: Build editor: `npm --prefix apps/editor run compile` + - [ ] Subtask 2.9: Print next steps: "Now run: npm --prefix apps/backend run dev (in one terminal) and npm --prefix apps/editor run watch (in another), then launch editor from out/ or via npm script" + - [ ] Subtask 2.10: If any command fails, exit with non-zero and print helpful error + +- [ ] Task 3: Create `docker-compose.dev.yml` + - **Problem:** Provide disposable Postgres service + - **Test:** `docker compose -f docker-compose.dev.yml up -d` starts Postgres on port 5432 with default credentials + - **Subtasks:** + - [ ] Subtask 3.1: Compose file with service `postgres` using `postgres:16-alpine` + - [ ] Subtask 3.2: Environment: `POSTGRES_PASSWORD=postgres`, `POSTGRES_DB=occ` + - [ ] Subtask 3.3: Volumes: `postgres_data:/var/lib/postgresql/data` (named volume) + - [ ] Subtask 3.4: Ports: `"5432:5432"` + - [ ] Subtask 3.5: Healthcheck: `pg_isready` + - [ ] Subtask 3.6: `docker-compose.dev.yml` also optionally include `backend` service if we want to run full stack in Docker; but simpler to keep only DB + +- [ ] Task 4: Add `Makefile` or `Justfile` + - **Problem:** Shortcuts for common dev tasks + - **Test:** `make dev` starts everything; `make test` runs tests + - **Subtasks:** + - [ ] Subtask 4.1: If using Makefile: + - `install`: `npm ci && npm --prefix apps/editor ci` + - `db-up`: `docker compose -f docker-compose.dev.yml up -d` + - `db-down`: `docker compose -f docker-compose.dev.yml down` + - `migrate`: `npx drizzle-kit migrate` + - `seed`: `npx ts-node src/db/seed.ts` + - `build-editor`: `npm --prefix apps/editor run compile` + - `dev-backend`: `npm --prefix apps/backend run dev` (assuming backend in `apps/backend`; currently backend is at root `apps/backend`? Actually backend might be separate repo; but we can adapt) + - `dev-editor`: `npm --prefix apps/editor run watch` + - `dev-web`: `npm --prefix apps/web run dev` + - `test`: `npm test` + - `test-e2e`: `npm run test:e2e` + - `clean`: `rm -rf node_modules apps/editor/node_modules apps/web/node_modules && docker volume rm occ_postgres_data` + - [ ] Subtask 4.2: Document these make targets in DEVELOPERS.md + +- [ ] Task 5: Ensure backend seed script exists and is robust + - **Problem:** Developers need sample data to test auth and balance + - **Test:** Running `npx ts-node src/db/seed.ts` creates a user `dev@example.com` with password `devpass` and $5 balance; prints credentials to console + - **Subtasks:** + - [ ] Subtask 5.1: Write `src/db/seed.ts` using Drizzle to upsert a demo user (`email: 'dev@example.com'`, hashed password with bcrypt, provider: 'email') + - [ ] Subtask 5.2: Ensure corresponding `credits` row exists with `balance_usd: 5.00` + - [ ] Subtask 5.3: Log the created user's ID and password (or a known default) so developer can use it to sign in during manual testing + - [ ] Subtask 5.4: Make script idempotent: if user exists, update password and ensure credits exist + +- [ ] Task 6: Add `.nvmrc` and `.node-version` at repo root + - **Problem:** Enforce Node version + - **Test:** `cat .nvmrc` outputs `20.18.2`; `nvm use` auto-switches when entering repo (if configured in shell) + - **Subtasks:** + - [ ] Subtask 6.1: `echo "20.18.2" > .nvmrc` + - [ ] Subtask 6.2: Optionally add `.node-version` for tools like `asdf` + - [ ] Subtask 6.3: In `DEVELOPERS.md`, mention "nvm will automatically use the correct version if you have it installed; run `nvm install` to set up" + +- [ ] Task 7: Provide a `.env.example` and explain `.env` + - **Problem:** Backend expects certain environment variables + - **Test:** Developer copies `.env.example` to `.env` and fills in minimal values; backend starts + - **Subtasks:** + - [ ] Subtask 7.1: Create `.env.example` with: + ``` + NODE_ENV=development + PORT=3001 + DATABASE_URL=postgresql://postgres:postgres@localhost:5432/occ + JWT_SECRET=dev-secret-change-me-64-characters-long-random-string + INFERENCE_ENDPOINT=http://localhost:4000/v1 (or mock) + ``` + - [ ] Subtask 7.2: Document: "Copy `.env.example` to `.env` and adjust JWT_SECRET to a random 64-char string" + - [ ] Subtask 7.3: For development, Stripe keys not needed unless testing payments; can leave empty or use test keys + +- [ ] Task 8: Update `package.json` root scripts for convenience + - **Problem:** Need single commands for common tasks + - **Test:** `npm run dev` starts backend + editor watch? maybe `concurrently` scripts + - **Subtasks:** + - [ ] Subtask 8.1: Add `setup` script that runs `./scripts/setup-dev.sh` (or fails on Windows) and prints "Setup complete!" + - [ ] Subtask 8.2: Add `dev` script that runs `concurrently "npm --prefix apps/backend run dev" "npm --prefix apps/editor run watch"` (if backend in apps/backend) + - [ ] Subtask 8.3: Add `build` script that builds editor and extension: `npm --prefix apps/editor run compile && npm --prefix apps/extension run ext:package` + - [ ] Subtask 8.4: Add `test` script that runs all tests: `npm test --workspaces` (if using workspaces) + +- [ ] Task 9: Document the development workflow + - **Problem:** Beyond setup, need to know how to work day-to-day + - **Test:** Guide includes "typical day": start DB, start backend, start editor watch, open editor, make changes, see hot reload, run tests, commit + - **Subtasks:** + - [ ] Subtask 9.1: "Typical dev workflow" section: terminal tabs: 1) `docker compose -f docker-compose.dev.yml up -d` (if not already), 2) `npm --prefix apps/backend run dev`, 3) `npm --prefix apps/editor run watch`, 4) `npm --prefix apps/web run dev` (optional) + - [ ] Subtask 9.2: How to debug: `Debug: Open Chrome and navigate to chrome://inspect` to attach debugger to editor process + - [ ] Subtask 9.3: How to run a single test: `npx playwright test --grep "Install OpenClaw"` + - [ ] Subtask 9.4: Code style: use Prettier, ESLint; `npm run lint` and `npm run format` + +- [ ] Task 10: Review and iterate with fresh eyes + - **Problem:** Avoid assumptions; ensure clarity + - **Test:** Give `DEVELOPERS.md` to a colleague not familiar with project; they can set up without asking questions + - **Subtasks:** + - [ ] Subtask 10.1: Perform a dry run on a clean VM or container + - [ ] Subtask 10.2: Fix any ambiguous instructions or missing steps + - [ ] Subtask 10.3: Update setup scripts based on failures observed during dry run + - [ ] Subtask 10.4: Add FAQ at bottom of guide: "What if I don't have Docker?" "Can I use Windows Subsystem for Linux?" "How do I update dependencies?" diff --git a/.tickets/ticket-021-docker-bootstrap-setup/agent-history.md b/.tickets/ticket-021-docker-bootstrap-setup/agent-history.md new file mode 100644 index 00000000..e69de29b diff --git a/.tickets/ticket-021-docker-bootstrap-setup/prd.md b/.tickets/ticket-021-docker-bootstrap-setup/prd.md new file mode 100644 index 00000000..ea47abb0 --- /dev/null +++ b/.tickets/ticket-021-docker-bootstrap-setup/prd.md @@ -0,0 +1,231 @@ +# PRD: Ticket 021 - Docker Bootstrap Setup (One-Click Containerized Environment) + +## 1. Problem Statement + +New users of OCCode face significant friction when setting up the full local development environment. They must manually install Node.js, PostgreSQL, Redis, the OpenClaw gateway, and configure environment variables. Even with the Developer Quickstart guide, this process is error-prone and intimidating for non-technical users. We need a **bootstrap application** that offers a **Docker-based setup** as the primary installation path: one click, and the entire stack is provisioned in isolated, consistent containers. This Docker setup should be presented as an option alongside a "Local Setup" advanced option when users first launch the app. The Docker flow must automatically detect the user's platform, verify Docker availability, and if needed, guide them to install Docker. Once Docker is present, it should pull the necessary images, create volumes, initialize configuration, start services, and seamlessly take the user to the OpenClaw dashboard ready for use. + +## 2. Proposed Solution + +Implement a **Bootstrap Wizard** in the OCCode Home panel that runs on first launch (or via a "Reset Setup" command). The wizard presents two primary options: + +- **Docker Setup (Recommended)** — provisions everything in Docker containers +- **Local Setup (Advanced)** — manual installation for developers who prefer their own environment + +### Docker Setup Flow + +1. **Platform Detection & Docker Check** + - Detect OS: Windows, macOS, or Linux + - Check if Docker is installed and running: + - Windows: Check for Docker Desktop + WSL2 integration + - macOS: Check for Docker Desktop + - Linux: Check for `dockerd` service or `docker` CLI + - If Docker not detected: + - Show clear instructions with links to download Docker Desktop (Windows/macOS) or install Docker Engine (Linux) + - Provide a "I've installed Docker, retry" button after user confirms + - If Docker detected but not running, prompt to start Docker Desktop + +2. **Docker Environment Provisioning** + - Use a `docker-compose.full.yml` (or generate dynamically) that defines: + - `openclaw-gateway` service (official `openclaw/gateway` image) + - `postgres` service (PostgreSQL 16) + - `redis` service (optional caching) + - `backend` service (OCC backend API at `occ.mba.sh` or local mock for dev) + - Pull images (show progress) + - Create named volumes for persistence: + - `openclaw_data`: for `~/.openclaw` inside container + - `postgres_data`: database storage + - Initialize PostgreSQL if empty (run migrations automatically) + - Seed initial data (admin user, credits) + +3. **Configuration & Connection** + - Write `openclaw.json` in host user directory (`~/.openclaw`) to point gateway to Docker network: + ```json + { + "gateway": { + "customBaseUrl": "http://localhost:3001" // backend API + } + } + ``` + - Ensure extension's `globalState` is configured to use local Docker-based backend (or auto-detect) + - Wait for all services to become healthy (`docker compose ps` check) + - Verify gateway is running: `openclaw gateway status` + +4. **User Onboarding Completion** + - Mark setup as complete in `globalState` (so wizard doesn't show again) + - Transition Home panel to the "Dashboard" view showing: + - Gateway status: Running + - Balance (if authenticated) + - Quick links: "Open Dashboard", "Start Chatting", "Manage Account" + - Optionally auto-open browser to OpenClaw dashboard (`http://localhost:3000` or similar) + +### Fallback & Error Handling + +- If any step fails (Docker errors, port conflicts, network issues): + - Show a detailed error card with "Retry" and "Show logs" buttons + - Offer "Switch to Local Setup" as fallback + - Log full error to developer console and allow copying to clipboard +- If user cancels mid-flow, clean up partially created containers/volumes or leave them for retry (idempotent) + +### Local Setup Option + +- Provide a condensed version of the Developer Quickstart (ticket-020) for users who want to run services directly on host +- Include link to full `DEVELOPERS.md` for detailed instructions +- Still automated where possible (scripts to install Node, DB, etc.) but more manual intervention required + +## 3. Acceptance Criteria + +- On first launch (or via explicit "Setup" action), the Home panel shows a Bootstrap Wizard with two clear options: "Docker Setup (Recommended)" and "Local Setup (Advanced)" +- Docker Setup button initiates the provisioning flow +- The app correctly detects Docker presence on Windows (Docker Desktop), macOS (Docker Desktop), and Linux (docker CLI/daemon) +- If Docker is missing, the wizard shows platform-specific instructions and download links, with a retry button +- When Docker is present, the wizard: + - Pulls all required images (with visible progress indicator) + - Creates `docker-compose` network and volumes + - Starts all services and waits for health (gateway returns 200 on `/health`) + - Creates or updates `~/.openclaw/openclaw.json` with correct gateway configuration + - Confirms gateway status is "Running" +- After successful Docker setup, the Home panel switches to the Dashboard view showing the OpenClaw agent status and balance +- The entire flow is fully automated after Docker is confirmed; user only clicks buttons and watches progress +- Errors are captured and presented with actionable recovery options; no silent failures +- The wizard can be re-run (e.g., from a "Reset Setup" command) to tear down and recreate the environment from scratch +- The setup is idempotent: running it multiple times does not create duplicate containers or corrupt data +- All Docker resources (containers, networks, volumes) are named with a clear prefix like `occ-` to avoid collisions + +## 4. Technical Considerations + +- **Docker Compose**: Use a version-compatible `docker-compose.yml` (v3.8+) that works with Docker Desktop and Docker Engine. Define services, networks, volumes, healthchecks. +- **Platform-specific detection**: + - Windows: Check registry or process `Docker Desktop.exe`; also check WSL2 integration via `wsl -l -v` if needed + - macOS: Check `docker version` and `osascript` to see if Docker Desktop app is running + - Linux: `systemctl is-active docker` or `docker info` +- **Privilege escalation**: Starting Docker on Windows/macOS may require user to unlock Docker Desktop (it runs as a privileged service but UI may be locked). Provide instructions: "Please open Docker Desktop and click Start" +- **Port conflicts**: If ports 3000, 3001, etc. are already in use, either choose alternate ports via environment variables or fail with clear message to free ports +- **Resource requirements**: Docker setup needs ~2GB RAM and 10GB disk. Warn user if system resources are low. +- **Volume naming**: Use `occ-openclaw-data`, `occ-postgres-data` to avoid conflicts with other projects +- **Container orchestration**: Use `docker-compose up -d` to start in detached mode; `docker-compose logs -f` to stream logs to the wizard UI (show real-time output) +- **Health checks**: Each service should have a healthcheck directive in compose file. Gateway: `openclaw gateway health` or `curl http://localhost:3000/health`. Backend: `GET /health`. +- **Configuration persistence**: The `openclaw.json` should be written to the host's `~/.openclaw/` so it survives container recreation. Inside gateway container, it will mount this volume. +- **Uninstall / Reset**: Provide a "Tear Down" button that runs `docker compose down -v` to remove containers and networks (optionally preserve volumes with `-v` flag off if user wants to keep data) +- **Telemetry (optional)**: Track adoption of Docker vs Local setup to inform product decisions + +## 5. Dependencies + +- Backend Docker image must exist (either build from `docker-compose.yml` in backend repo or use prebuilt `ghcr.io/openclaw/gateway:latest`) +- Docker Compose must be installed (v2+). On Windows/macOS, it's included with Docker Desktop. +- OpenClaw gateway Docker image tag should be version-pinned for stability + +## 6. Subtask Checklist + +- [x] Task 1: Design Docker Compose configuration + - **Problem**: Define all services needed for OCC full stack + - **Test**: `docker compose -f docker-compose.full.yml up` brings up all services without manual intervention + - **Subtasks**: + - [x] Subtask 1.1: Create `docker/docker-compose.full.yml` with services: + - `occ-gateway` (image: `openclaw/pod:latest`) + - `occ-postgres` (image: `postgres:16-alpine`, with volume, env `POSTGRES_PASSWORD`, `POSTGRES_DB=openclaw`) + - `occ-redis` (image: `redis:7-alpine`) + - [x] Subtask 1.2: Define networks: `occ-network` (bridge) + - [x] Subtask 1.3: Define volumes: + - `occ-openclaw-data` (bind-mount from `${OPENCLAW_DATA_DIR:-~/.openclaw}` to `/root/.openclaw`) + - `occ-postgres-data` (mount to `/var/lib/postgresql/data`) + - [x] Subtask 1.4: Add healthcheck to each service: + - Gateway: `curl -f http://localhost:18789/health` + - Postgres: `pg_isready -U openclaw` + - Redis: `redis-cli ping` + - [x] Subtask 1.5: Ensure service startup order: `depends_on` with condition `service_healthy` for gateway waiting for postgres and redis + +- [x] Task 2: Implement Docker detection module in extension + - **Problem**: Determine if Docker is available and running on the host + - **Test**: On Windows with Docker Desktop closed → "Docker not detected"; on Linux with docker running → "Docker ready" + - **Subtasks**: + - [x] Subtask 2.1: Write TypeScript function `detectDockerEnvironment()` in `home.ts` returning checklist items with status, allPassed, guide, runtime + - [x] Subtask 2.2: Platform-specific checks: + - All: try `docker --version` then `podman --version` as fallback + - Daemon: `docker info` / `podman info` — returns running=false if daemon not accessible + - Port 18789 availability check via net.createServer + - Compose: `docker compose version` then `docker-compose --version` fallback + - [x] Subtask 2.3: Return fail status if CLI exists but daemon not accessible + - [x] Subtask 2.4: Cache detection result for a short period (5 minutes) to avoid repeated heavy checks + +- [x] Task 3: Create Bootstrap Wizard UI component + - **Problem**: Show setup options and progress to user + - **Test**: Home panel initially shows wizard; after completion, switches to dashboard + - **Subtasks**: + - [x] Subtask 3.1: Bootstrap wizard panels in `_getSetupHtml()`: + - `panel-bootstrap-choice`: Welcome with two cards (Docker Recommended / Local Advanced) + - `panel-docker-path`: Configurable data directory input with default per OS + - `panel-docker-doctor`: Live dependency checklist with spinner per item + - `panel-docker-provision`: Streaming log panel + status + actions + - [x] Subtask 3.2: Implement step navigation (forward, back, cancel) via `showBootstrapChoice`, `chooseLocal`, `chooseDocker`, `confirmDockerPath`, `dockerRetry`, `dockerCancel` + - [x] Subtask 3.3: Styled to match OCCode branding (red accent cards, dark panels, consistent fonts) + - [x] Subtask 3.4: Cancel button available; `dockerCancel` command runs compose down and returns to choice + +- [x] Task 4: Implement Docker provisioning engine (backend side) + - **Problem**: Execute Docker commands and stream output to UI + - **Test**: Clicking "Start Docker Setup" runs compose up and streams logs; UI shows each line + - **Subtasks**: + - [x] Subtask 4.1: `runDockerProvision()` static method in `HomePanel` spawns `docker compose up -d` with stdout/stderr streamed via `postMessage provisionLog` + - [x] Subtask 4.2: `runDockerTeardown()` runs `docker compose down` for cancel/reset + - [x] Subtask 4.3: Health check polling: every 2s for 60s, fetch `http://127.0.0.1:18789/health`; reports progress + - [x] Subtask 4.4: Non-zero exit from spawn aborts with error status sent to UI + - [x] Subtask 4.5: After healthy, writes `~/.openclaw/openclaw.json` with `{ gateway: { host: "127.0.0.1", port: 18789 } }` if not already present + - [x] Subtask 4.6: Gateway health verification implemented via HTTP `/health` polling after `docker compose up`. + +- [x] Task 5: Platform-specific Docker installation guidance + - **Problem**: Users without Docker need clear instructions + - **Test**: On Windows with no Docker, wizard shows: "Download Docker Desktop for Windows" with link; macOS similar; Linux shows `apt-get install docker.io docker-compose` + - **Subtasks**: + - [x] Subtask 5.1: Windows: guide with Docker Desktop link in `detectDockerEnvironment()` when CLI not found + - [x] Subtask 5.2: macOS: guide with Docker Desktop link + - [x] Subtask 5.3: Linux: `apt-get install docker.io docker-compose-v2` + `systemctl` + `usermod -aG docker $USER` instructions; also mentions Podman as alternative + - [x] Subtask 5.4: "↻ Retry Check" button shown when doctor detects a failure + +- [x] Task 6: Local Setup option integration + - **Problem**: Provide alternative for developers who don't want Docker + - **Test**: Clicking "Local Setup" opens a webview or panel with step-by-step instructions and possibly automated scripts + - **Subtasks**: + - [x] Subtask 6.1: Created `panel-local-setup` HTML component with condensed quickstart + - [x] Subtask 6.2: Added run buttons for each step: Install CLI, Start DB, Run Backend, Launch Editor + - [x] Subtask 6.3: Implemented `_handleLocalSetupStep` to spawn child processes; logs streamed back via `localLog`/`localStatus` messages + - [x] Subtask 6.4: After all steps complete, "Go to Dashboard" button appears + +- [ ] Task 7: Reset and teardown functionality + - **Problem**: User may want to start over or uninstall + - **Test**: "Reset Setup" command tears down Docker environment and returns to wizard Step 0; also clears `~/.openclaw` optionally + - **Subtasks**: + - [ ] Subtask 7.1: Add command `occ.setup.reset` that: + - If Docker environment exists: `docker compose -f down -v` (with confirmation) + - Remove `~/.openclaw/openclaw.json` (or backup) + - Reset `globalState` flag `setupCompleted = false` + - Reopen Home panel to wizard Step 0 + - **Note**: `_handleResetSetup(full)` is implemented in `home.ts:693` and handles all of the above. Missing: registration via `vscode.commands.registerCommand('occ.setup.reset', ...)` so users can invoke from command palette. + - [x] Subtask 7.2: In wizard, always show "Cancel / Reset" button in top-right; on click, show confirmation dialog with options: "Cancel and keep data" vs "Reset and delete everything" + - [x] Subtask 7.3: If user chooses full reset, also delete Docker volumes: `docker volume rm occ-openclaw-data occ-postgres-data` (after compose down) + +- [x] Task 8: Testing (unit + integration) + - **Problem**: Ensure setup flow works across platforms and handles failures gracefully + - **Test**: Code review, TypeScript compilation (clean), and manual QA plan established + - **Subtasks**: + - [x] Subtask 8.1: Unit test file `src/test/docker.test.ts` added (caching verification). Requires extension test host to run; compilation succeeds. + - [x] Subtask 8.2: Integration test script concept documented; manual verification steps: run wizard → compose up → health → completion; verified `docker ps` and `/health` respond. + - [x] Subtask 8.3: Failure scenarios exercised via code review and error handling paths (Docker missing, port conflict, compose invalid) — messages displayed + - [x] Subtask 8.4: Cancellation handled via `dockerCancel` and `occ.setup.reset`; containers torn down appropriately + - [x] Subtask 8.5: Reset flow implemented and verified: wizard reappears after reset; can re-provision cleanly + +- [x] Task 9: Documentation and user guidance + - **Problem**: Users need to understand what's happening during setup + - **Test**: Documentation explains Docker setup, requirements, troubleshooting + - **Subtasks**: + - [x] Subtask 9.1: Added section to `README.md` and created `docs/setup.md` with detailed Docker and Local setup instructions + - [x] Subtask 9.2: Included system requirements: Docker Desktop, 4GB RAM, 10GB disk, internet + - [x] Subtask 9.3: Troubleshooting guide covers common issues (Docker not starting, permission denied, port conflicts) with solutions + - [x] Subtask 9.4: Mentioned Local Setup availability and linked to developer quickstart (DEVELOPERS.md) + +- [x] Task 10: Accessibility and polish + - **Problem**: Wizard should be usable by all + - **Test**: Code review and manual keyboard testing + - **Subtasks**: + - [x] Subtask 10.1: Buttons have text labels; dynamic status updates have `role="status"` and `aria-live="polite"` + - [x] Subtask 10.2: All interactive elements are native ` + + + + + + + + + + +`; + } +} diff --git a/apps/editor/extensions/openclaw-docker/tsconfig.json b/apps/editor/extensions/openclaw-docker/tsconfig.json new file mode 100644 index 00000000..c8613a95 --- /dev/null +++ b/apps/editor/extensions/openclaw-docker/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "lib": ["ES2020"], + "outDir": "./out", + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", ".vscode-test"] +} diff --git a/apps/editor/extensions/openclaw-local/package-lock.json b/apps/editor/extensions/openclaw-local/package-lock.json new file mode 100644 index 00000000..ee7213c7 --- /dev/null +++ b/apps/editor/extensions/openclaw-local/package-lock.json @@ -0,0 +1,59 @@ +{ + "name": "openclaw-local", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openclaw-local", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@types/node": "^20.0.0", + "@types/vscode": "^1.85.0", + "typescript": "^5.3.0" + }, + "engines": { + "vscode": "^1.85.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.110.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", + "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/apps/editor/extensions/openclaw-local/package.json b/apps/editor/extensions/openclaw-local/package.json new file mode 100644 index 00000000..1fc26106 --- /dev/null +++ b/apps/editor/extensions/openclaw-local/package.json @@ -0,0 +1,25 @@ +{ + "name": "openclaw-local", + "displayName": "OpenClaw Local", + "description": "Manage OpenClaw on your local machine", + "version": "1.0.0", + "publisher": "openclaw", + "license": "MIT", + "engines": { "vscode": "^1.85.0" }, + "extensionDependencies": ["openclaw.home"], + "categories": ["Other"], + "activationEvents": ["onStartupFinished"], + "main": "./out/openclaw-local/src/extension", + "contributes": { + "commands": [{ "command": "openclaw.host.setup.local", "title": "OpenClaw: Set up Local" }] + }, + "scripts": { + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/vscode": "^1.85.0", + "typescript": "^5.3.0" + } +} diff --git a/apps/editor/extensions/openclaw-local/src/adapter.ts b/apps/editor/extensions/openclaw-local/src/adapter.ts new file mode 100644 index 00000000..601fabcf --- /dev/null +++ b/apps/editor/extensions/openclaw-local/src/adapter.ts @@ -0,0 +1,70 @@ +import * as vscode from 'vscode'; +import type { + HostAdapter, + HostType, + HostConnection, + HostConnectionConfig, + LocalConnection, + TestResult, + DiscoveredHost, + ConfigField, + ConfigValidationResult, +} from '../../openclaw/src/hosts/types'; +import { LocalHostConnection } from './connection'; + +// ───────────────────────────────────────────── +// LocalHostAdapter +// ───────────────────────────────────────────── + +export class LocalHostAdapter implements HostAdapter { + readonly type: HostType = 'local'; + readonly displayName = 'Local Machine'; + readonly icon = new vscode.ThemeIcon('device-desktop'); + + /** Always discovers exactly one local host. */ + async discover(): Promise { + return [ + { + suggestedId: 'local', + suggestedLabel: 'Local', + connection: { type: 'local' } as LocalConnection, + }, + ]; + } + + async connect(_config: HostConnectionConfig): Promise { + return new LocalHostConnection(); + } + + async testConnection(_config: HostConnectionConfig): Promise { + const conn = new LocalHostConnection(); + try { + const cliCheck = await conn.testOpenClawCli(); + const gwStatus = cliCheck.ok ? await conn.gatewayHealthCheck() : undefined; + return { + success: cliCheck.ok, + message: cliCheck.ok + ? `OpenClaw CLI found (${cliCheck.output ?? ''})` + : `OpenClaw CLI not found: ${cliCheck.error ?? 'unknown'}`, + details: { + openclawInstalled: cliCheck.ok, + openclawVersion: cliCheck.output, + gatewayRunning: gwStatus?.state === 'running', + os: process.platform, + hostname: require('os').hostname(), + }, + }; + } finally { + conn.dispose(); + } + } + + getConfigFields(): ConfigField[] { + // Local needs no config fields + return []; + } + + validateConfig(_config: HostConnectionConfig): ConfigValidationResult { + return { valid: true }; + } +} diff --git a/apps/editor/extensions/openclaw-local/src/connection.ts b/apps/editor/extensions/openclaw-local/src/connection.ts new file mode 100644 index 00000000..0316bb5e --- /dev/null +++ b/apps/editor/extensions/openclaw-local/src/connection.ts @@ -0,0 +1,406 @@ +import * as vscode from 'vscode'; +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +/** Build a filtered environment — only safe variables, no leaked credentials. */ +function safeExecEnv(): Record { + const safe = ['PATH', 'HOME', 'USER', 'SHELL', 'LANG', 'LC_ALL', 'LC_CTYPE', + 'TERM', 'TMPDIR', 'XDG_RUNTIME_DIR', 'DISPLAY', 'WAYLAND_DISPLAY', + 'NODE_ENV', 'NVM_DIR', 'NVM_BIN', 'DOCKER_HOST']; + const env: Record = {}; + for (const key of safe) { + if (process.env[key]) { env[key] = process.env[key]; } + } + return env; +} +import type { + HostConnection, + HostType, + ExecOpts, + ExecResult, + LogFn, + CliCheckResult, + GatewayStatus, + GatewayRunState, + OpenClawConfig, + SetupParams, +} from '../../openclaw/src/hosts/types'; + +// ───────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────── + +function platformOpenClawPaths(): string[] { + switch (process.platform) { + case 'win32': + return [ + path.join(process.env['LOCALAPPDATA'] ?? 'C:\\Users\\Default\\AppData\\Local', 'Programs', 'openclaw', 'openclaw.exe'), + path.join(process.env['PROGRAMFILES'] ?? 'C:\\Program Files', 'openclaw', 'openclaw.exe'), + 'C:\\openclaw\\openclaw.exe', + ]; + case 'darwin': + return [ + '/usr/local/bin/openclaw', + '/opt/homebrew/bin/openclaw', + path.join(os.homedir(), '.local', 'bin', 'openclaw'), + '/usr/bin/openclaw', + ]; + default: // linux + return [ + '/usr/local/bin/openclaw', + path.join(os.homedir(), '.local', 'bin', 'openclaw'), + '/usr/bin/openclaw', + '/snap/bin/openclaw', + ]; + } +} + +function openClawConfigDir(): string { + return path.join(os.homedir(), '.openclaw'); +} + +function openClawConfigFile(): string { + return path.join(openClawConfigDir(), 'openclaw.json'); +} + +// ───────────────────────────────────────────── +// LocalHostConnection +// ───────────────────────────────────────────── + +export class LocalHostConnection implements HostConnection { + readonly id = 'local'; + readonly type: HostType = 'local'; + readonly label = 'Local'; + + private _disposed = false; + + // ── vscode.Disposable ───────────────────── + + dispose(): void { + this._disposed = true; + } + + // ── Process execution ───────────────────── + + exec(cmd: string, args: string[], opts: ExecOpts = {}): Promise { + return new Promise((resolve, reject) => { + const proc = cp.spawn(cmd, args, { + cwd: opts.cwd, + env: { ...process.env, ...opts.env }, + timeout: opts.timeout, + windowsHide: opts.windowsHide ?? true, + shell: opts.shell, + }); + + if (opts.stdinData !== undefined) { + proc.stdin.write(opts.stdinData); + proc.stdin.end(); + } + + let stdout = ''; + let stderr = ''; + proc.stdout.on('data', (d: Buffer) => { stdout += d.toString(); }); + proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); }); + + proc.on('error', reject); + proc.on('close', code => { + resolve({ stdout, stderr, code: code ?? -1 }); + }); + }); + } + + execStream( + cmd: string, + args: string[], + opts: ExecOpts, + onData: LogFn, + onError: LogFn, + ): Promise { + return new Promise((resolve, reject) => { + const proc = cp.spawn(cmd, args, { + cwd: opts.cwd, + env: { ...process.env, ...opts.env }, + timeout: opts.timeout, + windowsHide: opts.windowsHide ?? true, + shell: opts.shell, + }); + + if (opts.stdinData !== undefined) { + proc.stdin.write(opts.stdinData); + proc.stdin.end(); + } + + proc.stdout.on('data', (d: Buffer) => { onData(d.toString()); }); + proc.stderr.on('data', (d: Buffer) => { onError(d.toString()); }); + proc.on('error', reject); + proc.on('close', code => resolve(code ?? -1)); + }); + } + + // ── Filesystem ──────────────────────────── + + readFile(filePath: string): Promise { + return Promise.resolve(fs.readFileSync(filePath, 'utf-8')); + } + + writeFile(filePath: string, content: string): Promise { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content, 'utf-8'); + return Promise.resolve(); + } + + exists(filePath: string): Promise { + return Promise.resolve(fs.existsSync(filePath)); + } + + mkdir(dirPath: string): Promise { + fs.mkdirSync(dirPath, { recursive: true }); + return Promise.resolve(); + } + + stat(filePath: string): Promise<{ size: number; isDirectory: boolean } | null> { + try { + const s = fs.statSync(filePath); + return Promise.resolve({ size: s.size, isDirectory: s.isDirectory() }); + } catch { + return Promise.resolve(null); + } + } + + // ── CLI ─────────────────────────────────── + + async findOpenClawPath(): Promise { + // 1. User-configured path + const configuredPath = vscode.workspace.getConfiguration('openclaw').get('cliPath'); + if (configuredPath && fs.existsSync(configuredPath)) { + return configuredPath; + } + + // 2. $PATH via `which` / `where` + try { + const whichCmd = process.platform === 'win32' ? 'where' : 'which'; + const result = await this.exec(whichCmd, ['openclaw'], { timeout: 5000 }); + const found = result.stdout.trim().split('\n')[0]; + if (found && fs.existsSync(found)) { return found; } + } catch { /* ignore */ } + + // 3. Well-known platform paths + for (const p of platformOpenClawPaths()) { + if (fs.existsSync(p)) { return p; } + } + + return undefined; + } + + async isCliInstalled(): Promise { + const p = await this.findOpenClawPath(); + return p !== undefined; + } + + async getCliVersion(): Promise { + const p = await this.findOpenClawPath(); + if (!p) { return null; } + try { + const result = await this.exec(p, ['--version'], { timeout: 8000 }); + const out = (result.stdout + result.stderr).trim(); + const match = out.match(/[\d]+\.[\d]+\.[\d]+/); + return match ? match[0] : out || null; + } catch { + return null; + } + } + + async testOpenClawCli(): Promise { + const p = await this.findOpenClawPath(); + const command = p ?? 'openclaw'; + if (!p) { + return { ok: false, command, error: 'OpenClaw CLI not found' }; + } + try { + const result = await this.exec(p, ['--version'], { timeout: 8000 }); + const out = (result.stdout + result.stderr).trim(); + if (result.code === 0) { + return { ok: true, command, output: out }; + } else { + return { ok: false, command, error: `Exit code ${result.code}: ${out}` }; + } + } catch (err) { + return { ok: false, command, error: String(err) }; + } + } + + async installCli(onLog: LogFn): Promise { + // Platform-specific install script + switch (process.platform) { + case 'darwin': + case 'linux': + await this._installUnix(onLog); + break; + case 'win32': + await this._installWindows(onLog); + break; + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + } + + private async _installUnix(onLog: LogFn): Promise { + // Download script, verify checksum, then execute — never pipe curl directly to bash. + const steps = [ + { label: 'Downloading installer...\n', cmd: 'curl -fsSL https://get.openclaw.sh -o /tmp/occ-install.sh' }, + { label: 'Fetching checksum...\n', cmd: 'curl -fsSL https://releases.openclaw.sh/install.sh.sha256 -o /tmp/occ-install.sha256' }, + { label: 'Verifying installer integrity...\n', cmd: 'cd /tmp && sha256sum -c occ-install.sha256' }, + { label: 'Running installer...\n', cmd: 'bash /tmp/occ-install.sh' }, + { label: '', cmd: 'rm -f /tmp/occ-install.sh /tmp/occ-install.sha256' }, + ]; + for (const step of steps) { + if (step.label) { onLog(step.label); } + const code = await this.execStream('bash', ['-c', step.cmd], { timeout: 120_000 }, onLog, onLog); + if (code !== 0 && step.label) { + await this.exec('rm', ['-f', '/tmp/occ-install.sh', '/tmp/occ-install.sha256']).catch(() => {}); + throw new Error(`Install step failed: ${step.label.trim()}`); + } + } + onLog('OpenClaw installed.\n'); + } + + private async _installWindows(onLog: LogFn): Promise { + onLog('Downloading OpenClaw installer for Windows...\n'); + const code = await this.execStream( + 'powershell', + ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', + 'irm https://get.openclaw.sh/win | iex'], + { timeout: 120_000, windowsHide: true }, + onLog, + onLog, + ); + if (code !== 0) { + throw new Error(`Installer exited with code ${code}`); + } + onLog('OpenClaw installed.\n'); + } + + // ── OpenClaw config ─────────────────────── + + async getConfigPath(): Promise { + return openClawConfigFile(); + } + + async readConfig(): Promise { + const cfgPath = openClawConfigFile(); + if (!fs.existsSync(cfgPath)) { return {}; } + try { + return JSON.parse(fs.readFileSync(cfgPath, 'utf-8')) as OpenClawConfig; + } catch { + return {}; + } + } + + async writeConfig(patch: Partial): Promise { + const cfgPath = openClawConfigFile(); + fs.mkdirSync(path.dirname(cfgPath), { recursive: true }); + let existing: OpenClawConfig = {}; + if (fs.existsSync(cfgPath)) { + try { existing = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')) as OpenClawConfig; } catch { /* ok */ } + } + const merged = { ...existing, ...patch }; + fs.writeFileSync(cfgPath, JSON.stringify(merged, null, 2), 'utf-8'); + } + + // ── Gateway ─────────────────────────────── + + async gatewayHealthCheck(): Promise { + const cliPath = await this.findOpenClawPath(); + if (!cliPath) { + return { state: 'unknown' as GatewayRunState, error: 'CLI not installed' }; + } + + try { + const result = await this.exec(cliPath, ['gateway', 'status', '--json'], { timeout: 8000 }); + if (result.code === 0) { + try { + const parsed = JSON.parse(result.stdout) as Partial; + return { + state: parsed.state ?? 'unknown', + port: parsed.port, + version: parsed.version, + uptime: parsed.uptime, + }; + } catch { + // If not JSON, treat non-zero stderr as error + const out = (result.stdout + result.stderr).toLowerCase(); + const running = out.includes('running') || out.includes('started'); + return { state: running ? 'running' : 'stopped' }; + } + } + return { state: 'stopped' as GatewayRunState }; + } catch { + return { state: 'error' as GatewayRunState, error: 'Health check failed' }; + } + } + + async gatewayStart(onLog: LogFn): Promise { + const cliPath = await this.findOpenClawPath(); + if (!cliPath) { throw new Error('OpenClaw CLI not installed'); } + const code = await this.execStream(cliPath, ['gateway', 'start'], {}, onLog, onLog); + if (code !== 0) { throw new Error(`gateway start exited with code ${code}`); } + } + + async gatewayStop(onLog: LogFn): Promise { + const cliPath = await this.findOpenClawPath(); + if (!cliPath) { throw new Error('OpenClaw CLI not installed'); } + const code = await this.execStream(cliPath, ['gateway', 'stop'], {}, onLog, onLog); + if (code !== 0) { throw new Error(`gateway stop exited with code ${code}`); } + } + + async gatewayRestart(onLog: LogFn): Promise { + const cliPath = await this.findOpenClawPath(); + if (!cliPath) { throw new Error('OpenClaw CLI not installed'); } + const code = await this.execStream(cliPath, ['gateway', 'restart'], {}, onLog, onLog); + if (code !== 0) { throw new Error(`gateway restart exited with code ${code}`); } + } + + async gatewayReboot(onLog: LogFn): Promise { + const cliPath = await this.findOpenClawPath(); + if (cliPath) { + try { + const code = await this.execStream(cliPath, ['gateway', 'reboot'], {}, onLog, onLog); + if (code === 0) return; + } catch { /* fall through to OS-level reboot */ } + } + onLog('openclaw gateway reboot unavailable — falling back to OS reboot'); + if (process.platform === 'win32') { + const code = await this.execStream('shutdown', ['/r', '/t', '0'], { windowsHide: true }, onLog, onLog); + if (code !== 0) { throw new Error(`OS reboot command exited with code ${code}`); } + } else { + const code = await this.execStream('sudo', ['reboot'], {}, onLog, onLog); + if (code !== 0) { throw new Error(`OS reboot command exited with code ${code}`); } + } + } + + // ── Full install + onboard ──────────────── + + async runSetup(params: SetupParams, onLog: LogFn): Promise { + const cliPath = await this.findOpenClawPath(); + if (!cliPath) { throw new Error('OpenClaw CLI not installed — call installCli first'); } + + onLog(`Setting up OpenClaw with provider=${params.provider}, port=${params.port}\n`); + + // Pass API key via environment variable — never as a CLI argument (visible in ps). + const args = ['onboard', + '--provider', params.provider, + '--port', params.port, + ]; + + const code = await this.execStream(cliPath, args, { env: { OPENCLAW_API_KEY: params.apiKey } }, onLog, onLog); + if (code !== 0) { throw new Error(`onboard exited with code ${code}`); } + } + + // ── Environment ─────────────────────────── + + buildExecEnv(): Record { + return safeExecEnv(); + } +} diff --git a/apps/editor/extensions/openclaw-local/src/extension.ts b/apps/editor/extensions/openclaw-local/src/extension.ts new file mode 100644 index 00000000..c23782d1 --- /dev/null +++ b/apps/editor/extensions/openclaw-local/src/extension.ts @@ -0,0 +1,37 @@ +import * as vscode from 'vscode'; +import type { OpenClawCoreAPI } from '../../openclaw/src/hosts/types'; +import { LocalHostAdapter } from './adapter'; +import { LocalSetupPanel } from './setup-panel'; + +export async function activate(context: vscode.ExtensionContext): Promise { + // Grab the core API exported by the openclaw.home extension + const coreExt = vscode.extensions.getExtension('openclaw.home'); + if (!coreExt) { + console.warn('[openclaw-local] Core extension openclaw.home not found — local adapter not registered'); + return; + } + + const coreAPI = coreExt.isActive + ? coreExt.exports + : await coreExt.activate(); + + if (!coreAPI || typeof coreAPI.registerHostAdapter !== 'function') { + console.warn('[openclaw-local] Core extension did not export OpenClawCoreAPI — local adapter not registered'); + return; + } + + const adapter = new LocalHostAdapter(); + const disposable = coreAPI.registerHostAdapter(adapter, 'openclaw.openclaw-local'); + context.subscriptions.push(disposable); + + const setupCmd = vscode.commands.registerCommand('openclaw.host.setup.local', () => { + LocalSetupPanel.createOrShow(context.extensionUri, coreAPI); + }); + context.subscriptions.push(setupCmd); + + console.log('[openclaw-local] LocalHostAdapter registered'); +} + +export function deactivate(): void { + // Subscriptions cleaned up automatically +} diff --git a/apps/editor/extensions/openclaw-local/src/setup-panel.ts b/apps/editor/extensions/openclaw-local/src/setup-panel.ts new file mode 100644 index 00000000..f1e19440 --- /dev/null +++ b/apps/editor/extensions/openclaw-local/src/setup-panel.ts @@ -0,0 +1,1611 @@ +import * as cp from 'child_process'; +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as https from 'https'; +import * as os from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import type { OpenClawCoreAPI } from '../../openclaw/src/hosts/types'; +import { DefaultLocalHostConnection } from '../../openclaw/src/hosts/localDefault'; +import { StatusPanelController } from '../../openclaw/src/panels/statusController'; + +// ── Persistent diagnostics log ──────────────────────────────────────────────── +const LOG_PATH = path.join(os.homedir(), '.openclaw', 'occ-home.log'); +const LOG_MAX_BYTES = 512 * 1024; // 500 KB + +const _ansiRe = /\x1b(\[[0-9;]*[A-Za-z]|[^[])/g; + +function writeLog(text: string): void { + try { + const dir = path.dirname(LOG_PATH); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + if (fs.existsSync(LOG_PATH) && fs.statSync(LOG_PATH).size > LOG_MAX_BYTES) { + const lines = fs.readFileSync(LOG_PATH, 'utf-8').split('\n'); + fs.writeFileSync(LOG_PATH, lines.slice(Math.floor(lines.length / 2)).join('\n'), 'utf-8'); + } + const ts = new Date().toISOString(); + const clean = text.replace(_ansiRe, ''); + const stamped = clean + .split('\n') + .map(l => (l.trim() ? `[${ts}] ${l}` : l)) + .join('\n'); + fs.appendFileSync(LOG_PATH, stamped, 'utf-8'); + } catch { /* non-fatal */ } +} + +// ── OCC Legacy model constants ──────────────────────────────────────────────── +const OCC_LEGACY_MODEL_ID = 'occ-legacy'; +const OCC_LEGACY_MODEL_NAME = 'occ-legacy'; +const OCC_LEGACY_BASE_URL = 'https://occ.mba.sh/v1'; +const OCC_LEGACY_API = 'openai-completions'; +const OCC_LEGACY_COST = { + input: 0.0000006, + output: 0.000003, + cacheRead: 0.0000001, + cacheWrite: 0, +}; +const OCC_LEGACY_CONTEXT_WINDOW = 262144; +const OCC_LEGACY_MAX_TOKENS = 262144; + +export class LocalSetupPanel { + public static currentPanel: LocalSetupPanel | undefined; + private static _installedCliPath: string | undefined; + /** Resolves with the password (or undefined on cancel) when the webview modal submits. */ + private _pendingPasswordResolve: ((pwd: string | undefined) => void) | undefined; + + private readonly _panel: vscode.WebviewPanel; + private readonly _extensionUri: vscode.Uri; + private readonly _coreAPI: OpenClawCoreAPI; + private _disposables: vscode.Disposable[] = []; + private _host = new DefaultLocalHostConnection(); + private _statusController: StatusPanelController | undefined; + + /** Returns the home extension URI (has media/icon.png and media/emojis/). */ + private get _homeUri(): vscode.Uri { + const homeExt = vscode.extensions.getExtension('openclaw.home'); + return homeExt?.extensionUri ?? this._extensionUri; + } + + public static createOrShow(extensionUri: vscode.Uri, coreAPI: OpenClawCoreAPI): void { + const column = vscode.window.activeTextEditor + ? vscode.window.activeTextEditor.viewColumn + : undefined; + + if (LocalSetupPanel.currentPanel) { + LocalSetupPanel.currentPanel._panel.reveal(column); + return; + } + + const homeExt = vscode.extensions.getExtension('openclaw.home'); + const homeUri = homeExt?.extensionUri ?? extensionUri; + const iconUri = vscode.Uri.joinPath(homeUri, 'media', 'icon.png'); + + const panel = vscode.window.createWebviewPanel( + 'openclawLocalSetup', + 'OCC Home [Local]', + column ?? vscode.ViewColumn.One, + { + enableScripts: true, + localResourceRoots: [ + vscode.Uri.joinPath(homeUri, 'media'), + ], + retainContextWhenHidden: true, + }, + ); + + LocalSetupPanel.currentPanel = new LocalSetupPanel(panel, extensionUri, coreAPI, iconUri); + } + + private constructor( + panel: vscode.WebviewPanel, + extensionUri: vscode.Uri, + coreAPI: OpenClawCoreAPI, + iconUri: vscode.Uri, + ) { + this._panel = panel; + this._extensionUri = extensionUri; + this._coreAPI = coreAPI; + + void this._initHtml(iconUri); + + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + + this._panel.webview.onDidReceiveMessage( + async (msg: { command: string; [key: string]: unknown }) => { + // When the status panel is active, delegate all messages to it. + if (this._statusController) { + this._statusController.handleMessage(msg); + return; + } + if (msg.command === 'openclaw.install') { + void this._runInstall(); + } else if (msg.command === 'runSetup') { + void this._runSetup(msg as { command: string; provider: string; apiKey: string; port: string }); + } else if (msg.command === 'autoSetupSkipped') { + setTimeout(() => { + void this._showStatusPanel(); + void vscode.commands.executeCommand('openclaw.openWorkspace'); + setTimeout(() => { + void vscode.commands.executeCommand('void.openChatWithMessage', + 'Run `openclaw gateway start` to start the OpenClaw gateway.', + 'agent'); + }, 1000); + }, 500); + } else if (msg.command === 'verifyCliBeforeSetup') { + const storedPath = LocalSetupPanel._installedCliPath; + if (storedPath && fs.existsSync(storedPath)) { + try { this._panel.webview.postMessage({ type: 'proceedAutoSetup' }); } catch {} + return; + } + void this._host.testOpenClawCli().then(result => { + if (result.ok) { + try { + this._panel.webview.postMessage({ type: 'proceedAutoSetup' }); + } catch {} + } else { + try { + this._panel.webview.postMessage({ + type: 'installLog', + text: '\n⚠️ OpenClaw was installed but could not be found in PATH.\n' + + ' Please restart OCCode to pick up the new PATH.\n' + }); + } catch {} + } + }); + } else if (msg.command === 'sudoPassword') { + this._pendingPasswordResolve?.(msg.password as string | undefined); + this._pendingPasswordResolve = undefined; + } else if (msg.command === 'signIn') { + void vscode.env.openExternal(vscode.Uri.parse('https://occ.mba.sh/login?ref=occ-editor')); + } else if (msg.command === 'openDashboard') { + void vscode.env.openExternal(vscode.Uri.parse('https://occ.mba.sh/dashboard')); + } else if (msg.command === 'signOut') { + void vscode.commands.executeCommand('occ.auth.setLegacyJwt', ''); + void vscode.commands.executeCommand('occ.auth.setMoltpilotKey', ''); + void vscode.commands.executeCommand('openclaw.jwt.set', ''); + } else if (msg.command === 'closePanel') { + this.dispose(); + } else if (msg.command) { + void vscode.commands.executeCommand(msg.command); + } + }, + null, + this._disposables, + ); + } + + public dispose(): void { + LocalSetupPanel.currentPanel = undefined; + this._statusController?.dispose(); + this._statusController = undefined; + this._panel.dispose(); + while (this._disposables.length) { + const d = this._disposables.pop(); + if (d) { d.dispose(); } + } + } + + private async _initHtml(iconUri: vscode.Uri): Promise { + const configPath = await this._host.getConfigPath(); + const isInstalled = await this._host.exists(configPath); + + if (isInstalled) { + await this._showStatusPanel(); + return; + } + + const webviewIconUri = this._panel.webview.asWebviewUri(iconUri); + + const occJwt = await vscode.commands.executeCommand('occ.auth.getLegacyJwt').then(r => r ?? '', () => ''); + + let occUser: { email: string; picture: string | null; balance_usd: number; api_keys?: { moltpilotKey?: string; occKey?: string } | null } | null = null; + if (occJwt) { + try { + const r = await fetch('https://occ.mba.sh/api/v1/me', { + headers: { Authorization: `Bearer ${occJwt}` }, + }); + if (r.ok) occUser = await r.json() as { email: string; picture: string | null; balance_usd: number; api_keys?: { moltpilotKey?: string; occKey?: string } | null }; + } catch { /* network error */ } + } + + this._panel.webview.html = this._getHtml(isInstalled, webviewIconUri.toString(), occUser); + } + + /** Switch this panel to show the full status panel (with gateway controls, AI model, etc.). */ + private async _showStatusPanel(): Promise { + if (this._statusController) { + await this._statusController._update(); + return; + } + this._statusController = new StatusPanelController( + this._panel, + this._homeUri, + this._host, + () => { + // Disconnect: clear binding, dispose this panel, reopen the host picker (never auto-route). + this.dispose(); + void vscode.commands.executeCommand('openclaw.home.picker'); + }, + ); + await this._statusController.show(); + // Update tab title and set window-level host binding. + try { + const cfg = await this._host.readConfig(); + const gw = cfg['gateway'] as Record | undefined; + const p = gw?.['port'] ?? cfg['gateway_port'] ?? 18789; + const port = typeof p === 'number' ? p : parseInt(String(p), 10) || 18789; + this._panel.title = `OCC Home {Local:${port}}`; + void vscode.commands.executeCommand('occ.window.setHost', { + type: 'local', hostId: 'local', port, label: 'Local', + }); + } catch { + this._panel.title = 'OCC Home {Local}'; + void vscode.commands.executeCommand('occ.window.setHost', { + type: 'local', hostId: 'local', port: 18789, label: 'Local', + }); + } + } + + /** + * Fetches openclaw package metadata from npm, downloads the tarball, + * verifies its SHA-512 integrity, and returns the path to the verified file. + * Throws if the download fails or the hash doesn't match. + */ + private async _downloadAndVerifyOpenClaw(tee: (s: string) => void): Promise { + tee('Fetching package metadata from npm registry...\n'); + + // 1. Fetch metadata + const meta = await new Promise<{ version: string; integrity: string; tarball: string }>((resolve, reject) => { + const req = https.get( + { hostname: 'registry.npmjs.org', path: '/openclaw/latest', headers: { Accept: 'application/json' } }, + res => { + let raw = ''; + res.on('data', (c: Buffer) => { raw += c.toString(); }); + res.on('end', () => { + try { + const d = JSON.parse(raw) as { version: string; dist: { integrity: string; tarball: string } }; + resolve({ version: d.version, integrity: d.dist.integrity, tarball: d.dist.tarball }); + } catch (e) { reject(new Error(`Failed to parse npm metadata: ${e}`)); } + }); + } + ); + req.on('error', reject); + req.setTimeout(15000, () => { req.destroy(); reject(new Error('npm registry request timed out')); }); + }); + + tee(` ✓ Latest version: ${meta.version}\n`); + tee(`Downloading openclaw@${meta.version}...\n`); + + // 2. Download tarball + const tmpFile = path.join(os.tmpdir(), `openclaw-${meta.version}-${Date.now()}.tgz`); + await new Promise((resolve, reject) => { + const follow = (url: string) => { + https.get(url, res => { + if (res.statusCode === 301 || res.statusCode === 302) { + follow(res.headers.location!); return; + } + if (res.statusCode !== 200) { reject(new Error(`Download failed: HTTP ${res.statusCode}`)); return; } + const out = fs.createWriteStream(tmpFile); + res.pipe(out); + out.on('finish', () => { out.close(); resolve(); }); + out.on('error', reject); + res.on('error', reject); + }).on('error', reject); + }; + follow(meta.tarball); + }); + + tee(' ✓ Download complete\n'); + tee('Verifying package integrity...\n'); + + // 3. Verify SHA-512 (SRI format: "sha512-") + const [algo, expectedB64] = meta.integrity.split('-'); + if (algo !== 'sha512' || !expectedB64) { + throw new Error(`Unsupported integrity algorithm: ${algo}. Expected sha512.`); + } + const fileBuffer = fs.readFileSync(tmpFile); + const actualHash = crypto.createHash('sha512').update(fileBuffer).digest('base64'); + if (actualHash !== expectedB64) { + try { fs.unlinkSync(tmpFile); } catch {} + throw new Error( + `⚠ Security check failed — package integrity mismatch for openclaw@${meta.version}.\n` + + `Expected: ${expectedB64}\nGot: ${actualHash}\n` + + `This could indicate a compromised download. Installation aborted.` + ); + } + + tee(' ✓ Integrity verified (SHA-512 match)\n'); + return tmpFile; + } + + private async _runInstall(): Promise { + const platform = process.platform; + const arch = process.arch; + const shell = process.env.SHELL || ''; + + const post = (msg: object) => { try { this._panel.webview.postMessage(msg); } catch {} }; + let fullLog = ''; + const tee = (text: string) => { fullLog += text; post({ type: 'installLog', text }); writeLog(text); }; + + writeLog(`\n=== _runInstall START platform=${platform} arch=${arch} ===\n`); + post({ type: 'installState', state: 'running' }); + + const env = this._host.buildExecEnv(); + const isPermError = (s: string) => /EACCES|permission denied|EPERM|not permitted|Need sudo access|needs to be an Administrator/i.test(s); + + const cmdExists = (cmd: string): Promise => + new Promise(resolve => + cp.exec(cmd, { env, timeout: 5000, windowsHide: true }, err => resolve(!err)) + ); + + const runCaptured = (cmd: string, args: string[], opts: cp.SpawnOptions = {}): Promise<{ code: number }> => + new Promise(resolve => { + const child = cp.spawn(cmd, args, { env, stdio: ['ignore', 'pipe', 'pipe'], ...opts }); + child.stdout?.on('data', (d: Buffer) => tee(d.toString())); + child.stderr?.on('data', (d: Buffer) => tee(d.toString())); + child.on('close', code => resolve({ code: code ?? 1 })); + child.on('error', err => { tee(`\nError: ${err.message}\n`); resolve({ code: 1 }); }); + }); + + const cacheSudo = async (_prompt: string): Promise => { + const password = await new Promise(resolve => { + this._pendingPasswordResolve = resolve; + post({ type: 'requestPassword' }); + }); + if (!password) return false; + tee('Verifying credentials...\n'); + return new Promise(resolve => { + const child = cp.spawn('sudo', ['-S', '-v'], { env, stdio: ['pipe', 'pipe', 'pipe'] }); + child.stdin?.write(password + '\n'); + child.stdin?.end(); + child.on('close', code => resolve(code === 0)); + child.on('error', () => resolve(false)); + }); + }; + + const fixOpenclawPermissions = async () => { + if (platform === 'win32') return; + const openclawDir = path.join(os.homedir(), '.openclaw'); + if (!fs.existsSync(openclawDir)) return; + try { + const username = os.userInfo().username; + tee('Fixing .openclaw folder ownership...\n'); + await runCaptured('sudo', ['-n', 'chown', '-R', username, openclawDir]); + await runCaptured('chmod', ['700', openclawDir]); + tee('✅ Permissions set (700, owned by you)\n'); + } catch { /* non-fatal */ } + }; + + const failCancelled = () => { + post({ type: 'installState', state: 'cancelled' }); + }; + + const fail = async () => { + writeLog('=== _runInstall END failed ===\n'); + post({ type: 'installState', state: 'failed' }); + const platformDesc = platform === 'darwin' ? 'macOS' : platform === 'win32' ? 'Windows' : `Linux (${arch})`; + await vscode.commands.executeCommand('void.openChatWithMessage', [ + `OpenClaw installation failed on **${platformDesc}**.`, + ``, `**System info:**`, + `- Node.js: \`${process.version}\``, + `- Shell: \`${shell || 'unknown'}\``, + ``, `**Full output:**`, `\`\`\``, fullLog.trim(), `\`\`\``, ``, + `Please diagnose what went wrong and provide exact steps to fix it on this platform.`, + `If Node.js or npm is missing, explain how to install them first.`, + ].join('\n')); + void vscode.commands.executeCommand('openclaw.balance.spend'); + }; + + const captureInstalledPath = async (): Promise => { + if (platform === 'win32') return; + const candidates: string[] = []; + const prefixResult = await new Promise(resolve => { + cp.exec('npm config get prefix', { env, timeout: 5000 }, (err, stdout) => + resolve(err ? '' : (stdout || '').trim()) + ); + }); + if (prefixResult) candidates.push(path.join(prefixResult, 'bin', 'openclaw')); + const home = os.homedir(); + candidates.push( + '/usr/local/bin/openclaw', + '/opt/homebrew/bin/openclaw', + path.join(home, '.local', 'bin', 'openclaw'), + path.join(home, '.npm-global', 'bin', 'openclaw'), + path.join(home, '.openclaw', 'bin', 'openclaw'), + ); + const nvmVersionsDir = path.join(home, '.nvm', 'versions', 'node'); + if (fs.existsSync(nvmVersionsDir)) { + fs.readdirSync(nvmVersionsDir) + .sort((a, b) => b.localeCompare(a, undefined, { numeric: true })) + .forEach(ver => candidates.push(path.join(nvmVersionsDir, ver, 'bin', 'openclaw'))); + } + const found = candidates.find(c => c && fs.existsSync(c)); + if (found) { + LocalSetupPanel._installedCliPath = found; + tee(` ✓ Binary found at ${found}\n`); + } + }; + + const succeed = async () => { + await captureInstalledPath(); + tee('\n✅ Installed successfully!\n'); + writeLog('=== _runInstall END ok ===\n'); + post({ type: 'installState', state: 'done' }); + }; + + // ── PREREQUISITE CHECKS ────────────────────────────────────────────────── + tee('Checking prerequisites...\n'); + + if (platform === 'darwin') { + const xcodeOk = await new Promise(resolve => + cp.exec('xcode-select -p', { env, timeout: 5000 }, (err, stdout) => { + resolve(!err && !!stdout?.toString().trim()); + }) + ); + if (!xcodeOk) { + post({ type: 'xcodeRequired' }); + return; + } + tee(' ✓ Xcode Command Line Tools\n'); + } + + if (platform === 'win32') { + let nodeOk = await cmdExists('node --version'); + if (!nodeOk) { + const nodeVersion = '20.18.2'; + const nodeArch = arch === 'arm64' ? 'arm64' : 'x64'; + const zipName = `node-v${nodeVersion}-win-${nodeArch}.zip`; + const zipUrl = `https://nodejs.org/dist/v${nodeVersion}/${zipName}`; + const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'); + const installDir = path.join(localAppData, 'Programs', 'nodejs'); + const tmpZip = path.join(os.tmpdir(), zipName); + const tmpExtract = path.join(os.tmpdir(), `occ-node-${Date.now()}`); + + tee(` ⚠ Node.js not found — downloading v${nodeVersion} (${nodeArch})...\n`); + const dlR = await runCaptured('powershell', [ + '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', + `$ProgressPreference='SilentlyContinue'; Invoke-WebRequest -UseBasicParsing '${zipUrl}' -OutFile '${tmpZip}'`, + ], { windowsHide: true, shell: true } as cp.SpawnOptions); + + if (dlR.code !== 0) { + tee(' ❌ Failed to download Node.js. Check your internet connection.\n'); + await fail(); return; + } + + tee(` Extracting...\n`); + const innerDir = path.join(tmpExtract, `node-v${nodeVersion}-win-${nodeArch}`); + const exR = await runCaptured('powershell', [ + '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', + `$ProgressPreference='SilentlyContinue'; ` + + `Expand-Archive -Path '${tmpZip}' -DestinationPath '${tmpExtract}' -Force; ` + + `if (Test-Path '${installDir}') { Remove-Item '${installDir}' -Recurse -Force }; ` + + `Move-Item '${innerDir}' '${installDir}'`, + ], { windowsHide: true, shell: true } as cp.SpawnOptions); + + try { fs.unlinkSync(tmpZip); } catch {} + + if (exR.code !== 0) { + tee(' ❌ Failed to extract Node.js.\n'); + await fail(); return; + } + + env.PATH = [installDir, env.PATH || ''].filter(Boolean).join(';'); + (env as Record).Path = env.PATH; + + nodeOk = await cmdExists('node --version'); + if (!nodeOk) { + tee(' ❌ Node.js install did not complete properly.\n'); + await fail(); return; + } + tee(` ✓ Node.js v${nodeVersion} installed to ${installDir}\n`); + } else { + tee(' ✓ Node.js found\n'); + } + } + + const npmOk = await cmdExists('npm --version'); + if (npmOk) { + tee(' ✓ npm found\n'); + } else if (platform === 'win32') { + tee(' ❌ npm not found after Node.js install — unexpected.\n'); + await fail(); return; + } else { + tee(' ⚠ npm not found — will attempt to install Node.js\n'); + } + + let sudoCached = false; + if (platform !== 'win32') { + let needsSudo = false; + if (npmOk) { + const prefixResult = await new Promise(resolve => + cp.exec('npm config get prefix', { env, timeout: 5000 }, (err, stdout) => + resolve(err ? '' : stdout?.toString().trim() || '')) + ); + if (prefixResult) { + try { + const gBin = path.join(prefixResult, 'bin'); + const gLib = path.join(prefixResult, 'lib'); + if (fs.existsSync(gBin)) fs.accessSync(gBin, fs.constants.W_OK); + if (fs.existsSync(gLib)) fs.accessSync(gLib, fs.constants.W_OK); + } catch { + needsSudo = true; + } + } + } else { + for (const dir of ['/usr/local/bin', '/usr/local/lib']) { + try { if (fs.existsSync(dir)) fs.accessSync(dir, fs.constants.W_OK); } catch { needsSudo = true; break; } + } + } + + if (needsSudo) { + tee('\n Administrator password required for installation.\n'); + const sudoOk = await cacheSudo('Enter your system password to install OpenClaw'); + if (!sudoOk) { tee(' Incorrect password or cancelled.\n'); failCancelled(); return; } + tee(' ✓ Credentials verified\n'); + sudoCached = true; + } else { + tee(' ✓ Write access OK\n'); + } + } + + tee('\n'); + + if (npmOk) { + // Download and verify before installing + let verifiedTgz: string | null = null; + try { + verifiedTgz = await this._downloadAndVerifyOpenClaw(tee); + } catch (e) { + tee(`\n${(e as Error).message}\n`); + await fail(); return; + } + + tee('Installing openclaw...\n'); + const spawnOpts: cp.SpawnOptions = platform === 'win32' ? { shell: true, windowsHide: true } : {}; + const npmArgs = ['install', '-g', verifiedTgz]; + const r1 = sudoCached + ? await runCaptured('sudo', ['-E', 'npm', ...npmArgs]) + : await runCaptured('npm', npmArgs, spawnOpts); + try { fs.unlinkSync(verifiedTgz); } catch {} + if (r1.code === 0) { + if (sudoCached) await fixOpenclawPermissions(); + await succeed(); return; + } + if (!sudoCached && platform !== 'win32' && isPermError(fullLog)) { + tee('\nPermission error — elevated access required.\n'); + const ok = await cacheSudo('Enter your system password to install OpenClaw'); + if (!ok) { tee('Incorrect password or cancelled.\n'); failCancelled(); return; } + sudoCached = true; + tee('Retrying with elevated permissions...\n'); + // Re-download and re-verify for the sudo retry + let retryTgz: string | null = null; + try { + retryTgz = await this._downloadAndVerifyOpenClaw(tee); + } catch (e) { + tee(`\n${(e as Error).message}\n`); + await fail(); return; + } + const r2 = await runCaptured('sudo', ['-E', 'npm', 'install', '-g', retryTgz]); + try { fs.unlinkSync(retryTgz); } catch {} + if (r2.code === 0) { + await fixOpenclawPermissions(); + await succeed(); return; + } + } + tee('\nnpm install did not succeed — trying full installer script...\n'); + } else if (platform !== 'win32') { + + const nvmSh = path.join(os.homedir(), '.nvm', 'nvm.sh'); + if (fs.existsSync(nvmSh)) { + tee('nvm detected — installing Node.js LTS...\n'); + let nvmTgz: string | null = null; + try { + nvmTgz = await this._downloadAndVerifyOpenClaw(tee); + } catch (e) { + tee(`\n${(e as Error).message}\n`); + await fail(); return; + } + const nvmR = await runCaptured('bash', ['-c', + `. "${nvmSh}" && nvm install --lts && nvm use --lts && npm install -g '${nvmTgz}'` + ]); + try { fs.unlinkSync(nvmTgz); } catch {} + if (nvmR.code === 0) { await fixOpenclawPermissions(); await succeed(); return; } + tee('nvm install failed — falling back to system install...\n'); + } + + if (!sudoCached) { + tee('\nNode.js is required. Your password is needed once to install it.\n'); + const sudoOk = await cacheSudo('Enter your password to install Node.js'); + if (!sudoOk) { tee('Incorrect password or cancelled.\n'); failCancelled(); return; } + sudoCached = true; + } + + if (platform === 'darwin') { + const nodeVersion = '20.18.2'; + const pkgUrl = `https://nodejs.org/dist/v${nodeVersion}/node-v${nodeVersion}.pkg`; + const pkgPath = `/tmp/.occ-node-${nodeVersion}.pkg`; + tee(`Downloading Node.js v${nodeVersion}...\n`); + const dlR = await runCaptured('curl', ['-fsSL', pkgUrl, '-o', pkgPath]); + if (dlR.code !== 0) { try { fs.unlinkSync(pkgPath); } catch {} await fail(); return; } + tee('Installing Node.js (this may take a moment)...\n'); + const instR = await runCaptured('sudo', ['-n', 'installer', '-pkg', pkgPath, '-target', '/']); + try { fs.unlinkSync(pkgPath); } catch { /* non-fatal */ } + if (instR.code !== 0) { await fail(); return; } + } else { + const hasCmdSync = (cmd: string): boolean => { + try { cp.execSync(`which ${cmd}`, { env, stdio: 'ignore' }); return true; } catch { return false; } + }; + tee('Installing Node.js via package manager...\n'); + let pkgResult: { code: number } | undefined; + if (hasCmdSync('apt-get')) { + pkgResult = await runCaptured('sudo', ['-n', 'bash', '-c', + 'curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && apt-get install -y nodejs' + ]); + } else if (hasCmdSync('dnf')) { + pkgResult = await runCaptured('sudo', ['-n', 'bash', '-c', + 'curl -fsSL https://rpm.nodesource.com/setup_lts.x | bash - && dnf install -y nodejs' + ]); + } else if (hasCmdSync('yum')) { + pkgResult = await runCaptured('sudo', ['-n', 'bash', '-c', + 'curl -fsSL https://rpm.nodesource.com/setup_lts.x | bash - && yum install -y nodejs' + ]); + } + if (!pkgResult) { tee('No supported package manager found (tried apt-get, dnf, yum).\n'); await fail(); return; } + if (pkgResult.code !== 0) { await fail(); return; } + } + + tee('Installing OpenClaw...\n'); + const npmCandidates = ['/usr/local/bin/npm', '/usr/bin/npm']; + const npmBin = npmCandidates.find(p => fs.existsSync(p)) ?? 'npm'; + // Always use verified tarball — never install bare package name + let postNodeTgz: string | null = null; + try { + postNodeTgz = await this._downloadAndVerifyOpenClaw(tee); + } catch (e) { + tee(`\n${(e as Error).message}\n`); + await fail(); return; + } + const npmR1 = await runCaptured(npmBin, ['install', '-g', postNodeTgz]); + try { fs.unlinkSync(postNodeTgz); } catch {} + if (npmR1.code === 0) { await fixOpenclawPermissions(); await succeed(); return; } + if (isPermError(fullLog)) { + let sudoTgz: string | null = null; + try { + sudoTgz = await this._downloadAndVerifyOpenClaw(tee); + } catch (e) { + tee(`\n${(e as Error).message}\n`); + await fail(); return; + } + const npmR2 = await runCaptured('sudo', ['-n', npmBin, 'install', '-g', sudoTgz]); + try { fs.unlinkSync(sudoTgz); } catch {} + if (npmR2.code === 0) { await fixOpenclawPermissions(); await succeed(); return; } + } + await fail(); return; + + } else { + tee('npm not found — running full installer script...\n'); + } + + // Last-resort: download installer script, verify checksum, then run. + // Never pipe remote scripts directly into a shell. + const INSTALL_CHECKSUM_URL = 'https://releases.openclaw.sh/install.sh.sha256'; + + if (platform === 'win32') { + tee('Running PowerShell installer...\n'); + const psScript = path.join(os.tmpdir(), `occ-install-${Date.now()}.ps1`); + const psChecksumFile = path.join(os.tmpdir(), `occ-install-${Date.now()}.ps1.sha256`); + const dlScript = await runCaptured('powershell', ['-NoProfile', '-Command', + `Invoke-WebRequest -UseBasicParsing https://openclaw.ai/install.ps1 -OutFile '${psScript}'` + ], { windowsHide: true } as cp.SpawnOptions); + if (dlScript.code !== 0) { tee('Failed to download installer script.\n'); await fail(); return; } + const dlChecksum = await runCaptured('powershell', ['-NoProfile', '-Command', + `Invoke-WebRequest -UseBasicParsing https://releases.openclaw.sh/install.ps1.sha256 -OutFile '${psChecksumFile}'` + ], { windowsHide: true } as cp.SpawnOptions); + if (dlChecksum.code === 0) { + const verifyR = await runCaptured('powershell', ['-NoProfile', '-Command', + `$expected = (Get-Content '${psChecksumFile}').Split(' ')[0]; ` + + `$actual = (Get-FileHash -Algorithm SHA256 '${psScript}').Hash.ToLower(); ` + + `if ($expected -ne $actual) { Write-Error "Checksum mismatch: expected $expected got $actual"; exit 1 }` + ], { windowsHide: true } as cp.SpawnOptions); + if (verifyR.code !== 0) { + tee('⚠ Installer script integrity check failed — aborting.\n'); + try { fs.unlinkSync(psScript); } catch {} + try { fs.unlinkSync(psChecksumFile); } catch {} + await fail(); return; + } + tee(' ✓ Installer integrity verified (SHA-256)\n'); + } else { + tee('Warning: could not fetch checksum — proceeding with unverified installer.\n'); + } + try { fs.unlinkSync(psChecksumFile); } catch {} + const r = await runCaptured('powershell', [ + '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', psScript, + ], { windowsHide: true } as cp.SpawnOptions); + try { fs.unlinkSync(psScript); } catch {} + if (r.code === 0) { await succeed(); return; } + } else { + tee('Downloading install script...\n'); + const tmpScript = path.join(os.tmpdir(), `occ-install-${Date.now()}.sh`); + const tmpChecksum = path.join(os.tmpdir(), `occ-install-${Date.now()}.sha256`); + const dlScript = await runCaptured('curl', ['-fsSL', 'https://openclaw.ai/install.sh', '-o', tmpScript]); + if (dlScript.code !== 0) { tee('Failed to download installer script.\n'); await fail(); return; } + const dlChecksum = await runCaptured('curl', ['-fsSL', INSTALL_CHECKSUM_URL, '-o', tmpChecksum]); + if (dlChecksum.code === 0) { + // Rewrite checksum file to reference actual tmp filename, then verify + const checksumContent = fs.readFileSync(tmpChecksum, 'utf-8').trim(); + const expectedHash = checksumContent.split(/\s+/)[0]; + const scriptName = path.basename(tmpScript); + fs.writeFileSync(tmpChecksum, `${expectedHash} ${scriptName}\n`); + const verifyR = await runCaptured('bash', ['-c', + `cd "${path.dirname(tmpScript)}" && (sha256sum -c '${tmpChecksum}' 2>/dev/null || shasum -a 256 -c '${tmpChecksum}')` + ]); + if (verifyR.code !== 0) { + tee('⚠ Installer script integrity check failed — aborting.\n'); + try { fs.unlinkSync(tmpScript); } catch {} + try { fs.unlinkSync(tmpChecksum); } catch {} + await fail(); return; + } + tee(' ✓ Installer integrity verified (SHA-256)\n'); + } else { + tee('Warning: could not fetch checksum — proceeding with unverified installer.\n'); + } + try { fs.unlinkSync(tmpChecksum); } catch {} + const r1 = await runCaptured('bash', [tmpScript]); + if (r1.code === 0) { + try { fs.unlinkSync(tmpScript); } catch {} + await fixOpenclawPermissions(); + await succeed(); return; + } + if (isPermError(fullLog)) { + tee('\nPermission error in installer — elevated access required.\n'); + const ok = await cacheSudo('Enter your system password to complete installation'); + if (!ok) { tee('Incorrect password or cancelled.\n'); try { fs.unlinkSync(tmpScript); } catch {} failCancelled(); return; } + tee('Retrying with elevated permissions...\n'); + const r2 = await runCaptured('sudo', ['-E', 'bash', tmpScript]); + try { fs.unlinkSync(tmpScript); } catch {} + if (r2.code === 0) { + await fixOpenclawPermissions(); + await succeed(); return; + } + } + try { fs.unlinkSync(tmpScript); } catch {} + } + + await fail(); + } + + private async _runSetup(data: { provider: string; apiKey: string; port: string }): Promise { + const post = (msg: object) => { try { this._panel.webview.postMessage(msg); } catch {} }; + const wizardPost = (text: string, done: boolean, ok: boolean) => { + writeLog(text); + post({ type: 'wizardLog', text, done, ok }); + }; + const env = this._host.buildExecEnv(); + + if (process.platform !== 'win32') { + const nodeResult = await this._host.exec('node', ['--version'], { env: env as Record, timeout: 5000 }); + const nodeVerRaw = nodeResult.stdout.trim(); + const nodeMinor = parseInt((nodeVerRaw.match(/^v?(\d+)/) || [])[1] || '0', 10); + if (nodeMinor < 22) { + const nvmSh = path.join(os.homedir(), '.nvm', 'nvm.sh'); + if (fs.existsSync(nvmSh)) { + wizardPost(`Node.js ${nodeVerRaw || 'unknown'} detected — openclaw requires v22+. Installing Node.js 22 via nvm...\n`, false, false); + const nvmCode = await this._host.execStream( + 'bash', + ['-c', `. "${nvmSh}" && nvm install 22 && nvm use 22 && nvm alias default 22`], + { env: env as Record }, + (d) => wizardPost(d, false, false), + (d) => wizardPost(d, false, false), + ); + if (nvmCode === 0) { + const nvmVersionsDir = path.join(os.homedir(), '.nvm', 'versions', 'node'); + const v22dirs = fs.existsSync(nvmVersionsDir) + ? fs.readdirSync(nvmVersionsDir).filter(v => /^v?22/.test(v)) + : []; + if (v22dirs.length > 0) { + const v22bin = path.join(nvmVersionsDir, v22dirs[0], 'bin'); + env.PATH = [v22bin, env.PATH || ''].filter(Boolean).join(':'); + (env as Record).Path = env.PATH; + } + wizardPost(' ✓ Node.js 22 ready.\n', false, false); + } else { + wizardPost(' ⚠️ Node.js 22 install via nvm failed. Setup may fail.\n', false, false); + } + } else { + wizardPost(`⚠️ Node.js ${nodeVerRaw || 'v20'} is active but openclaw requires v22+.\n Please install Node.js 22 (e.g. via https://nodejs.org) and restart OCCode.\n`, true, false); + return; + } + } + } + + const cliPath = await this._host.findOpenClawPath() ?? 'openclaw'; + const port = data.port && /^\d+$/.test(data.port) ? data.port : '18789'; + const isFree = data.provider === 'free'; + + const providerFlags: Record = { + free: [ + '--auth-choice', 'custom-api-key', + '--custom-base-url', 'https://occ.mba.sh/v1', + '--custom-api-key', data.apiKey, + '--custom-model-id', 'occ-legacy', + '--custom-compatibility', 'openai', + ], + anthropic: ['--auth-choice', 'apiKey', '--anthropic-api-key', data.apiKey], + openai: ['--auth-choice', 'openai-api-key', '--openai-api-key', data.apiKey], + openrouter: ['--auth-choice', 'openrouter-api-key', '--openrouter-api-key', data.apiKey], + gemini: ['--auth-choice', 'gemini-api-key', '--gemini-api-key', data.apiKey], + ollama: [ + '--auth-choice', 'custom-api-key', + '--custom-base-url', data.apiKey || 'http://localhost:11434', + '--custom-api-key', 'ollama', + '--custom-model-id', 'llama3', + '--custom-compatibility', 'openai', + ], + }; + const flags = providerFlags[data.provider]; + if (!flags) { + wizardPost('Unknown provider selected.\n', true, false); + return; + } + + const args = [ + 'onboard', + '--non-interactive', '--accept-risk', + '--flow', 'quickstart', + '--gateway-auth', 'token', + '--gateway-port', port, + '--skip-channels', '--skip-skills', '--skip-health', + ...flags, + ]; + + writeLog(`\n=== _runSetup START provider=${data.provider} port=${port} ===\n`); + wizardPost(isFree ? 'Installing Inference for MoltPilot...\nInstalling Inference for your new OpenClaw...\n' : 'Installing Inference for your new OpenClaw...\n', false, false); + + let exitCode: number; + try { + exitCode = await this._host.execStream( + cliPath, args, + { + env: env as Record, + timeout: 120_000, + ...(process.platform === 'win32' ? { shell: true, windowsHide: true } : {}), + }, + (d) => wizardPost(d, false, false), + (d) => wizardPost(d, false, false), + ); + } catch (err) { + wizardPost(`Error: ${String(err)}\n`, true, false); + return; + } + + const ok = exitCode === 0; + writeLog(`=== _runSetup END code=${exitCode} ===\n`); + wizardPost(ok ? '\n✅ Setup complete!\n' : `\nSetup exited with code ${exitCode}.\n`, true, ok); + + if (ok) { + if (isFree) { + try { + const occDir = path.join(os.homedir(), '.occ'); + if (!fs.existsSync(occDir)) { fs.mkdirSync(occDir, { recursive: true }); } + fs.writeFileSync( + path.join(occDir, 'moltpilot-tier.json'), + JSON.stringify({ tier: 'free', grantedAt: new Date().toISOString(), limitUsd: 1.00 }), + ); + } catch { /* non-fatal */ } + try { + const cfg = await this._host.readConfig() as Record; + const patchModel = (obj: unknown): boolean => { + if (!obj || typeof obj !== 'object') return false; + if (Array.isArray(obj)) { + for (const item of obj) { if (patchModel(item)) return true; } + return false; + } + const o = obj as Record; + if (o['id'] === OCC_LEGACY_MODEL_ID) { + o['name'] = OCC_LEGACY_MODEL_NAME; + o['reasoning'] = false; + o['input'] = ['text']; + o['cost'] = { ...OCC_LEGACY_COST }; + o['contextWindow'] = OCC_LEGACY_CONTEXT_WINDOW; + o['maxTokens'] = OCC_LEGACY_MAX_TOKENS; + return true; + } + for (const v of Object.values(o)) { patchModel(v); } + return false; + }; + patchModel(cfg); + await this._host.writeConfig(cfg); + } catch { /* non-fatal */ } + } + setTimeout(() => { + void this._showStatusPanel(); + if (isFree) { + void vscode.commands.executeCommand('openclaw.openWorkspace'); + } + setTimeout(() => { + void vscode.commands.executeCommand('void.openChatWithMessage', + 'Run `openclaw gateway start` to start the OpenClaw gateway.', + 'agent'); + }, 1000); + }, 1500); + } + } + + private _getHtml( + isInstalled: boolean, + iconUri: string, + occUser: { email: string; picture: string | null; balance_usd: number; api_keys?: { moltpilotKey?: string; occKey?: string } | null } | null = null + ): string { + let userAreaHtml: string; + if (!occUser) { + userAreaHtml = ``; + } else { + const initial = (occUser.email || '?')[0].toUpperCase(); + const safeEmail = occUser.email.replace(/"/g, '"').replace(/` + : initial; + userAreaHtml = ` +
+ +
+
+
${avatarImg}
+
${safeEmail}
+
+ +
+ +
+
`; + } + + const providers = [ + { id: 'anthropic', label: 'Anthropic Claude', hint: 'console.anthropic.com/settings/keys', placeholder: 'sk-ant-...' }, + { id: 'openai', label: 'OpenAI', hint: 'platform.openai.com/api-keys', placeholder: 'sk-...' }, + { id: 'openrouter', label: 'OpenRouter', hint: 'openrouter.ai/settings/keys', placeholder: 'sk-or-...' }, + { id: 'gemini', label: 'Google Gemini', hint: 'aistudio.google.com/apikey', placeholder: 'AIza...' }, + ]; + + const providerCards = providers.map(p => + `` + ).join('\n '); + + return ` + + + + + + + + +
${userAreaHtml}
+ + + +
Set up OpenClaw
+
Follow the steps below to get started
+ + +
+
+
${isInstalled ? '✓' : '1'}
+
Install
OpenClaw
+
+
+
2
+
Configure
AI Model
+
+
+
3
+
Ready
+
+
+ + +
+ + +
+ + + + + + + + + + + + + + +
+
+
Working
+ + + +
+ + + + + + +`; + } +} diff --git a/apps/editor/extensions/openclaw-local/tsconfig.json b/apps/editor/extensions/openclaw-local/tsconfig.json new file mode 100644 index 00000000..c8613a95 --- /dev/null +++ b/apps/editor/extensions/openclaw-local/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "lib": ["ES2020"], + "outDir": "./out", + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", ".vscode-test"] +} diff --git a/apps/editor/extensions/openclaw/package-lock.json b/apps/editor/extensions/openclaw-ssh/package-lock.json similarity index 72% rename from apps/editor/extensions/openclaw/package-lock.json rename to apps/editor/extensions/openclaw-ssh/package-lock.json index 687d28e1..aae5847f 100644 --- a/apps/editor/extensions/openclaw/package-lock.json +++ b/apps/editor/extensions/openclaw-ssh/package-lock.json @@ -1,26 +1,26 @@ { - "name": "home", - "version": "0.2.2", + "name": "openclaw-ssh", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "home", - "version": "0.2.2", + "name": "openclaw-ssh", + "version": "0.1.0", "license": "MIT", "devDependencies": { - "@types/node": "^20.19.35", + "@types/node": "^20.0.0", "@types/vscode": "^1.85.0", - "typescript": "^5.9.3" + "typescript": "^5.3.0" }, "engines": { "vscode": "^1.85.0" } }, "node_modules/@types/node": { - "version": "20.19.35", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", - "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "dev": true, "license": "MIT", "dependencies": { @@ -28,9 +28,9 @@ } }, "node_modules/@types/vscode": { - "version": "1.85.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.85.0.tgz", - "integrity": "sha512-CF/RBon/GXwdfmnjZj0WTUMZN5H6YITOfBCP4iEZlOtVQXuzw6t7Le7+cR+7JzdMrnlm7Mfp49Oj2TuSXIWo3g==", + "version": "1.110.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", + "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", "dev": true, "license": "MIT" }, diff --git a/apps/editor/extensions/openclaw-ssh/package.json b/apps/editor/extensions/openclaw-ssh/package.json new file mode 100644 index 00000000..47df9f67 --- /dev/null +++ b/apps/editor/extensions/openclaw-ssh/package.json @@ -0,0 +1,25 @@ +{ + "name": "openclaw-ssh", + "displayName": "OpenClaw SSH (Preview)", + "description": "Manage OpenClaw on remote servers via SSH — coming soon", + "version": "0.1.0", + "publisher": "openclaw", + "license": "MIT", + "engines": { "vscode": "^1.85.0" }, + "extensionDependencies": ["openclaw.home"], + "categories": ["Other"], + "activationEvents": ["onStartupFinished"], + "main": "./out/openclaw-ssh/src/extension", + "contributes": { + "commands": [{ "command": "openclaw.host.setup.ssh", "title": "OpenClaw: Set up SSH" }] + }, + "scripts": { + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/vscode": "^1.85.0", + "typescript": "^5.3.0" + } +} diff --git a/apps/editor/extensions/openclaw-ssh/src/adapter.ts b/apps/editor/extensions/openclaw-ssh/src/adapter.ts new file mode 100644 index 00000000..e79c2e3d --- /dev/null +++ b/apps/editor/extensions/openclaw-ssh/src/adapter.ts @@ -0,0 +1,78 @@ +import * as vscode from 'vscode'; +import type { + HostAdapter, + HostType, + HostConnection, + HostConnectionConfig, + SSHConnection, + TestResult, + DiscoveredHost, + ConfigField, + ConfigValidationResult, +} from '../../openclaw/src/hosts/types'; +import { SSHHostConnection, isHostKnown } from './connection'; + +export class SSHHostAdapter implements HostAdapter { + readonly type: HostType = 'ssh'; + readonly displayName = 'SSH Server'; + readonly icon = new vscode.ThemeIcon('remote'); + + async discover(): Promise { + // Future: parse ~/.ssh/config for known hosts + return []; + } + + async connect(config: HostConnectionConfig): Promise { + return new SSHHostConnection(config as SSHConnection); + } + + async testConnection(config: HostConnectionConfig): Promise { + const sshCfg = config as SSHConnection; + // Refuse to connect if host key is unknown (prevents MITM on first connect) + if (!isHostKnown(sshCfg.host, sshCfg.port)) { + return { + success: false, + message: `Host ${sshCfg.host} is not in known_hosts. Use the SSH setup wizard to verify the host key fingerprint before connecting.`, + }; + } + const conn = new SSHHostConnection(sshCfg); + try { + const r = await conn.exec('echo', ['ok'], { timeout: 15000 }); + if (r.code !== 0 || !r.stdout.includes('ok')) { + return { success: false, message: `SSH connection failed: ${r.stderr.trim()}` }; + } + const cliCheck = await conn.testOpenClawCli(); + return { + success: true, + message: cliCheck.ok + ? `Connected to ${sshCfg.user}@${sshCfg.host}. OpenClaw ${cliCheck.output ?? ''}` + : `Connected to ${sshCfg.user}@${sshCfg.host}. OpenClaw not installed.`, + details: { + openclawInstalled: cliCheck.ok, + openclawVersion: cliCheck.output, + os: 'remote', + hostname: sshCfg.host, + }, + }; + } catch (err) { + return { success: false, message: `SSH error: ${String(err)}` }; + } + } + + getConfigFields(): ConfigField[] { + return [ + { id: 'host', label: 'Hostname or IP', type: 'text', placeholder: 'example.com or 192.168.1.10', required: true }, + { id: 'user', label: 'SSH User', type: 'text', placeholder: 'ubuntu, ec2-user, root...', required: true }, + { id: 'port', label: 'Port', type: 'number', placeholder: '22', defaultValue: 22 }, + { id: 'keyPath', label: 'SSH Key Path', type: 'text', placeholder: '~/.ssh/id_rsa (blank = SSH agent)', required: false }, + ]; + } + + validateConfig(config: HostConnectionConfig): ConfigValidationResult { + const sshCfg = config as SSHConnection; + const errors: { fieldId: string; message: string }[] = []; + if (!sshCfg.host) { errors.push({ fieldId: 'host', message: 'Hostname is required' }); } + if (!sshCfg.user) { errors.push({ fieldId: 'user', message: 'SSH user is required' }); } + return errors.length ? { valid: false, errors } : { valid: true }; + } +} diff --git a/apps/editor/extensions/openclaw-ssh/src/connection.ts b/apps/editor/extensions/openclaw-ssh/src/connection.ts new file mode 100644 index 00000000..20e5e02f --- /dev/null +++ b/apps/editor/extensions/openclaw-ssh/src/connection.ts @@ -0,0 +1,378 @@ +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +/** Build a filtered environment — only safe variables, no leaked credentials. */ +function safeExecEnv(): Record { + const safe = ['PATH', 'HOME', 'USER', 'SHELL', 'LANG', 'LC_ALL', 'LC_CTYPE', + 'TERM', 'TMPDIR', 'XDG_RUNTIME_DIR', 'DISPLAY', 'WAYLAND_DISPLAY', + 'NODE_ENV', 'NVM_DIR', 'NVM_BIN', 'SSH_AUTH_SOCK']; + const env: Record = {}; + for (const key of safe) { + if (process.env[key]) { env[key] = process.env[key]; } + } + return env; +} +import type { + HostConnection, + HostType, + ExecOpts, + ExecResult, + LogFn, + CliCheckResult, + GatewayStatus, + GatewayRunState, + OpenClawConfig, + SetupParams, + SSHConnection, +} from '../../openclaw/src/hosts/types'; + +// ───────────────────────────────────────────── +// SSH host key verification helpers +// ───────────────────────────────────────────── + +/** + * Checks whether a host+port combination is already in ~/.ssh/known_hosts. + * Returns true if the host key is known, false otherwise. + */ +export function isHostKnown(host: string, port?: number): boolean { + const knownHostsPath = path.join(os.homedir(), '.ssh', 'known_hosts'); + if (!fs.existsSync(knownHostsPath)) { return false; } + // ssh-keygen -F looks up a host in known_hosts + const lookup = port && port !== 22 ? `[${host}]:${port}` : host; + const result = cp.spawnSync('ssh-keygen', ['-F', lookup], { + timeout: 5000, + windowsHide: true, + }); + return result.status === 0 && result.stdout.toString().trim().length > 0; +} + +/** + * Fetches the SSH host key fingerprint for a remote host using ssh-keyscan. + * Returns the fingerprint string or null if unreachable. + */ +export function fetchHostFingerprint(host: string, port?: number): string | null { + const args = ['-T', '10']; + if (port && port !== 22) { args.push('-p', String(port)); } + args.push(host); + const scan = cp.spawnSync('ssh-keyscan', args, { + timeout: 15000, + windowsHide: true, + }); + if (scan.status !== 0 || !scan.stdout.toString().trim()) { return null; } + // Write scanned keys to a temp file so ssh-keygen can read the fingerprint + const tmpFile = path.join(os.tmpdir(), `occ-hostkey-${Date.now()}.pub`); + try { + fs.writeFileSync(tmpFile, scan.stdout); + const fp = cp.spawnSync('ssh-keygen', ['-lf', tmpFile], { + timeout: 5000, + windowsHide: true, + }); + return fp.status === 0 ? fp.stdout.toString().trim() : null; + } finally { + try { fs.unlinkSync(tmpFile); } catch {} + } +} + +/** + * Adds a host key to ~/.ssh/known_hosts using ssh-keyscan output. + */ +export function addHostToKnownHosts(host: string, port?: number): boolean { + const args = ['-T', '10']; + if (port && port !== 22) { args.push('-p', String(port)); } + args.push(host); + const scan = cp.spawnSync('ssh-keyscan', args, { + timeout: 15000, + windowsHide: true, + }); + if (scan.status !== 0 || !scan.stdout.toString().trim()) { return false; } + const sshDir = path.join(os.homedir(), '.ssh'); + if (!fs.existsSync(sshDir)) { fs.mkdirSync(sshDir, { recursive: true, mode: 0o700 }); } + const knownHostsPath = path.join(sshDir, 'known_hosts'); + fs.appendFileSync(knownHostsPath, scan.stdout.toString(), { mode: 0o600 }); + return true; +} + +// ───────────────────────────────────────────── + +export class SSHHostConnection implements HostConnection { + readonly type: HostType = 'ssh'; + + get id(): string { return `ssh:${this._cfg.user}@${this._cfg.host}`; } + get label(): string { return `${this._cfg.user}@${this._cfg.host}`; } + + constructor(private readonly _cfg: SSHConnection) {} + + dispose(): void {} + + // ── SSH helpers ─────────────────────────── + + private _sshBaseArgs(): string[] { + const args = [ + '-o', 'BatchMode=yes', + '-o', 'ConnectTimeout=15', + '-o', 'StrictHostKeyChecking=yes', + ]; + if (this._cfg.port && this._cfg.port !== 22) { args.push('-p', String(this._cfg.port)); } + if (this._cfg.keyPath) { args.push('-i', this._cfg.keyPath); } + if (this._cfg.jumpHost) { args.push('-J', this._cfg.jumpHost); } + return args; + } + + private _remote(): string { + return `${this._cfg.user}@${this._cfg.host}`; + } + + private _remoteHome(): string { + return this._cfg.user === 'root' ? '/root' : `/home/${this._cfg.user}`; + } + + // ── Process execution ───────────────────── + + exec(cmd: string, args: string[], opts: ExecOpts = {}): Promise { + const remoteCmd = [cmd, ...args] + .map(a => `'${a.replace(/'/g, "'\\''")}'`) + .join(' '); + return new Promise((resolve, reject) => { + const proc = cp.spawn('ssh', [...this._sshBaseArgs(), this._remote(), remoteCmd], { + env: { ...process.env, ...opts.env }, + timeout: opts.timeout, + windowsHide: opts.windowsHide ?? true, + }); + if (opts.stdinData !== undefined) { proc.stdin.write(opts.stdinData); proc.stdin.end(); } + let stdout = ''; + let stderr = ''; + proc.stdout.on('data', (d: Buffer) => { stdout += d.toString(); }); + proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); }); + proc.on('error', reject); + proc.on('close', code => resolve({ stdout, stderr, code: code ?? -1 })); + }); + } + + execStream(cmd: string, args: string[], opts: ExecOpts, onData: LogFn, onError: LogFn): Promise { + const remoteCmd = [cmd, ...args] + .map(a => `'${a.replace(/'/g, "'\\''")}'`) + .join(' '); + return new Promise((resolve, reject) => { + const proc = cp.spawn('ssh', [...this._sshBaseArgs(), this._remote(), remoteCmd], { + env: { ...process.env, ...opts.env }, + timeout: opts.timeout, + windowsHide: opts.windowsHide ?? true, + }); + if (opts.stdinData !== undefined) { proc.stdin.write(opts.stdinData); proc.stdin.end(); } + proc.stdout.on('data', (d: Buffer) => { onData(d.toString()); }); + proc.stderr.on('data', (d: Buffer) => { onError(d.toString()); }); + proc.on('error', reject); + proc.on('close', code => resolve(code ?? -1)); + }); + } + + // ── Filesystem ──────────────────────────── + + async readFile(filePath: string): Promise { + const r = await this.exec('cat', [filePath]); + if (r.code !== 0) { throw new Error(`cat ${filePath} failed: ${r.stderr}`); } + return r.stdout; + } + + async writeFile(filePath: string, content: string): Promise { + if (/\0/.test(filePath)) { throw new Error('Invalid file path: null byte'); } + if (!/^[/~]/.test(filePath)) { throw new Error('Invalid file path: must be absolute or home-relative'); } + const dir = path.posix.dirname(filePath); + await this.exec('mkdir', ['-p', dir]); + await new Promise((resolve, reject) => { + const quotedPath = filePath.replace(/'/g, "'\\''"); + const proc = cp.spawn('ssh', [...this._sshBaseArgs(), this._remote(), `cat > '${quotedPath}'`], { windowsHide: true }); + proc.stdin.write(content); + proc.stdin.end(); + proc.on('error', reject); + proc.on('close', code => code === 0 ? resolve() : reject(new Error(`writeFile failed: code ${code}`))); + }); + } + + async exists(filePath: string): Promise { + const r = await this.exec('test', ['-e', filePath]); + return r.code === 0; + } + + async mkdir(dirPath: string): Promise { + await this.exec('mkdir', ['-p', dirPath]); + } + + async stat(filePath: string): Promise<{ size: number; isDirectory: boolean } | null> { + const r = await this.exec('stat', ['-c', '%s %F', filePath]); + if (r.code !== 0) { return null; } + const parts = r.stdout.trim().split(' '); + return { + size: parseInt(parts[0] ?? '0', 10), + isDirectory: (parts[1] ?? '').includes('directory'), + }; + } + + // ── CLI ─────────────────────────────────── + + async findOpenClawPath(): Promise { + const r = await this.exec('which', ['openclaw']); + if (r.code === 0) { + const p = r.stdout.trim().split('\n')[0]; + if (p) { return p; } + } + for (const p of [ + '/usr/local/bin/openclaw', + '/usr/bin/openclaw', + `${this._remoteHome()}/.local/bin/openclaw`, + ]) { + if (await this.exists(p)) { return p; } + } + return undefined; + } + + async isCliInstalled(): Promise { + return (await this.findOpenClawPath()) !== undefined; + } + + async getCliVersion(): Promise { + const p = await this.findOpenClawPath(); + if (!p) { return null; } + try { + const r = await this.exec(p, ['--version'], { timeout: 8000 }); + const out = (r.stdout + r.stderr).trim(); + const m = out.match(/[\d]+\.[\d]+\.[\d]+/); + return m ? m[0] : out || null; + } catch { return null; } + } + + async testOpenClawCli(): Promise { + const p = await this.findOpenClawPath(); + const command = p ? `ssh ${this._remote()} ${p} --version` : `ssh ${this._remote()} openclaw --version`; + if (!p) { return { ok: false, command, error: 'OpenClaw CLI not found on remote host' }; } + try { + const r = await this.exec(p, ['--version'], { timeout: 8000 }); + const out = (r.stdout + r.stderr).trim(); + return r.code === 0 + ? { ok: true, command, output: out } + : { ok: false, command, error: `Exit ${r.code}: ${out}` }; + } catch (err) { return { ok: false, command, error: String(err) }; } + } + + async installCli(onLog: LogFn): Promise { + onLog('Installing OpenClaw on remote host...\n'); + // Download script, verify checksum, then execute — never pipe curl directly to bash. + const steps = [ + { label: 'Downloading installer...\n', cmd: 'curl -fsSL https://get.openclaw.sh -o /tmp/occ-install.sh' }, + { label: 'Fetching checksum...\n', cmd: 'curl -fsSL https://releases.openclaw.sh/install.sh.sha256 -o /tmp/occ-install.sha256' }, + { label: 'Verifying installer integrity...\n', cmd: 'cd /tmp && sha256sum -c occ-install.sha256' }, + { label: 'Running installer...\n', cmd: 'bash /tmp/occ-install.sh' }, + { label: '', cmd: 'rm -f /tmp/occ-install.sh /tmp/occ-install.sha256' }, + ]; + for (const step of steps) { + if (step.label) { onLog(step.label); } + const code = await this.execStream('bash', ['-c', step.cmd], { timeout: 120_000 }, onLog, onLog); + if (code !== 0 && step.label) { + // Clean up on failure + await this.exec('rm', ['-f', '/tmp/occ-install.sh', '/tmp/occ-install.sha256']).catch(() => {}); + throw new Error(`Install step failed: ${step.label.trim()}`); + } + } + onLog('OpenClaw installed.\n'); + } + + // ── OpenClaw config ─────────────────────── + + async getConfigPath(): Promise { + return `${this._remoteHome()}/.openclaw/openclaw.json`; + } + + async readConfig(): Promise { + const p = await this.getConfigPath(); + if (!(await this.exists(p))) { return {}; } + try { return JSON.parse(await this.readFile(p)) as OpenClawConfig; } catch { return {}; } + } + + async writeConfig(patch: Partial): Promise { + const p = await this.getConfigPath(); + let existing: OpenClawConfig = {}; + if (await this.exists(p)) { + try { existing = JSON.parse(await this.readFile(p)) as OpenClawConfig; } catch { /* ok */ } + } + await this.writeFile(p, JSON.stringify({ ...existing, ...patch }, null, 2)); + } + + // ── Gateway ─────────────────────────────── + + async gatewayHealthCheck(): Promise { + const cliPath = await this.findOpenClawPath(); + if (!cliPath) { return { state: 'unknown' as GatewayRunState, error: 'CLI not installed' }; } + try { + const r = await this.exec(cliPath, ['gateway', 'status', '--json'], { timeout: 8000 }); + if (r.code === 0) { + try { + const parsed = JSON.parse(r.stdout) as Partial; + return { + state: parsed.state ?? 'unknown', + port: parsed.port ?? this._cfg.gatewayPort, + version: parsed.version, + uptime: parsed.uptime, + }; + } catch { + const out = (r.stdout + r.stderr).toLowerCase(); + return { state: out.includes('running') ? 'running' : 'stopped', port: this._cfg.gatewayPort }; + } + } + return { state: 'stopped' as GatewayRunState }; + } catch { return { state: 'error' as GatewayRunState, error: 'Health check failed' }; } + } + + async gatewayStart(onLog: LogFn): Promise { + const p = await this.findOpenClawPath(); + if (!p) { throw new Error('OpenClaw CLI not installed'); } + const code = await this.execStream(p, ['gateway', 'start'], {}, onLog, onLog); + if (code !== 0) { throw new Error(`gateway start exited with code ${code}`); } + } + + async gatewayStop(onLog: LogFn): Promise { + const p = await this.findOpenClawPath(); + if (!p) { throw new Error('OpenClaw CLI not installed'); } + const code = await this.execStream(p, ['gateway', 'stop'], {}, onLog, onLog); + if (code !== 0) { throw new Error(`gateway stop exited with code ${code}`); } + } + + async gatewayRestart(onLog: LogFn): Promise { + const p = await this.findOpenClawPath(); + if (!p) { throw new Error('OpenClaw CLI not installed'); } + const code = await this.execStream(p, ['gateway', 'restart'], {}, onLog, onLog); + if (code !== 0) { throw new Error(`gateway restart exited with code ${code}`); } + } + + async gatewayReboot(onLog: LogFn): Promise { + const p = await this.findOpenClawPath(); + if (p) { + try { + const code = await this.execStream(p, ['gateway', 'reboot'], {}, onLog, onLog); + if (code === 0) return; + } catch { /* fall through to OS-level reboot */ } + } + onLog('openclaw gateway reboot unavailable — falling back to OS reboot'); + const code = await this.execStream('sudo', ['reboot'], {}, onLog, onLog); + if (code !== 0) { throw new Error(`OS reboot command exited with code ${code}`); } + } + + async runSetup(params: SetupParams, onLog: LogFn): Promise { + const p = await this.findOpenClawPath(); + if (!p) { throw new Error('OpenClaw CLI not installed — call installCli first'); } + // Pass API key via environment variable — never as a CLI argument (visible in ps). + const code = await this.execStream( + p, + ['onboard', '--provider', params.provider, '--port', params.port], + { env: { OPENCLAW_API_KEY: params.apiKey } }, onLog, onLog, + ); + if (code !== 0) { throw new Error(`onboard exited with code ${code}`); } + } + + // ── Environment ─────────────────────────── + + buildExecEnv(): Record { + return safeExecEnv(); + } +} diff --git a/apps/editor/extensions/openclaw-ssh/src/extension.ts b/apps/editor/extensions/openclaw-ssh/src/extension.ts new file mode 100644 index 00000000..bbe76220 --- /dev/null +++ b/apps/editor/extensions/openclaw-ssh/src/extension.ts @@ -0,0 +1,28 @@ +import * as vscode from 'vscode'; +import type { OpenClawCoreAPI } from '../../openclaw/src/hosts/types'; +import { SSHHostAdapter } from './adapter'; +import { SSHSetupPanel } from './setup-panel'; + +export async function activate(context: vscode.ExtensionContext): Promise { + const coreExt = vscode.extensions.getExtension('openclaw.home'); + if (!coreExt) { + console.warn('[openclaw-ssh] Core extension not found'); + return; + } + const coreAPI = coreExt.isActive ? coreExt.exports : await coreExt.activate(); + if (!coreAPI) { + console.warn('[openclaw-ssh] Core extension did not export API'); + return; + } + const disposable = coreAPI.registerHostAdapter(new SSHHostAdapter(), 'openclaw.openclaw-ssh'); + context.subscriptions.push(disposable); + + const setupCmd = vscode.commands.registerCommand('openclaw.host.setup.ssh', () => { + SSHSetupPanel.createOrShow(context.extensionUri, coreAPI); + }); + context.subscriptions.push(setupCmd); + + console.log('[openclaw-ssh] SSHHostAdapter registered'); +} + +export function deactivate(): void {} diff --git a/apps/editor/extensions/openclaw-ssh/src/setup-panel.ts b/apps/editor/extensions/openclaw-ssh/src/setup-panel.ts new file mode 100644 index 00000000..69d7f022 --- /dev/null +++ b/apps/editor/extensions/openclaw-ssh/src/setup-panel.ts @@ -0,0 +1,413 @@ +import * as cp from 'child_process'; +import * as vscode from 'vscode'; +import type { OpenClawCoreAPI } from '../../openclaw/src/hosts/types'; +import { isHostKnown, fetchHostFingerprint, addHostToKnownHosts } from './connection'; + +export class SSHSetupPanel { + public static currentPanel: SSHSetupPanel | undefined; + + private readonly _panel: vscode.WebviewPanel; + private readonly _extensionUri: vscode.Uri; + private _disposables: vscode.Disposable[] = []; + + public static createOrShow(extensionUri: vscode.Uri, coreAPI: OpenClawCoreAPI): void { + const column = vscode.window.activeTextEditor + ? vscode.window.activeTextEditor.viewColumn + : undefined; + + if (SSHSetupPanel.currentPanel) { + SSHSetupPanel.currentPanel._panel.reveal(column); + return; + } + + const homeExt = vscode.extensions.getExtension('openclaw.home'); + const homeUri = homeExt?.extensionUri ?? extensionUri; + const iconUri = vscode.Uri.joinPath(homeUri, 'media', 'icon.png'); + + const panel = vscode.window.createWebviewPanel( + 'openclawSSHSetup', + 'OCC Home [SSH]', + column ?? vscode.ViewColumn.One, + { + enableScripts: true, + localResourceRoots: [ + vscode.Uri.joinPath(homeUri, 'media'), + ], + retainContextWhenHidden: true, + }, + ); + + SSHSetupPanel.currentPanel = new SSHSetupPanel(panel, extensionUri, coreAPI, iconUri); + } + + private constructor( + panel: vscode.WebviewPanel, + extensionUri: vscode.Uri, + private readonly _coreAPI: OpenClawCoreAPI, + iconUri: vscode.Uri, + ) { + this._panel = panel; + this._extensionUri = extensionUri; + + const webviewIconUri = panel.webview.asWebviewUri(iconUri).toString(); + this._panel.webview.html = this._getHtml(webviewIconUri); + + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + + this._panel.webview.onDidReceiveMessage( + async (msg: { command: string; host?: string; user?: string; port?: string; keyPath?: string }) => { + switch (msg.command) { + case 'sshInstallBegin': { + const host = msg.host ?? ''; + const user = msg.user ?? ''; + const port = parseInt(msg.port ?? '22', 10) || 22; + const keyPath = msg.keyPath || undefined; + await this._handleSSHInstall(host, user, port, keyPath); + break; + } + case 'closePanel': + this.dispose(); + break; + default: + if (msg.command) { + void vscode.commands.executeCommand(msg.command); + } + break; + } + }, + null, + this._disposables, + ); + } + + public dispose(): void { + SSHSetupPanel.currentPanel = undefined; + this._panel.dispose(); + while (this._disposables.length) { + const d = this._disposables.pop(); + if (d) { d.dispose(); } + } + } + + // ── Handler ──────────────────────────────────────────────────────────────── + + private async _handleSSHInstall(host: string, user: string, port: number, keyPath?: string): Promise { + const log = (text: string, isErr = false) => { + try { this._panel.webview.postMessage({ type: 'sshLog', text, isErr }); } catch { /* ignore */ } + }; + const fail = (text: string) => { + try { this._panel.webview.postMessage({ type: 'sshError', text }); } catch { /* ignore */ } + }; + + try { + // ── Host key verification ── + // Before connecting, verify the server's identity to prevent MITM attacks. + if (!isHostKnown(host, port)) { + log(`Host ${host} is not in known_hosts — fetching fingerprint...\n`); + const fingerprint = fetchHostFingerprint(host, port); + if (!fingerprint) { + fail(`Could not fetch SSH host key for ${host}. Verify the host is reachable and try again.`); + return; + } + const choice = await vscode.window.showWarningMessage( + `First connection to ${host}.\n\nSSH host key fingerprint:\n${fingerprint}\n\nDo you trust this host?`, + { modal: true, detail: 'If you did not expect this fingerprint, someone may be intercepting your connection (MITM attack). Only continue if you trust this server.' }, + 'Trust & Connect', + 'Cancel', + ); + if (choice !== 'Trust & Connect') { + fail('Connection cancelled — host key not trusted.'); + return; + } + log('Adding host key to known_hosts...\n'); + if (!addHostToKnownHosts(host, port)) { + fail('Failed to add host key to known_hosts.'); + return; + } + log(' ✓ Host key saved\n'); + } + + const sshBase = [ + '-o', 'BatchMode=yes', + '-o', 'ConnectTimeout=15', + '-o', 'StrictHostKeyChecking=yes', + ]; + if (port !== 22) { sshBase.push('-p', String(port)); } + if (keyPath) { sshBase.push('-i', keyPath); } + const remote = `${user}@${host}`; + + // Test connection + log(`Testing SSH connection to ${remote}...\n`); + const test = cp.spawnSync('ssh', [...sshBase, remote, 'echo ok'], { timeout: 15000, windowsHide: true }); + if (test.status !== 0 || !test.stdout.toString().includes('ok')) { + fail(`Cannot connect to ${remote}.\n${test.stderr?.toString().trim() || 'Check hostname, user, and SSH key.'}`); + return; + } + log(`\u2713 Connected to ${remote}\n`); + + // Check if openclaw already installed + log('Checking for OpenClaw CLI...\n'); + const check = cp.spawnSync('ssh', [...sshBase, remote, 'which openclaw'], { timeout: 8000, windowsHide: true }); + const alreadyInstalled = check.status === 0 && check.stdout.toString().trim().length > 0; + + if (!alreadyInstalled) { + log('Installing OpenClaw...\n'); + + const INSTALL_SCRIPT_URL = 'https://get.openclaw.sh'; + const INSTALL_CHECKSUM_URL = 'https://releases.openclaw.sh/install.sh.sha256'; + const TMP_SCRIPT = '/tmp/occ-install.sh'; + const TMP_CHECKSUM = '/tmp/occ-install.sha256'; + + const installSteps: Array<{ label: string; cmd: string }> = [ + { + label: 'Downloading installer...\n', + cmd: `curl -fsSL ${INSTALL_SCRIPT_URL} -o ${TMP_SCRIPT}`, + }, + { + label: 'Fetching checksum...\n', + cmd: `curl -fsSL ${INSTALL_CHECKSUM_URL} -o ${TMP_CHECKSUM}`, + }, + { + label: 'Verifying installer integrity...\n', + cmd: `cd /tmp && sha256sum -c ${TMP_CHECKSUM}`, + }, + { + label: 'Running installer...\n', + cmd: `bash ${TMP_SCRIPT}`, + }, + ]; + + let installFailed = false; + for (const step of installSteps) { + log(step.label); + const code = await new Promise((resolve) => { + const proc = cp.spawn('ssh', [...sshBase, remote, step.cmd], { windowsHide: true }); + proc.stdout.on('data', (d: Buffer) => log(d.toString())); + proc.stderr.on('data', (d: Buffer) => log(d.toString(), true)); + proc.on('close', (c) => resolve(c ?? -1)); + proc.on('error', () => resolve(-1)); + }); + if (code !== 0) { + // Always clean up temp files before reporting failure + cp.spawn('ssh', [...sshBase, remote, `rm -f ${TMP_SCRIPT} ${TMP_CHECKSUM}`], { windowsHide: true }); + if (step.label.startsWith('Verifying')) { + fail('Installer integrity check failed — the downloaded script does not match the expected checksum. Installation aborted.'); + } else { + fail(`OpenClaw installation failed at: ${step.label.trim()}. See the log above for details.`); + } + installFailed = true; + break; + } + } + + // Clean up temp files after successful install too + cp.spawn('ssh', [...sshBase, remote, `rm -f ${TMP_SCRIPT} ${TMP_CHECKSUM}`], { windowsHide: true }); + + if (installFailed) { return; } + log('\n\u2713 OpenClaw installed\n'); + } else { + log('\u2713 OpenClaw already installed \u2014 skipping\n'); + } + + // Register SSH host + log('\nRegistering SSH host...\n'); + const entry = await this._coreAPI.addHost({ + type: 'ssh', + label: `SSH (${user}@${host})`, + connection: { + type: 'ssh', + host, + port: port !== 22 ? port : undefined, + user, + authMethod: keyPath ? 'key' : 'agent', + keyPath: keyPath || undefined, + }, + lastStatus: 'online', + }); + await this._coreAPI.setActiveHost(entry.id); + + log('\u2713 Done \u2014 loading setup...\n'); + try { this._panel.webview.postMessage({ type: 'sshInstallDone' }); } catch { /* ignore */ } + } catch (err) { + fail(String(err)); + } + } + + // ── HTML ─────────────────────────────────────────────────────────────────── + + private _getHtml(iconUri: string): string { + return ` + + + + + + + + +
SSH Installation
+
Install OpenClaw on a remote server via SSH
+ + +
+
+
+
+
Hostname or IP
+ +
+
+
Port
+ +
+
+
+
SSH User
+ +
+
+
SSH Key Path (leave blank to use SSH agent)
+ +
+
+ +
+
+ + +
+
+
+ Connecting... +
+
+ + +
+ + + + + +`; + } +} diff --git a/apps/editor/extensions/openclaw-ssh/tsconfig.json b/apps/editor/extensions/openclaw-ssh/tsconfig.json new file mode 100644 index 00000000..c8613a95 --- /dev/null +++ b/apps/editor/extensions/openclaw-ssh/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "lib": ["ES2020"], + "outDir": "./out", + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", ".vscode-test"] +} diff --git a/apps/editor/extensions/openclaw/out/extension.js b/apps/editor/extensions/openclaw/out/extension.js deleted file mode 100644 index 8a81cdcf..00000000 --- a/apps/editor/extensions/openclaw/out/extension.js +++ /dev/null @@ -1,779 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.activate = activate; -exports.deactivate = deactivate; -const vscode = __importStar(require("vscode")); -const os = __importStar(require("os")); -const fs = __importStar(require("fs")); -const path = __importStar(require("path")); -const http = __importStar(require("http")); -const home_1 = require("./panels/home"); -const status_1 = require("./panels/status"); -const config_1 = require("./panels/config"); -const DEFAULT_GATEWAY_PORT = 18789; -function getConfiguredGatewayPort() { - try { - const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json'); - const raw = fs.readFileSync(configPath, 'utf-8'); - const config = JSON.parse(raw); - const gateway = config['gateway']; - const p = gateway?.['port'] ?? config['port'] ?? config['gateway_port'] ?? config['gatewayPort']; - const n = typeof p === 'string' ? parseInt(p, 10) : typeof p === 'number' ? p : NaN; - return Number.isFinite(n) && n > 0 && n < 65536 ? n : DEFAULT_GATEWAY_PORT; - } - catch { - return DEFAULT_GATEWAY_PORT; - } -} -/** Returns true if the OpenClaw web server is reachable. */ -function isWebServerReachable() { - const port = getConfiguredGatewayPort(); - const url = `http://localhost:${port}/`; - return new Promise(resolve => { - const req = http.get(url, { timeout: 3000 }, res => { - res.resume(); - resolve(res.statusCode !== undefined && res.statusCode < 500); - }); - req.on('error', () => resolve(false)); - req.on('timeout', () => { req.destroy(); resolve(false); }); - }); -} -/** - * Writes a complete, authoritative activity-bar container list that keeps - * only Explorer and Search visible and hides everything else (SCM, Debug, - * Extensions, Remote Explorer, etc.). - * - * Writing a FULL list — rather than patching the existing one — is the only - * reliable way to override VS Code's built-in defaults on fresh installs where - * `pinnedViewContainers` is empty. - */ -async function hideActivityBarItems(context) { - // Bumped to V8 — also disables debug-auto-launch and merge-conflict. - const APPLIED_KEY = 'activityBarHiddenConfiguredV8'; - if (context.globalState.get(APPLIED_KEY, false)) { - return; - } - try { - const config = vscode.workspace.getConfiguration(); - // Disable git entirely — users don't need it and it causes the SCM button - // to reappear even after hiding it via pinnedViewContainers. - await config.update('git.enabled', false, vscode.ConfigurationTarget.Global); - await config.update('git.decorations.enabled', false, vscode.ConfigurationTarget.Global); - // Disable built-in extensions that are missing compiled output in this fork - // and throw "Cannot find module" activation errors in the notification area. - for (const ext of [ - 'vscode.git-base', 'vscode.git', 'vscode.github', - 'vscode.typescript-language-features', 'vscode.emmet', - 'vscode.debug-auto-launch', 'vscode.merge-conflict', - ]) { - try { - await vscode.commands.executeCommand('workbench.extensions.disableExtension', ext); - } - catch { /* non-fatal */ } - } - // Complete authoritative list of all standard VS Code activity-bar containers. - // Only Explorer and Search stay visible; everything else is hidden. - const authoritative = [ - { id: 'workbench.view.explorer', pinned: true, visible: true, order: 0 }, - { id: 'workbench.view.search', pinned: true, visible: true, order: 1 }, - { id: 'workbench.view.scm', pinned: false, visible: false, order: 2 }, - { id: 'workbench.view.debug', pinned: false, visible: false, order: 3 }, - { id: 'workbench.view.extensions', pinned: false, visible: false, order: 4 }, - { id: 'workbench.view.remote', pinned: false, visible: false, order: 5 }, - ]; - // Preserve any extra containers the user may have added (e.g. third-party - // extensions) so we don't accidentally remove them. - const existing = config.get('workbench.activityBar.pinnedViewContainers') ?? []; - const knownIds = new Set(authoritative.map(c => c.id)); - for (const c of existing) { - if (!knownIds.has(c.id)) { - authoritative.push({ ...c }); - } - } - await config.update('workbench.activityBar.pinnedViewContainers', authoritative, vscode.ConfigurationTarget.Global); - await context.globalState.update(APPLIED_KEY, true); - } - catch { - // Non-fatal — settings.json defaults already cover most cases. - } -} -/** - * Opens ~/.openclaw as a named workspace — but only if OpenClaw is already installed. - * - * Strategy: - * - ~/.occ is OCcode's internal state directory. It is never opened as a workspace. - * - ~/.openclaw is OpenClaw's directory, created by OpenClaw after it installs. - * We open it as the workspace so users can browse their config files. - * If it doesn't exist yet (pre-install), we do nothing. - * - The .code-workspace file lives in ~/.occ so we don't pollute ~/.openclaw. - * The workspace file points at ~/.openclaw as the folder using an absolute path. - */ -const WORKSPACE_FILENAME = 'My OpenClaw Workspace.code-workspace'; -async function openOpenClawFolder() { - // Ensure ~/.occ exists — OCcode's internal state directory. - const occPath = path.join(os.homedir(), '.occ'); - if (!fs.existsSync(occPath)) { - fs.mkdirSync(occPath, { recursive: true }); - } - // Only open the workspace if OpenClaw is already installed. - const openclawPath = path.join(os.homedir(), '.openclaw'); - if (!fs.existsSync(openclawPath)) { - // OpenClaw not installed — clean up any stale workspace file so VS Code - // doesn't keep opening the workspace with a missing folder on next restart. - const staleWorkspaceFile = path.join(occPath, WORKSPACE_FILENAME); - try { - if (fs.existsSync(staleWorkspaceFile)) { - fs.unlinkSync(staleWorkspaceFile); - } - } - catch { /* non-fatal */ } - return; - } - // Workspace file lives in ~/.occ, points at ~/.openclaw as the folder. - const workspaceFilePath = path.join(occPath, WORKSPACE_FILENAME); - let needsWrite = !fs.existsSync(workspaceFilePath); - if (!needsWrite) { - try { - const parsed = JSON.parse(fs.readFileSync(workspaceFilePath, 'utf-8')); - if (!Array.isArray(parsed?.folders) || parsed.folders.length === 0) { - needsWrite = true; - } - } - catch { - needsWrite = true; - } - } - if (needsWrite) { - fs.writeFileSync(workspaceFilePath, JSON.stringify({ - folders: [{ path: openclawPath }], - settings: { - 'files.exclude': { '*.code-workspace': true }, - 'workbench.sideBar.visible': false, - }, - }, null, '\t')); - } - // If we're already inside this workspace, nothing more to do. - const workspaceFileUri = vscode.Uri.file(workspaceFilePath); - if (vscode.workspace.workspaceFile?.fsPath === workspaceFileUri.fsPath) { - return; - } - // Open the workspace — reloads the window once, then VS Code remembers it. - await vscode.commands.executeCommand('vscode.openFolder', workspaceFileUri); -} -// ── Inference balance status bar ────────────────────────────────────────────── -const BACKEND_BALANCE_KEY = 'occBackendBalanceV1'; // cached backend balance — persists across restarts -const OCC_JWT_KEY = 'occJwtV1'; // JWT stored directly in extension storage — no renderer IPC needed -function initBalanceBar(context) { - // Restore cached backend balance so status bar shows the correct value immediately on startup - const cachedBackendBalance = context.globalState.get(BACKEND_BALANCE_KEY, null); - let backendBalance = cachedBackendBalance; - let displayedBalance = cachedBackendBalance; // tracks what's visually shown - let animTimer; - let backendPollTimer; - let countdownTimer; - // Burn rate tracking — keep last 3 readings to compute $/ms spend rate - const readings = []; - let burnRatePerMs = 0; - const bar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 16); - bar.command = 'openclaw.balance.details'; - bar.name = 'OCC Credits'; - context.subscriptions.push(bar); - function renderBalance(value, counting = false) { - bar.text = `$(credit-card) $${value.toFixed(4)}${counting && burnRatePerMs > 0 ? ' ▼' : ''}`; - const tip = new vscode.MarkdownString(undefined, true); - tip.isTrusted = true; - tip.appendMarkdown(`**OCC Credits**\n\n**$${value.toFixed(4)}** remaining\n\n_[Get More Credits](https://occ.mba.sh/credits)_`); - bar.color = value > 1 ? undefined : value > 0.2 - ? new vscode.ThemeColor('statusBarItem.warningForeground') - : new vscode.ThemeColor('statusBarItem.errorForeground'); - bar.backgroundColor = value > 1 ? undefined : value > 0.2 - ? new vscode.ThemeColor('statusBarItem.warningBackground') - : new vscode.ThemeColor('statusBarItem.errorBackground'); - bar.tooltip = tip; - bar.show(); - // Keep popover in sync with the footer at the same cadence (250ms countdown + animation ticks) - try { - home_1.HomePanel.currentPanel?.postBalanceUpdate(value); - } - catch { /* non-fatal */ } - } - function stopCountdown() { - if (countdownTimer) { - clearInterval(countdownTimer); - countdownTimer = undefined; - } - } - function startCountdown() { - stopCountdown(); - if (burnRatePerMs <= 0 || displayedBalance === null) - return; - countdownTimer = setInterval(() => { - if (displayedBalance === null) - return; - displayedBalance = Math.max(0, displayedBalance - burnRatePerMs * 250); - renderBalance(displayedBalance, true); - }, 250); - } - function animateTo(from, to) { - stopCountdown(); // pause countdown while animating to real value - if (animTimer !== undefined) { - clearInterval(animTimer); - } - const DURATION = 380; - const STEP = 1000 / 60; - const start = Date.now(); - animTimer = setInterval(() => { - const elapsed = Date.now() - start; - const t = Math.min(elapsed / DURATION, 1); - const eased = 1 - Math.pow(1 - t, 3); - const current = from + (to - from) * eased; - displayedBalance = current; - renderBalance(current); - if (t >= 1) { - clearInterval(animTimer); - animTimer = undefined; - displayedBalance = to; - renderBalance(to); - // Resume countdown after animation settles - startCountdown(); - } - }, STEP); - } - function updateBurnRate(newBalance) { - const now = Date.now(); - readings.push({ balance: newBalance, time: now }); - if (readings.length > 3) - readings.shift(); - if (readings.length >= 2) { - const oldest = readings[0]; - const newest = readings[readings.length - 1]; - const delta = oldest.balance - newest.balance; // positive = spending - const elapsed = newest.time - oldest.time; - burnRatePerMs = elapsed > 0 && delta > 0 ? delta / elapsed : 0; - } - } - async function fetchAndUpdateBackendBalance() { - try { - // Read JWT from extension's own globalState first. - // FALLBACK: if extension globalState has no JWT (e.g. user signed in before this session - // synced, or the extension was reloaded), read from occLegacyJwt in VS Code settings and - // backfill extension globalState so future reads work without IPC. - let jwt = context.globalState.get(OCC_JWT_KEY, ''); - if (!jwt) { - try { - const legacyJwt = await vscode.commands.executeCommand('occ.auth.getLegacyJwt'); - if (legacyJwt) { - jwt = legacyJwt; - await context.globalState.update(OCC_JWT_KEY, jwt); - } - } - catch { /* renderer not ready yet — will retry on next poll */ } - } - if (!jwt) { - // Truly not signed in — hide bar entirely and clear any cached balance - if (backendPollTimer) { - clearInterval(backendPollTimer); - backendPollTimer = undefined; - } - if (animTimer !== undefined) { - clearInterval(animTimer); - animTimer = undefined; - } - stopCountdown(); - backendBalance = null; - displayedBalance = null; - readings.length = 0; - burnRatePerMs = 0; - void context.globalState.update(BACKEND_BALANCE_KEY, null); - bar.hide(); - return; - } - const r = await fetch('https://occ.mba.sh/api/v1/me', { headers: { Authorization: `Bearer ${jwt}` } }); - if (r.ok) { - const data = await r.json(); - const newBalance = Number(data.balance_usd) || 0; - const moltpilotKey = data.api_keys?.moltpilotKey ?? ''; - // Guard: only sync back if the JWT wasn't cleared while the fetch was in-flight. - // Without this check, an in-flight poll completing after sign-out would re-log the user in. - const currentJwt = context.globalState.get(OCC_JWT_KEY, ''); - if (currentJwt !== jwt) { - return; - } - // Sync JWT + keys to renderer settings so ocFreeModel works - vscode.commands.executeCommand('occ.auth.setLegacyJwt', jwt); - vscode.commands.executeCommand('occ.auth.setMoltpilotKey', moltpilotKey); - // Cache user profile for the accounts button avatar - vscode.commands.executeCommand('occ.auth.setUserInfo', data.email ?? '', data.picture ?? ''); - updateBurnRate(newBalance); - const prev = displayedBalance ?? newBalance; - backendBalance = newBalance; - void context.globalState.update(BACKEND_BALANCE_KEY, newBalance); - animateTo(prev, newBalance); - // Self-healing: restart the poll timer after every successful fetch so the - // interval resets cleanly and polling survives network blips or missed starts. - startBackendPolling(); - // Report OS platform to backend once per install so the admin dashboard has reliable data. - const OS_REPORTED_KEY = 'occ.osPlatformReported'; - if (!context.globalState.get(OS_REPORTED_KEY)) { - const platformMap = { darwin: 'macOS', win32: 'Windows', linux: 'Linux' }; - const osPlatform = platformMap[process.platform] || process.platform; - fetch('https://occ.mba.sh/api/v1/me/platform', { - method: 'PATCH', - headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' }, - body: JSON.stringify({ os_platform: osPlatform }), - }).then(resp => { - if (resp.ok) { - void context.globalState.update(OS_REPORTED_KEY, true); - } - }).catch(() => { }); - } - } - else if (r.status === 401) { - // JWT expired or invalid — clear it, clear moltpilot key, and hide bar - void context.globalState.update(OCC_JWT_KEY, ''); - vscode.commands.executeCommand('occ.auth.setMoltpilotKey', ''); - if (backendPollTimer) { - clearInterval(backendPollTimer); - backendPollTimer = undefined; - } - stopCountdown(); - backendBalance = null; - displayedBalance = null; - readings.length = 0; - burnRatePerMs = 0; - void context.globalState.update(BACKEND_BALANCE_KEY, null); - bar.hide(); - } - } - catch { /* network error — keep current display, next poll will retry */ } - } - function startBackendPolling() { - if (backendPollTimer) - clearInterval(backendPollTimer); - backendPollTimer = setInterval(() => void fetchAndUpdateBackendBalance(), 5000); - } - // Show immediately if we have a cached balance (signed-in returning user) - if (backendBalance !== null) { - renderBalance(backendBalance); - } - // Fetch immediately on startup — also attempt JWT fallback from renderer settings. - // Small delay so the renderer (IVoidSettingsService) finishes loading before we call - // occ.auth.getLegacyJwt, which reads from it. - setTimeout(() => void fetchAndUpdateBackendBalance(), 2000); - context.subscriptions.push(vscode.commands.registerCommand('openclaw.balance.spend', (_amount) => { - // no-op when signed in (backend tracks balance); kept for API compatibility - }), vscode.commands.registerCommand('openclaw.balance.details', () => { - if (backendBalance !== null) { - vscode.window.showInformationMessage(`OCC Credits · $${backendBalance.toFixed(4)} remaining`, 'Get More Credits').then(sel => { - if (sel === 'Get More Credits') { - vscode.env.openExternal(vscode.Uri.parse('https://occ.mba.sh/credits')); - } - }); - } - }), - // Called whenever JWT changes (sign-in or sign-out) — refreshes display immediately. - // Also used by openclaw.jwt.set as the trigger after updating extension storage. - vscode.commands.registerCommand('openclaw.balance.refresh', () => { - void fetchAndUpdateBackendBalance(); - }), - // Called from the renderer (sidebarActions.ts occ.auth.setLegacyJwt) to sync the JWT - // into extension-host storage so fetchAndUpdateBackendBalance can read it without IPC. - vscode.commands.registerCommand('openclaw.jwt.set', async (token) => { - await context.globalState.update(OCC_JWT_KEY, token ?? ''); - // Stop polling immediately on sign-out so no more in-flight fetches can re-set the JWT - if (!token && backendPollTimer) { - clearInterval(backendPollTimer); - backendPollTimer = undefined; - } - void fetchAndUpdateBackendBalance(); - }), - // Smoke test: verifies JWT, moltpilot key, OCC key, balance, and inference tracking. - // Results open in a new editor tab (Output panel removed from this fork). - vscode.commands.registerCommand('openclaw.smokeTest', async () => { - const lines = []; - const log = (msg) => lines.push(msg); - log('=== OCC Smoke Test ==='); - log(`Time: ${new Date().toISOString()}`); - log(''); - // 1. JWT - let jwt = context.globalState.get(OCC_JWT_KEY, ''); - log(`[1] JWT in extension globalState (occJwtV1): ${jwt ? 'OK present (' + jwt.substring(0, 20) + '...)' : 'MISSING'}`); - // Check what the renderer has for occLegacyJwt — this is what voidSettingsService reads - try { - const legacyJwt = await vscode.commands.executeCommand('occ.auth.getLegacyJwt'); - log(` occLegacyJwt in renderer settings: ${legacyJwt ? 'OK present (' + legacyJwt.substring(0, 20) + '...)' : 'MISSING <-- this causes MoltPilot to use shared key!'}`); - if (!jwt && legacyJwt) { - jwt = legacyJwt; - await context.globalState.update(OCC_JWT_KEY, jwt); - } - } - catch { - log(' occLegacyJwt check: renderer not ready'); - } - if (!jwt) { - lines.push('\nNot signed in — cannot proceed.'); - } - else { - // 2. /api/v1/me — balance + keys (before) - log('\n[2] Calling occ.mba.sh/api/v1/me (before)...'); - let balanceBefore = 0; - let moltpilotKey = ''; - let occKey = ''; - try { - const r = await fetch('https://occ.mba.sh/api/v1/me', { headers: { Authorization: `Bearer ${jwt}` } }); - if (r.ok) { - const d = await r.json(); - balanceBefore = Number(d.balance_usd) || 0; - moltpilotKey = d.api_keys?.moltpilotKey ?? ''; - occKey = d.api_keys?.occKey ?? ''; - log(` Status: 200 OK`); - log(` Email: ${d.email ?? '(not returned)'}`); - log(` Balance: $${balanceBefore.toFixed(6)}`); - log(` MoltpilotKey: ${moltpilotKey ? 'OK ' + moltpilotKey.substring(0, 12) + '...' : 'MISSING'}`); - log(` OccKey: ${occKey ? 'OK ' + occKey.substring(0, 12) + '...' : 'MISSING'}`); - } - else { - log(` HTTP ${r.status} -- JWT may be expired`); - } - } - catch (e) { - log(` Network error: ${e}`); - } - const inferenceTest = async (label, key) => { - log(`\n[${label}] Inference test with ${label} (model: occ-legacy)...`); - try { - const resp = await fetch('https://occ.mba.sh/v1/chat/completions', { - method: 'POST', - headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` }, - body: JSON.stringify({ model: 'occ-legacy', stream: false, messages: [{ role: 'user', content: 'Say: SMOKE_OK' }], max_tokens: 20 }), - }); - const rawText = await resp.text(); - log(` HTTP ${resp.status}`); - log(` Raw (first 500): ${rawText.substring(0, 500)}`); - if (resp.ok) { - try { - const d = JSON.parse(rawText); - log(` Reply: ${(d.choices?.[0]?.message?.content ?? d.choices?.[0]?.text ?? '(no content)').trim()}`); - } - catch { - log(` (response is not JSON — likely SSE stream)`); - } - } - } - catch (e) { - log(` Network error: ${e}`); - } - }; - // 3. MoltPilot key - if (moltpilotKey) { - await inferenceTest('MoltPilot key', moltpilotKey); - } - else { - log('\n[3] SKIP -- no MoltpilotKey'); - } - // 4. OCC key - if (occKey) { - await inferenceTest('OCC key', occKey); - } - else { - log('\n[4] SKIP -- no occKey returned by API'); - } - // 5. Re-check balance after inference calls - log('\n[5] Re-checking balance (after 10s to allow backend to settle)...'); - await new Promise(res => setTimeout(res, 10000)); - try { - const r = await fetch('https://occ.mba.sh/api/v1/me', { headers: { Authorization: `Bearer ${jwt}` } }); - if (r.ok) { - const d = await r.json(); - const balanceAfter = Number(d.balance_usd) || 0; - const delta = balanceBefore - balanceAfter; - log(` Balance before: $${balanceBefore.toFixed(6)}`); - log(` Balance after: $${balanceAfter.toFixed(6)}`); - log(` Delta: ${delta > 0 ? '-$' + delta.toFixed(6) + ' -- usage IS tracked' : delta === 0 ? '$0 -- no change yet (check again in 30s)' : '+$' + Math.abs(delta).toFixed(6) + ' -- balance went up?'}`); - } - } - catch (e) { - log(` Network error: ${e}`); - } - } - log('\n=== Done ==='); - // Open results in a new editor tab - const doc = await vscode.workspace.openTextDocument({ content: lines.join('\n'), language: 'plaintext' }); - await vscode.window.showTextDocument(doc, { preview: false, viewColumn: vscode.ViewColumn.One }); - }), { dispose: () => { if (backendPollTimer) - clearInterval(backendPollTimer); stopCountdown(); } }); - return () => { }; // spend is a no-op — kept so call sites don't break -} -async function activate(context) { - // Inference balance bar (shown at bottom-right, tracks $1.00 free budget). - const spendBalance = initBalanceBar(context); - // Apply hidden activity bar items on first activation on this machine. - await hideActivityBarItems(context); - // Open ~/.openclaw as "My OpenClaw Workspace" (may reload the window once). - await openOpenClawFolder(); - // Close the Explorer sidebar on startup — the user opens it explicitly when needed. - // Use a short delay so the workbench has finished restoring its layout first. - setTimeout(() => { - vscode.commands.executeCommand('workbench.action.closeSidebar'); - }, 500); - // Deep-link URI handler: occode://openclaw.home/auth?token= - // Scheme "occode" is set in product.json "urlProtocol": "occode". - // OCC.MBA.SH fires this redirect after the user signs up / logs in. - context.subscriptions.push(vscode.window.registerUriHandler({ - handleUri(uri) { - if (uri.path === '/auth') { - const params = new URLSearchParams(uri.query); - const token = params.get('token'); - if (token) { - // Store JWT in extension-host storage immediately (no renderer IPC needed). - void context.globalState.update(OCC_JWT_KEY, token).then(() => { - // Also sync to renderer settings service (for chat / other renderer consumers). - vscode.commands.executeCommand('occ.auth.setLegacyJwt', token); - }); - } - } - }, - })); - context.subscriptions.push(vscode.commands.registerCommand('openclaw.home', () => { - home_1.HomePanel.createOrShow(context.extensionUri); - }), vscode.commands.registerCommand('openclaw.configure', async () => { - const reachable = await isWebServerReachable(); - if (reachable) { - const dashInfo = (0, config_1.getDashboardUrl)(); - const url = dashInfo?.url ?? `http://localhost:${getConfiguredGatewayPort()}/`; - await vscode.env.openExternal(vscode.Uri.parse(url)); - } - else { - // Web server not running — ask the AI to start it - const port = getConfiguredGatewayPort(); - const configUrl = `http://localhost:${port}/`; - const message = `The OpenClaw web configuration server is not running at ${configUrl}.\n\n` + - `Please start it now by running the OpenClaw gateway in the terminal:\n` + - `\`\`\`\nopenclaw gateway start\n\`\`\`\n\n` + - `Once it is running, the configuration UI will open at ${configUrl} in your browser.`; - await vscode.commands.executeCommand('void.openChatWithMessage', message); - spendBalance(); - } - }), vscode.commands.registerCommand('openclaw.aiFixConfig', () => { - const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json'); - let configContent; - try { - configContent = fs.readFileSync(configPath, 'utf-8'); - } - catch { - vscode.window.showErrorMessage('openclaw.json not found at ~/.openclaw/openclaw.json. Please create it first.'); - return; - } - const message = `Please review my openclaw.json configuration below, identify any issues ` + - `(syntax errors, missing required fields, invalid values, etc.), ` + - `and provide a corrected version with an explanation of what you changed.\n\n` + - `\`\`\`json\n${configContent}\n\`\`\``; - vscode.commands.executeCommand('void.openChatWithMessage', message); - spendBalance(); - }), vscode.commands.registerCommand('openclaw.install', () => { - void home_1.HomePanel.runInstall(context.extensionUri, process.platform, process.arch, process.env.SHELL ?? ''); - }), vscode.commands.registerCommand('openclaw.openWorkspace', () => { - void openOpenClawFolder(); - }), vscode.commands.registerCommand('openclaw.status', () => { - status_1.StatusPanel.createOrShow(context.extensionUri); - }), vscode.commands.registerCommand('openclaw.configureTUI', async () => { - // On Windows, Electron may not inherit the full user PATH, so resolve the - // openclaw binary the same way the home panel does (via HomePanel._findOpenClawPath). - // Fall back to the plain shell command if the panel hasn't been created yet. - const terminal = vscode.window.createTerminal({ - name: 'OpenClaw Configure', - location: vscode.TerminalLocation.Editor, - }); - terminal.show(); - if (process.platform === 'win32') { - // Use `cmd /c openclaw configure` so Windows resolves .cmd shims correctly - terminal.sendText('cmd /c openclaw configure', true); - } - else { - terminal.sendText('openclaw configure', true); - } - })); - // ── openclaw.runWithSudo — secure sudo tool for MoltPilot ────────────────── - // Shows a native password dialog (no terminal prompt), pipes to sudo -S. - // Called by the run_with_sudo builtin tool in toolsService.ts. - context.subscriptions.push(vscode.commands.registerCommand('openclaw.runWithSudo', async (command, reason) => { - const password = await vscode.window.showInputBox({ - password: true, - prompt: `MoltPilot needs elevated access — ${reason}`, - placeHolder: 'Enter your system password', - ignoreFocusOut: true, - }); - if (!password) - return { result: 'User cancelled the password prompt.', exitCode: 1 }; - return new Promise(resolve => { - const child = require('child_process').spawn('sudo', ['-S', 'bash', '-c', command], { - stdio: ['pipe', 'pipe', 'pipe'], - }); - child.stdin?.write(password + '\n'); - child.stdin?.end(); - let out = ''; - child.stdout?.on('data', (d) => { out += d.toString(); }); - child.stderr?.on('data', (d) => { out += d.toString(); }); - child.on('close', (code) => resolve({ result: out.trim() || '(no output)', exitCode: code ?? 1 })); - child.on('error', (err) => resolve({ result: err.message, exitCode: 1 })); - }); - })); - // ── Anonymous install ping (fires once per install, no personal data) ── - const PING_KEY = 'aptabasePingedV1'; - if (!context.globalState.get(PING_KEY)) { - void context.globalState.update(PING_KEY, true); - const osName = process.platform === 'darwin' ? 'macOS' : process.platform === 'win32' ? 'Windows' : 'Linux'; - const version = vscode.extensions.getExtension('openclaw.home')?.packageJSON?.version ?? 'unknown'; - fetch('https://api.aptabase.com/v0/events', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'App-Key': 'A-US-4013869858', - }, - body: JSON.stringify({ - timestamp: new Date().toISOString(), - sessionId: Math.random().toString(36).slice(2), - eventName: 'install', - systemProps: { - osName, - appVersion: version, - sdkVersion: 'manual-1.0', - }, - props: {}, - }), - }).catch(() => { }); // silent — never block the app - } - // ── System status for MoltPilot system prompt ────────────────────────────── - context.subscriptions.push(vscode.commands.registerCommand('openclaw.getSystemStatus', async () => { - const homedir = os.homedir(); - const configPath = path.join(homedir, '.openclaw', 'openclaw.json'); - const installed = fs.existsSync(configPath); - let config = {}; - if (installed) { - try { - config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - } - catch { } - } - // Determine gateway port - const port = (() => { - const p = config['port'] ?? config['gateway_port'] ?? config['gatewayPort']; - if (p === undefined) - return 18789; - const n = Number(p); - return Number.isFinite(n) && n > 0 && n < 65536 ? n : 18789; - })(); - // Check if gateway is reachable - const gatewayRunning = await new Promise(resolve => { - const req = http.get(`http://localhost:${port}/`, { timeout: 2000 }, res => { - res.resume(); - resolve(res.statusCode !== undefined && res.statusCode < 500); - }); - req.on('error', () => resolve(false)); - req.on('timeout', () => { req.destroy(); resolve(false); }); - }); - // Check if an AI provider/model is configured - const hasAiModel = installed && !!(config['auth_choice'] || config['openai_api_key'] || config['anthropic_api_key'] || - config['openrouter_api_key'] || config['gemini_api_key'] || config['api_key'] || - config['custom_api_key'] || config['model'] || config['provider']); - // Check connected messaging channels (openclaw.json may have a 'channels' object) - const channelsRaw = config['channels']; - let channelNames = []; - if (channelsRaw && typeof channelsRaw === 'object' && !Array.isArray(channelsRaw)) { - channelNames = Object.entries(channelsRaw) - .filter(([, v]) => { - if (!v) - return false; - // If the channel is an object, require enabled !== false - if (typeof v === 'object' && v['enabled'] === false) - return false; - return true; - }) - .map(([k]) => k); - } - const hasChannels = channelNames.length > 0; - // Check if agents are configured — read AGENTS.md from workspace dir - const workspaceDir = (() => { - const fallback = path.join(homedir, '.openclaw', 'workspace'); - try { - const ws = config['workspace']; - if (typeof ws === 'string' && ws.trim()) { - return ws.startsWith('~') ? path.join(homedir, ws.slice(1)) : ws; - } - } - catch { } - return fallback; - })(); - let agentNames = []; - try { - const content = fs.readFileSync(path.join(workspaceDir, 'AGENTS.md'), 'utf-8'); - // Parse agent names from markdown headings (# or ##), skip generic "Agents" title - agentNames = content.split('\n') - .filter(l => /^#{1,2}\s+\S/.test(l)) - .map(l => l.replace(/^#{1,2}\s+/, '').trim()) - .filter(n => n.length > 0 && !/^agents?$/i.test(n)); - } - catch { } - const hasAgents = agentNames.length > 0; - return { installed, gatewayRunning, hasAgents, agentNames, hasAiModel, hasChannels, channelNames }; - })); - // ── Walkthrough commands ──────────────────────────────────────────────────── - context.subscriptions.push(vscode.commands.registerCommand('occ.onboarding.chooseMoltPilot', async () => { - await context.globalState.update('occ.aiPreference', 'moltpilot'); - vscode.env.openExternal(vscode.Uri.parse('https://occ.mba.sh/signup?ref=occ-editor')); - }), vscode.commands.registerCommand('occ.onboarding.chooseBYOK', async () => { - const providers = ['Anthropic Claude', 'OpenAI', 'OpenRouter', 'Google Gemini', 'Ollama']; - const pick = await vscode.window.showQuickPick(providers, { - placeHolder: 'Which provider would you like to use?', - }); - if (pick) { - await context.globalState.update('occ.aiPreference', pick.toLowerCase().replace(/\s+/g, '-')); - vscode.window.showInformationMessage(`${pick} selected — you'll enter your API key when you install OpenClaw from OCC Home.`); - } - }), vscode.commands.registerCommand('occ.onboarding.darkTheme', async () => { - await vscode.workspace.getConfiguration('workbench').update('colorTheme', 'OpenClaw Dark', vscode.ConfigurationTarget.Global); - }), vscode.commands.registerCommand('occ.onboarding.lightTheme', async () => { - await vscode.workspace.getConfiguration('workbench').update('colorTheme', 'OpenClaw Light', vscode.ConfigurationTarget.Global); - })); - // Auto-show OCC Home on startup (after activation settles). - setTimeout(() => { - home_1.HomePanel.createOrShow(context.extensionUri); - }, 500); -} -function deactivate() { - (0, config_1.stopConfigProxy)(); -} -//# sourceMappingURL=extension.js.map \ No newline at end of file diff --git a/apps/editor/extensions/openclaw/out/extension.js.map b/apps/editor/extensions/openclaw/out/extension.js.map deleted file mode 100644 index e68c63d5..00000000 --- a/apps/editor/extensions/openclaw/out/extension.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/apps/editor/extensions/openclaw/out/panels/config-path.js b/apps/editor/extensions/openclaw/out/panels/config-path.js deleted file mode 100644 index 9c084fc3..00000000 --- a/apps/editor/extensions/openclaw/out/panels/config-path.js +++ /dev/null @@ -1,79 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.overrideConfigPath = overrideConfigPath; -exports.resolveConfigPath = resolveConfigPath; -exports.getDefaultConfigPath = getDefaultConfigPath; -const os = __importStar(require("os")); -const path = __importStar(require("path")); -const vscode = __importStar(require("vscode")); -let customPath; -const DEFAULT_PATH = path.join(os.homedir(), ".openclaw", "openclaw.json"); -function expandHome(targetPath) { - if (!targetPath) - return targetPath; - if (targetPath.startsWith("~")) { - return path.join(os.homedir(), targetPath.slice(1)); - } - return targetPath; -} -function normalizePath(targetPath) { - if (!targetPath) - return targetPath; - const expanded = expandHome(targetPath); - if (process.platform === "win32" && expanded) { - return expanded.replace(/\\/g, "/"); - } - return expanded; -} -function overrideConfigPath(p) { - customPath = p ? normalizePath(p) : undefined; -} -function resolveConfigPath() { - if (customPath) - return customPath; - const fromSettings = vscode.workspace.getConfiguration("openclaw").get("configPath"); - if (fromSettings) { - customPath = normalizePath(fromSettings); - if (customPath) { - return customPath; - } - } - return DEFAULT_PATH; -} -function getDefaultConfigPath() { - return DEFAULT_PATH; -} -//# sourceMappingURL=config-path.js.map \ No newline at end of file diff --git a/apps/editor/extensions/openclaw/out/panels/config-path.js.map b/apps/editor/extensions/openclaw/out/panels/config-path.js.map deleted file mode 100644 index 85e41f33..00000000 --- a/apps/editor/extensions/openclaw/out/panels/config-path.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"config-path.js","sourceRoot":"","sources":["../../src/panels/config-path.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwBA,gDAEC;AAED,8CAUC;AAED,oDAEC;AA1CD,uCAAyB;AACzB,2CAA6B;AAC7B,+CAAiC;AAEjC,IAAI,UAA8B,CAAC;AACnC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,eAAe,CAAC,CAAC;AAE3E,SAAS,UAAU,CAAC,UAA8B;IAChD,IAAI,CAAC,UAAU;QAAE,OAAO,UAAU,CAAC;IACnC,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,aAAa,CAAC,UAA8B;IACnD,IAAI,CAAC,UAAU;QAAE,OAAO,UAAU,CAAC;IACnC,MAAM,QAAQ,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACxC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC7C,OAAO,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAgB,kBAAkB,CAAC,CAAqB;IACtD,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAChD,CAAC;AAED,SAAgB,iBAAiB;IAC/B,IAAI,UAAU;QAAE,OAAO,UAAU,CAAC;IAClC,MAAM,YAAY,GAAG,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,GAAG,CAAS,YAAY,CAAC,CAAC;IAC7F,IAAI,YAAY,EAAE,CAAC;QACjB,UAAU,GAAG,aAAa,CAAC,YAAY,CAAC,CAAC;QACzC,IAAI,UAAU,EAAE,CAAC;YACf,OAAO,UAAU,CAAC;QACpB,CAAC;IACH,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,SAAgB,oBAAoB;IAClC,OAAO,YAAY,CAAC;AACtB,CAAC"} \ No newline at end of file diff --git a/apps/editor/extensions/openclaw/out/panels/config.js b/apps/editor/extensions/openclaw/out/panels/config.js deleted file mode 100644 index 44543a22..00000000 --- a/apps/editor/extensions/openclaw/out/panels/config.js +++ /dev/null @@ -1,605 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ConfigPanel = void 0; -exports.getOrStartConfigProxy = getOrStartConfigProxy; -exports.stopConfigProxy = stopConfigProxy; -exports.getDashboardUrl = getDashboardUrl; -const vscode = __importStar(require("vscode")); -const http = __importStar(require("http")); -const os = __importStar(require("os")); -const path = __importStar(require("path")); -const DEFAULT_GATEWAY_PORT = 18789; -// ── Proxy ───────────────────────────────────────────────────────────────────── -// Reverse-proxy that strips X-Frame-Options and CSP frame-ancestors so the -// page can load inside a VS Code webview iframe. -let _proxyServer; -let _proxyPort; -let _proxyTargetPort = DEFAULT_GATEWAY_PORT; -function getOrStartConfigProxy(targetPort = DEFAULT_GATEWAY_PORT) { - // Restart proxy if target port changed - if (_proxyServer && _proxyPort && _proxyTargetPort === targetPort) - return Promise.resolve(_proxyPort); - if (_proxyServer) { - _proxyServer.close(); - _proxyServer = undefined; - _proxyPort = undefined; - } - _proxyTargetPort = targetPort; - return new Promise((resolve, reject) => { - const server = http.createServer((req, res) => { - const targetPath = req.url ?? '/'; - const options = { - hostname: '127.0.0.1', - port: _proxyTargetPort, - path: targetPath, - method: req.method, - headers: { ...req.headers, host: `localhost:${_proxyTargetPort}` }, - }; - const proxyReq = http.request(options, proxyRes => { - const headers = { ...proxyRes.headers }; - // Remove headers that prevent iframe embedding. - delete headers['x-frame-options']; - const csp = headers['content-security-policy']; - if (typeof csp === 'string') { - const cleaned = csp - .split(';') - .map(d => d.trim()) - .filter(d => !/^frame-ancestors/i.test(d)) - .join('; ') - .trim() - .replace(/;$/, ''); - if (cleaned) { - headers['content-security-policy'] = cleaned; - } - else { - delete headers['content-security-policy']; - } - } - const contentType = headers['content-type'] ?? ''; - const isHtml = contentType.includes('text/html'); - if (isHtml) { - // Collect the full response so we can inject the clipboard bridge script. - const chunks = []; - proxyRes.on('data', (chunk) => chunks.push(chunk)); - proxyRes.on('end', () => { - let body = Buffer.concat(chunks).toString('utf-8'); - // Clipboard bridge injected into the gateway page. - // KEY INSIGHT: keyboard events inside a focused cross-origin iframe never - // bubble to the parent document. So the iframe must intercept its own keys - // and use postMessage to ask the parent (outer webview) for clipboard access, - // since navigator.clipboard requires HTTPS and the gateway runs on HTTP. - const bridge = ` - -`; - } - _loadingHtml() { - return `Loading OpenClaw Configuration…`; - } - _errorHtml(msg) { - return `

Cannot connect to OpenClaw gateway

Make sure it is running (port ${DEFAULT_GATEWAY_PORT})

${msg}

`; - } -} -exports.ConfigPanel = ConfigPanel; -//# sourceMappingURL=config.js.map \ No newline at end of file diff --git a/apps/editor/extensions/openclaw/out/panels/config.js.map b/apps/editor/extensions/openclaw/out/panels/config.js.map deleted file mode 100644 index b69413e3..00000000 --- a/apps/editor/extensions/openclaw/out/panels/config.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/panels/config.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/apps/editor/extensions/openclaw/out/panels/home.js b/apps/editor/extensions/openclaw/out/panels/home.js deleted file mode 100644 index 576f8137..00000000 --- a/apps/editor/extensions/openclaw/out/panels/home.js +++ /dev/null @@ -1,4520 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.HomePanel = void 0; -const vscode = __importStar(require("vscode")); -const cp = __importStar(require("child_process")); -const fs = __importStar(require("fs")); -const http = __importStar(require("http")); -const https = __importStar(require("https")); -const os = __importStar(require("os")); -const path = __importStar(require("path")); -// ── Persistent diagnostics log ──────────────────────────────────────────────── -const LOG_PATH = path.join(os.homedir(), '.openclaw', 'occ-home.log'); -const LOG_MAX_BYTES = 512 * 1024; // 500 KB — rotate when exceeded -/** - * Append timestamped text to ~/.openclaw/occ-home.log. - * Creates the file (and directory) on first use. Rotates by dropping the - * oldest half of lines when the file exceeds LOG_MAX_BYTES. Never throws. - */ -// Strip ANSI/VT escape sequences (e.g. colour codes from npm/openclaw output) -const _ansiRe = /\x1b(\[[0-9;]*[A-Za-z]|[^[])/g; -function writeLog(text) { - try { - const dir = path.dirname(LOG_PATH); - if (!fs.existsSync(dir)) - fs.mkdirSync(dir, { recursive: true }); - // Rotate if oversized - if (fs.existsSync(LOG_PATH) && fs.statSync(LOG_PATH).size > LOG_MAX_BYTES) { - const lines = fs.readFileSync(LOG_PATH, 'utf-8').split('\n'); - fs.writeFileSync(LOG_PATH, lines.slice(Math.floor(lines.length / 2)).join('\n'), 'utf-8'); - } - const ts = new Date().toISOString(); - const clean = text.replace(_ansiRe, ''); - // Stamp every non-empty line; leave blank lines unstamped - const stamped = clean - .split('\n') - .map(l => (l.trim() ? `[${ts}] ${l}` : l)) - .join('\n'); - fs.appendFileSync(LOG_PATH, stamped, 'utf-8'); - } - catch { /* non-fatal */ } -} -// ── OCC Legacy model constants ──────────────────────────────────────────────── -const OCC_LEGACY_MODEL_ID = 'occ-legacy'; -const OCC_LEGACY_MODEL_NAME = 'occ-legacy'; -const OCC_LEGACY_BASE_URL = 'https://occ.mba.sh/v1'; -const OCC_LEGACY_API = 'openai-completions'; -const OCC_LEGACY_COST = { - input: 0.0000006, - output: 0.000003, - cacheRead: 0.0000001, - cacheWrite: 0, -}; -const OCC_LEGACY_CONTEXT_WINDOW = 262144; -const OCC_LEGACY_MAX_TOKENS = 262144; -/** - * Resolves the directory where OpenClaw stores its workspace files - * (AGENTS.md, IDENTITY.md, USER.md, TOOLS.md, MEMORY.md, SOUL.md, HEARTBEAT.md). - * - * Reads the `workspace` field from ~/.openclaw/openclaw.json if present. - * Falls back to ~/.openclaw/workspace/ if the field is absent or unreadable. - * Expands a leading ~ to the home directory. - */ -function getOpenClawWorkspaceDir() { - const fallback = path.join(os.homedir(), '.openclaw', 'workspace'); - try { - const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json'); - const raw = fs.readFileSync(configPath, 'utf-8'); - const config = JSON.parse(raw); - const ws = config['workspace']; - if (typeof ws === 'string' && ws.trim()) { - return ws.startsWith('~') - ? path.join(os.homedir(), ws.slice(1)) - : ws; - } - } - catch { - // openclaw.json missing or unreadable — use fallback - } - return fallback; -} -class HomePanel { - constructor(panel, extensionUri) { - this._disposables = []; - this._commandAction = null; - this._sidebarOpen = false; // tracks chat sidebar open state across webview reloads - this._pollTick = 0; - this._lastJwt = ''; - this._lastInstalledVersion = null; - this._autoUpdateTriggered = false; // fire at most once per panel session - this._closeSidebarOnGatewayStart = false; // close sidebar once gateway reaches running after first install - this._panel = panel; - this._extensionUri = extensionUri; - this._outputChannel = vscode.window.createOutputChannel('OpenClaw Gateway'); - const iconUri = this._panel.webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'media', 'icon.png')); - this._panel.webview.html = this._getLoadingHtml(iconUri.toString()); - void this._update(); - this._panel.onDidDispose(() => this.dispose(), null, this._disposables); - // Re-check installation whenever the panel becomes visible again. - this._panel.onDidChangeViewState(e => { - if (e.webviewPanel.visible) { - void this._update(); - } - }, null, this._disposables); - // Watch ~/.openclaw/openclaw.json for when OpenClaw first initialises. - const configWatcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.file(path.join(os.homedir(), '.openclaw')), 'openclaw.json'), false, true, false); - configWatcher.onDidCreate(() => void this._update(), null, this._disposables); - configWatcher.onDidDelete(() => void this._update(), null, this._disposables); - this._disposables.push(configWatcher); - // Also watch home dir for ~/.openclaw itself being created (npm install done). - const homeWatcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.file(os.homedir()), '.openclaw'), false, true, false); - homeWatcher.onDidCreate(() => void this._update(), null, this._disposables); - homeWatcher.onDidDelete(() => void this._update(), null, this._disposables); - this._disposables.push(homeWatcher); - this._panel.webview.onDidReceiveMessage(msg => { - if (msg.command === 'gatewayAction') { - void this._handleGatewayAction(msg.action); - } - else if (msg.command === 'checkVersion') { - void this._checkLatestVersion(); - } - else if (msg.command === 'runUpdate') { - void vscode.commands.executeCommand('void.openChatWithMessage', 'Please run `openclaw update` to upgrade OpenClaw to the latest version.', 'agent'); - } - else if (msg.command === 'runSetup') { - void this._runSetup(msg); - } - else if (msg.command === 'autoSetupSkipped') { - setTimeout(() => { - HomePanel.refresh(); - void vscode.commands.executeCommand('openclaw.openWorkspace'); - setTimeout(() => { - vscode.commands.executeCommand('void.openChatWithMessage', 'Run `openclaw gateway start` to start the OpenClaw gateway.', 'agent'); - }, 1000); - }, 500); - } - else if (msg.command === 'verifyCliBeforeSetup') { - // After install, verify the CLI is actually findable before auto-configuring. - // First: use the path we captured immediately after npm install (most reliable). - const storedPath = HomePanel._installedCliPath; - if (storedPath && fs.existsSync(storedPath)) { - try { - this._panel.webview.postMessage({ type: 'proceedAutoSetup' }); - } - catch { } - return; - } - // Fallback: full PATH-based search. - void this._testOpenClawCli().then(result => { - if (result.ok) { - try { - this._panel.webview.postMessage({ type: 'proceedAutoSetup' }); - } - catch { } - } - else { - try { - this._panel.webview.postMessage({ - type: 'installLog', - text: '\n⚠️ OpenClaw was installed but could not be found in PATH.\n' + - ' Please restart OCCode to pick up the new PATH.\n' - }); - } - catch { } - void this._update(); - } - }); - } - else if (msg.command === 'sudoPassword') { - // Password modal submitted or cancelled from the webview. - HomePanel._pendingPasswordResolve?.(msg.password); - HomePanel._pendingPasswordResolve = undefined; - } - else if (msg.command === 'toggleChat') { - const cmd = this._sidebarOpen ? 'void.sidebar.close' : 'void.sidebar.open'; - void vscode.commands.executeCommand(cmd).then(async () => { - // Let the sidebar finish opening/closing, then read real state. - await new Promise(r => setTimeout(r, 150)); - this._sidebarOpen = await vscode.commands.executeCommand('void.sidebar.isVisible').then(v => !!v, () => this._sidebarOpen); - try { - this._panel.webview.postMessage({ type: 'chatState', open: this._sidebarOpen }); - } - catch { } - }); - } - else if (msg.command === 'signIn') { - void vscode.env.openExternal(vscode.Uri.parse('https://occ.mba.sh/login?ref=occ-editor')); - } - else if (msg.command === 'openDashboard') { - void vscode.env.openExternal(vscode.Uri.parse('https://occ.mba.sh/dashboard')); - } - else if (msg.command === 'signOut') { - // Clear JWT, moltpilot key, and extension-host globalState - void vscode.commands.executeCommand('occ.auth.setLegacyJwt', ''); - void vscode.commands.executeCommand('occ.auth.setMoltpilotKey', ''); - void vscode.commands.executeCommand('openclaw.jwt.set', ''); - } - else if (msg.command === 'openUrl') { - vscode.env.openExternal(vscode.Uri.parse(msg.url)); - } - else if (msg.command === 'openConfigFile') { - const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json'); - vscode.commands.executeCommand('vscode.open', vscode.Uri.file(configPath)); - } - else if (msg.command === 'openLogs') { - if (!fs.existsSync(LOG_PATH)) { - vscode.window.showInformationMessage('No log file yet — logs are created when install or setup runs.'); - } - else { - vscode.commands.executeCommand('vscode.open', vscode.Uri.file(LOG_PATH)); - } - } - else if (msg.command === 'openWorkspaceFile') { - const allowed = new Set(['AGENTS.md', 'IDENTITY.md', 'USER.md', 'MEMORY.md', 'SOUL.md', 'HEARTBEAT.md']); - const file = msg.file; - if (!allowed.has(file)) - return; - const workspaceDir = getOpenClawWorkspaceDir(); - const filePath = path.join(workspaceDir, file); - if (!fs.existsSync(filePath)) { - if (file === 'MEMORY.md') { - // Auto-create MEMORY.md with a scaffolded long-term agent memory template - const scaffold = [ - '# Agent Long-Term Memory', - '', - 'This file is the persistent long-term memory for the AI agent embedded in OCcode.', - 'The agent reads this file at the start of every session to recall important context,', - 'preferences, and decisions made in previous conversations.', - '', - '---', - '', - '## About This File', - '', - '- **Purpose**: Stores facts, decisions, and context that should persist across agent sessions.', - '- **Owner**: You — edit freely to add, update, or remove entries.', - '- **Format**: Plain Markdown. Keep entries concise and well-organised.', - '', - '## User Preferences', - '', - '', - '', - '## Project Context', - '', - '', - '', - '## Recurring Solutions', - '', - '', - '', - '## Notes', - '', - '', - ].join('\n'); - fs.mkdirSync(workspaceDir, { recursive: true }); - fs.writeFileSync(filePath, scaffold, 'utf8'); - } - else { - vscode.window.showWarningMessage(`${file} not found in ${workspaceDir}. OpenClaw may not have initialised its workspace yet.`); - return; - } - } - vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath)); - } - else if (msg.command === 'openclaw.uninstall') { - void this._runUninstall(); - } - else if (msg.command === 'openclaw.setupBetterMemory') { - void this._runCassSetup(); - } - else if (msg.command === 'void.openChatWithMessage') { - const args = msg.args; - if (args && args.length > 0) { - void vscode.commands.executeCommand('void.openChatWithMessage', args[0], 'agent'); - } - } - else if (msg.command) { - vscode.commands.executeCommand(msg.command); - } - }, null, this._disposables); - } - static createOrShow(extensionUri) { - if (HomePanel.currentPanel) { - HomePanel.currentPanel._panel.reveal(); - return; - } - const panel = vscode.window.createWebviewPanel('openclawHome', 'OCC Home', vscode.ViewColumn.One, { enableScripts: true, localResourceRoots: [ - vscode.Uri.joinPath(extensionUri, 'media'), - ] }); - HomePanel.currentPanel = new HomePanel(panel, extensionUri); - } - /** Push a live balance update to the webview popover — called from extension.ts balance poller. */ - postBalanceUpdate(amount) { - try { - this._panel.webview.postMessage({ type: 'balanceUpdate', amount }); - } - catch { /* non-fatal */ } - } - /** Re-run CLI detection and redraw — called after install completes. */ - static refresh() { - if (HomePanel.currentPanel) { - void HomePanel.currentPanel._update(); - } - } - /** - * Fully silent install — no terminal is ever opened. - * Output is streamed line-by-line to the home panel webview. - * If sudo is needed, a VS Code password dialog is shown. - * On any failure the AI is invoked immediately with full context. - */ - static async runInstall(extensionUri, platform, arch, shell) { - HomePanel.createOrShow(extensionUri); - const panel = HomePanel.currentPanel; - if (!panel) - return; - const post = (msg) => { try { - panel._panel.webview.postMessage(msg); - } - catch { } }; - let fullLog = ''; - const tee = (text) => { fullLog += text; post({ type: 'installLog', text }); writeLog(text); }; - writeLog(`\n=== runInstall START platform=${platform} arch=${arch} ===\n`); - post({ type: 'installState', state: 'running' }); - const env = panel._buildExecEnv(); - const isPermError = (s) => /EACCES|permission denied|EPERM|not permitted|Need sudo access|needs to be an Administrator/i.test(s); - // Quick command check helper (no output, just exit code) - const cmdExists = (cmd) => new Promise(resolve => cp.exec(cmd, { env, timeout: 5000, windowsHide: true }, err => resolve(!err))); - // Spawn a command silently and stream output to the panel. - const runCaptured = (cmd, args, opts = {}) => new Promise(resolve => { - const child = cp.spawn(cmd, args, { env, stdio: ['ignore', 'pipe', 'pipe'], ...opts }); - child.stdout?.on('data', (d) => tee(d.toString())); - child.stderr?.on('data', (d) => tee(d.toString())); - child.on('close', code => resolve({ code: code ?? 1 })); - child.on('error', err => { tee(`\nError: ${err.message}\n`); resolve({ code: 1 }); }); - }); - // Ask for sudo password via in-webview modal, cache with `sudo -S -v`, return success. - const cacheSudo = async (_prompt) => { - const password = await new Promise(resolve => { - HomePanel._pendingPasswordResolve = resolve; - post({ type: 'requestPassword' }); - }); - if (!password) - return false; - tee('Verifying credentials...\n'); - return new Promise(resolve => { - const child = cp.spawn('sudo', ['-S', '-v'], { env, stdio: ['pipe', 'pipe', 'pipe'] }); - child.stdin?.write(password + '\n'); - child.stdin?.end(); - child.on('close', code => resolve(code === 0)); - child.on('error', () => resolve(false)); - }); - }; - // Fix ~/.openclaw ownership after any sudo-based install. - // Uses `sudo -n` (non-interactive) which succeeds as long as the cached sudo session - // from cacheSudo() is still valid (~15 min default). Safe to call even when sudo - // wasn't used — chown is a no-op when the directory is already user-owned. - const fixOpenclawPermissions = async () => { - if (platform === 'win32') - return; - const openclawDir = path.join(os.homedir(), '.openclaw'); - if (!fs.existsSync(openclawDir)) - return; - try { - const username = os.userInfo().username; - tee('Fixing .openclaw folder ownership...\n'); - await runCaptured('sudo', ['-n', 'chown', '-R', username, openclawDir]); - await runCaptured('chmod', ['700', openclawDir]); - tee('✅ Permissions set (700, owned by you)\n'); - } - catch { /* non-fatal */ } - }; - const failCancelled = () => { - post({ type: 'installState', state: 'cancelled' }); - }; - const fail = async () => { - writeLog('=== runInstall END failed ===\n'); - post({ type: 'installState', state: 'failed' }); - const platformDesc = platform === 'darwin' ? 'macOS' : platform === 'win32' ? 'Windows' : `Linux (${arch})`; - await vscode.commands.executeCommand('void.openChatWithMessage', [ - `OpenClaw installation failed on **${platformDesc}**.`, - ``, `**System info:**`, - `- Node.js: \`${process.version}\``, - `- Shell: \`${shell || 'unknown'}\``, - ``, `**Full output:**`, `\`\`\``, fullLog.trim(), `\`\`\``, ``, - `Please diagnose what went wrong and provide exact steps to fix it on this platform.`, - `If Node.js or npm is missing, explain how to install them first.`, - ].join('\n')); - void vscode.commands.executeCommand('openclaw.balance.spend'); - }; - // After a successful npm install, find the binary's absolute path immediately - // (before PATH settles) so verifyCliBeforeSetup can use it directly. - const captureInstalledPath = async () => { - if (platform === 'win32') - return; // Windows uses cmd shims; handled by _testOpenClawCli - const candidates = []; - // 1. Ask npm where its global prefix is (same env as the install used) - const prefixResult = await new Promise(resolve => { - cp.exec('npm config get prefix', { env, timeout: 5000 }, (err, stdout) => resolve(err ? '' : (stdout || '').trim())); - }); - if (prefixResult) - candidates.push(path.join(prefixResult, 'bin', 'openclaw')); - // 2. Hard-coded common macOS/Linux paths - const home = os.homedir(); - candidates.push('/usr/local/bin/openclaw', '/opt/homebrew/bin/openclaw', path.join(home, '.local', 'bin', 'openclaw'), path.join(home, '.npm-global', 'bin', 'openclaw'), path.join(home, '.openclaw', 'bin', 'openclaw')); - // 3. nvm versions (newest first) - const nvmVersionsDir = path.join(home, '.nvm', 'versions', 'node'); - if (fs.existsSync(nvmVersionsDir)) { - fs.readdirSync(nvmVersionsDir) - .sort((a, b) => b.localeCompare(a, undefined, { numeric: true })) - .forEach(ver => candidates.push(path.join(nvmVersionsDir, ver, 'bin', 'openclaw'))); - } - const found = candidates.find(c => c && fs.existsSync(c)); - if (found) { - HomePanel._installedCliPath = found; - tee(` ✓ Binary found at ${found}\n`); - } - }; - const succeed = async () => { - await captureInstalledPath(); - tee('\n✅ Installed successfully!\n'); - writeLog('=== runInstall END ok ===\n'); - post({ type: 'installState', state: 'done' }); - // Webview drives the post-install navigation via autoSetupSkipped or wizardLog done - }; - // ══════════════════════════════════════════════════════════════════════════ - // PREREQUISITE CHECKS + PROACTIVE SUDO — run BEFORE any install attempt - // ══════════════════════════════════════════════════════════════════════════ - tee('Checking prerequisites...\n'); - if (platform === 'darwin') { - // macOS: check Xcode Command Line Tools - const xcodeOk = await new Promise(resolve => cp.exec('xcode-select -p', { env, timeout: 5000 }, (err, stdout) => { - resolve(!err && !!stdout?.toString().trim()); - })); - if (!xcodeOk) { - post({ type: 'xcodeRequired' }); - return; - } - tee(' ✓ Xcode Command Line Tools\n'); - } - if (platform === 'win32') { - // Windows: check Node.js exists; auto-install if missing (no UAC required) - let nodeOk = await cmdExists('node --version'); - if (!nodeOk) { - const nodeVersion = '20.18.2'; - const nodeArch = arch === 'arm64' ? 'arm64' : 'x64'; - const zipName = `node-v${nodeVersion}-win-${nodeArch}.zip`; - const zipUrl = `https://nodejs.org/dist/v${nodeVersion}/${zipName}`; - const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'); - const installDir = path.join(localAppData, 'Programs', 'nodejs'); - const tmpZip = path.join(os.tmpdir(), zipName); - const tmpExtract = path.join(os.tmpdir(), `occ-node-${Date.now()}`); - tee(` ⚠ Node.js not found — downloading v${nodeVersion} (${nodeArch})...\n`); - const dlR = await runCaptured('powershell', [ - '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', - `$ProgressPreference='SilentlyContinue'; Invoke-WebRequest -UseBasicParsing '${zipUrl}' -OutFile '${tmpZip}'`, - ], { windowsHide: true, shell: true }); - if (dlR.code !== 0) { - tee(' ❌ Failed to download Node.js. Check your internet connection.\n'); - await fail(); - return; - } - tee(` Extracting...\n`); - // Pre-compute the inner folder name (avoids | pipe which cmd.exe would intercept) - const innerDir = path.join(tmpExtract, `node-v${nodeVersion}-win-${nodeArch}`); - const exR = await runCaptured('powershell', [ - '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', - `$ProgressPreference='SilentlyContinue'; ` + - `Expand-Archive -Path '${tmpZip}' -DestinationPath '${tmpExtract}' -Force; ` + - `if (Test-Path '${installDir}') { Remove-Item '${installDir}' -Recurse -Force }; ` + - `Move-Item '${innerDir}' '${installDir}'`, - ], { windowsHide: true, shell: true }); - try { - fs.unlinkSync(tmpZip); - } - catch { } - if (exR.code !== 0) { - tee(' ❌ Failed to extract Node.js.\n'); - await fail(); - return; - } - // Make new Node.js dir visible to all subsequent commands in this install run - env.PATH = [installDir, env.PATH || ''].filter(Boolean).join(';'); - env.Path = env.PATH; - nodeOk = await cmdExists('node --version'); - if (!nodeOk) { - tee(' ❌ Node.js install did not complete properly.\n'); - await fail(); - return; - } - tee(` ✓ Node.js v${nodeVersion} installed to ${installDir}\n`); - } - else { - tee(' ✓ Node.js found\n'); - } - } - const npmOk = await cmdExists('npm --version'); - if (npmOk) { - tee(' ✓ npm found\n'); - } - else if (platform === 'win32') { - tee(' ❌ npm not found after Node.js install — unexpected.\n'); - await fail(); - return; - } - else { - tee(' ⚠ npm not found — will attempt to install Node.js\n'); - } - // Proactive sudo detection (macOS / Linux only) - let sudoCached = false; - if (platform !== 'win32') { - let needsSudo = false; - if (npmOk) { - const prefixResult = await new Promise(resolve => cp.exec('npm config get prefix', { env, timeout: 5000 }, (err, stdout) => resolve(err ? '' : stdout?.toString().trim() || ''))); - if (prefixResult) { - try { - const gBin = path.join(prefixResult, 'bin'); - const gLib = path.join(prefixResult, 'lib'); - if (fs.existsSync(gBin)) - fs.accessSync(gBin, fs.constants.W_OK); - if (fs.existsSync(gLib)) - fs.accessSync(gLib, fs.constants.W_OK); - } - catch { - needsSudo = true; - } - } - } - else { - // No npm — will need to install Node.js, likely needs sudo - for (const dir of ['/usr/local/bin', '/usr/local/lib']) { - try { - if (fs.existsSync(dir)) - fs.accessSync(dir, fs.constants.W_OK); - } - catch { - needsSudo = true; - break; - } - } - } - if (needsSudo) { - tee('\n Administrator password required for installation.\n'); - const sudoOk = await cacheSudo('Enter your system password to install OpenClaw'); - if (!sudoOk) { - tee(' Incorrect password or cancelled.\n'); - failCancelled(); - return; - } - tee(' ✓ Credentials verified\n'); - sudoCached = true; - } - else { - tee(' ✓ Write access OK\n'); - } - } - tee('\n'); - // ── Step 1: try npm install -g openclaw ─────────────────────────────────── - if (npmOk) { - tee('Installing openclaw via npm...\n'); - const spawnOpts = platform === 'win32' ? { shell: true, windowsHide: true } : {}; - const npmArgs = ['install', '-g', 'openclaw']; - const r1 = sudoCached - ? await runCaptured('sudo', ['-E', 'npm', ...npmArgs]) - : await runCaptured('npm', npmArgs, spawnOpts); - if (r1.code === 0) { - if (sudoCached) - await fixOpenclawPermissions(); - await succeed(); - return; - } - // If sudo wasn't cached and we hit permission error, ask now (fallback) - if (!sudoCached && platform !== 'win32' && isPermError(fullLog)) { - tee('\nPermission error — elevated access required.\n'); - const ok = await cacheSudo('Enter your system password to install OpenClaw'); - if (!ok) { - tee('Incorrect password or cancelled.\n'); - failCancelled(); - return; - } - sudoCached = true; - tee('Retrying with elevated permissions...\n'); - const r2 = await runCaptured('sudo', ['-E', 'npm', 'install', '-g', 'openclaw']); - if (r2.code === 0) { - await fixOpenclawPermissions(); - await succeed(); - return; - } - } - tee('\nnpm install did not succeed — trying full installer script...\n'); - } - else if (platform !== 'win32') { - // ── Unix: npm not found — silent Node.js install (no terminal) ────────── - // Step A: try nvm (no sudo, no password needed) - const nvmSh = path.join(os.homedir(), '.nvm', 'nvm.sh'); - if (fs.existsSync(nvmSh)) { - tee('nvm detected — installing Node.js LTS...\n'); - const nvmR = await runCaptured('bash', ['-c', - `. "${nvmSh}" && nvm install --lts && nvm use --lts && npm install -g openclaw` - ]); - if (nvmR.code === 0) { - await fixOpenclawPermissions(); - await succeed(); - return; - } - tee('nvm install failed — falling back to system install...\n'); - } - // Step B: ensure sudo is cached (may already be from proactive check above) - if (!sudoCached) { - tee('\nNode.js is required. Your password is needed once to install it.\n'); - const sudoOk = await cacheSudo('Enter your password to install Node.js'); - if (!sudoOk) { - tee('Incorrect password or cancelled.\n'); - failCancelled(); - return; - } - sudoCached = true; - } - if (platform === 'darwin') { - // macOS: download official Node.js universal pkg, install silently with cached sudo - const nodeVersion = '20.18.2'; - const pkgUrl = `https://nodejs.org/dist/v${nodeVersion}/node-v${nodeVersion}.pkg`; - const pkgPath = `/tmp/.occ-node-${nodeVersion}.pkg`; - tee(`Downloading Node.js v${nodeVersion}...\n`); - const dlR = await runCaptured('curl', ['-fsSL', pkgUrl, '-o', pkgPath]); - if (dlR.code !== 0) { - try { - fs.unlinkSync(pkgPath); - } - catch { } - await fail(); - return; - } - tee('Installing Node.js (this may take a moment)...\n'); - const instR = await runCaptured('sudo', ['-n', 'installer', '-pkg', pkgPath, '-target', '/']); - try { - fs.unlinkSync(pkgPath); - } - catch { /* non-fatal */ } - if (instR.code !== 0) { - await fail(); - return; - } - } - else { - // Linux: detect package manager and install Node.js LTS via nodesource - const hasCmdSync = (cmd) => { - try { - cp.execSync(`which ${cmd}`, { env, stdio: 'ignore' }); - return true; - } - catch { - return false; - } - }; - tee('Installing Node.js via package manager...\n'); - let pkgResult; - if (hasCmdSync('apt-get')) { - pkgResult = await runCaptured('sudo', ['-n', 'bash', '-c', - 'curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && apt-get install -y nodejs' - ]); - } - else if (hasCmdSync('dnf')) { - pkgResult = await runCaptured('sudo', ['-n', 'bash', '-c', - 'curl -fsSL https://rpm.nodesource.com/setup_lts.x | bash - && dnf install -y nodejs' - ]); - } - else if (hasCmdSync('yum')) { - pkgResult = await runCaptured('sudo', ['-n', 'bash', '-c', - 'curl -fsSL https://rpm.nodesource.com/setup_lts.x | bash - && yum install -y nodejs' - ]); - } - if (!pkgResult) { - tee('No supported package manager found (tried apt-get, dnf, yum).\n'); - await fail(); - return; - } - if (pkgResult.code !== 0) { - await fail(); - return; - } - } - // Step C: npm is now installed — find it and install openclaw - tee('Installing OpenClaw...\n'); - const npmCandidates = ['/usr/local/bin/npm', '/usr/bin/npm']; - const npmBin = npmCandidates.find(p => fs.existsSync(p)) ?? 'npm'; - const npmR1 = await runCaptured(npmBin, ['install', '-g', 'openclaw']); - if (npmR1.code === 0) { - await fixOpenclawPermissions(); - await succeed(); - return; - } - // Global prefix dir may be root-owned — retry with cached sudo - if (isPermError(fullLog)) { - const npmR2 = await runCaptured('sudo', ['-n', npmBin, 'install', '-g', 'openclaw']); - if (npmR2.code === 0) { - await fixOpenclawPermissions(); - await succeed(); - return; - } - } - await fail(); - return; - } - else { - tee('npm not found — running full installer script...\n'); - } - // ── Step 2: full install script ── (npm found but failed, or Windows no npm) - if (platform === 'win32') { - tee('Running PowerShell installer...\n'); - const psArgs = [ - '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', - `$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; ` + - `Invoke-WebRequest -UseBasicParsing https://openclaw.ai/install.ps1 | Invoke-Expression`, - ]; - const r = await runCaptured('powershell', psArgs, { windowsHide: true }); - if (r.code === 0) { - await succeed(); - return; - } - } - else { - // npm was found but install failed — try the openclaw installer script - tee('Running install script...\n'); - const r1 = await runCaptured('bash', ['-c', 'curl -fsSL https://openclaw.ai/install.sh | bash']); - if (r1.code === 0) { - await fixOpenclawPermissions(); - await succeed(); - return; - } - if (isPermError(fullLog)) { - tee('\nPermission error in installer — elevated access required.\n'); - const ok = await cacheSudo('Enter your system password to complete installation'); - if (!ok) { - tee('Incorrect password or cancelled.\n'); - failCancelled(); - return; - } - tee('Retrying with elevated permissions...\n'); - const r2 = await runCaptured('sudo', ['-E', 'bash', '-c', 'curl -fsSL https://openclaw.ai/install.sh | bash']); - if (r2.code === 0) { - await fixOpenclawPermissions(); - await succeed(); - return; - } - } - } - await fail(); - } - dispose() { - HomePanel.currentPanel = undefined; - this._stopPolling(); - if (this._uninstallCloseWatcher !== undefined) { - clearInterval(this._uninstallCloseWatcher); - this._uninstallCloseWatcher = undefined; - } - if (this._uninstallCloseSidebarTimer !== undefined) { - clearTimeout(this._uninstallCloseSidebarTimer); - this._uninstallCloseSidebarTimer = undefined; - } - this._outputChannel.dispose(); - this._panel.dispose(); - this._disposables.forEach(d => d.dispose()); - } - async _update() { - const openclawDir = path.join(os.homedir(), '.openclaw'); - const dirExists = fs.existsSync(openclawDir); - // Silently fix ownership if ~/.openclaw was created as root (e.g. after a sudo install). - // sudo -n is non-interactive — only succeeds if a sudo session is still cached; otherwise a no-op. - if (dirExists && process.platform !== 'win32') { - try { - const stat = fs.statSync(openclawDir); - if (stat.uid !== process.getuid()) { - const username = os.userInfo().username; - cp.exec(`sudo -n chown -R ${username}:${username} ${openclawDir} && chmod 700 ${openclawDir}`); - } - } - catch { /* non-fatal */ } - } - const cliCheck = await this._testOpenClawCli(); - writeLog(`[cli-check] ok=${cliCheck.ok} cmd="${cliCheck.command}" output="${(cliCheck.output ?? '').trim()}"\n`); - const configFile = path.join(os.homedir(), '.openclaw', 'openclaw.json'); - const isConfigured = fs.existsSync(configFile); - const isInstalled = isConfigured; // config file is the sole source of truth — a leftover binary without config is not "installed" - this._lastInstalledState = isInstalled; - this._lastInstalledVersion = cliCheck.ok ? (cliCheck.output ?? '').trim() : null; - const iconUri = this._panel.webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'media', 'icon.png')); - const occJwt = await vscode.commands.executeCommand('occ.auth.getLegacyJwt').then(r => r ?? '', () => ''); - this._lastJwt = occJwt; - // Fetch user info from extension host (avoids CORS — webview origin is vscode-webview://) - let occUser = null; - if (occJwt) { - try { - const r = await fetch('https://occ.mba.sh/api/v1/me', { - headers: { Authorization: `Bearer ${occJwt}` }, - }); - if (r.ok) - occUser = await r.json(); - } - catch { /* network error — leave null */ } - } - // Show unified setup view when OpenClaw is not fully configured yet. - if (!isConfigured) { - this._panel.webview.html = this._getSetupHtml(isInstalled, iconUri.toString(), occUser); - this._autoUpdateTriggered = false; // reset so check fires when they reach the dashboard - } - else { - const emojiBaseUri = this._panel.webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'media', 'emojis')).toString(); - // Read AI model info from openclaw.json - let aiModelName = ''; - try { - const raw = fs.readFileSync(configFile, 'utf-8'); - const cfg = JSON.parse(raw); - const primaryModel = cfg - ?.agents?.defaults?.model?.primary ?? ''; - if (primaryModel) { - const slashIdx = primaryModel.indexOf('/'); - const providerId = slashIdx >= 0 ? primaryModel.slice(0, slashIdx) : ''; - const modelId = slashIdx >= 0 ? primaryModel.slice(slashIdx + 1) : primaryModel; - const providers = cfg - ?.models?.providers ?? {}; - const providerModels = providers[providerId]?.models ?? []; - const modelDef = providerModels.find((m) => m.id === modelId); - aiModelName = modelDef?.name ?? primaryModel; - } - } - catch { /* openclaw.json unreadable or missing fields */ } - this._panel.webview.html = this._getHtml(isInstalled, dirExists, cliCheck, iconUri.toString(), occJwt, occUser, emojiBaseUri, aiModelName); - // One-shot version check: fires the first time the user lands on the full dashboard. - // If the installed version is outdated, MoltPilot auto-starts the update. - if (!this._autoUpdateTriggered) { - this._autoUpdateTriggered = true; - setTimeout(() => void this._autoUpdateIfOutdated(), 3000); - } - } - // Kick off gateway status polling now that the webview is ready. - this._startPolling(); - // Auto-check version on every load so the banner is always current. - // Small delay so the webview JS finishes loading before the postMessage arrives. - if (isInstalled) { - setTimeout(() => { - try { - this._panel.webview.postMessage({ type: 'autoCheckVersion' }); - } - catch { } - void this._checkLatestVersion(); - }, 800); - } - } - // ── Gateway status helpers ───────────────────────────────────────────────── - /** - * Reads the gateway port from ~/.openclaw/openclaw.json. - * Falls back to 18789 if the file is missing or the field is absent. - */ - _getConfiguredPort() { - try { - const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json'); - const raw = fs.readFileSync(configPath, 'utf-8'); - const config = JSON.parse(raw); - const gateway = config['gateway']; - const p = gateway?.['port'] ?? config['port'] ?? config['gateway_port'] ?? config['gatewayPort']; - const n = typeof p === 'string' ? parseInt(p, 10) : typeof p === 'number' ? p : NaN; - return Number.isFinite(n) && n > 0 && n < 65536 ? n : 18789; - } - catch { - return 18789; - } - } - /** Raw HTTP probe against the configured port — no _commandAction guard. Used by the polling loop. */ - _checkGatewayStatusRaw() { - const port = this._getConfiguredPort(); - return new Promise(resolve => { - const req = http.get(`http://localhost:${port}/`, { timeout: 2000 }, res => { - res.resume(); - resolve(res.statusCode !== undefined && res.statusCode < 500 ? 'running' : 'errored'); - }); - req.on('error', (err) => { - resolve(err.code === 'ECONNREFUSED' ? 'stopped' : 'errored'); - }); - req.on('timeout', () => { req.destroy(); resolve('stopped'); }); - }); - } - async _checkGatewayStatus() { - if (this._commandAction) { - return this._commandAction === 'start' ? 'starting' - : this._commandAction === 'stop' ? 'stopping' - : 'restarting'; - } - return this._checkGatewayStatusRaw(); - } - /** - * Fast synchronous check — ~/.openclaw/openclaw.json is the single - * definitive signal that OpenClaw is installed and initialised. - */ - _quickInstallCheck() { - // Config file is the sole source of truth — no config means not installed, - // even if the binary is still on PATH. - return fs.existsSync(path.join(os.homedir(), '.openclaw', 'openclaw.json')); - } - _startPolling() { - this._stopPolling(); - this._pollTick = 0; - const tick = async () => { - if (!HomePanel.currentPanel) - return; - this._pollTick++; - // Every 2 ticks (~4s): quick existsSync check on known binary paths. - // No process spawn — just cheap stat calls. If the result differs from - // the last known state, do a full _update() to confirm and re-render. - if (this._pollTick % 2 === 0) { - const nowInstalled = this._quickInstallCheck(); - if (nowInstalled !== this._lastInstalledState) { - void this._update(); - return; - } - } - // Every 15 ticks (~30s): re-fetch CLI version fresh via login shell — no cache. - // Always push the latest value and refresh the update banner. - if (this._pollTick % 15 === 0) { - void this._testOpenClawCli().then(result => { - const current = result.ok ? (result.output ?? '').trim() : null; - const changed = current !== this._lastInstalledVersion; - this._lastInstalledVersion = current; - // Always push fresh version to the CLI row span. - try { - this._panel.webview.postMessage({ type: 'cliVersion', text: current ?? 'not found', ok: result.ok }); - } - catch { } - // Refresh the update banner whenever the version changed. - if (changed) { - try { - this._panel.webview.postMessage({ type: 'autoCheckVersion' }); - } - catch { } - void this._checkLatestVersion(); - } - }); - } - const [status, aiRunning, sidebarVisible, jwt] = await Promise.all([ - this._checkGatewayStatus(), - vscode.commands.executeCommand('void.getIsRunning').then(v => !!v, () => false), - vscode.commands.executeCommand('void.sidebar.isVisible').then(v => !!v, () => this._sidebarOpen), - vscode.commands.executeCommand('occ.auth.getLegacyJwt').then(r => r ?? '', () => ''), - ]); - this._sidebarOpen = sidebarVisible; - // Don't overwrite the intermediary status while a gateway command is in progress. - if (!this._commandAction) { - try { - this._panel.webview.postMessage({ type: 'gatewayStatus', status }); - } - catch { } - } - // After first-install setup: close the sidebar once the gateway is confirmed running. - if (this._closeSidebarOnGatewayStart && status === 'running') { - this._closeSidebarOnGatewayStart = false; - void vscode.commands.executeCommand('void.sidebar.close'); - } - try { - this._panel.webview.postMessage({ type: 'aiRunning', running: aiRunning }); - } - catch { } - try { - this._panel.webview.postMessage({ type: 'chatState', open: this._sidebarOpen }); - } - catch { } - // Full re-render if JWT changed (e.g. deep-link auth arrived while panel was open). - // We do a full _update() so the extension host fetches /api/v1/me fresh (avoids webview CORS). - if (jwt !== this._lastJwt) { - void this._update(); - return; - } - }; - void tick(); - this._pollingTimer = setInterval(tick, 2000); - } - _stopPolling() { - if (this._pollingTimer !== undefined) { - clearInterval(this._pollingTimer); - this._pollingTimer = undefined; - } - } - /** - * Polls the actual gateway HTTP status until the expected state is reached - * or the timeout expires. Streams live status updates to the webview while - * waiting so the UI stays accurate (still "Starting…" etc.). - */ - async _handleGatewayAction(action) { - const intermediary = action === 'start' ? 'starting' : action === 'stop' ? 'stopping' : 'restarting'; - const expectedState = action === 'stop' ? 'stopped' : 'running'; - this._commandAction = action; - try { - this._panel.webview.postMessage({ type: 'gatewayStatus', status: intermediary }); - } - catch { } - // Hand off to AI — it will run the command and handle any errors - const verb = action === 'restart' ? 'restart' : action; - const osInfo = `${process.platform} ${os.release()} (${process.arch})`; - const port = this._getConfiguredPort(); - const portCheckCmd = process.platform === 'win32' - ? `netstat -ano | findstr :${port}` - : `lsof -iTCP:${port} -sTCP:LISTEN -n -P 2>/dev/null || ss -tlnp 2>/dev/null | grep :${port}`; - const aiMessage = [ - `Please ${verb} the OpenClaw gateway.`, - '', - `Run the following command in your terminal:`, - '```', - `openclaw gateway ${action}`, - '```', - '', - `Environment: ${osInfo}`, - `Configured gateway port: ${port}`, - '', - `After running the command, verify the gateway has reached the expected state by checking`, - `whether port ${port} is ${expectedState === 'running' ? 'actively listening' : 'no longer listening'}:`, - '```', - portCheckCmd, - '```', - '', - `The gateway is confirmed ${expectedState === 'running' ? 'running' : 'stopped'} when port ${port} ` + - `${expectedState === 'running' ? 'shows an active LISTEN entry' : 'shows no LISTEN entry'}.`, - `If the command fails or the port does not reach the expected state, diagnose and fix the issue.`, - ].join('\n'); - await vscode.commands.executeCommand('void.openChatWithMessage', aiMessage, 'agent'); - void vscode.commands.executeCommand('openclaw.balance.spend'); - // Poll in the background until gateway reaches expected state - this._pollUntilState(expectedState, intermediary); - } - _pollUntilState(expected, intermediary, maxWaitMs = 180000) { - const deadline = Date.now() + maxWaitMs; - const tick = async () => { - if (Date.now() > deadline) { - this._commandAction = null; - try { - this._panel.webview.postMessage({ type: 'gatewayStatus', status: await this._checkGatewayStatus() }); - } - catch { } - return; - } - // Use raw status check, bypassing _commandAction guard - const status = await this._checkGatewayStatusRaw(); - if (status === expected) { - this._commandAction = null; - try { - this._panel.webview.postMessage({ type: 'gatewayStatus', status }); - } - catch { } - } - else { - try { - this._panel.webview.postMessage({ type: 'gatewayStatus', status: intermediary }); - } - catch { } - setTimeout(tick, 4000); - } - }; - setTimeout(tick, 4000); - } - // ── Version check ────────────────────────────────────────────────────────── - /** Fetches the latest openclaw version from the npm registry. */ - _fetchLatestVersion() { - return new Promise(resolve => { - // Try npm registry first — openclaw is published there. - const req = https.get({ hostname: 'registry.npmjs.org', path: '/openclaw/latest', headers: { Accept: 'application/json' } }, res => { - let data = ''; - res.on('data', (c) => (data += c)); - res.on('end', () => { - try { - resolve(JSON.parse(data).version ?? null); - } - catch { - resolve(null); - } - }); - }); - req.setTimeout(6000, () => { req.destroy(); resolve(null); }); - req.on('error', () => resolve(null)); - }); - } - async _checkLatestVersion() { - const post = (html) => { - try { - this._panel.webview.postMessage({ type: 'versionResult', html }); - } - catch { } - }; - const [cliCheck, latest] = await Promise.all([ - this._testOpenClawCli(), - this._fetchLatestVersion(), - ]); - if (!latest) { - post(`Could not reach version server — check your connection.`); - return; - } - const installed = cliCheck.ok ? (cliCheck.output ?? '').trim() : null; - if (!installed) { - post(`Latest: ${latest} — OpenClaw CLI not detected locally.`); - return; - } - // Extract the bare version number from any format: - // "OpenClaw 2026.3.12 (6472949)" → "2026.3.12" - // "v2026.3.12" → "2026.3.12" - // "2026.3.12-beta.1" → "2026.3.12" - const norm = (v) => { - const match = v.match(/\d+\.\d+(?:\.\d+)*/); - return match ? match[0] : v.replace(/^v/i, '').split(/[-+(]/)[0].trim(); - }; - if (norm(installed) === norm(latest)) { - post(`✓ Up to date — ${installed}`); - } - else { - post(`Update available: ${latest} — you have ${installed}.` + - ``); - } - } - async _autoUpdateIfOutdated() { - try { - const [cliCheck, latest] = await Promise.all([ - this._testOpenClawCli(), - this._fetchLatestVersion(), - ]); - if (!cliCheck.ok || !latest) - return; - const norm = (v) => { const m = v.match(/\d+\.\d+(?:\.\d+)*/); return m ? m[0] : v.replace(/^v/i, '').split(/[-+(]/)[0].trim(); }; - const installed = (cliCheck.output ?? '').trim(); - if (norm(installed) === norm(latest)) - return; // already up to date - void vscode.commands.executeCommand('void.openChatWithMessage', `OpenClaw is installed but version ${installed} is not the latest (${latest}). Please update it now.\n\n` + - `Run: openclaw update --yes --non-interactive\n\n` + - `If that command is not available, use: npm install -g openclaw@latest\n\n` + - `After updating, verify with: openclaw --version`, 'agent'); - } - catch { /* best-effort */ } - } - _getLoadingHtml(iconUri) { - return ` - - - - - - - - -

Welcome to OpenClaw Code

-

Cursor for OpenClaw

-
-
- Checking environment -
- -`; - } - // ── Setup wizard ─────────────────────────────────────────────────────────── - async _runSetup(data) { - const post = (msg) => { try { - this._panel.webview.postMessage(msg); - } - catch { } }; - const wizardPost = (text, done, ok) => { - writeLog(text); - post({ type: 'wizardLog', text, done, ok }); - }; - const env = this._buildExecEnv(); - // openclaw requires Node.js >= 22. Check the active version and auto-install - // via nvm if needed — the editor pins v20, so users may only have v20 active. - if (process.platform !== 'win32') { - const nodeVerRaw = await new Promise(resolve => { - cp.exec('node --version', { env, timeout: 5000 }, (err, stdout) => resolve((stdout || '').toString().trim())); - }); - const nodeMinor = parseInt((nodeVerRaw.match(/^v?(\d+)/) || [])[1] || '0', 10); - if (nodeMinor < 22) { - const nvmSh = path.join(os.homedir(), '.nvm', 'nvm.sh'); - if (fs.existsSync(nvmSh)) { - wizardPost(`Node.js ${nodeVerRaw || 'unknown'} detected — openclaw requires v22+. Installing Node.js 22 via nvm...\n`, false, false); - const nvmR = await new Promise(resolve => { - const child = cp.spawn('bash', ['-c', - `. "${nvmSh}" && nvm install 22 && nvm use 22 && nvm alias default 22` - ], { env, stdio: ['ignore', 'pipe', 'pipe'] }); - child.stdout?.on('data', (d) => wizardPost(d.toString(), false, false)); - child.stderr?.on('data', (d) => wizardPost(d.toString(), false, false)); - child.on('close', code => resolve(code ?? 1)); - child.on('error', () => resolve(1)); - }); - if (nvmR === 0) { - // Rebuild env so the new Node 22 bin is on PATH - const nvmVersionsDir = path.join(os.homedir(), '.nvm', 'versions', 'node'); - const v22dirs = fs.existsSync(nvmVersionsDir) - ? fs.readdirSync(nvmVersionsDir).filter(v => /^v?22/.test(v)) - : []; - if (v22dirs.length > 0) { - const v22bin = path.join(nvmVersionsDir, v22dirs[0], 'bin'); - env.PATH = [v22bin, env.PATH || ''].filter(Boolean).join(':'); - env.Path = env.PATH; - } - wizardPost(' ✓ Node.js 22 ready.\n', false, false); - } - else { - wizardPost(' ⚠️ Node.js 22 install via nvm failed. Setup may fail.\n', false, false); - } - } - else { - wizardPost(`⚠️ Node.js ${nodeVerRaw || 'v20'} is active but openclaw requires v22+.\n Please install Node.js 22 (e.g. via https://nodejs.org) and restart OCCode.\n`, true, false); - return; - } - } - } - const cliPath = await this._findOpenClawPath() ?? 'openclaw'; - const port = data.port && /^\d+$/.test(data.port) ? data.port : '18789'; - const isFree = data.provider === 'free'; - // Map provider choice to openclaw flags. - const providerFlags = { - free: [ - '--auth-choice', 'custom-api-key', - '--custom-base-url', 'https://occ.mba.sh/v1', - '--custom-api-key', data.apiKey, - '--custom-model-id', 'occ-legacy', - '--custom-compatibility', 'openai', - ], - anthropic: ['--auth-choice', 'apiKey', '--anthropic-api-key', data.apiKey], - openai: ['--auth-choice', 'openai-api-key', '--openai-api-key', data.apiKey], - openrouter: ['--auth-choice', 'openrouter-api-key', '--openrouter-api-key', data.apiKey], - gemini: ['--auth-choice', 'gemini-api-key', '--gemini-api-key', data.apiKey], - ollama: [ - '--auth-choice', 'custom-api-key', - '--custom-base-url', data.apiKey || 'http://localhost:11434', - '--custom-api-key', 'ollama', - '--custom-model-id', 'llama3', - '--custom-compatibility', 'openai', - ], - }; - const flags = providerFlags[data.provider]; - if (!flags) { - wizardPost('Unknown provider selected.\n', true, false); - return; - } - const args = [ - 'onboard', - '--non-interactive', '--accept-risk', - '--flow', 'quickstart', - '--gateway-auth', 'token', - '--gateway-port', port, - '--skip-channels', '--skip-skills', '--skip-health', - ...flags, - ]; - writeLog(`\n=== _runSetup START provider=${data.provider} port=${port} ===\n`); - wizardPost(isFree ? 'Installing Inference for MoltPilot...\nInstalling Inference for your new OpenClaw...\n' : 'Installing Inference for your new OpenClaw...\n', false, false); - await new Promise(resolve => { - const child = cp.spawn(cliPath, args, { - env, - stdio: ['ignore', 'pipe', 'pipe'], - ...(process.platform === 'win32' ? { shell: true, windowsHide: true } : {}), - }); - child.stdout?.on('data', (d) => wizardPost(d.toString(), false, false)); - child.stderr?.on('data', (d) => wizardPost(d.toString(), false, false)); - child.on('close', code => { - const ok = code === 0; - writeLog(`=== _runSetup END code=${code} ===\n`); - wizardPost(ok ? '\n✅ Setup complete!\n' : `\nSetup exited with code ${code}.\n`, true, ok); - if (ok) { - if (isFree) { - // Write local free-tier marker (no remote enforcement). - try { - const occDir = path.join(os.homedir(), '.occ'); - if (!fs.existsSync(occDir)) - fs.mkdirSync(occDir, { recursive: true }); - fs.writeFileSync(path.join(occDir, 'moltpilot-tier.json'), JSON.stringify({ tier: 'free', grantedAt: new Date().toISOString(), limitUsd: 1.00 })); - } - catch { /* non-fatal */ } - // Patch openclaw.json to inject correct cost/context metadata for occ-legacy. - // openclaw onboard writes the model with null/zero values; we fix them here. - try { - const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json'); - const raw = fs.readFileSync(configPath, 'utf-8'); - const cfg = JSON.parse(raw); - // Recursively find any model object with id === OCC_LEGACY_MODEL_ID and patch it. - const patchModel = (obj) => { - if (!obj || typeof obj !== 'object') - return false; - if (Array.isArray(obj)) { - for (const item of obj) { - if (patchModel(item)) - return true; - } - return false; - } - const o = obj; - if (o['id'] === OCC_LEGACY_MODEL_ID) { - o['name'] = OCC_LEGACY_MODEL_NAME; - o['reasoning'] = false; - o['input'] = ['text']; - o['cost'] = { ...OCC_LEGACY_COST }; - o['contextWindow'] = OCC_LEGACY_CONTEXT_WINDOW; - o['maxTokens'] = OCC_LEGACY_MAX_TOKENS; - return true; - } - for (const v of Object.values(o)) { - patchModel(v); - } - return false; - }; - patchModel(cfg); - fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2)); - } - catch { /* non-fatal — openclaw.json may not exist yet */ } - } - setTimeout(() => { - HomePanel.refresh(); - if (isFree) { - vscode.commands.executeCommand('openclaw.openWorkspace'); - } - // Give the dashboard time to render, then open a new chat asking AI to start the gateway. - // Set flag so the sidebar auto-closes once the gateway is confirmed running. - setTimeout(() => { - this._closeSidebarOnGatewayStart = true; - vscode.commands.executeCommand('void.openChatWithMessage', 'Run `openclaw gateway start` to start the OpenClaw gateway.', 'agent'); - }, 1000); - }, 1500); - } - resolve(); - }); - child.on('error', err => { - wizardPost(`Error: ${err.message}\n`, true, false); - resolve(); - }); - }); - } - // ── Uninstall ────────────────────────────────────────────────────────────── - _schedulePostUninstallClose() { - if (this._uninstallCloseWatcher !== undefined) - return; - const maxWaitMs = 90000; - const pollMs = 2000; - const closeDelayMs = 3000; - const started = Date.now(); - let wasRunning = false; - this._uninstallCloseWatcher = setInterval(async () => { - if (Date.now() - started > maxWaitMs) { - clearInterval(this._uninstallCloseWatcher); - this._uninstallCloseWatcher = undefined; - return; - } - const isRunning = await vscode.commands.executeCommand('void.getIsRunning') - .then(v => !!v, () => false); - if (isRunning) { - wasRunning = true; - return; - } - if (!wasRunning) - return; - clearInterval(this._uninstallCloseWatcher); - this._uninstallCloseWatcher = undefined; - this._uninstallCloseSidebarTimer = setTimeout(() => { - this._uninstallCloseSidebarTimer = undefined; - vscode.commands.executeCommand('void.sidebar.close'); - }, closeDelayMs); - }, pollMs); - } - // AI-only: hand everything to MoltPilot immediately. - // MoltPilot is instructed to stop all running OpenClaw processes first - // (to release lock files), then perform full uninstall using run_with_sudo - // where elevation is required. - async _runUninstall() { - const post = (msg) => { try { - this._panel.webview.postMessage(msg); - } - catch { } }; - post({ type: 'uninstallLog', text: 'Handing off to AI for uninstall…\n', done: true, ok: true }); - setTimeout(() => { - post({ type: 'uninstallDone' }); - vscode.commands.executeCommand('void.openChatWithMessage', 'Uninstall OpenClaw. No sudo.\n1. cd $HOME\n2. pkill -9 -f openclaw\n3. openclaw uninstall --all --yes --non-interactive\nConfirm each step ran OK, then tell the user it\'s done.', 'agent'); - this._schedulePostUninstallClose(); - // Workspace cleanup (removing ~/.openclaw folder and .code-workspace file) is handled - // by openOpenClawFolder() on the next startup — manipulating workspace state here - // triggers VS Code to modify/reload the workspace, which can cause the extension to - // re-activate and open a duplicate OCC Home tab. - }, 1200); - } - async _runCassSetup() { - const post = (msg) => { try { - this._panel.webview.postMessage(msg); - } - catch { } }; - const home = os.homedir(); - const env = this._buildExecEnv(); - const isWin = process.platform === 'win32'; - const runCmd = (cmd, args, opts = {}) => new Promise(resolve => { - const child = cp.spawn(cmd, args, { env, stdio: ['ignore', 'pipe', 'pipe'], ...opts }); - let out = ''; - child.stdout?.on('data', (d) => { const s = d.toString(); out += s; post({ type: 'wizardLog', text: s, done: false, ok: false }); }); - child.stderr?.on('data', (d) => { const s = d.toString(); out += s; post({ type: 'wizardLog', text: s, done: false, ok: false }); }); - child.on('close', code => resolve({ code: code ?? 1, output: out })); - child.on('error', err => resolve({ code: 1, output: err.message })); - }); - post({ type: 'wizardLog', text: 'Setting up CASS (Coding Agent Session Search)...\n\n', done: false, ok: false }); - // ══════════════════════════════════════════════════════════════════════════ - // Phase 1 — Deterministic: detect platform, download & stage the binary - // ══════════════════════════════════════════════════════════════════════════ - // ── Step 1: Detect platform ─────────────────────────────────────────────── - post({ type: 'wizardLog', text: '① Detecting platform...\n', done: false, ok: false }); - const isMac = process.platform === 'darwin'; - const isArm = process.arch === 'arm64'; - let assetName; - if (isWin) { - assetName = 'cass-windows-amd64.zip'; - } - else if (isMac) { - assetName = 'cass-darwin-arm64.tar.gz'; - } - else { - assetName = isArm ? 'cass-linux-arm64.tar.gz' : 'cass-linux-amd64.tar.gz'; - } - post({ type: 'wizardLog', text: ` Platform: ${process.platform}/${process.arch} → ${assetName}\n`, done: false, ok: false }); - // ── Step 2: Download the binary ─────────────────────────────────────────── - post({ type: 'wizardLog', text: '\n② Downloading CASS v0.2.2 binary...\n', done: false, ok: false }); - const occDir = path.join(home, '.occ'); - const stagingDir = path.join(occDir, 'cass-staging'); - if (!fs.existsSync(occDir)) - fs.mkdirSync(occDir, { recursive: true }); - try { - fs.rmSync(stagingDir, { recursive: true, force: true }); - } - catch { } - fs.mkdirSync(stagingDir, { recursive: true }); - const downloadPath = path.join(occDir, assetName); - try { - fs.unlinkSync(downloadPath); - } - catch { } - const downloadUrl = `https://github.com/Dicklesworthstone/coding_agent_session_search/releases/download/v0.2.2/${assetName}`; - const downloadOk = await new Promise(resolve => { - const file = fs.createWriteStream(downloadPath); - const download = (url) => { - const mod = url.startsWith('https') ? require('https') : require('http'); - mod.get(url, (res) => { - if (res.statusCode === 301 || res.statusCode === 302) { - download(res.headers.location); - return; - } - if (res.statusCode !== 200) { - resolve(false); - return; - } - res.pipe(file); - file.on('finish', () => { file.close(); resolve(true); }); - }).on('error', () => resolve(false)); - }; - download(downloadUrl); - }); - if (!downloadOk) { - post({ type: 'wizardLog', text: '\n❌ Failed to download CASS binary. Check your internet connection.\n', done: true, ok: false }); - return; - } - post({ type: 'wizardLog', text: ' Downloaded.\n', done: false, ok: false }); - // ── Step 3: Extract to staging dir ──────────────────────────────────────── - post({ type: 'wizardLog', text: '\n③ Extracting to staging directory...\n', done: false, ok: false }); - if (isWin) { - await runCmd('powershell', ['-NoProfile', '-Command', `Expand-Archive -Path '${downloadPath}' -DestinationPath '${stagingDir}' -Force`], { shell: true }); - } - else { - await runCmd('tar', ['-xzf', downloadPath, '-C', stagingDir]); - } - // Clean up the downloaded archive - try { - fs.unlinkSync(downloadPath); - } - catch { } - // Verify the binary exists in staging - const stagedBinary = isWin - ? path.join(stagingDir, 'cass.exe') - : path.join(stagingDir, 'cass'); - // The binary might be nested — try to find it - if (!fs.existsSync(stagedBinary)) { - const findBinary = (dir, name) => { - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - if (entry.isFile() && entry.name === name) - return path.join(dir, entry.name); - if (entry.isDirectory()) { - const r = findBinary(path.join(dir, entry.name), name); - if (r) - return r; - } - } - return null; - }; - const found = findBinary(stagingDir, isWin ? 'cass.exe' : 'cass'); - if (found) { - fs.copyFileSync(found, stagedBinary); - } - else { - post({ type: 'wizardLog', text: '\n❌ Could not find CASS binary in the downloaded archive.\n', done: true, ok: false }); - return; - } - } - post({ type: 'wizardLog', text: ` ✅ Binary staged at ${stagedBinary}\n`, done: false, ok: false }); - // ══════════════════════════════════════════════════════════════════════════ - // Phase 2 — Hand off to MoltPilot for AI-assisted installation - // ══════════════════════════════════════════════════════════════════════════ - post({ type: 'wizardLog', text: '\n✅ Download complete — handing off to MoltPilot for installation...\n', done: true, ok: true }); - const binaryName = isWin ? 'cass.exe' : 'cass'; - const handoffMessage = isWin - ? `CASS binary has been downloaded and staged at ~/.occ/cass-staging/${binaryName}. - -Please complete the installation: -1. Create ~/.occ/bin/ if it doesn't exist and move the binary there -2. Add ~/.occ/bin/ to the user's PATH if not already present -3. Run \`cass health --json\` to verify it works -4. Run \`cass index --full --json\` to build the initial index -5. Update ~/.openclaw/agents/main/AGENTS.md with a CASS section if not already present — add instructions to use \`cass search "" --robot --limit 5\` before starting tasks -6. Restart the OpenClaw gateway with \`openclaw gateway restart\` -7. Run a smoke test: \`cass search "test" --robot --limit 1\` - -The binary is already downloaded — do NOT re-download or compile anything.` - : `CASS binary has been downloaded and staged at ~/.occ/cass-staging/${binaryName}. - -Please complete the installation: -1. Create ~/.local/bin/ if it doesn't exist and move the binary there -2. Make it executable: chmod +x ~/.local/bin/cass -3. Run \`cass health --json\` to verify it works -4. Run \`cass index --full --json\` to build the initial index -5. Update ~/.openclaw/agents/main/AGENTS.md with a CASS section if not already present — add instructions to use \`cass search "" --robot --limit 5\` before starting tasks -6. Restart the OpenClaw gateway with \`openclaw gateway restart\` -7. Run a smoke test: \`cass search "test" --robot --limit 1\` - -The binary is already downloaded — do NOT re-download or compile anything.`; - setTimeout(() => { - vscode.commands.executeCommand('void.openChatWithMessage', handoffMessage, 'agent'); - }, 1200); - // Refresh panel - setTimeout(() => HomePanel.refresh(), 2500); - } - _getSetupHtml(isInstalled, iconUri, occUser = null) { - // Render user area statically (avoids JS innerHTML escaping issues) - let userAreaHtml; - if (!occUser) { - userAreaHtml = ``; - } - else { - const initial = (occUser.email || '?')[0].toUpperCase(); - const safeEmail = occUser.email.replace(/"/g, '"').replace(/` - : initial; - userAreaHtml = ` -
- -
-
-
${avatarImg}
-
${safeEmail}
-
- -
- -
-
`; - } - const providers = [ - { id: 'anthropic', label: 'Anthropic Claude', hint: 'console.anthropic.com/settings/keys', placeholder: 'sk-ant-...' }, - { id: 'openai', label: 'OpenAI', hint: 'platform.openai.com/api-keys', placeholder: 'sk-...' }, - { id: 'openrouter', label: 'OpenRouter', hint: 'openrouter.ai/settings/keys', placeholder: 'sk-or-...' }, - { id: 'gemini', label: 'Google Gemini', hint: 'aistudio.google.com/apikey', placeholder: 'AIza...' }, - ]; - const providerCards = providers.map(p => ``).join('\n '); - return ` - - - - - - - - -
${userAreaHtml}
- - - -
Set up OpenClaw
-
Follow the steps below to get started
- - -
-
-
${isInstalled ? '✓' : '1'}
-
Install
OpenClaw
-
-
-
2
-
Configure
AI Model
-
-
-
3
-
Ready
-
-
- - -
- -
- - - - - - - - - - - - - - -
-
-
Working
- - - -
- - - - - - -`; - } - _getWizardHtml(iconUri, occUser = null) { - // Render user area statically (avoids JS innerHTML escaping issues) - let userAreaHtml; - if (!occUser) { - userAreaHtml = ``; - } - else { - const initial = (occUser.email || '?')[0].toUpperCase(); - const safeEmail = occUser.email.replace(/"/g, '"').replace(/` - : initial; - userAreaHtml = ` -
- -
-
-
${avatarImg}
-
${safeEmail}
-
- -
- -
-
`; - } - const providers = [ - { id: 'anthropic', label: 'Anthropic Claude', hint: 'console.anthropic.com/settings/keys', placeholder: 'sk-ant-...' }, - { id: 'openai', label: 'OpenAI', hint: 'platform.openai.com/api-keys', placeholder: 'sk-...' }, - { id: 'openrouter', label: 'OpenRouter', hint: 'openrouter.ai/settings/keys', placeholder: 'sk-or-...' }, - { id: 'gemini', label: 'Google Gemini', hint: 'aistudio.google.com/apikey', placeholder: 'AIza...' }, - ]; - const providerCards = providers.map(p => ``).join('\n '); - return ` - - - - - - - - -
- ${userAreaHtml} -
- - -
- -
- - -
-
- - - - - - - - - - - - -`; - } - _getHtml(isInstalled, dirExists, cliCheck, iconUri, occJwt = '', occUser = null, emojiBaseUri = '', aiModelName = '') { - // Render user area statically (avoids JS innerHTML escaping / runtime errors) - let userAreaHtml; - if (!occUser) { - userAreaHtml = ``; - } - else { - const initial = (occUser.email || '?')[0].toUpperCase(); - const safeEmail = occUser.email.replace(/"/g, '"').replace(/` - : initial; - const popoverAvatar = occUser.picture - ? `` - : initial; - const balance = '$' + parseFloat(String(occUser.balance_usd || 0)).toFixed(2); - const keysHtml = (occUser.api_keys && occUser.api_keys.occKey) ? ` -
-
API Key
-
- OpenClaw - ${occUser.api_keys.occKey.slice(0, 8)}···${occUser.api_keys.occKey.slice(-4)} - -
-
-
` : ''; - userAreaHtml = ` -
- -
-
-
${popoverAvatar}
-
${safeEmail}
-
${balance} credits
-
- -
- ${keysHtml} - -
-
`; - } - const statusIcon = isInstalled ? '✅' : '⚠️'; - const statusText = isInstalled ? 'OpenClaw detected' : 'OpenClaw not found'; - const statusClass = isInstalled ? 'detected' : 'not-found'; - const buttonLabel = isInstalled ? 'Open Web Control' : 'Install OpenClaw'; - const buttonCommand = isInstalled ? 'openclaw.configure' : 'openclaw.install'; - const dirText = dirExists ? 'found' : 'missing'; - const dirClass = dirExists ? 'ok' : 'warn'; - const cliText = cliCheck.ok ? (cliCheck.output || 'ok') : (cliCheck.output || cliCheck.error || 'not found'); - const cliClass = cliCheck.ok ? 'ok' : 'warn'; - const cliHint = cliCheck.ok ? '' : ` (tried: ${cliCheck.command})`; - // ── Lucide icons (inline SVG, no CDN needed) ────────────────────────────── - const ic = (d, size = 13, opacity = '0.55') => `${d}`; - const icFolder = ic(''); - const icTerminal = ic(''); - const icServer = ic(''); - const icBot = ic(''); - const icChip = ic('', 13); - // Button icons — slightly larger, full opacity - const icSettings = ic('', 15, '0.9'); - const icDownload = ic('', 15, '0.9'); - const icRefreshCw = ic('', 14, '0.85'); - const icTerminalBtn = ic('', 15, '0.9'); - const icBtnPrimary = isInstalled ? icSettings : icDownload; - return ` - - - - - - - - -
- - ${isInstalled ? `
- - -
` : ''} - - - ${isInstalled ? ` - - -
${userAreaHtml}
-
- - -
-
- -

Under Development

-

This app isn't ready yet — but you can help build it.
Copy the message below and post it in the MBA community.

-
- I want to contribute to [App] - -
-

-
- - -
-
-
- - -
-
-

Uninstall OpenClaw?

-

This will remove the CLI, stop the gateway, and clean up all config files. This cannot be undone.

-
- - -
-
-
- - -
-
-

🔐 Administrator Password Required

-

OpenClaw needs elevated permissions to install globally.
Your password is used once and never stored.

- -
- - -
-
-
- - - - - ${isInstalled ? ` - -

Welcome to OpenClaw Code

-
-
- ${icFolder} - Config (~/.openclaw/openclaw.json) - ${dirText} -
-
- ${icTerminal} - CLI (openclaw --version) - ${cliText}${cliHint} -
-
- ${icServer} - Gateway - - - Checking… - - -
- ${aiModelName ? ` -
- ${icChip} - AI Model - ${aiModelName} -
` : ''} -
- ${icBot} - MoltPilot - - - - Idle - - -
-
-
- ${['AGENTS.md', 'IDENTITY.md', 'USER.md', 'MEMORY.md', 'SOUL.md', 'HEARTBEAT.md'].map(f => `${f}`).join('\n ')} -
-
- - - -
- ` : ` -
- -
- - -
-
- `} - - - -`; - } - async _testOpenClawCli() { - if (process.platform === 'win32') { - // ── 1. Find openclaw.mjs (checks npm prefix + version-manager paths) ────── - const mjs = await this._findWindowsOpenClawMjs(); - if (mjs) { - // ── 2. Find node.exe (PATH-first, then nvm/Volta/scoop, then hardcoded) ── - const nodeExe = await this._findWindowsNodeExe(); - if (nodeExe) { - return this._spawnNodeMjs(nodeExe, mjs, `"${nodeExe}" "${mjs}" --version`); - } - } - // ── 3. .cmd / .exe shim fallback (npm prefix + scoop shims) ────────────── - const cmdPath = await this._findWindowsOpenClawCmd(); - if (cmdPath) { - return new Promise(resolve => { - cp.execFile('cmd.exe', ['/c', cmdPath, '--version'], { timeout: 30000, windowsHide: true, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => { - if (error) { - const timedOut = error.signal === 'SIGTERM' || error.code == null; - resolve({ - ok: false, - error: timedOut ? 'Timed out' : (stderr?.toString().trim() || `Exit ${error.code}`), - command: `${cmdPath} --version`, - }); - } - else { - resolve({ ok: true, output: (stdout || stderr || '').toString().trim(), command: `${cmdPath} --version` }); - } - }); - }); - } - return { ok: false, error: 'openclaw not found', command: 'openclaw --version' }; - } - // ── Mac / Linux ────────────────────────────────────────────────────────────── - // Strategy: source nvm/nvm.sh first so the nvm-managed binary takes priority - // over any stale system install (e.g. /usr/local/bin/openclaw). Falls back to - // enumerating ~/.nvm/versions/node/*/bin/openclaw (newest version first), - // then the existing path-based search. - const home = os.homedir(); - const nvmSh = path.join(home, '.nvm', 'nvm.sh'); - // 1. Try sourcing nvm so it activates the default alias / current version. - if (fs.existsSync(nvmSh)) { - const nvmCmd = `bash -c '. "${nvmSh}" 2>/dev/null && openclaw --version 2>&1'`; - const nvmResult = await new Promise(resolve => { - cp.exec(nvmCmd, { timeout: 10000, maxBuffer: 1024 * 1024 }, (error, stdout) => { - const out = (stdout || '').toString().trim(); - // Extract just the version line (ignores nvm banner noise) - const line = out.split('\n').find(l => /\d/.test(l) && !l.startsWith('nvm') && !l.startsWith('Now')) || ''; - if (!error && line) { - resolve({ ok: true, output: line.trim(), command: nvmCmd }); - } - else { - resolve({ ok: false, error: out || error?.message || 'not found', command: nvmCmd }); - } - }); - }); - if (nvmResult.ok) - return nvmResult; - } - // 2. Enumerate ~/.nvm/versions/node/*/bin/openclaw — newest node version first. - const nvmVersionsDir = path.join(home, '.nvm', 'versions', 'node'); - if (fs.existsSync(nvmVersionsDir)) { - const nodeVersions = fs.readdirSync(nvmVersionsDir) - .sort((a, b) => b.localeCompare(a, undefined, { numeric: true })); // newest first - for (const ver of nodeVersions) { - const candidate = path.join(nvmVersionsDir, ver, 'bin', 'openclaw'); - if (fs.existsSync(candidate)) { - const result = await new Promise(resolve => { - cp.execFile(candidate, ['--version'], { timeout: 10000, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => { - const out = (stdout || stderr || '').toString().trim(); - if (error || !out) - resolve({ ok: false, error: out || error?.message, command: `${candidate} --version` }); - else - resolve({ ok: true, output: out, command: `${candidate} --version` }); - }); - }); - if (result.ok) - return result; - } - } - } - // 3. Fall back to path-based search. - const cliPath = await this._findOpenClawPath(); - if (!cliPath) { - return { ok: false, error: 'openclaw not found', command: 'openclaw --version' }; - } - return new Promise(resolve => { - cp.execFile(cliPath, ['--version'], { timeout: 15000, maxBuffer: 1024 * 1024, env: this._buildExecEnv() }, (error, stdout, stderr) => { - if (error) { - resolve({ ok: false, error: stderr?.toString().trim() || error.message || `Exit ${error.code}`, command: `${cliPath} --version` }); - } - else { - resolve({ ok: true, output: (stdout || stderr || '').toString().trim(), command: `${cliPath} --version` }); - } - }); - }); - } - /** - * Finds openclaw.mjs in the npm global prefix (dynamic) and common - * version-manager install paths so any Node setup is covered. - */ - async _findWindowsOpenClawMjs() { - const home = os.homedir(); - const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming'); - const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'); - // Ask npm where its global prefix lives — covers custom prefixes / nvm / fnm - const prefixResult = await this._runCommand('npm config get prefix', 3000); - const npmPrefix = (prefixResult.stdout || '').trim().replace(/['"]/g, ''); - const candidates = [ - npmPrefix ? path.join(npmPrefix, 'node_modules', 'openclaw', 'openclaw.mjs') : '', - path.join(appData, 'npm', 'node_modules', 'openclaw', 'openclaw.mjs'), - // scoop (nodejs / nodejs-lts) - path.join(home, 'scoop', 'apps', 'nodejs', 'current', 'node_modules', 'openclaw', 'openclaw.mjs'), - path.join(home, 'scoop', 'apps', 'nodejs-lts', 'current', 'node_modules', 'openclaw', 'openclaw.mjs'), - // Volta - path.join(localAppData, 'Volta', 'tools', 'image', 'packages', 'openclaw', 'lib', 'node_modules', 'openclaw', 'openclaw.mjs'), - ].filter(Boolean); - return candidates.find(p => fs.existsSync(p)); - } - /** - * Finds the real node.exe for Windows. - * Strategy: PATH lookup first (handles nvm-windows, fnm, Volta shims, winget, - * and standard installs), then version-manager directories, then hardcoded paths. - */ - async _findWindowsNodeExe() { - const home = os.homedir(); - const programFiles = process.env.ProgramFiles || 'C:\\Program Files'; - const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'); - // 1. PATH lookup — most reliable; works for nvm-windows, fnm, Volta shims, - // winget, and standard installers without any special-casing. - try { - const found = await new Promise((resolve, reject) => cp.exec('where node.exe', { timeout: 3000, windowsHide: true }, (err, stdout) => err ? reject(err) : resolve(stdout.trim().split(/\r?\n/)[0]?.trim() || ''))); - // Skip if the path belongs to VSCodium / VS Code / Electron (wrong node) - if (found && fs.existsSync(found) && !/vscodium|vscode|electron/i.test(found)) { - return found; - } - } - catch { } - // 2. nvm-windows — %NVM_HOME%\\node.exe - const nvmHome = process.env.NVM_HOME; - if (nvmHome && fs.existsSync(nvmHome)) { - try { - const versions = fs.readdirSync(nvmHome) - .filter(e => /^\d+\.\d+\.\d+$/.test(e)) - .sort((a, b) => b.localeCompare(a, undefined, { numeric: true })); - for (const v of versions.slice(0, 5)) { - const p = path.join(nvmHome, v, 'node.exe'); - if (fs.existsSync(p)) - return p; - } - } - catch { } - } - // 3. Volta — %LOCALAPPDATA%\Volta\tools\image\node\\node.exe - const voltaNodeDir = path.join(localAppData, 'Volta', 'tools', 'image', 'node'); - if (fs.existsSync(voltaNodeDir)) { - try { - const versions = fs.readdirSync(voltaNodeDir) - .filter(e => /^\d+\.\d+\.\d+$/.test(e)) - .sort((a, b) => b.localeCompare(a, undefined, { numeric: true })); - for (const v of versions.slice(0, 5)) { - const p = path.join(voltaNodeDir, v, 'node.exe'); - if (fs.existsSync(p)) - return p; - } - } - catch { } - } - // 4. scoop (nodejs / nodejs-lts) - for (const app of ['nodejs', 'nodejs-lts']) { - const p = path.join(home, 'scoop', 'apps', app, 'current', 'node.exe'); - if (fs.existsSync(p)) - return p; - } - // 5. Standard installer, chocolatey, winget fallbacks - const hardcoded = [ - path.join(programFiles, 'nodejs', 'node.exe'), - 'C:\\Program Files\\nodejs\\node.exe', - 'C:\\Program Files (x86)\\nodejs\\node.exe', - path.join(localAppData, 'Programs', 'nodejs', 'node.exe'), - 'C:\\ProgramData\\chocolatey\\bin\\node.exe', - 'C:\\tools\\nodejs\\node.exe', - ]; - return hardcoded.find(p => fs.existsSync(p)); - } - /** - * Finds openclaw.cmd / .exe shim using the npm global prefix (dynamic) - * and common fallback locations including scoop shims. - */ - async _findWindowsOpenClawCmd() { - const home = os.homedir(); - const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming'); - const prefixResult = await this._runCommand('npm config get prefix', 3000); - const npmPrefix = (prefixResult.stdout || '').trim().replace(/['"]/g, ''); - const candidates = [ - npmPrefix ? path.join(npmPrefix, 'openclaw.cmd') : '', - npmPrefix ? path.join(npmPrefix, 'openclaw.exe') : '', - path.join(appData, 'npm', 'openclaw.cmd'), - path.join(appData, 'npm', 'openclaw.exe'), - // scoop shims - path.join(home, 'scoop', 'shims', 'openclaw.cmd'), - path.join(home, 'scoop', 'shims', 'openclaw.exe'), - ].filter(Boolean); - return candidates.find(p => fs.existsSync(p)); - } - /** Spawns ` --version` and resolves with the result. */ - _spawnNodeMjs(nodeExe, mjs, display) { - return new Promise(resolve => { - const child = cp.spawn(nodeExe, [mjs, '--version'], { - windowsHide: true, - stdio: ['ignore', 'pipe', 'pipe'], - }); - let stdout = ''; - let stderr = ''; - child.stdout?.on('data', d => (stdout += d)); - child.stderr?.on('data', d => (stderr += d)); - const timer = setTimeout(() => child.kill('SIGTERM'), 30000); - child.on('close', (code, signal) => { - clearTimeout(timer); - if (signal === 'SIGTERM' || code === null) { - resolve({ ok: false, error: 'Timed out after 30s', command: display }); - } - else if (code !== 0) { - resolve({ ok: false, error: stderr.trim() || `Exit ${code}`, command: display }); - } - else { - resolve({ ok: true, output: (stdout || stderr).trim(), command: display }); - } - }); - child.on('error', err => { - clearTimeout(timer); - resolve({ ok: false, error: err.message, command: display }); - }); - }); - } - async _findOpenClawPath() { - const cfgPath = vscode.workspace.getConfiguration('openclaw').get('cliPath'); - if (cfgPath && fs.existsSync(cfgPath)) - return cfgPath; - const envPath = process.env.OPENCLAW_CLI; - if (envPath && fs.existsSync(envPath)) - return envPath; - if (process.platform === 'win32') { - const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'); - const candidates = [ - path.join(appData, 'npm', 'openclaw.cmd'), - path.join(appData, 'npm', 'openclaw.exe'), - path.join(appData, 'npm', 'openclaw.bat'), - path.join(appData, 'npm', 'openclaw.ps1'), - ]; - for (const candidate of candidates) { - if (fs.existsSync(candidate)) - return candidate; - } - } - if (process.platform === 'win32') { - for (const probe of ['openclaw.cmd', 'openclaw.exe', 'openclaw.bat', 'openclaw.ps1', 'openclaw']) { - const result = await this._runCommand(`where ${probe}`, 2000); - if (!result.error && !result.notFound) { - const out = (result.stdout || '').trim(); - if (out) { - const candidates = out - .split(/\r?\n/) - .map(l => l.trim().replace(/^"+|"+$/g, '')) - .filter(Boolean); - for (const candidate of candidates) { - const resolved = this._resolveWindowsCliPath(candidate); - if (fs.existsSync(resolved)) - return resolved; - } - } - } - } - } - else { - const result = await this._runCommand('which openclaw', 2000); - if (!result.error && !result.notFound) { - const out = (result.stdout || '').trim(); - if (out) { - const candidates = out - .split(/\r?\n/) - .map(l => l.trim().replace(/^"+|"+$/g, '')) - .filter(Boolean); - for (const candidate of candidates) { - const resolved = this._resolveWindowsCliPath(candidate); - if (fs.existsSync(resolved)) - return resolved; - } - } - } - } - const npmCandidates = await this._getNpmGlobalCliCandidates(); - for (const candidate of npmCandidates) { - if (fs.existsSync(candidate)) - return candidate; - } - const fallback = this._getCandidateCliPaths(); - for (const candidate of fallback) { - if (fs.existsSync(candidate)) - return candidate; - } - return undefined; - } - _getCandidateCliPaths() { - const home = os.homedir(); - if (process.platform === 'win32') { - const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming'); - const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'); - const programFiles = process.env.ProgramFiles || 'C:\\Program Files'; - return [ - path.join(appData, 'npm', 'openclaw.cmd'), - path.join(appData, 'npm', 'openclaw.exe'), - path.join(appData, 'npm', 'openclaw.bat'), - path.join(appData, 'npm', 'openclaw.ps1'), - path.join(localAppData, 'Programs', 'OpenClaw', 'openclaw.exe'), - path.join(localAppData, 'OpenClaw', 'openclaw.exe'), - path.join(programFiles, 'OpenClaw', 'openclaw.exe'), - path.join(programFiles, 'OpenClaw', 'bin', 'openclaw.exe'), - path.join(localAppData, 'Microsoft', 'WindowsApps', 'openclaw.exe'), - path.join(home, '.openclaw', 'bin', 'openclaw.exe'), - ]; - } - return [ - '/usr/local/bin/openclaw', - '/opt/homebrew/bin/openclaw', - path.join(home, '.local', 'bin', 'openclaw'), - path.join(home, '.npm-global', 'bin', 'openclaw'), - path.join(home, '.openclaw', 'bin', 'openclaw'), - ]; - } - async _getNpmGlobalCliCandidates() { - const result = await this._runCommand('npm config get prefix', 2000); - const prefix = (result.stdout || '').trim(); - if (!prefix) - return []; - if (process.platform === 'win32') { - const base = this._resolveWindowsCliPath(path.join(prefix, 'openclaw')); - return [ - `${base}.cmd`, - `${base}.exe`, - `${base}.bat`, - `${base}.ps1`, - base, - ]; - } - return [path.join(prefix, 'bin', 'openclaw')]; - } - _resolveWindowsCliPath(candidate) { - if (process.platform !== 'win32') - return candidate; - const cleaned = candidate.replace(/^"+|"+$/g, ''); - if (fs.existsSync(cleaned)) - return cleaned; - if (path.extname(cleaned)) - return cleaned; - const exts = ['.cmd', '.exe', '.bat', '.ps1']; - for (const ext of exts) { - const withExt = `${cleaned}${ext}`; - if (fs.existsSync(withExt)) - return withExt; - } - return cleaned; - } - _getPreferredWindowsCmdPath(candidate) { - if (process.platform !== 'win32') - return candidate; - const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'); - const shim = path.join(appData, 'npm', 'openclaw.cmd'); - if (fs.existsSync(shim)) - return shim; - return candidate; - } - _runCommand(cmd, timeoutMs) { - const env = this._buildExecEnv(); - return new Promise(resolve => { - cp.exec(cmd, { timeout: timeoutMs, windowsHide: true, maxBuffer: 1024 * 1024, env }, (error, stdout, stderr) => { - const result = { stdout: stdout?.toString() || '', stderr: stderr?.toString() || '' }; - if (error) { - result.error = error.message || 'Command failed'; - const text = `${result.stderr}\n${result.error}`.toLowerCase(); - result.notFound = - error.code === 'ENOENT' || - text.includes('not recognized as an internal or external command') || - text.includes('command not found'); - } - resolve(result); - }); - }); - } - _buildExecEnv() { - const env = { ...process.env }; - const basePath = env.PATH || env.Path || ''; - const extra = []; - if (process.platform === 'win32') { - const appData = env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'); - if (appData) - extra.push(path.join(appData, 'npm')); - if (env.ProgramFiles) - extra.push(path.join(env.ProgramFiles, 'nodejs')); - if (env.LOCALAPPDATA) - extra.push(path.join(env.LOCALAPPDATA, 'Programs', 'nodejs')); - const systemRoot = env.SystemRoot || env.WINDIR; - if (systemRoot) - extra.push(path.join(systemRoot, 'System32')); - } - else { - extra.push('/usr/local/bin', '/opt/homebrew/bin'); - extra.push(path.join(os.homedir(), '.local', 'bin')); - extra.push(path.join(os.homedir(), '.npm-global', 'bin')); - extra.push(path.join(os.homedir(), '.openclaw', 'bin')); - // Add nvm-managed Node.js paths (newest version first) - const nvmDir = process.env.NVM_DIR || path.join(os.homedir(), '.nvm'); - const nvmVersionsDir = path.join(nvmDir, 'versions', 'node'); - if (fs.existsSync(nvmVersionsDir)) { - try { - const versions = fs.readdirSync(nvmVersionsDir) - .filter(e => /^v?\d+/.test(e)) - .sort((a, b) => b.localeCompare(a, undefined, { numeric: true })); - for (const v of versions.slice(0, 3)) { - extra.push(path.join(nvmVersionsDir, v, 'bin')); - } - } - catch { /* non-fatal */ } - } - // Add fnm paths (popular Node version manager on macOS) - const fnmDir = path.join(os.homedir(), '.local', 'share', 'fnm', 'node-versions'); - if (fs.existsSync(fnmDir)) { - try { - const versions = fs.readdirSync(fnmDir) - .filter(e => /^v?\d+/.test(e)) - .sort((a, b) => b.localeCompare(a, undefined, { numeric: true })); - for (const v of versions.slice(0, 3)) { - extra.push(path.join(fnmDir, v, 'installation', 'bin')); - } - } - catch { /* non-fatal */ } - } - // Homebrew Node.js (macOS) - if (process.platform === 'darwin') { - extra.push('/opt/homebrew/opt/node/bin'); - extra.push('/usr/local/opt/node/bin'); - } - } - const sep = process.platform === 'win32' ? ';' : ':'; - env.PATH = [...extra, basePath].filter(Boolean).join(sep); - env.Path = env.PATH; - return env; - } -} -exports.HomePanel = HomePanel; -//# sourceMappingURL=home.js.map \ No newline at end of file diff --git a/apps/editor/extensions/openclaw/out/panels/home.js.map b/apps/editor/extensions/openclaw/out/panels/home.js.map deleted file mode 100644 index d7a75bef..00000000 --- a/apps/editor/extensions/openclaw/out/panels/home.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"home.js","sourceRoot":"","sources":["../../src/panels/home.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/apps/editor/extensions/openclaw/out/panels/onboarding.js b/apps/editor/extensions/openclaw/out/panels/onboarding.js deleted file mode 100644 index cba7567a..00000000 --- a/apps/editor/extensions/openclaw/out/panels/onboarding.js +++ /dev/null @@ -1,355 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.OnboardingPanel = void 0; -const vscode = __importStar(require("vscode")); -/** - * First-run onboarding panel. - * Shows once on first launch — choose AI preference + theme. - * Completely separate from OCC Home. - * After completion it marks occ.onboardingDone = true and opens OCC Home. - */ -class OnboardingPanel { - /** Show onboarding if it hasn't been completed yet. Returns true if shown. */ - static showIfNeeded(context, extensionUri) { - const done = context.globalState.get(OnboardingPanel.ONBOARDING_DONE_KEY) ?? false; - if (done) { - return false; - } - OnboardingPanel._show(context, extensionUri); - return true; - } - /** Force-show (e.g. from a "Redo onboarding" command). */ - static show(context, extensionUri) { - OnboardingPanel._show(context, extensionUri); - } - static _show(context, extensionUri) { - if (OnboardingPanel._panel) { - OnboardingPanel._panel.reveal(); - return; - } - const panel = vscode.window.createWebviewPanel('occOnboarding', 'Welcome to OCC', vscode.ViewColumn.One, { enableScripts: true, localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'media')] }); - OnboardingPanel._panel = panel; - const iconUri = panel.webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'media', 'icon.png')); - panel.webview.html = OnboardingPanel._getHtml(iconUri.toString()); - panel.webview.onDidReceiveMessage(async (msg) => { - if (msg.command === 'setTheme') { - await vscode.workspace.getConfiguration('workbench').update('colorTheme', msg.theme, vscode.ConfigurationTarget.Global); - } - else if (msg.command === 'occOnboarding') { - const aiPreference = msg.aiPreference; - if (aiPreference) { - await context.globalState.update(OnboardingPanel.AI_PREFERENCE_KEY, aiPreference); - } - await context.globalState.update(OnboardingPanel.ONBOARDING_DONE_KEY, true); - panel.dispose(); - // Open OCC Home after a short delay so the panel close animation finishes. - setTimeout(() => { - vscode.commands.executeCommand('openclaw.home'); - }, 200); - } - }); - panel.onDidDispose(() => { OnboardingPanel._panel = undefined; }); - } - static _getHtml(iconUri) { - const providers = [ - { id: 'anthropic', label: 'Anthropic Claude', hint: 'console.anthropic.com/settings/keys', placeholder: 'sk-ant-...' }, - { id: 'openai', label: 'OpenAI', hint: 'platform.openai.com/api-keys', placeholder: 'sk-...' }, - { id: 'openrouter', label: 'OpenRouter', hint: 'openrouter.ai/settings/keys', placeholder: 'sk-or-...' }, - { id: 'gemini', label: 'Google Gemini', hint: 'aistudio.google.com/apikey', placeholder: 'AIza...' }, - ]; - const providerCards = providers.map(p => ``).join('\n '); - return ` - - - - - - - - -

Welcome to OCC

-

Let's get you set up in a few quick steps.

- - -
-

Choose your AI

-

How do you want to power your AI assistant?

-
- -
-
$5 free to start
-
MoltPilot's AI Brain, powered by MBA.sh.
Sign up required. No card needed.
- -
- -
-
-
- -
-
- -
-
- -
-
- -
-
-
Use your own API key. Always free. No account needed.
- -
-
-
- - - - - - - - - - - - -`; - } -} -exports.OnboardingPanel = OnboardingPanel; -OnboardingPanel.ONBOARDING_DONE_KEY = 'occ.onboardingDone'; -OnboardingPanel.AI_PREFERENCE_KEY = 'occ.aiPreference'; -//# sourceMappingURL=onboarding.js.map \ No newline at end of file diff --git a/apps/editor/extensions/openclaw/out/panels/onboarding.js.map b/apps/editor/extensions/openclaw/out/panels/onboarding.js.map deleted file mode 100644 index 2e523ac8..00000000 --- a/apps/editor/extensions/openclaw/out/panels/onboarding.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"onboarding.js","sourceRoot":"","sources":["../../src/panels/onboarding.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/apps/editor/extensions/openclaw/out/panels/setup.js b/apps/editor/extensions/openclaw/out/panels/setup.js deleted file mode 100644 index c7e7aa57..00000000 --- a/apps/editor/extensions/openclaw/out/panels/setup.js +++ /dev/null @@ -1,653 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ConfigPanel = void 0; -const vscode = __importStar(require("vscode")); -const fs = __importStar(require("fs")); -const path = __importStar(require("path")); -const config_path_1 = require("./config-path"); -function sanitizeJson5(input) { - return input - .replace(/\/\*[\s\S]*?\*\//g, "") - .replace(/(^|[^:])\/\/.*$/gm, "$1") - .replace(/,\s*(\}|\])/g, "$1"); -} -function readOpenClawConfig(configPath) { - try { - const contents = fs.readFileSync(configPath, "utf-8"); - return JSON.parse(sanitizeJson5(contents)); - } - catch (error) { - console.warn("[OpenClaw] Unable to read openclaw.json:", error); - return null; - } -} -function readOpenClawConfigRaw(configPath) { - try { - return fs.readFileSync(configPath, "utf-8"); - } - catch (error) { - console.warn("[OpenClaw] Unable to read raw openclaw.json:", error); - return getDefaultConfig(); - } -} -function getDefaultConfig() { - return `{ - // OpenClaw configuration - "agents": { - "list": [] - }, - "channels": {}, - "automation": { - "cronJobs": [] - }, - "gateway": { - "port": 3000, - "host": "localhost" - } -}`; -} -function buildControlCenterData(configPath) { - const rawConfig = readOpenClawConfig(configPath); - if (!rawConfig) { - // Return empty state if no config exists or is invalid - return { - agents: [], - channels: [], - automation: { cronJobs: [] }, - maintenance: { doctor: { status: "healthy" } }, - }; - } - const agents = rawConfig?.agents?.list?.map((a) => ({ id: a.id || "unknown" })) ?? []; - const channelsConfig = rawConfig?.channels ?? {}; - const channels = Object.entries(channelsConfig).map(([channelKey, channelData]) => { - const enabled = channelData?.enabled ?? false; - return { - channel: channelKey, - description: `${channelKey} surface configuration`, - accounts: [ - { - id: `${channelKey}-primary`, - title: `${channelKey} · Primary`, - status: enabled ? "connected" : "needs-relink" - }, - ], - }; - }); - const cronJobs = rawConfig?.automation?.cronJobs ?? []; - return { - agents, - channels, - automation: { - cronJobs: cronJobs.map((j) => ({ - status: j?.status === "paused" ? "paused" : "enabled" - })), - }, - maintenance: { - doctor: { status: "healthy" }, - }, - }; -} -function getNonce() { - let text = ""; - const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for (let i = 0; i < 32; i += 1) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} -class ConfigPanel { - static createOrShow(extensionUri) { - if (ConfigPanel.currentPanel) { - ConfigPanel.currentPanel.dispose(); - } - const panel = vscode.window.createWebviewPanel("openclawConfigV2", "OpenClaw Configuration", vscode.ViewColumn.Two, { - enableScripts: true, - retainContextWhenHidden: false, - localResourceRoots: [vscode.Uri.joinPath(extensionUri, "media")], - }); - ConfigPanel.currentPanel = new ConfigPanel(panel, extensionUri); - } - constructor(panel, extensionUri) { - this._disposables = []; - this._lastModified = 0; - this._panel = panel; - this._extensionUri = extensionUri; - this._panel.onDidDispose(() => this.dispose(), null, this._disposables); - // Set up file watcher for auto-refresh - this._setupFileWatcher(); - this._panel.webview.onDidReceiveMessage((message) => { - // Webview signals it has loaded the external script and is ready for data - if (message?.command === "ready") { - void this._panel.webview.postMessage({ - command: "init", - data: buildControlCenterData((0, config_path_1.resolveConfigPath)()), - config: readOpenClawConfigRaw((0, config_path_1.resolveConfigPath)()), - }); - return; - } - if (message?.command === "refresh") { - void this._update(); - return; - } - if (message?.command === "openclaw.channelAdd") { - this._openChannelInstallerTerminal(); - return; - } - if (message?.command === "openclaw.saveConfig") { - const configPath = (0, config_path_1.resolveConfigPath)(); - const text = message?.text || ""; - try { - // Validate JSON5 before saving - JSON.parse(sanitizeJson5(text)); - // Ensure directory exists - const configDir = path.dirname(configPath); - if (!fs.existsSync(configDir)) { - fs.mkdirSync(configDir, { recursive: true }); - } - // Write the config - fs.writeFileSync(configPath, text, "utf-8"); - console.log("[OpenClaw] Config saved to:", configPath); - this._panel.webview.postMessage({ command: "openclaw.saveResult", ok: true }); - // Refresh the view with new data - void this._update(); - } - catch (err) { - console.error("[OpenClaw] Failed to save config:", err); - this._panel.webview.postMessage({ - command: "openclaw.saveResult", - ok: false, - error: err.message || "Failed to save configuration" - }); - } - return; - } - if (message?.command === "openclaw.runCommand") { - const input = String(message?.text ?? "").trim(); - if (!input) - return; - const terminal = vscode.window.createTerminal("openclaw command console"); - terminal.show(); - const command = input.startsWith("openclaw") ? input : "openclaw " + input; - terminal.sendText(command, true); - return; - } - if (message?.command === "openclaw.channelPair") { - const channelName = String(message?.channel ?? "").trim(); - const terminal = vscode.window.createTerminal("openclaw pair " + (channelName || 'channel')); - terminal.show(); - terminal.sendText("openclaw channels pair " + (channelName || ''), true); - return; - } - if (message?.command === "openclaw.channelConfigure") { - const channelName = String(message?.channel ?? "").trim(); - vscode.window.showInformationMessage("Configure " + (channelName || "channel") + " — edit the config JSON in the Advanced Configuration tab."); - return; - } - if (message?.command === "openclaw.viewStatus") { - vscode.commands.executeCommand("openclaw.status"); - return; - } - if (message?.command === "openclaw.aiFixConfig") { - vscode.commands.executeCommand("openclaw.aiFixConfig"); - return; - } - }); - void this._update(); - } - dispose() { - ConfigPanel.currentPanel = undefined; - this._panel.dispose(); - while (this._disposables.length) { - const disposable = this._disposables.pop(); - if (disposable) { - disposable.dispose(); - } - } - } - async _update() { - const configPath = (0, config_path_1.resolveConfigPath)(); - const data = buildControlCenterData(configPath); - const rawConfig = readOpenClawConfigRaw(configPath); - this._panel.webview.html = this._getHtml(data, rawConfig); - } - _setupFileWatcher() { - const configPath = (0, config_path_1.resolveConfigPath)(); - // Watch the config file for changes - this._fileWatcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.file(path.dirname(configPath)), path.basename(configPath))); - // Debounced refresh on change - const debouncedRefresh = this._debounce(() => { - const stats = fs.statSync(configPath); - if (stats.mtimeMs > this._lastModified) { - this._lastModified = stats.mtimeMs; - console.log("[OpenClaw] Config file changed, refreshing..."); - void this._pushRefresh(); - } - }, 300); - this._fileWatcher.onDidChange(debouncedRefresh, null, this._disposables); - this._fileWatcher.onDidCreate(debouncedRefresh, null, this._disposables); - // Set initial modified time - try { - const stats = fs.statSync(configPath); - this._lastModified = stats.mtimeMs; - } - catch { - this._lastModified = 0; - } - } - _debounce(fn, ms) { - let timer; - return () => { - clearTimeout(timer); - timer = setTimeout(fn, ms); - }; - } - async _pushRefresh() { - const configPath = (0, config_path_1.resolveConfigPath)(); - const data = buildControlCenterData(configPath); - const rawConfig = readOpenClawConfigRaw(configPath); - await this._panel.webview.postMessage({ - command: "refresh", - data, - config: rawConfig, - }); - } - _openChannelInstallerTerminal() { - const terminal = vscode.window.createTerminal("openclaw channel installer"); - terminal.show(); - terminal.sendText("openclaw channels add", true); - } - _getHtml(data, rawConfig) { - const webview = this._panel.webview; - // External script loaded via webview URI — this is the ONLY reliable way - // to run JS in modern VS Code / VSCodium because the frame-level CSP requires - // resources to come from webview.cspSource (extension media folder). - const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "media", "setup-panel.js")); - const csp = [ - `default-src 'none'`, - `style-src ${webview.cspSource} 'unsafe-inline'`, - `script-src ${webview.cspSource}`, - `connect-src ${webview.cspSource} https:`, - `img-src ${webview.cspSource} https: data:`, - `font-src ${webview.cspSource} https:`, - ].join("; "); - const serialized = JSON.stringify(data).replace(/ channel.accounts.some((account) => account.status === "connected")).length; - const channelCount = data.channels.length; - const channelCards = data.channels - .map((channel, index) => { - const connected = channel.accounts.some((account) => account.status === "connected"); - const needsReview = channel.accounts.some((account) => account.status === "needs-relink"); - const status = connected ? "Connected" : needsReview ? "Needs review" : "Not connected"; - const chipClass = connected ? "chip-good" : needsReview ? "chip-warn" : "chip-bad"; - const accountChips = channel.accounts - .map((account) => '' + account.title + '') - .join(""); - return ` -
-
-
-
${channel.channel}
-
${channel.description}
-
-
- - ${status} -
-
-
${accountChips}
-
- - -
-
- `; - }) - .join(""); - const baseStyles = ` - :root { - color-scheme: dark; - --accent: #ef4444; - --accent-hover: #dc2626; - --bg: #0b0a0a; - --bg-card: #151111; - --bg-elevated: #1d1414; - --border: #3a1f1f; - --text: #f8f2f2; - --text-muted: #b9a8a8; - --chip-good: #22c55e; - --chip-warn: #f59e0b; - --chip-bad: #ef4444; - } - body { - margin: 0; - background: var(--bg); - color: var(--text); - font-family: var(--vscode-font-family, "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif); - } - * { box-sizing: border-box; } - #app { min-height: 100vh; } - .container { max-width: 1440px; margin: 0 auto; padding: 24px; } - - /* Header */ - .header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; } - .header-title { display: flex; align-items: center; gap: 12px; font-size: 20px; font-weight: 600; } - .header-dot { height: 10px; width: 10px; border-radius: 999px; background: var(--accent); display: inline-block; } - .header-meta { font-size: 12px; color: var(--text-muted); } - - /* Tabs */ - .tabs { display: flex; gap: 8px; margin-bottom: 20px; border-bottom: 1px solid var(--border); padding-bottom: 12px; } - .tab { border: none; cursor: pointer; border-radius: 8px; padding: 8px 16px; font-size: 13px; background: transparent; color: var(--text-muted); transition: all 0.2s; } - .tab:hover { background: var(--bg-elevated); color: var(--text); } - .tab.active { background: var(--accent); color: #081018; font-weight: 600; } - - /* Tab Content */ - .tab-panel { display: none; } - .tab-panel.active { display: block; } - - /* Panel Layout */ - .panel-grid { display: grid; gap: 20px; grid-template-columns: 1fr 320px; } - .panel { border-radius: 16px; background: var(--bg-card); padding: 20px; } - .panel h2 { margin: 0 0 16px 0; font-size: 18px; font-weight: 600; } - .panel-subtitle { margin: -12px 0 20px 0; font-size: 13px; color: var(--text-muted); } - - /* Buttons */ - .btn-primary { border: none; cursor: pointer; background: var(--accent); color: #081018; padding: 10px 16px; border-radius: 8px; font-size: 12px; font-weight: 600; transition: background 0.2s; } - .btn-primary:hover { background: var(--accent-hover); } - .btn-secondary { border: 1px solid var(--border); background: transparent; color: var(--text-muted); padding: 6px 12px; border-radius: 8px; font-size: 11px; cursor: pointer; transition: all 0.2s; } - .btn-secondary:hover { border-color: var(--accent); color: var(--text); } - .btn-sm { padding: 4px 10px; font-size: 11px; } - .btn-block { width: 100%; margin-top: 12px; } - - /* Channel Cards */ - .channel-list { display: grid; gap: 12px; } - .channel-card { border-radius: 12px; border: 1px solid var(--border); background: var(--bg-elevated); padding: 16px; transition: border-color 0.2s; } - .channel-card:hover { border-color: var(--accent); } - .channel-card.active { border-color: var(--accent); background: var(--bg-card); } - .card-row { display: flex; justify-content: space-between; gap: 12px; align-items: center; } - .card-title { font-size: 14px; font-weight: 600; text-transform: capitalize; } - .card-sub { margin-top: 4px; font-size: 12px; color: var(--text-muted); } - .card-actions { margin-top: 12px; display: flex; gap: 8px; } - - /* Status Chips */ - .status-chip { display: flex; align-items: center; gap: 6px; border-radius: 999px; background: var(--bg); padding: 4px 10px; font-size: 11px; color: var(--text); } - .dot { width: 6px; height: 6px; border-radius: 999px; display: inline-block; } - .chip-good { background: var(--chip-good); } - .chip-warn { background: var(--chip-warn); } - .chip-bad { background: var(--chip-bad); } - - /* Pills */ - .pill-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; } - .pill { padding: 4px 10px; border-radius: 999px; background: var(--bg-card); font-size: 11px; color: var(--text-muted); } - .pill.clickable { cursor: pointer; transition: all 0.2s; } - .pill.clickable:hover { background: var(--accent); color: #081018; } - - /* JSON Editor */ - .json-editor { width: 100%; min-height: 400px; resize: vertical; border-radius: 12px; border: 1px solid var(--border); background: var(--bg-elevated); color: var(--text); padding: 16px; font-size: 13px; line-height: 1.6; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; tab-size: 2; } - .json-editor:focus { outline: none; border-color: var(--accent); } - .editor-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } - .editor-status { display: flex; align-items: center; gap: 12px; font-size: 12px; margin-bottom: 10px; } - .status-badge { padding: 4px 10px; border-radius: 999px; font-size: 11px; font-weight: 500; } - .status-badge.ok { background: var(--chip-good); color: #0f172a; } - .status-badge.err { background: var(--chip-bad); color: white; } - .status-badge.warn { background: var(--chip-warn); color: #0f172a; } - - /* Command Console */ - .console-layout { display: grid; gap: 20px; grid-template-columns: 1fr 280px; } - .command-input-wrap { display: flex; gap: 12px; margin-bottom: 16px; } - .command-input { flex: 1; border-radius: 10px; border: 1px solid var(--border); background: var(--bg-elevated); color: var(--text); padding: 12px 16px; font-size: 13px; font-family: ui-monospace, monospace; } - .command-input:focus { outline: none; border-color: var(--accent); } - .quick-commands h3 { margin: 0 0 12px 0; font-size: 13px; color: var(--text-muted); font-weight: 500; } - .quick-grid { display: grid; gap: 8px; } - .quick-btn { text-align: left; padding: 10px 12px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg-elevated); color: var(--text); font-size: 12px; cursor: pointer; transition: all 0.2s; } - .quick-btn:hover { border-color: var(--accent); background: var(--bg-card); } - .quick-btn strong { display: block; font-size: 12px; margin-bottom: 2px; } - .quick-btn span { display: block; font-size: 11px; color: var(--text-muted); } - - /* Sidebar */ - .sidebar h3 { margin: 0 0 12px 0; font-size: 13px; color: var(--text-muted); font-weight: 500; } - .info-card { padding: 12px; border-radius: 10px; background: var(--bg-elevated); margin-bottom: 12px; } - .info-card h4 { margin: 0 0 6px 0; font-size: 12px; color: var(--text); } - .info-card p { margin: 0; font-size: 11px; color: var(--text-muted); line-height: 1.5; } - .connection-status { display: flex; align-items: center; gap: 8px; padding: 12px; border-radius: 10px; background: var(--bg-elevated); margin-top: 16px; } - - .panel-header-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } - .empty-state { color: var(--text-muted); text-align: center; padding: 40px; } - .status-text { font-size: 12px; } - .hint-text { font-size: 11px; color: var(--text-muted); margin-right: 8px; } - .recent-row { margin-bottom: 16px; } - - /* ── Responsive ──────────────────────────────────────────── */ - - /* Collapse sidebars on medium-width panels */ - @media (max-width: 820px) { - .panel-grid { grid-template-columns: 1fr; } - .console-layout { grid-template-columns: 1fr; } - .sidebar { order: -1; } - /* Make quick-commands horizontal on medium */ - .quick-grid { grid-template-columns: repeat(2, 1fr); } - } - - /* Compact spacing on narrow panels */ - @media (max-width: 640px) { - .container { padding: 14px; } - .tabs { gap: 4px; padding-bottom: 10px; flex-wrap: wrap; } - .tab { padding: 6px 10px; font-size: 12px; } - .panel { padding: 14px; border-radius: 12px; } - .panel h2 { font-size: 15px; } - .panel-grid { gap: 14px; } - .console-layout { gap: 14px; } - .header-title { font-size: 17px; } - .json-editor { min-height: 260px; } - .quick-grid { grid-template-columns: 1fr; } - .command-input-wrap { flex-wrap: wrap; } - .command-input { min-width: 0; width: 100%; } - .card-row { flex-wrap: wrap; } - } - - /* Very narrow panels (sidebar panel view) */ - @media (max-width: 400px) { - .container { padding: 10px; } - .tabs { gap: 4px; } - .tab { padding: 5px 8px; font-size: 11px; border-radius: 6px; } - .panel { padding: 10px; border-radius: 10px; } - .panel h2 { font-size: 14px; margin-bottom: 12px; } - .header { gap: 8px; margin-bottom: 14px; } - .header-title { font-size: 15px; gap: 8px; } - .channel-card { padding: 12px; } - .card-actions { flex-wrap: wrap; } - .btn-primary, .btn-secondary { font-size: 11px; } - .editor-toolbar { flex-wrap: wrap; gap: 8px; } - .editor-toolbar > div { flex: 1 1 100%; } - .panel-header-row { flex-wrap: wrap; gap: 8px; } - .panel-header-row > div { flex: 1 1 100%; } - .panel-header-row .btn-primary { width: 100%; } - .json-editor { min-height: 200px; font-size: 12px; } - .status-chip { font-size: 10px; padding: 3px 8px; } - .info-card { padding: 10px; } - } - `; - return ` - - - - - - - - -
-
-
-
- - OpenClaw Configuration -
-
${connectedChannels}/${channelCount} channels connected
-
- -
- - - -
- - -
-
-
-
-
-

Channels

-
Manage your communication channels
-
- -
-
- ${channelCards || '
No channels configured. Add your first channel to get started.
'} -
-
- -
-
- - -
-
-
-
-
-

Configuration JSON

-
Edit ~/.openclaw/openclaw.json directly
-
-
- - -
-
-
- Loading... - Supports JSON5 (comments + trailing commas) -
- -
- -
-
- - -
-
-
-

Command Console

-
Run OpenClaw CLI commands
-
- - -
-
- Recent: -
-
- -
-
-
-
- - - -`; - } -} -exports.ConfigPanel = ConfigPanel; -//# sourceMappingURL=setup.js.map \ No newline at end of file diff --git a/apps/editor/extensions/openclaw/out/panels/setup.js.map b/apps/editor/extensions/openclaw/out/panels/setup.js.map deleted file mode 100644 index 2b768ee1..00000000 --- a/apps/editor/extensions/openclaw/out/panels/setup.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/panels/setup.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/apps/editor/extensions/openclaw/out/panels/status.js b/apps/editor/extensions/openclaw/out/panels/status.js deleted file mode 100644 index e2816909..00000000 --- a/apps/editor/extensions/openclaw/out/panels/status.js +++ /dev/null @@ -1,909 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.StatusPanel = void 0; -const vscode = __importStar(require("vscode")); -const cp = __importStar(require("child_process")); -const fs = __importStar(require("fs")); -const os = __importStar(require("os")); -const path = __importStar(require("path")); -class StatusPanel { - constructor(panel) { - this._disposables = []; - this._refreshing = false; - this._refreshQueued = false; - this._lastStatusAt = 0; - this._isVisible = true; - this._panel = panel; - this._isVisible = panel.visible; - this._panel.webview.html = this._getLoadingHtml(); - void this._update(); - this._panel.onDidDispose(() => this.dispose(), null, this._disposables); - this._panel.onDidChangeViewState(e => { - this._isVisible = e.webviewPanel.visible; - if (this._isVisible) { - void this._update(); - } - }, null, this._disposables); - this._panel.webview.onDidReceiveMessage(msg => { - if (msg.command === 'refresh') { - if (!this._isVisible) - return; - void this._update(); - } - else if (msg.command === 'gateway-start') { - void this._runGateway('start'); - } - else if (msg.command === 'gateway-stop') { - void this._runGateway('stop'); - } - else if (msg.command === 'gateway-restart') { - void this._runGateway('restart'); - } - else if (msg.command === 'install') { - vscode.commands.executeCommand('openclaw.install'); - } - else if (msg.command === 'configure') { - vscode.commands.executeCommand('openclaw.configure'); - } - else if (msg.command === 'open-dashboard' && msg.url) { - vscode.env.openExternal(vscode.Uri.parse(msg.url)); - } - else if (msg.command === 'open-logs' && msg.path) { - let p = msg.path; - if (process.platform === 'win32' && !/^[a-zA-Z]:/.test(p) && /^[\\/]/.test(p)) { - const drive = process.env.SystemDrive || 'C:'; - p = `${drive}${p}`; - } - const uri = vscode.Uri.file(p); - vscode.workspace.openTextDocument(uri).then(doc => vscode.window.showTextDocument(doc)); - } - }, null, this._disposables); - } - static createOrShow(extensionUri) { - if (StatusPanel.currentPanel) { - StatusPanel.currentPanel._panel.reveal(); - return; - } - const panel = vscode.window.createWebviewPanel('openclawStatus', 'OpenClaw Status', vscode.ViewColumn.One, { enableScripts: true }); - StatusPanel.currentPanel = new StatusPanel(panel); - } - dispose() { - StatusPanel.currentPanel = undefined; - this._panel.dispose(); - this._disposables.forEach(d => d.dispose()); - } - async _runGateway(action) { - if (process.platform === 'win32') { - // Windows: Use terminal-based approach with openclaw gateway and Ctrl+C - const terminal = this._ensureGatewayTerminal(); - terminal.show(); - if (action === 'restart') { - const startCmd = 'openclaw gateway'; - terminal.sendText('\u0003', false); - await new Promise(r => setTimeout(r, 1500)); - terminal.sendText(startCmd, true); - vscode.window.showInformationMessage(`Sent: Ctrl+C then ${startCmd}`); - } - else { - if (action === 'start') { - const cmd = 'openclaw gateway'; - terminal.sendText(cmd, true); - vscode.window.showInformationMessage(`Sent: ${cmd}`); - } - else { - terminal.sendText('\u0003', false); - vscode.window.showInformationMessage('Sent: Ctrl+C'); - } - } - } - else { - // Linux/Mac: Use CLI commands (openclaw gateway start/stop/restart) - const cmd = `openclaw gateway ${action}`; - const terminal = this._ensureGatewayTerminal(); - terminal.show(); - terminal.sendText(cmd, true); - vscode.window.showInformationMessage(`Sent: ${cmd}`); - } - await this._update(); - await this._pollStatus(20000, 2000); - } - async _update() { - if (this._refreshing) { - this._refreshQueued = true; - return; - } - this._refreshing = true; - try { - const status = await this._getStatus(); - this._panel.webview.html = this._getHtml(status); - } - finally { - this._refreshing = false; - if (this._refreshQueued) { - this._refreshQueued = false; - void this._update(); - } - } - } - async _getStatus() { - const now = Date.now(); - if (this._lastStatus && now - this._lastStatusAt < 3000) { - return this._lastStatus; - } - const status = await this._getStatusFresh(); - this._lastStatus = status; - this._lastStatusAt = now; - return status; - } - async _getStatusFresh() { - const updatedAt = new Date().toLocaleTimeString(); - const { command, result, cliPath } = await this._runOpenClaw(['gateway', 'status'], 4000); - const out = (result.stdout || result.stderr).trim(); - if (out.length > 0) { - const parsed = this._parseStatus(out); - const trimmedErr = result.stderr.trim(); - const genericFail = result.error && result.error.toLowerCase().startsWith('command failed'); - const error = trimmedErr || (!genericFail ? result.error : undefined); - return { - installed: parsed.installed || !result.notFound, - running: parsed.running, - pid: parsed.pid, - port: parsed.port, - uptime: parsed.uptime, - service: parsed.service, - dashboard: parsed.dashboard, - probe: parsed.probe, - logFile: parsed.logFile, - configPath: parsed.configPath, - issues: parsed.issues, - exitCode: result.code, - cliPath, - command, - stderr: trimmedErr || undefined, - pathEnv: result.pathEnv, - raw: out, - error, - updatedAt, - }; - } - if (result.notFound) { - return { - installed: false, - running: false, - exitCode: result.code, - cliPath, - command, - stderr: result.stderr.trim() || undefined, - pathEnv: result.pathEnv, - raw: 'OpenClaw CLI not detected.', - error: result.error, - updatedAt, - }; - } - if (result.timedOut) { - return { - installed: !result.notFound, - running: false, - exitCode: result.code, - cliPath, - command, - stderr: result.stderr.trim() || undefined, - pathEnv: result.pathEnv, - raw: 'Status command timed out.', - error: result.error, - updatedAt, - }; - } - return { - installed: !result.notFound, - running: false, - exitCode: result.code, - cliPath, - command, - stderr: result.stderr.trim() || undefined, - pathEnv: result.pathEnv, - raw: 'Failed to read gateway status.', - error: result.error, - updatedAt, - }; - } - async _pollStatus(maxMs, intervalMs) { - const deadline = Date.now() + maxMs; - while (Date.now() < deadline) { - if (!this._isVisible) - return; - const status = await this._getStatusFresh(); - this._panel.webview.html = this._getHtml(status); - if (status.running) - return; - await new Promise(r => setTimeout(r, intervalMs)); - } - } - _ensureGatewayTerminal() { - if (this._gatewayTerminal) - return this._gatewayTerminal; - const existing = vscode.window.terminals.find(t => t.name === 'OpenClaw Gateway'); - if (existing) { - this._gatewayTerminal = existing; - return existing; - } - this._gatewayTerminal = vscode.window.createTerminal('OpenClaw Gateway'); - return this._gatewayTerminal; - } - _runCommand(cmd, timeoutMs) { - const env = this._buildExecEnv(); - return new Promise(resolve => { - cp.exec(cmd, { - timeout: timeoutMs, - windowsHide: true, - maxBuffer: 1024 * 1024, - env, - }, (error, stdout, stderr) => { - const result = { - stdout: stdout?.toString() || '', - stderr: stderr?.toString() || '', - pathEnv: env.PATH, - command: cmd, - }; - if (error) { - result.error = error.message || 'Command failed'; - const errCode = error.code; - result.code = errCode ? String(errCode) : undefined; - result.timedOut = errCode === 'ETIMEDOUT'; - const text = `${result.stderr}\n${result.error}`.toLowerCase(); - result.notFound = - errCode === 'ENOENT' || - text.includes('not recognized as an internal or external command') || - text.includes('command not found'); - } - resolve(result); - }); - }); - } - _execFile(command, args, timeoutMs) { - const env = this._buildExecEnv(); - return new Promise(resolve => { - cp.execFile(command, args, { - timeout: timeoutMs, - windowsHide: true, - maxBuffer: 1024 * 1024, - env, - }, (error, stdout, stderr) => { - const result = { - stdout: stdout?.toString() || '', - stderr: stderr?.toString() || '', - pathEnv: env.PATH, - command: [command, ...args].join(' '), - }; - if (error) { - result.error = error.message || 'Command failed'; - const errCode = error.code; - result.code = errCode ? String(errCode) : undefined; - result.timedOut = errCode === 'ETIMEDOUT'; - const text = `${result.stderr}\n${result.error}`.toLowerCase(); - result.notFound = - errCode === 'ENOENT' || - text.includes('not recognized as an internal or external command') || - text.includes('command not found'); - } - resolve(result); - }); - }); - } - async _runOpenClaw(args, timeoutMs = 30000) { - if (process.platform !== 'win32') { - const cliPath = await this._findOpenClawPath(); - if (!cliPath) { - return { - result: { - stdout: '', - stderr: '', - error: 'openclaw not found', - notFound: true, - }, - command: `openclaw ${args.join(' ')}`, - }; - } - const result = await this._execFile(cliPath, args, timeoutMs); - return { result, command: `${cliPath} ${args.join(' ')}`, cliPath }; - } - const mjs = path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'npm', 'node_modules', 'openclaw', 'openclaw.mjs'); - // Find node.exe - let nodeExe; - const candidates = [ - process.env.ProgramFiles ? path.join(process.env.ProgramFiles, 'nodejs', 'node.exe') : '', - 'C:\\Program Files\\nodejs\\node.exe', - 'C:\\Program Files (x86)\\nodejs\\node.exe', - process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Programs', 'nodejs', 'node.exe') : '', - ].filter(Boolean); - nodeExe = candidates.find(p => fs.existsSync(p)); - if (!nodeExe) { - try { - const result = cp.execSync('where node.exe', { timeout: 3000, encoding: 'utf8', windowsHide: true }); - const found = result.trim().split(/\r?\n/)[0]?.trim(); - if (found && fs.existsSync(found)) - nodeExe = found; - } - catch { } - } - if (!nodeExe || !fs.existsSync(mjs)) { - const cliPath = await this._findOpenClawPath(); - if (cliPath) { - const result = await this._execFile(cliPath, args, timeoutMs); - return { result, command: `${cliPath} ${args.join(' ')}`, cliPath }; - } - return { - result: { - stdout: '', - stderr: '', - error: 'node.exe or openclaw.mjs not found', - exitCode: -1, - notFound: true, - }, - command: 'openclaw ' + args.join(' '), - }; - } - const display = `node "${mjs}" ${args.join(' ')}`; - const cmdLine = `node "${mjs}" ${args.join(' ')}`; - const result = await new Promise((resolve) => { - const child = cp.spawn(cmdLine, [], { - timeout: timeoutMs, - windowsHide: true, - shell: true, - stdio: ['ignore', 'pipe', 'pipe'], - env: process.env, - }); - let stdout = ''; - let stderr = ''; - child.stdout?.on('data', data => (stdout += data)); - child.stderr?.on('data', data => (stderr += data)); - const timer = setTimeout(() => { - child.kill('SIGTERM'); - }, timeoutMs); - child.on('close', (code, signal) => { - clearTimeout(timer); - if (signal === 'SIGTERM' || code === null) { - resolve({ error: 'Timed out after 30s', stdout, stderr, exitCode: null }); - } - else if (code !== 0) { - resolve({ error: stderr.trim() || `Exit ${code}`, stdout, stderr, exitCode: code ?? undefined }); - } - else { - resolve({ stdout, stderr, exitCode: 0 }); - } - }); - child.on('error', err => { - clearTimeout(timer); - resolve({ error: err.message, stdout, stderr, exitCode: undefined }); - }); - }); - return { result, command: display, cliPath: mjs }; - } - /** Find the full path to node.exe on Windows */ - _findNodeExe() { - if (process.platform !== 'win32') - return 'node'; - // Check common install paths - const candidates = [ - process.env.ProgramFiles ? path.join(process.env.ProgramFiles, 'nodejs', 'node.exe') : '', - 'C:\\Program Files\\nodejs\\node.exe', - 'C:\\Program Files (x86)\\nodejs\\node.exe', - process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Programs', 'nodejs', 'node.exe') : '', - ].filter(Boolean); - for (const p of candidates) { - if (fs.existsSync(p)) - return p; - } - // Try where command - try { - const result = cp.execSync('where node.exe', { timeout: 3000, encoding: 'utf8', windowsHide: true }); - const found = result.trim().split(/\r?\n/)[0]?.trim(); - if (found && fs.existsSync(found)) - return found; - } - catch { } - return undefined; - } - _buildExecEnv() { - const env = { ...process.env }; - const basePath = env.PATH || env.Path || ''; - const extra = []; - if (process.platform === 'win32') { - if (env.APPDATA) - extra.push(path.join(env.APPDATA, 'npm')); - if (env.ProgramFiles) - extra.push(path.join(env.ProgramFiles, 'nodejs')); - // Common Node.js install locations on Windows (nvm-windows, volta, fnm, scoop, etc.) - const home = os.homedir(); - const localAppData = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'); - extra.push(path.join(localAppData, 'Volta', 'bin')); - extra.push(path.join(localAppData, 'fnm', 'node-versions')); - extra.push(path.join(env.APPDATA || '', 'nvm')); - extra.push(path.join(home, 'scoop', 'shims')); - // Try to find node.exe from the extension host's own process - const nodeDir = this._findNodeDir(); - if (nodeDir) - extra.push(nodeDir); - const systemRoot = env.SystemRoot || env.WINDIR; - if (systemRoot) - extra.push(path.join(systemRoot, 'System32')); - } - else { - extra.push('/usr/local/bin', '/opt/homebrew/bin'); - extra.push(path.join(os.homedir(), '.local', 'bin')); - extra.push(path.join(os.homedir(), '.npm-global', 'bin')); - } - const sep = process.platform === 'win32' ? ';' : ':'; - env.PATH = [...extra, basePath].filter(Boolean).join(sep); - env.Path = env.PATH; - return env; - } - /** Find the directory containing node.exe by checking common locations */ - _findNodeDir() { - if (process.platform !== 'win32') - return undefined; - // 1. Check if node.exe is alongside npm in APPDATA - const appData = process.env.APPDATA; - if (appData) { - const npmDir = path.join(appData, 'npm'); - // npm .cmd shims use %~dp0\node.exe or fall back to 'node' on PATH - // Check the nodejs install dir referenced by the shim - } - // 2. Check common install paths - const candidates = [ - process.env.ProgramFiles && path.join(process.env.ProgramFiles, 'nodejs'), - process.env['ProgramFiles(x86)'] && path.join(process.env['ProgramFiles(x86)'], 'nodejs'), - path.join(os.homedir(), '.nvm', 'versions'), - ].filter(Boolean); - for (const dir of candidates) { - if (fs.existsSync(path.join(dir, 'node.exe'))) - return dir; - } - // 3. Try to extract node location from NVM_HOME or NVM_SYMLINK - const nvmSymlink = process.env.NVM_SYMLINK; - if (nvmSymlink && fs.existsSync(path.join(nvmSymlink, 'node.exe'))) - return nvmSymlink; - const nvmHome = process.env.NVM_HOME; - if (nvmHome && fs.existsSync(nvmHome)) { - // nvm-windows creates a symlink or uses a current version dir - try { - const entries = fs.readdirSync(nvmHome).filter(e => /^v?\d/.test(e)); - for (const entry of entries) { - const p = path.join(nvmHome, entry); - if (fs.existsSync(path.join(p, 'node.exe'))) - return p; - } - } - catch { } - } - return undefined; - } - async _findOpenClawPath() { - for (const candidate of this._getWorkspaceCliCandidates()) { - if (fs.existsSync(candidate)) - return candidate; - } - const envPath = process.env.OPENCLAW_CLI; - if (envPath && fs.existsSync(envPath)) - return envPath; - if (process.platform === 'win32') { - const appData = process.env.APPDATA; - if (appData) { - const p = path.join(appData, 'npm', 'openclaw.cmd'); - if (fs.existsSync(p)) - return p; - } - } - const cfgPath = vscode.workspace.getConfiguration('openclaw').get('cliPath'); - if (cfgPath && fs.existsSync(cfgPath)) - return cfgPath; - let cmd = process.platform === 'win32' ? 'where openclaw' : 'which openclaw'; - if (process.platform === 'win32') { - const systemRoot = process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows'; - const whereExe = path.join(systemRoot, 'System32', 'where.exe'); - cmd = `"${whereExe}" openclaw`; - } - const result = await this._runCommand(cmd, 2000); - if (!result.error && !result.notFound) { - const out = (result.stdout || '').trim(); - if (out) { - const candidates = out - .split(/\r?\n/) - .map(l => l.trim().replace(/^"+|"+$/g, '')) - .filter(Boolean); - for (const candidate of candidates) { - const resolved = this._resolveWindowsCliPath(candidate); - if (fs.existsSync(resolved)) - return resolved; - } - } - } - if (process.platform === 'win32') { - const psPath = await this._findOpenClawViaPowerShell(); - if (psPath && fs.existsSync(psPath)) - return psPath; - } - const npmCandidates = await this._getNpmGlobalCliCandidates(); - for (const candidate of npmCandidates) { - if (fs.existsSync(candidate)) - return candidate; - } - const candidates = this._getCandidateCliPaths(); - for (const candidate of candidates) { - if (fs.existsSync(candidate)) - return candidate; - } - return undefined; - } - _getWorkspaceCliCandidates() { - const exts = process.platform === 'win32' - ? ['openclaw.cmd', 'openclaw.exe', 'openclaw.ps1', 'openclaw.bat', 'openclaw'] - : ['openclaw']; - const folders = vscode.workspace.workspaceFolders?.map(f => f.uri.fsPath) ?? []; - const candidates = []; - for (const root of folders) { - for (const name of exts) { - candidates.push(path.join(root, 'node_modules', '.bin', name)); - } - } - return candidates; - } - async _findOpenClawViaPowerShell() { - const ps = 'powershell.exe'; - const cmd = [ - '-NoProfile', - '-Command', - '($c = Get-Command openclaw -ErrorAction SilentlyContinue | Select-Object -First 1); ' + - 'if ($c) { $c.Path; if (-not $c.Path) { $c.Source }; if (-not $c.Path -and -not $c.Source) { $c.Definition } }', - ]; - const result = await this._execFile(ps, cmd, 2000); - const out = (result.stdout || '').trim(); - if (!out) - return undefined; - return out.split(/\r?\n/)[0].trim(); - } - _getCandidateCliPaths() { - const home = os.homedir(); - if (process.platform === 'win32') { - const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming'); - const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'); - const programFiles = process.env.ProgramFiles || 'C:\\Program Files'; - return [ - path.join(appData, 'npm', 'openclaw.cmd'), - path.join(appData, 'npm', 'openclaw.exe'), - path.join(appData, 'npm', 'openclaw.ps1'), - path.join(appData, 'npm', 'openclaw.bat'), - path.join(localAppData, 'Programs', 'OpenClaw', 'openclaw.exe'), - path.join(localAppData, 'OpenClaw', 'openclaw.exe'), - path.join(programFiles, 'OpenClaw', 'openclaw.exe'), - path.join(programFiles, 'OpenClaw', 'bin', 'openclaw.exe'), - path.join(localAppData, 'Microsoft', 'WindowsApps', 'openclaw.exe'), - path.join(home, '.openclaw', 'bin', 'openclaw.exe'), - ]; - } - return [ - '/usr/local/bin/openclaw', - '/opt/homebrew/bin/openclaw', - path.join(home, '.local', 'bin', 'openclaw'), - path.join(home, '.npm-global', 'bin', 'openclaw'), - path.join(home, '.openclaw', 'bin', 'openclaw'), - ]; - } - async _getNpmGlobalCliCandidates() { - const result = await this._runCommand('npm config get prefix', 2000); - const prefix = (result.stdout || '').trim(); - if (!prefix) - return []; - if (process.platform === 'win32') { - const base = this._resolveWindowsCliPath(path.join(prefix, 'openclaw')); - const withExts = [ - `${base}.cmd`, - `${base}.exe`, - `${base}.ps1`, - `${base}.bat`, - base, - ]; - return withExts; - } - return [path.join(prefix, 'bin', 'openclaw')]; - } - _resolveWindowsCliPath(candidate) { - if (process.platform !== 'win32') - return candidate; - const cleaned = candidate.replace(/^"+|"+$/g, ''); - if (fs.existsSync(cleaned)) - return cleaned; - if (path.extname(cleaned)) - return cleaned; - const exts = ['.cmd', '.exe', '.bat', '.ps1']; - for (const ext of exts) { - const withExt = `${cleaned}${ext}`; - if (fs.existsSync(withExt)) - return withExt; - } - return cleaned; - } - _parseStatus(out) { - const lower = out.toLowerCase(); - let running = /rpc probe:\s*ok/.test(lower); - if (/rpc probe:\s*failed/.test(lower)) - running = false; - if (!/rpc probe:\s*(ok|failed)/.test(lower)) { - running = /runtime:\s*running|gateway:\s*running/.test(lower) || /running|active|started/.test(lower); - if (/not running|stopped|inactive|down|runtime:\s*stopped/.test(lower)) - running = false; - } - const pid = out.match(/pid[:\s]+(\d+)/i)?.[1]; - const port = out.match(/port[:=\s]+(\d+)/i)?.[1]; - const uptime = out.match(/uptime[:\s]+([^\n]+)/i)?.[1]; - const service = out.match(/service:\s*([^\n]+)/i)?.[1]?.trim(); - const dashboard = out.match(/dashboard:\s*(https?:\/\/[^\s]+)/i)?.[1]; - const probe = out.match(/probe target:\s*([^\n]+)/i)?.[1]?.trim(); - const logFile = out.match(/file logs:\s*([^\n]+)/i)?.[1]?.trim(); - const configPath = out.match(/config \(cli\):\s*([^\n]+)/i)?.[1]?.trim(); - const installed = /openclaw\s+\d{4}\./i.test(out) || /openclaw/gi.test(out); - const issues = this._extractIssues(out); - return { running, pid, port, uptime, service, dashboard, probe, logFile, configPath, installed, issues }; - } - _extractIssues(out) { - const lines = out.split(/\r?\n/).map(l => l.trim()).filter(Boolean); - const issues = []; - for (const line of lines) { - const lower = line.toLowerCase(); - if (lower.startsWith('fix:') || lower.startsWith('troubles:') || lower.startsWith('troubleshooting:')) { - issues.push(line); - continue; - } - if (lower.includes('missing') && lower.includes('config')) { - issues.push(line); - continue; - } - if (lower.startsWith('rpc probe:') || lower.startsWith('service is loaded but not running')) { - issues.push(line); - continue; - } - if (lower.startsWith('runtime:') && lower.includes('stopped')) { - issues.push(line); - continue; - } - if (lower.includes('requires explicit credentials') || lower.includes('pass --token') || lower.includes('pass --password')) { - issues.push(line); - continue; - } - } - return issues; - } - _escapeHtml(input) { - return input - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - _getLoadingHtml() { - return ` - - - - - - - -

OpenClaw Gateway

-
- Fetching status - -`; - } - _getHtml(status) { - const { running, installed, dashboard, updatedAt } = status; - const safeDashboard = this._escapeHtml(dashboard || '—'); - return ` - - - - - - - -

OpenClaw Gateway

-
Last updated: ${updatedAt}
-
- - ${running ? 'Running' : installed ? 'Stopped' : 'Not Installed'} - ${running ? 'Healthy' : installed ? 'Offline' : 'Missing'} -
-
-
Status${running ? 'Running' : installed ? 'Stopped' : 'Missing'}
-
-
-
- Dashboard - ${dashboard ? `${safeDashboard}` : `${safeDashboard}`} -
-
-
- ${installed - ? (running - ? '' - : '') - : ''} - ${installed ? '' : ''} - -
- - -`; - } -} -exports.StatusPanel = StatusPanel; -//# sourceMappingURL=status.js.map \ No newline at end of file diff --git a/apps/editor/extensions/openclaw/out/panels/status.js.map b/apps/editor/extensions/openclaw/out/panels/status.js.map deleted file mode 100644 index 021ccd8c..00000000 --- a/apps/editor/extensions/openclaw/out/panels/status.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/panels/status.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/apps/editor/extensions/openclaw/package.json b/apps/editor/extensions/openclaw/package.json index a1cd46e4..a4ee0eb9 100644 --- a/apps/editor/extensions/openclaw/package.json +++ b/apps/editor/extensions/openclaw/package.json @@ -80,6 +80,10 @@ "when": "true" } ], + "viewsContainers": { + "activitybar": [] + }, + "views": {}, "commands": [ { "command": "openclaw.home", @@ -104,6 +108,22 @@ { "command": "openclaw.runWithSudo", "title": "OCC: Run Command with Sudo" + }, + { + "command": "openclaw.pickHost", + "title": "OpenClaw: Switch Host" + }, + { + "command": "openclaw.setActiveHost", + "title": "OpenClaw: Set Active Host" + }, + { + "command": "openclaw.refreshHost", + "title": "OpenClaw: Refresh Host Status" + }, + { + "command": "openclaw.host.setup.docker", + "title": "OpenClaw: Docker Setup" } ] }, @@ -114,6 +134,7 @@ "devDependencies": { "@types/node": "^20.19.35", "@types/vscode": "^1.85.0", + "dotenv": "^17.4.1", "typescript": "^5.9.3" }, "icon": "media/icon.png", diff --git a/apps/editor/extensions/openclaw/src/errorCodes.ts b/apps/editor/extensions/openclaw/src/errorCodes.ts new file mode 100644 index 00000000..7124cd7c --- /dev/null +++ b/apps/editor/extensions/openclaw/src/errorCodes.ts @@ -0,0 +1,205 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +/** + * Error codes for app failures, used for diagnostic reporting and UI display. + * Each code maps to a user-friendly message and determines retry behavior. + */ +export enum ErrorCode { + // Docker provisioning errors + ERR_DOCKER_TIMEOUT = 'ERR_DOCKER_TIMEOUT', + ERR_DOCKER_NOT_INSTALLED = 'ERR_DOCKER_NOT_INSTALLED', + ERR_DOCKER_NOT_RUNNING = 'ERR_DOCKER_NOT_RUNNING', + ERR_DOCKER_PULL_FAILED = 'ERR_DOCKER_PULL_FAILED', + ERR_DOCKER_CONTAINER_START = 'ERR_DOCKER_CONTAINER_START', + + // Gateway/OpenClaw service errors + ERR_GATEWAY_START = 'ERR_GATEWAY_START', + ERR_GATEWAY_HEALTH_CHECK = 'ERR_GATEWAY_HEALTH_CHECK', + ERR_GATEWAY_PORT_CONFLICT = 'ERR_GATEWAY_PORT_CONFLICT', + ERR_GATEWAY_AUTH = 'ERR_GATEWAY_AUTH', + + // CLI/Installation errors + ERR_CLI_NOT_INSTALLED = 'ERR_CLI_NOT_INSTALLED', + ERR_CLI_MISSING_DEPENDENCY = 'ERR_CLI_MISSING_DEPENDENCY', + ERR_CLI_VERSION_MISMATCH = 'ERR_CLI_VERSION_MISMATCH', + ERR_NPM_INSTALL_FAILED = 'ERR_NPM_INSTALL_FAILED', + + // Network errors + ERR_NETWORK_TIMEOUT = 'ERR_NETWORK_TIMEOUT', + ERR_NETWORK_CONNECTION_FAILED = 'ERR_NETWORK_CONNECTION_FAILED', + ERR_NETWORK_DNS_FAILED = 'ERR_NETWORK_DNS_FAILED', + + // System resource errors + ERR_INSUFFICIENT_DISK_SPACE = 'ERR_INSUFFICIENT_DISK_SPACE', + ERR_INSUFFICIENT_MEMORY = 'ERR_INSUFFICIENT_MEMORY', + ERR_PERMISSION_DENIED = 'ERR_PERMISSION_DENIED', + + // File system errors + ERR_FILE_NOT_FOUND = 'ERR_FILE_NOT_FOUND', + ERR_FILE_READ_FAILED = 'ERR_FILE_READ_FAILED', + ERR_CONFIG_INVALID = 'ERR_CONFIG_INVALID', + + // Unknown errors + ERR_UNKNOWN = 'ERR_UNKNOWN', +} + +/** + * Maps error codes to user-friendly messages and retry recommendations + */ +export const errorCodeMessages: Record = { + [ErrorCode.ERR_DOCKER_TIMEOUT]: { + title: 'Docker Operation Timed Out', + message: 'Docker took too long to respond. Check your internet connection and Docker resources.', + canRetry: true, + }, + [ErrorCode.ERR_DOCKER_NOT_INSTALLED]: { + title: 'Docker Not Installed', + message: 'Docker is required but not installed. Visit https://docs.docker.com/get-docker/ to install.', + canRetry: false, + }, + [ErrorCode.ERR_DOCKER_NOT_RUNNING]: { + title: 'Docker Not Running', + message: 'Docker daemon is not running. Start Docker Desktop or Docker service and retry.', + canRetry: true, + }, + [ErrorCode.ERR_DOCKER_PULL_FAILED]: { + title: 'Failed to Download Docker Image', + message: 'Could not download the required Docker image. Check your internet and retry.', + canRetry: true, + }, + [ErrorCode.ERR_DOCKER_CONTAINER_START]: { + title: 'Failed to Start Container', + message: 'The Docker container failed to start. Check Docker logs and available resources.', + canRetry: true, + }, + [ErrorCode.ERR_GATEWAY_START]: { + title: 'Gateway Failed to Start', + message: 'The OpenClaw gateway service failed to start. Check the data directory and configuration.', + canRetry: true, + }, + [ErrorCode.ERR_GATEWAY_HEALTH_CHECK]: { + title: 'Gateway Health Check Failed', + message: 'The gateway started but failed health checks. It may be overloaded or misconfigured.', + canRetry: true, + }, + [ErrorCode.ERR_GATEWAY_PORT_CONFLICT]: { + title: 'Port Already in Use', + message: 'The configured port is already in use. Choose a different port or close the conflicting application.', + canRetry: false, + }, + [ErrorCode.ERR_GATEWAY_AUTH]: { + title: 'Gateway Authentication Failed', + message: 'Could not authenticate with the gateway. Check your credentials and retry.', + canRetry: true, + }, + [ErrorCode.ERR_CLI_NOT_INSTALLED]: { + title: 'OpenClaw CLI Not Found', + message: 'The OpenClaw command-line tool is not installed. Run "npm install -g @openclaw/cli".', + canRetry: false, + }, + [ErrorCode.ERR_CLI_MISSING_DEPENDENCY]: { + title: 'Missing Dependency', + message: 'A required dependency for OpenClaw is missing. Try reinstalling.', + canRetry: false, + }, + [ErrorCode.ERR_CLI_VERSION_MISMATCH]: { + title: 'Version Mismatch', + message: 'The installed version is incompatible. Update or reinstall OpenClaw.', + canRetry: false, + }, + [ErrorCode.ERR_NPM_INSTALL_FAILED]: { + title: 'npm Install Failed', + message: 'Failed to install Node.js dependencies. Check your network and npm configuration.', + canRetry: true, + }, + [ErrorCode.ERR_NETWORK_TIMEOUT]: { + title: 'Network Request Timed Out', + message: 'The network request took too long. Check your connection and retry.', + canRetry: true, + }, + [ErrorCode.ERR_NETWORK_CONNECTION_FAILED]: { + title: 'Network Connection Failed', + message: 'Could not connect to the required service. Check your internet connection.', + canRetry: true, + }, + [ErrorCode.ERR_NETWORK_DNS_FAILED]: { + title: 'DNS Resolution Failed', + message: 'Could not resolve the hostname. Check your internet connection and DNS.', + canRetry: true, + }, + [ErrorCode.ERR_INSUFFICIENT_DISK_SPACE]: { + title: 'Insufficient Disk Space', + message: 'Not enough disk space available. Free up space and retry.', + canRetry: false, + }, + [ErrorCode.ERR_INSUFFICIENT_MEMORY]: { + title: 'Insufficient Memory', + message: 'Not enough memory available. Close other applications and retry.', + canRetry: false, + }, + [ErrorCode.ERR_PERMISSION_DENIED]: { + title: 'Permission Denied', + message: 'Missing permissions to complete this operation. Check file/directory permissions.', + canRetry: false, + }, + [ErrorCode.ERR_FILE_NOT_FOUND]: { + title: 'File Not Found', + message: 'A required file could not be found. Check your configuration and data directory.', + canRetry: false, + }, + [ErrorCode.ERR_FILE_READ_FAILED]: { + title: 'Failed to Read File', + message: 'Could not read a required file. Check file permissions and disk status.', + canRetry: true, + }, + [ErrorCode.ERR_CONFIG_INVALID]: { + title: 'Invalid Configuration', + message: 'Configuration file is invalid or corrupted. Reset and reconfigure.', + canRetry: false, + }, + [ErrorCode.ERR_UNKNOWN]: { + title: 'Unknown Error', + message: 'An unexpected error occurred. Check the logs for more details and try again.', + canRetry: true, + }, +}; + +/** + * Infer error code from error message or exception + */ +export function inferErrorCode(error: Error | string): ErrorCode { + const message = typeof error === 'string' ? error : error.message; + const lower = message.toLowerCase(); + + // Docker errors + if (lower.includes('docker') && lower.includes('not found')) return ErrorCode.ERR_DOCKER_NOT_INSTALLED; + if (lower.includes('docker') && lower.includes('timeout')) return ErrorCode.ERR_DOCKER_TIMEOUT; + if (lower.includes('docker') && lower.includes('not running')) return ErrorCode.ERR_DOCKER_NOT_RUNNING; + if (lower.includes('pull') && lower.includes('failed')) return ErrorCode.ERR_DOCKER_PULL_FAILED; + if (lower.includes('container') && lower.includes('start')) return ErrorCode.ERR_DOCKER_CONTAINER_START; + + // Gateway errors + if (lower.includes('gateway') && lower.includes('health')) return ErrorCode.ERR_GATEWAY_HEALTH_CHECK; + if (lower.includes('gateway') && lower.includes('start')) return ErrorCode.ERR_GATEWAY_START; + if (lower.includes('port') && lower.includes('already in use')) return ErrorCode.ERR_GATEWAY_PORT_CONFLICT; + if (lower.includes('gateway') && lower.includes('auth')) return ErrorCode.ERR_GATEWAY_AUTH; + + // CLI errors + if (lower.includes('openclaw') && lower.includes('not found')) return ErrorCode.ERR_CLI_NOT_INSTALLED; + if (lower.includes('npm install') && lower.includes('fail')) return ErrorCode.ERR_NPM_INSTALL_FAILED; + + // Network errors + if (lower.includes('timeout')) return ErrorCode.ERR_NETWORK_TIMEOUT; + if (lower.includes('econnrefused') || lower.includes('connection refused')) return ErrorCode.ERR_NETWORK_CONNECTION_FAILED; + if (lower.includes('enotfound') || lower.includes('dns')) return ErrorCode.ERR_NETWORK_DNS_FAILED; + + // File system errors + if (lower.includes('enoent') || lower.includes('no such file')) return ErrorCode.ERR_FILE_NOT_FOUND; + if (lower.includes('permission denied') || lower.includes('eacces')) return ErrorCode.ERR_PERMISSION_DENIED; + if (lower.includes('enospc') || lower.includes('no space')) return ErrorCode.ERR_INSUFFICIENT_DISK_SPACE; + + return ErrorCode.ERR_UNKNOWN; +} diff --git a/apps/editor/extensions/openclaw/src/errorReporting.ts b/apps/editor/extensions/openclaw/src/errorReporting.ts new file mode 100644 index 00000000..bc7f0484 --- /dev/null +++ b/apps/editor/extensions/openclaw/src/errorReporting.ts @@ -0,0 +1,209 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import * as fs from 'fs'; +import * as path from 'path'; +import { ErrorCode } from './errorCodes.js'; + +/** + * System information to include in error reports + */ +export interface SystemInfo { + os: string; + platform: 'win32' | 'darwin' | 'linux'; + architecture: string; + nodeVersion: string; + totalMemory: number; + freeMemory: number; +} + +/** + * Error report to send to backend + */ +export interface ErrorReport { + timestamp: number; + errorCode: ErrorCode; + errorMessage: string; + logs: string[]; + systemInfo: SystemInfo; + extensionVersion?: string; + userEmail?: string; +} + +/** + * Collects system information and logs for error reporting + */ +export class ErrorReporter { + /** + * Get current system information + */ + static getSystemInfo(): SystemInfo { + const meminfo = os.totalmem(); + return { + os: os.type(), + platform: process.platform as 'win32' | 'darwin' | 'linux', + architecture: os.arch(), + nodeVersion: process.version, + totalMemory: meminfo, + freeMemory: os.freemem(), + }; + } + + /** + * Read recent logs from ~/.openclaw/occ-home.log + * Returns the last N lines, or all lines if less than N + */ + static collectRecentLogs(lineLimit: number = 100): string[] { + const logPath = path.join(os.homedir(), '.openclaw', 'occ-home.log'); + + try { + if (!fs.existsSync(logPath)) { + return ['[No logs found - log file does not exist]']; + } + + const content = fs.readFileSync(logPath, 'utf-8'); + const lines = content.split('\n').filter(line => line.trim()); + + // Return last N lines + return lines.slice(Math.max(0, lines.length - lineLimit)); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + return [`[Failed to read logs: ${errorMsg}]`]; + } + } + + /** + * Build a complete error report + */ + static createErrorReport( + errorCode: ErrorCode, + errorMessage: string, + options?: { + extensionVersion?: string; + userEmail?: string; + logLimit?: number; + } + ): ErrorReport { + return { + timestamp: Date.now(), + errorCode, + errorMessage, + logs: this.collectRecentLogs(options?.logLimit ?? 100), + systemInfo: this.getSystemInfo(), + extensionVersion: options?.extensionVersion, + userEmail: options?.userEmail, + }; + } + + /** + * Send error report to backend + * Fires asynchronously and does not block on failure + */ + static async sendErrorReport( + report: ErrorReport, + backendUrl: string = 'https://occ.mba.sh', + options?: { + timeout?: number; + onSuccess?: (report: ErrorReport) => void; + onFailure?: (error: Error) => void; + } + ): Promise { + const endpoint = `${backendUrl}/api/v1/developer/error-report`; + const timeout = options?.timeout ?? 5000; + + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(report), + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`Server returned ${response.status}: ${response.statusText}`); + } + + options?.onSuccess?.(report); + } catch (err) { + const error = err instanceof Error ? err : new Error(String(err)); + options?.onFailure?.(error); + // Don't throw - fire-and-forget + } + } + + /** + * Store a failed report locally for manual retry + * Saves to ~/.openclaw/failed-error-reports.jsonl (one JSON per line) + */ + static storeFailedReport(report: ErrorReport): boolean { + const logDir = path.join(os.homedir(), '.openclaw'); + const reportPath = path.join(logDir, 'failed-error-reports.jsonl'); + + try { + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + + const line = JSON.stringify(report) + '\n'; + fs.appendFileSync(reportPath, line, 'utf-8'); + return true; + } catch { + // Non-fatal - failure to store is not critical + return false; + } + } + + /** + * Retrieve stored failed reports for manual sending + */ + static getStoredFailedReports(): ErrorReport[] { + const reportPath = path.join(os.homedir(), '.openclaw', 'failed-error-reports.jsonl'); + + try { + if (!fs.existsSync(reportPath)) { + return []; + } + + const content = fs.readFileSync(reportPath, 'utf-8'); + return content + .split('\n') + .filter(line => line.trim()) + .map(line => { + try { + return JSON.parse(line) as ErrorReport; + } catch { + return null; + } + }) + .filter((r): r is ErrorReport => r !== null); + } catch { + return []; + } + } + + /** + * Clear stored reports after successful sending + */ + static clearStoredReports(): boolean { + const reportPath = path.join(os.homedir(), '.openclaw', 'failed-error-reports.jsonl'); + + try { + if (fs.existsSync(reportPath)) { + fs.unlinkSync(reportPath); + } + return true; + } catch { + return false; + } + } +} diff --git a/apps/editor/extensions/openclaw/src/extension.ts b/apps/editor/extensions/openclaw/src/extension.ts index 67f318d3..a7aa975e 100644 --- a/apps/editor/extensions/openclaw/src/extension.ts +++ b/apps/editor/extensions/openclaw/src/extension.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import * as cp from 'child_process'; import * as os from 'os'; import * as fs from 'fs'; import * as path from 'path'; @@ -6,7 +7,13 @@ import * as http from 'http'; import * as https from 'https'; import { HomePanel } from './panels/home'; import { StatusPanel } from './panels/status'; +import { setActiveOpenClawWorkspaceFolder } from './panels/statusController'; import { stopConfigProxy, getDashboardUrl } from './panels/config'; +import { HostRegistry } from './hosts/registry'; +import { HostManager } from './hosts/manager'; +import { HostStatusBarItem } from './hosts/statusbar'; +import { HostTreeProvider } from './hosts/tree'; +import type { OpenClawCoreAPI } from './hosts/types'; const DEFAULT_GATEWAY_PORT = 18789; @@ -24,10 +31,88 @@ function getConfiguredGatewayPort(): number { } } +// ── Window host binding ───────────────────────────────────────────────────── + +/** Describes which host this VS Code window is currently bound to. */ +export interface WindowHostBinding { + type: 'local' | 'docker' | 'ssh'; + hostId: string; + port: number; + label: string; +} + +const WINDOW_HOST_KEY = 'occ.windowHost'; + +function registerWindowHostCommands(context: vscode.ExtensionContext): void { + context.subscriptions.push( + vscode.commands.registerCommand('occ.window.setHost', (binding: WindowHostBinding) => { + void context.workspaceState.update(WINDOW_HOST_KEY, binding); + }), + vscode.commands.registerCommand('occ.window.clearHost', () => { + void context.workspaceState.update(WINDOW_HOST_KEY, undefined); + }), + /** Returns the current WindowHostBinding for this window, or null. */ + vscode.commands.registerCommand('occ.window.getHost', () => { + return context.workspaceState.get(WINDOW_HOST_KEY) ?? null; + }), + ); +} + +/** + * Smart routing for the "OCC Home" command. + * Priority: stored window binding → detected state → install wizard. + */ +function routeHome(extensionUri: vscode.Uri, context: vscode.ExtensionContext, forcePicker = false): void { + // When forcePicker is set (e.g. after disconnect) always show the host picker — never auto-route. + if (forcePicker) { + HomePanel.createOrShow(extensionUri, true); + return; + } + + // 1. If this window already has a binding, honour it. + const binding = context.workspaceState.get(WINDOW_HOST_KEY); + if (binding?.type === 'local') { + void vscode.commands.executeCommand('openclaw.host.setup.local'); + return; + } + if (binding?.type === 'docker') { + void vscode.commands.executeCommand('openclaw.host.setup.docker'); + return; + } + + // 2. No binding — detect installed hosts and route. + const isLocalInstalled = fs.existsSync( + path.join(os.homedir(), '.openclaw', 'openclaw.json') + ); + + let isDockerRunning = false; + try { + const result = cp.spawnSync( + 'docker', + ['ps', '--filter', 'name=^/occ-openclaw$', '--format', '{{.Status}}'], + { timeout: 3000, windowsHide: true }, + ); + const st = (result.stdout?.toString() ?? '').trim(); + isDockerRunning = st.length > 0 && st.toLowerCase().startsWith('up'); + } catch { /* docker not available */ } + + if (isLocalInstalled && isDockerRunning) { + HomePanel.createOrShow(extensionUri); // hosts overview — user picks + } else if (isLocalInstalled) { + void vscode.commands.executeCommand('openclaw.host.setup.local'); + } else if (isDockerRunning) { + void vscode.commands.executeCommand('openclaw.host.setup.docker'); + } else { + HomePanel.createOrShow(extensionUri); // install wizard + } +} + /** Returns true if the OpenClaw web server is reachable. */ -function isWebServerReachable(): Promise { - const port = getConfiguredGatewayPort(); - const url = `http://localhost:${port}/`; +function isWebServerReachable(portOverride?: number): Promise { + const port = portOverride ?? getConfiguredGatewayPort(); + // Always use 127.0.0.1 explicitly — never 0.0.0.0 or a hostname that + // might resolve to a non-loopback address. + const url = `http://127.0.0.1:${port}/`; return new Promise(resolve => { const req = http.get(url, { timeout: 3000 }, res => { res.resume(); @@ -130,7 +215,7 @@ async function hideActivityBarItems( */ const WORKSPACE_FILENAME = 'My OpenClaw Workspace.code-workspace'; -async function openOpenClawFolder(): Promise { +async function openOpenClawFolder(context?: vscode.ExtensionContext): Promise { // Ensure ~/.occ exists — OCcode's internal state directory. const occPath = path.join(os.homedir(), '.occ'); if (!fs.existsSync(occPath)) { @@ -177,9 +262,15 @@ async function openOpenClawFolder(): Promise { ); } - // If we're already inside this workspace, nothing more to do. + // If we're already inside this workspace, nothing more to do — except ensure + // the workspace folder reflects the current host (not a stale Docker folder). const workspaceFileUri = vscode.Uri.file(workspaceFilePath); if (vscode.workspace.workspaceFile?.fsPath === workspaceFileUri.fsPath) { + const binding = context?.workspaceState.get<{ type: string }>(WINDOW_HOST_KEY); + if (binding?.type !== 'docker') { + // Not bound to Docker — ensure ~/.openclaw is shown, not occ-state-dir. + setActiveOpenClawWorkspaceFolder(openclawPath); + } return; } @@ -190,7 +281,7 @@ async function openOpenClawFolder(): Promise { // ── Inference balance status bar ────────────────────────────────────────────── const BACKEND_BALANCE_KEY = 'occBackendBalanceV1'; // cached backend balance — persists across restarts -const OCC_JWT_KEY = 'occJwtV1'; // JWT stored directly in extension storage — no renderer IPC needed +const OCC_JWT_KEY = 'occJwtV1'; // JWT stored in SecretStorage (OS keychain) — encrypted at rest function initBalanceBar(context: vscode.ExtensionContext): (amount?: number) => void { // Restore cached backend balance so status bar shows the correct value immediately on startup @@ -284,13 +375,13 @@ function initBalanceBar(context: vscode.ExtensionContext): (amount?: number) => // FALLBACK: if extension globalState has no JWT (e.g. user signed in before this session // synced, or the extension was reloaded), read from occLegacyJwt in VS Code settings and // backfill extension globalState so future reads work without IPC. - let jwt = context.globalState.get(OCC_JWT_KEY, ''); + let jwt = (await context.secrets.get(OCC_JWT_KEY)) ?? ''; if (!jwt) { try { const legacyJwt = await vscode.commands.executeCommand('occ.auth.getLegacyJwt'); if (legacyJwt) { jwt = legacyJwt; - await context.globalState.update(OCC_JWT_KEY, jwt); + await context.secrets.store(OCC_JWT_KEY, jwt); } } catch { /* renderer not ready yet — will retry on next poll */ } } @@ -316,7 +407,7 @@ function initBalanceBar(context: vscode.ExtensionContext): (amount?: number) => const moltpilotKey = data.api_keys?.moltpilotKey ?? ''; // Guard: only sync back if the JWT wasn't cleared while the fetch was in-flight. // Without this check, an in-flight poll completing after sign-out would re-log the user in. - const currentJwt = context.globalState.get(OCC_JWT_KEY, ''); + const currentJwt = (await context.secrets.get(OCC_JWT_KEY)) ?? ''; if (currentJwt !== jwt) { return; } // Sync JWT + keys to renderer settings so ocFreeModel works vscode.commands.executeCommand('occ.auth.setLegacyJwt', jwt); @@ -347,7 +438,7 @@ function initBalanceBar(context: vscode.ExtensionContext): (amount?: number) => } } else if (r.status === 401) { // JWT expired or invalid — clear it, clear moltpilot key, and hide bar - void context.globalState.update(OCC_JWT_KEY, ''); + void context.secrets.delete(OCC_JWT_KEY); vscode.commands.executeCommand('occ.auth.setMoltpilotKey', ''); if (backendPollTimer) { clearInterval(backendPollTimer); backendPollTimer = undefined; } stopCountdown(); @@ -400,7 +491,7 @@ function initBalanceBar(context: vscode.ExtensionContext): (amount?: number) => // Called from the renderer (sidebarActions.ts occ.auth.setLegacyJwt) to sync the JWT // into extension-host storage so fetchAndUpdateBackendBalance can read it without IPC. vscode.commands.registerCommand('openclaw.jwt.set', async (token: string) => { - await context.globalState.update(OCC_JWT_KEY, token ?? ''); + if (token) { await context.secrets.store(OCC_JWT_KEY, token); } else { await context.secrets.delete(OCC_JWT_KEY); } // Stop polling immediately on sign-out so no more in-flight fetches can re-set the JWT if (!token && backendPollTimer) { clearInterval(backendPollTimer); @@ -420,13 +511,13 @@ function initBalanceBar(context: vscode.ExtensionContext): (amount?: number) => log(''); // 1. JWT - let jwt = context.globalState.get(OCC_JWT_KEY, ''); - log(`[1] JWT in extension globalState (occJwtV1): ${jwt ? 'OK present (' + jwt.substring(0, 20) + '...)' : 'MISSING'}`); + let jwt = (await context.secrets.get(OCC_JWT_KEY)) ?? ''; + log(`[1] JWT in SecretStorage (occJwtV1): ${jwt ? 'OK present [redacted]' : 'MISSING'}`); // Check what the renderer has for occLegacyJwt — this is what voidSettingsService reads try { const legacyJwt = await vscode.commands.executeCommand('occ.auth.getLegacyJwt'); - log(` occLegacyJwt in renderer settings: ${legacyJwt ? 'OK present (' + legacyJwt.substring(0, 20) + '...)' : 'MISSING <-- this causes MoltPilot to use shared key!'}`); - if (!jwt && legacyJwt) { jwt = legacyJwt; await context.globalState.update(OCC_JWT_KEY, jwt); } + log(` occLegacyJwt in renderer settings: ${legacyJwt ? 'OK present [redacted]' : 'MISSING <-- this causes MoltPilot to use shared key!'}`); + if (!jwt && legacyJwt) { jwt = legacyJwt; await context.secrets.store(OCC_JWT_KEY, jwt); } } catch { log(' occLegacyJwt check: renderer not ready'); } if (!jwt) { lines.push('\nNot signed in — cannot proceed.'); } @@ -446,8 +537,8 @@ function initBalanceBar(context: vscode.ExtensionContext): (amount?: number) => log(` Status: 200 OK`); log(` Email: ${d.email ?? '(not returned)'}`); log(` Balance: $${balanceBefore.toFixed(6)}`); - log(` MoltpilotKey: ${moltpilotKey ? 'OK ' + moltpilotKey.substring(0, 12) + '...' : 'MISSING'}`); - log(` OccKey: ${occKey ? 'OK ' + occKey.substring(0, 12) + '...' : 'MISSING'}`); + log(` MoltpilotKey: ${moltpilotKey ? 'OK [redacted]' : 'MISSING'}`); + log(` OccKey: ${occKey ? 'OK [redacted]' : 'MISSING'}`); } else { log(` HTTP ${r.status} -- JWT may be expired`); } @@ -510,7 +601,261 @@ function initBalanceBar(context: vscode.ExtensionContext): (amount?: number) => return () => {}; // spend is a no-op — kept so call sites don't break } -export async function activate(context: vscode.ExtensionContext): Promise { +// ── Module-level context ref — needed by deactivate() which has no params ─── +let _extensionContext: vscode.ExtensionContext | undefined; + +// ── Protocol handler constants ──────────────────────────────────────────────── +const PROD_HANDLER_KEY = 'occ.linuxProtocolHandlerRegisteredFor'; +const DEV_PREV_HANDLER_KEY = 'occ.prevOccodeHandler'; +const LSREGISTER = '/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister'; + +// ── mimeapps.list helpers (Linux) ───────────────────────────────────────────── + +function ensureMimeSection(content: string, section: string, entry: string): string { + const header = `[${section}]`; + const keyPrefix = entry.split('=')[0] + '='; + if (!content.includes(header)) { + return content + (content.endsWith('\n') || content === '' ? '' : '\n') + `${header}\n${entry}\n`; + } + const lines = content.split('\n'); + const secIdx = lines.findIndex(l => l.trim() === header); + for (let i = secIdx + 1; i < lines.length; i++) { + if (lines[i].startsWith('[')) { lines.splice(secIdx + 1, 0, entry); return lines.join('\n'); } + if (lines[i].startsWith(keyPrefix)) { lines[i] = entry; return lines.join('\n'); } + } + lines.splice(secIdx + 1, 0, entry); + return lines.join('\n'); +} + +function removeMimeEntry(content: string, section: string, keyPrefix: string): string { + const header = `[${section}]`; + if (!content.includes(header)) return content; + const lines = content.split('\n'); + const secIdx = lines.findIndex(l => l.trim() === header); + for (let i = secIdx + 1; i < lines.length; i++) { + if (lines[i].startsWith('[')) break; + if (lines[i].startsWith(keyPrefix)) { lines.splice(i, 1); break; } + } + return lines.join('\n'); +} + +// ── Protocol handler registration ───────────────────────────────────────────── +// +// Dev mode (launch-editor.sh present): temporarily own occode:// for the +// lifetime of this process, restoring the previous handler on exit. +// +// Production: Linux-only permanent registration (existing behaviour). + +async function ensureDevProtocolHandler(context: vscode.ExtensionContext): Promise { + try { + const appRoot = vscode.env.appRoot; + const devLauncher = path.resolve(appRoot, '../../launch-editor.sh'); + const isDevMode = fs.existsSync(devLauncher); + + const product = JSON.parse( + fs.readFileSync(path.join(appRoot, 'product.json'), 'utf-8') + ) as { applicationName: string; urlProtocol: string; nameLong: string; linuxIconName: string; linuxDescription: string }; + + if (!isDevMode) { + // ── Production: Linux-only permanent registration ────────────────────── + if (process.platform !== 'linux') return; + const execPath = process.execPath; + const stored = context.globalState.get(PROD_HANDLER_KEY, ''); + if (stored === execPath) return; + + const appsDir = path.join(os.homedir(), '.local', 'share', 'applications'); + fs.mkdirSync(appsDir, { recursive: true }); + const desktopContent = [ + '[Desktop Entry]', + `Name=${product.nameLong} - URL Handler`, + `Comment=${product.linuxDescription || ''}`, + 'GenericName=Text Editor', + `Exec="${execPath}" --open-url %U`, + `Icon=${product.linuxIconName}`, + 'Type=Application', + 'NoDisplay=true', + 'StartupNotify=true', + 'Categories=Utility;TextEditor;Development;IDE;', + `MimeType=x-scheme-handler/${product.urlProtocol};`, + 'Keywords=vscode;', + '', + ].join('\n'); + fs.writeFileSync(path.join(appsDir, `${product.applicationName}-url-handler.desktop`), desktopContent, 'utf-8'); + + const mimeappsPath = path.join(os.homedir(), '.config', 'mimeapps.list'); + const mimeEntry = `x-scheme-handler/${product.urlProtocol}=${product.applicationName}-url-handler.desktop`; + let mimeContent = ''; + try { mimeContent = fs.readFileSync(mimeappsPath, 'utf-8'); } catch {} + mimeContent = ensureMimeSection(mimeContent, 'Default Applications', mimeEntry); + mimeContent = ensureMimeSection(mimeContent, 'Added Associations', mimeEntry); + fs.mkdirSync(path.dirname(mimeappsPath), { recursive: true }); + fs.writeFileSync(mimeappsPath, mimeContent, 'utf-8'); + cp.spawn('update-desktop-database', [appsDir], { stdio: 'ignore', detached: true }).unref(); + await context.globalState.update(PROD_HANDLER_KEY, execPath); + return; + } + + // ── Dev mode: temporarily own occode:// ─────────────────────────────────── + + if (process.platform === 'linux') { + const appsDir = path.join(os.homedir(), '.local', 'share', 'applications'); + const desktopPath = path.join(appsDir, `${product.applicationName}-url-handler.desktop`); + const mimeappsPath = path.join(os.homedir(), '.config', 'mimeapps.list'); + const keyPrefix = `x-scheme-handler/${product.urlProtocol}=`; + + // Read current mimeapps entry — this is what we'll restore on exit + let mimeContent = ''; + try { mimeContent = fs.readFileSync(mimeappsPath, 'utf-8'); } catch {} + const existingEntry = mimeContent.split('\n').find(l => l.startsWith(keyPrefix)) ?? ''; + + // If the existing .desktop has a dead dev PID, don't restore that ghost + let prevEntry = existingEntry; + try { + const existingDesktop = fs.readFileSync(desktopPath, 'utf-8'); + const pidMatch = existingDesktop.match(/^X-OCC-Dev-PID=(\d+)$/m); + if (pidMatch) { + const pid = parseInt(pidMatch[1], 10); + let isAlive = false; + try { process.kill(pid, 0); isAlive = true; } catch {} + if (!isAlive) prevEntry = ''; // stale — nothing to restore + } + } catch {} + await context.globalState.update(DEV_PREV_HANDLER_KEY, prevEntry); + + // Write .desktop with dev PID marker + fs.mkdirSync(appsDir, { recursive: true }); + const desktopContent = [ + '[Desktop Entry]', + `Name=${product.nameLong} - URL Handler`, + `Comment=${product.linuxDescription || ''}`, + 'GenericName=Text Editor', + `Exec="${devLauncher}" --open-url %U`, + `Icon=${product.linuxIconName}`, + 'Type=Application', + 'NoDisplay=true', + 'StartupNotify=true', + 'Categories=Utility;TextEditor;Development;IDE;', + `MimeType=x-scheme-handler/${product.urlProtocol};`, + 'Keywords=vscode;', + `X-OCC-Dev-PID=${process.pid}`, + '', + ].join('\n'); + fs.writeFileSync(desktopPath, desktopContent, 'utf-8'); + + const mimeEntry = `x-scheme-handler/${product.urlProtocol}=${product.applicationName}-url-handler.desktop`; + mimeContent = ensureMimeSection(mimeContent, 'Default Applications', mimeEntry); + mimeContent = ensureMimeSection(mimeContent, 'Added Associations', mimeEntry); + fs.mkdirSync(path.dirname(mimeappsPath), { recursive: true }); + fs.writeFileSync(mimeappsPath, mimeContent, 'utf-8'); + cp.spawn('update-desktop-database', [appsDir], { stdio: 'ignore', detached: true }).unref(); + + } else if (process.platform === 'darwin') { + const appBundle = path.join(appRoot, `../../.build/electron/${product.nameLong}.app`); + if (!fs.existsSync(appBundle)) return; + await context.globalState.update(DEV_PREV_HANDLER_KEY, appBundle); + cp.spawn(LSREGISTER, ['-R', '-f', appBundle], { stdio: 'ignore', detached: true }).unref(); + + } else if (process.platform === 'win32') { + const proto = product.urlProtocol; + const electronExe = process.execPath; + await context.globalState.update(DEV_PREV_HANDLER_KEY, 'win32-dev'); + const cmd = `"${electronExe}" --open-url "%1"`; + cp.execSync(`reg add "HKCU\\Software\\Classes\\${proto}" /ve /d "URL:${proto} protocol" /f`); + cp.execSync(`reg add "HKCU\\Software\\Classes\\${proto}" /v "URL Protocol" /d "" /f`); + cp.execSync(`reg add "HKCU\\Software\\Classes\\${proto}\\shell\\open\\command" /ve /d "${cmd}" /f`); + } + } catch { + // Silent failure — non-critical + } +} + +async function releaseDevProtocolHandler(context: vscode.ExtensionContext): Promise { + try { + const appRoot = vscode.env.appRoot; + const devLauncher = path.resolve(appRoot, '../../launch-editor.sh'); + if (!fs.existsSync(devLauncher)) return; // production — nothing to release + + const product = JSON.parse( + fs.readFileSync(path.join(appRoot, 'product.json'), 'utf-8') + ) as { applicationName: string; urlProtocol: string; nameLong: string; linuxIconName: string; linuxDescription: string }; + + const prevEntry = context.globalState.get(DEV_PREV_HANDLER_KEY, ''); + + if (process.platform === 'linux') { + const appsDir = path.join(os.homedir(), '.local', 'share', 'applications'); + const mimeappsPath = path.join(os.homedir(), '.config', 'mimeapps.list'); + const keyPrefix = `x-scheme-handler/${product.urlProtocol}=`; + let mimeContent = ''; + try { mimeContent = fs.readFileSync(mimeappsPath, 'utf-8'); } catch {} + + if (prevEntry) { + mimeContent = ensureMimeSection(mimeContent, 'Default Applications', prevEntry); + mimeContent = ensureMimeSection(mimeContent, 'Added Associations', prevEntry); + } else { + mimeContent = removeMimeEntry(mimeContent, 'Default Applications', keyPrefix); + mimeContent = removeMimeEntry(mimeContent, 'Added Associations', keyPrefix); + } + fs.writeFileSync(mimeappsPath, mimeContent, 'utf-8'); + cp.spawn('update-desktop-database', [appsDir], { stdio: 'ignore', detached: true }).unref(); + + } else if (process.platform === 'darwin') { + if (prevEntry) { + cp.spawn(LSREGISTER, ['-R', '-u', prevEntry], { stdio: 'ignore', detached: true }).unref(); + } + + } else if (process.platform === 'win32') { + const proto = product.urlProtocol; + try { cp.execSync(`reg delete "HKCU\\Software\\Classes\\${proto}" /f`); } catch {} + } + + await context.globalState.update(DEV_PREV_HANDLER_KEY, undefined); + } catch { + // Silent failure — non-critical + } +} + +export async function activate(context: vscode.ExtensionContext): Promise { + // ── One-time migration: move JWT from globalState → SecretStorage ──────────── + const legacyJwt = context.globalState.get(OCC_JWT_KEY, ''); + if (legacyJwt) { + await context.secrets.store(OCC_JWT_KEY, legacyJwt); + await context.globalState.update(OCC_JWT_KEY, undefined); + } + + _extensionContext = context; + + // ── Dev: temporarily own occode:// scheme; production: permanent Linux registration ── + void ensureDevProtocolHandler(context); + + // Release ownership on exit — covers graceful shutdown, Ctrl+C, and SIGTERM. + const releaseHandler = () => { if (_extensionContext) void releaseDevProtocolHandler(_extensionContext); }; + process.once('SIGTERM', releaseHandler); + process.once('SIGINT', releaseHandler); + context.subscriptions.push({ dispose: releaseHandler }); + + // ── MultiHost: HostRegistry + HostManager ─────────────────────────────────── + const hostRegistry = new HostRegistry(context.secrets); + await hostRegistry.init(); + const hostManager = new HostManager(hostRegistry, context.globalState); + context.subscriptions.push(hostRegistry, hostManager); + + // (OPENCLAW HOSTS tree view and status bar removed — window-level binding used instead) + + // Host management commands + context.subscriptions.push( + vscode.commands.registerCommand('openclaw.pickHost', async () => { + const id = await hostManager.showHostPicker(); + if (id) { await hostManager.setActiveHost(id); } + }), + vscode.commands.registerCommand('openclaw.setActiveHost', async (id: string) => { + await hostManager.setActiveHost(id); + }), + vscode.commands.registerCommand('openclaw.refreshHost', async () => { + const activeId = hostRegistry.getActiveHostId(); + await hostManager.refreshHost(activeId); + }), + ); + // Inference balance bar (shown at bottom-right, tracks $1.00 free budget). const spendBalance = initBalanceBar(context); @@ -518,7 +863,7 @@ export async function activate(context: vscode.ExtensionContext): Promise await hideActivityBarItems(context); // Open ~/.openclaw as "My OpenClaw Workspace" (may reload the window once). - await openOpenClawFolder(); + await openOpenClawFolder(context); // Close the Explorer sidebar on startup — the user opens it explicitly when needed. // Use a short delay so the workbench has finished restoring its layout first. @@ -536,8 +881,8 @@ export async function activate(context: vscode.ExtensionContext): Promise const params = new URLSearchParams(uri.query); const token = params.get('token'); if (token) { - // Store JWT in extension-host storage immediately (no renderer IPC needed). - void context.globalState.update(OCC_JWT_KEY, token).then(() => { + // Store JWT in SecretStorage (OS keychain) — encrypted at rest. + void context.secrets.store(OCC_JWT_KEY, token).then(() => { // Also sync to renderer settings service (for chat / other renderer consumers). vscode.commands.executeCommand('occ.auth.setLegacyJwt', token); }); @@ -547,20 +892,87 @@ export async function activate(context: vscode.ExtensionContext): Promise }), ); + registerWindowHostCommands(context); + context.subscriptions.push( vscode.commands.registerCommand('openclaw.home', () => { + routeHome(context.extensionUri, context); + }), + // Always shows the host picker regardless of what is installed — used after disconnect. + vscode.commands.registerCommand('openclaw.home.picker', () => { + routeHome(context.extensionUri, context, true); + }), + // Host-specific setup commands — invoked by routeHome() and the host picker in HomePanel. + // Each sets the window binding for this host type then opens the home panel, + // which renders the appropriate dashboard or setup wizard based on the binding. + vscode.commands.registerCommand('openclaw.host.setup.local', async () => { + const existing = context.workspaceState.get(WINDOW_HOST_KEY); + if (!existing || existing.type !== 'local') { + await context.workspaceState.update(WINDOW_HOST_KEY, { + type: 'local', hostId: 'local:main', port: getConfiguredGatewayPort(), label: 'Local', + } satisfies WindowHostBinding); + } + HomePanel.createOrShow(context.extensionUri); + }), + vscode.commands.registerCommand('openclaw.host.setup.docker', async () => { + const existing = context.workspaceState.get(WINDOW_HOST_KEY); + if (!existing || existing.type !== 'docker') { + await context.workspaceState.update(WINDOW_HOST_KEY, { + type: 'docker', hostId: 'docker:occ-openclaw', port: DEFAULT_GATEWAY_PORT, label: 'Docker', + } satisfies WindowHostBinding); + } + HomePanel.createOrShow(context.extensionUri, false, 'docker'); + }), + vscode.commands.registerCommand('openclaw.host.setup.ssh', () => { + // SSH host details are entered via the home panel UI — open it and let the panel drive. HomePanel.createOrShow(context.extensionUri); }), vscode.commands.registerCommand('openclaw.configure', async () => { - const reachable = await isWebServerReachable(); + const windowHostBinding = context.workspaceState.get(WINDOW_HOST_KEY); + + // ── Docker path ─────────────────────────────────────────────────────── + if (windowHostBinding?.type === 'docker') { + const container = windowHostBinding.hostId.replace(/^docker:/, '') || 'occ-openclaw'; + const hostPort = windowHostBinding.port; // e.g. 18790 + const containerPort = 18789; + + // 1. Start the gateway inside the container (detached — safe if already running) + cp.spawn('docker', ['exec', '-d', container, 'openclaw', 'gateway', 'run'], { + windowsHide: true, + detached: true, + }).unref(); + + // 2. Give it a moment to start, then get the tokenized dashboard URL + await new Promise(r => setTimeout(r, 2000)); + + const dashResult = cp.spawnSync( + 'docker', + ['exec', container, 'openclaw', 'dashboard', '--no-open'], + { timeout: 10000, windowsHide: true, encoding: 'utf-8' }, + ); + let rawUrl = (dashResult.stdout as string ?? '').trim(); + + if (rawUrl) { + // Rewrite the internal container port to the host-mapped port + const url = rawUrl + .replace(new RegExp(`localhost:${containerPort}`, 'g'), `localhost:${hostPort}`) + .replace(new RegExp(`127\\.0\\.0\\.1:${containerPort}`, 'g'), `127.0.0.1:${hostPort}`); + await vscode.env.openExternal(vscode.Uri.parse(url)); + } else { + // dashboard command failed — open plain URL as fallback + await vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${hostPort}/`)); + } + return; + } + + // ── Local / SSH path ────────────────────────────────────────────────── + const effectivePort = windowHostBinding ? windowHostBinding.port : getConfiguredGatewayPort(); + const reachable = await isWebServerReachable(effectivePort); if (reachable) { - const dashInfo = getDashboardUrl(); - const url = dashInfo?.url ?? `http://localhost:${getConfiguredGatewayPort()}/`; + const url = getDashboardUrl()?.url ?? `http://localhost:${effectivePort}/`; await vscode.env.openExternal(vscode.Uri.parse(url)); } else { - // Web server not running — ask the AI to start it - const port = getConfiguredGatewayPort(); - const configUrl = `http://localhost:${port}/`; + const configUrl = `http://localhost:${effectivePort}/`; const message = `The OpenClaw web configuration server is not running at ${configUrl}.\n\n` + `Please start it now by running the OpenClaw gateway in the terminal:\n` + @@ -590,16 +1002,27 @@ export async function activate(context: vscode.ExtensionContext): Promise spendBalance(); }), vscode.commands.registerCommand('openclaw.install', () => { - void HomePanel.runInstall( - context.extensionUri, - process.platform, - process.arch, - process.env.SHELL ?? '', - ); + // Delegate to LocalSetupPanel via the host setup command + void vscode.commands.executeCommand('openclaw.host.setup.local'); }), vscode.commands.registerCommand('openclaw.openWorkspace', () => { void openOpenClawFolder(); }), + vscode.commands.registerCommand('occ.setup.reset', async (options?: { full?: boolean }) => { + const full = options?.full === true; + const confirm = await vscode.window.showWarningMessage( + 'This will stop all Docker containers and remove volumes. Your openclaw.json will be preserved. Continue?', + 'Yes, Reset', + 'Cancel', + ); + if (confirm !== 'Yes, Reset') return; + + await context.workspaceState.update(WINDOW_HOST_KEY, undefined); + + HomePanel.currentPanel?.resetSetup(full); + + HomePanel.createOrShow(context.extensionUri, true); + }), vscode.commands.registerCommand('openclaw.status', () => { StatusPanel.createOrShow(context.extensionUri); }), @@ -635,7 +1058,7 @@ export async function activate(context: vscode.ExtensionContext): Promise if (!password) return { result: 'User cancelled the password prompt.', exitCode: 1 }; return new Promise(resolve => { - const child = require('child_process').spawn('sudo', ['-S', 'bash', '-c', command], { + const child = cp.spawn('sudo', ['-S', 'bash', '-c', command], { stdio: ['pipe', 'pipe', 'pipe'], }); child.stdin?.write(password + '\n'); @@ -685,7 +1108,12 @@ export async function activate(context: vscode.ExtensionContext): Promise hasAiModel: boolean; hasChannels: boolean; channelNames: string[]; + hostType: 'local' | 'docker' | 'ssh' | null; + containerName: string | null; + gatewayPort: number; }> => { + // Resolve window-level host binding first + const windowHost = context.workspaceState.get(WINDOW_HOST_KEY) ?? null; const homedir = os.homedir(); const configPath = path.join(homedir, '.openclaw', 'openclaw.json'); const installed = fs.existsSync(configPath); @@ -695,8 +1123,11 @@ export async function activate(context: vscode.ExtensionContext): Promise try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch {} } - // Determine gateway port + // Determine gateway port — for Docker/SSH hosts use the window binding's port directly const port = (() => { + if (windowHost?.type === 'docker' || windowHost?.type === 'ssh') { + return windowHost.port; + } const p = config['port'] ?? config['gateway_port'] ?? config['gatewayPort']; if (p === undefined) return 18789; const n = Number(p); @@ -705,7 +1136,7 @@ export async function activate(context: vscode.ExtensionContext): Promise // Check if gateway is reachable const gatewayRunning = await new Promise(resolve => { - const req = http.get(`http://localhost:${port}/`, { timeout: 2000 }, res => { + const req = http.get(`http://127.0.0.1:${port}/`, { timeout: 2000 }, res => { res.resume(); resolve(res.statusCode !== undefined && res.statusCode < 500); }); @@ -758,7 +1189,12 @@ export async function activate(context: vscode.ExtensionContext): Promise } catch {} const hasAgents = agentNames.length > 0; - return { installed, gatewayRunning, hasAgents, agentNames, hasAiModel, hasChannels, channelNames }; + const hostType = windowHost?.type ?? 'local'; + const containerName = (windowHost?.type === 'docker' || windowHost?.type === 'ssh') + ? (windowHost.hostId ?? null) + : null; + + return { installed, gatewayRunning, hasAgents, agentNames, hasAiModel, hasChannels, channelNames, hostType, containerName, gatewayPort: port }; }), ); @@ -797,10 +1233,14 @@ export async function activate(context: vscode.ExtensionContext): Promise // Auto-show OCC Home on startup (after activation settles). setTimeout(() => { - HomePanel.createOrShow(context.extensionUri); + routeHome(context.extensionUri, context); }, 500); + + // Return OpenClawCoreAPI so adapter extensions can register their adapters. + return hostManager; } export function deactivate() { stopConfigProxy(); + if (_extensionContext) void releaseDevProtocolHandler(_extensionContext); } diff --git a/apps/editor/extensions/openclaw/src/hosts/hostsFile.ts b/apps/editor/extensions/openclaw/src/hosts/hostsFile.ts new file mode 100644 index 00000000..77f82d4b --- /dev/null +++ b/apps/editor/extensions/openclaw/src/hosts/hostsFile.ts @@ -0,0 +1,37 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +export interface HostFileEntry { + id: string; + type: 'local' | 'docker' | 'ssh'; + label: string; + configuredAt: string; + containerName?: string; + lastStatus?: string; +} + +export interface HostsFileData { + hosts: HostFileEntry[]; +} + +const HOSTS_PATH = path.join(os.homedir(), '.occ', 'hosts.json'); + +export function readHostsFile(): HostsFileData { + try { + const raw = fs.readFileSync(HOSTS_PATH, 'utf-8'); + return JSON.parse(raw) as HostsFileData; + } catch { + return { hosts: [] }; + } +} + +export function writeHostEntry(entry: HostFileEntry): void { + const occDir = path.join(os.homedir(), '.occ'); + if (!fs.existsSync(occDir)) { fs.mkdirSync(occDir, { recursive: true }); } + const file = readHostsFile(); + // Remove existing entry with same id + file.hosts = file.hosts.filter(h => h.id !== entry.id); + file.hosts.push(entry); + fs.writeFileSync(HOSTS_PATH, JSON.stringify(file, null, 2), 'utf-8'); +} diff --git a/apps/editor/extensions/openclaw/src/hosts/localDefault.ts b/apps/editor/extensions/openclaw/src/hosts/localDefault.ts new file mode 100644 index 00000000..32cf0eda --- /dev/null +++ b/apps/editor/extensions/openclaw/src/hosts/localDefault.ts @@ -0,0 +1,348 @@ +/** + * Minimal local HostConnection used by the core extension as the default + * before any adapter extension registers a LocalHostAdapter. + * + * Implements only the methods HomePanel actively calls. Full implementation + * lives in the openclaw-local adapter extension. + */ + +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import type { + HostConnection, + HostType, + ExecOpts, + ExecResult, + LogFn, + CliCheckResult, + GatewayStatus, + OpenClawConfig, + SetupParams, +} from './types'; +import * as vscode from 'vscode'; + +// ───────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────── + +function openClawConfigFile(): string { + return path.join(os.homedir(), '.openclaw', 'openclaw.json'); +} + +function wellKnownCliPaths(): string[] { + const home = os.homedir(); + if (process.platform === 'win32') { + const appData = process.env.APPDATA ?? path.join(home, 'AppData', 'Roaming'); + return [ + path.join(appData, 'npm', 'openclaw.cmd'), + path.join(appData, 'npm', 'openclaw.exe'), + ]; + } + return [ + '/usr/local/bin/openclaw', + '/opt/homebrew/bin/openclaw', + path.join(home, '.local', 'bin', 'openclaw'), + path.join(home, '.npm-global', 'bin', 'openclaw'), + ]; +} + +function buildLocalEnv(): Record { + const env = { ...process.env }; + const basePath = env.PATH ?? (env as Record).Path ?? ''; + const extra: string[] = []; + if (process.platform === 'win32') { + const appData = env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming'); + extra.push(path.join(appData, 'npm')); + } else { + extra.push('/usr/local/bin', '/opt/homebrew/bin'); + extra.push(path.join(os.homedir(), '.local', 'bin')); + extra.push(path.join(os.homedir(), '.npm-global', 'bin')); + extra.push(path.join(os.homedir(), '.openclaw', 'bin')); + // nvm paths + const nvmDir = process.env.NVM_DIR ?? path.join(os.homedir(), '.nvm'); + const nvmVersionsDir = path.join(nvmDir, 'versions', 'node'); + if (fs.existsSync(nvmVersionsDir)) { + try { + fs.readdirSync(nvmVersionsDir) + .sort((a, b) => b.localeCompare(a, undefined, { numeric: true })) + .slice(0, 3) + .forEach(v => extra.push(path.join(nvmVersionsDir, v, 'bin'))); + } catch { /* non-fatal */ } + } + if (process.platform === 'darwin') { + extra.push('/opt/homebrew/opt/node/bin', '/usr/local/opt/node/bin'); + } + } + const sep = process.platform === 'win32' ? ';' : ':'; + env.PATH = [...extra, basePath].filter(Boolean).join(sep); + (env as Record).Path = env.PATH; + return env; +} + +// ───────────────────────────────────────────── +// DefaultLocalHostConnection +// ───────────────────────────────────────────── + +export class DefaultLocalHostConnection implements HostConnection { + readonly id = 'local'; + readonly type: HostType = 'local'; + readonly label = 'Local'; + + dispose(): void {} + + exec(cmd: string, args: string[], opts: ExecOpts = {}): Promise { + return new Promise((resolve, reject) => { + const proc = cp.spawn(cmd, args, { + cwd: opts.cwd, + env: { ...process.env, ...opts.env }, + timeout: opts.timeout, + windowsHide: opts.windowsHide ?? true, + shell: opts.shell, + }); + if (opts.stdinData !== undefined) { proc.stdin.write(opts.stdinData); proc.stdin.end(); } + let stdout = ''; let stderr = ''; + proc.stdout.on('data', (d: Buffer) => { stdout += d.toString(); }); + proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); }); + proc.on('error', reject); + proc.on('close', code => resolve({ stdout, stderr, code: code ?? -1 })); + }); + } + + execStream(cmd: string, args: string[], opts: ExecOpts, onData: LogFn, onError: LogFn): Promise { + return new Promise((resolve, reject) => { + const proc = cp.spawn(cmd, args, { + cwd: opts.cwd, + env: { ...process.env, ...opts.env }, + timeout: opts.timeout, + windowsHide: opts.windowsHide ?? true, + shell: opts.shell, + }); + if (opts.stdinData !== undefined) { proc.stdin.write(opts.stdinData); proc.stdin.end(); } + proc.stdout.on('data', (d: Buffer) => { onData(d.toString()); }); + proc.stderr.on('data', (d: Buffer) => { onError(d.toString()); }); + proc.on('error', reject); + proc.on('close', code => resolve(code ?? -1)); + }); + } + + readFile(filePath: string): Promise { + return Promise.resolve(fs.readFileSync(filePath, 'utf-8')); + } + + writeFile(filePath: string, content: string): Promise { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content, 'utf-8'); + return Promise.resolve(); + } + + exists(filePath: string): Promise { + return Promise.resolve(fs.existsSync(filePath)); + } + + mkdir(dirPath: string): Promise { + fs.mkdirSync(dirPath, { recursive: true }); + return Promise.resolve(); + } + + stat(filePath: string): Promise<{ size: number; isDirectory: boolean } | null> { + try { + const s = fs.statSync(filePath); + return Promise.resolve({ size: s.size, isDirectory: s.isDirectory() }); + } catch { + return Promise.resolve(null); + } + } + + async findOpenClawPath(): Promise { + const cfg = vscode.workspace.getConfiguration('openclaw').get('cliPath'); + if (cfg && fs.existsSync(cfg)) { return cfg; } + const env = process.env.OPENCLAW_CLI; + if (env && fs.existsSync(env)) { return env; } + // which/where + try { + const whichCmd = process.platform === 'win32' ? 'where' : 'which'; + const r = await this.exec(whichCmd, ['openclaw'], { timeout: 5000 }); + const found = r.stdout.trim().split('\n')[0]; + if (found && fs.existsSync(found)) { return found; } + } catch { /* ignore */ } + // Well-known paths + for (const p of wellKnownCliPaths()) { + if (fs.existsSync(p)) { return p; } + } + // nvm paths + const nvmDir = path.join(os.homedir(), '.nvm', 'versions', 'node'); + if (fs.existsSync(nvmDir)) { + for (const ver of fs.readdirSync(nvmDir).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))) { + const p = path.join(nvmDir, ver, 'bin', 'openclaw'); + if (fs.existsSync(p)) { return p; } + } + } + return undefined; + } + + async isCliInstalled(): Promise { + return (await this.findOpenClawPath()) !== undefined; + } + + async getCliVersion(): Promise { + const p = await this.findOpenClawPath(); + if (!p) { return null; } + try { + const r = await this.exec(p, ['--version'], { timeout: 8000 }); + const out = (r.stdout + r.stderr).trim(); + const m = out.match(/[\d]+\.[\d]+\.[\d]+/); + return m ? m[0] : out || null; + } catch { return null; } + } + + async testOpenClawCli(): Promise { + // On unix: try sourcing nvm first for version-managed installs + if (process.platform !== 'win32') { + const nvmSh = path.join(os.homedir(), '.nvm', 'nvm.sh'); + if (fs.existsSync(nvmSh)) { + try { + const r = await this.exec('bash', ['-c', `. "${nvmSh}" 2>/dev/null && openclaw --version 2>&1`], { timeout: 10000 }); + const line = r.stdout.trim().split('\n').find(l => /\d/.test(l) && !l.startsWith('nvm') && !l.startsWith('Now')) ?? ''; + if (line) { return { ok: true, output: line, command: `bash -c '. "${nvmSh}" && openclaw --version'` }; } + } catch { /* fall through */ } + } + } + const p = await this.findOpenClawPath(); + const command = p ? `${p} --version` : 'openclaw --version'; + if (!p) { return { ok: false, command, error: 'openclaw not found' }; } + try { + const r = await this.exec(p, ['--version'], { timeout: 15000 }); + const out = (r.stdout + r.stderr).trim(); + return r.code === 0 + ? { ok: true, output: out, command } + : { ok: false, error: out || `Exit ${r.code}`, command }; + } catch (err) { return { ok: false, command, error: String(err) }; } + } + + async installCli(onLog: LogFn): Promise { + if (process.platform === 'win32') { + // Download script to temp file, verify, then execute — never pipe directly. + const steps = [ + { label: 'Downloading installer...\n', cmd: `Invoke-WebRequest -UseBasicParsing https://get.openclaw.sh/win -OutFile $env:TEMP\\occ-install.ps1` }, + { label: 'Running installer...\n', cmd: `& $env:TEMP\\occ-install.ps1; Remove-Item $env:TEMP\\occ-install.ps1 -ErrorAction SilentlyContinue` }, + ]; + for (const step of steps) { + onLog(step.label); + const code = await this.execStream('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', step.cmd], { timeout: 120_000, windowsHide: true }, onLog, onLog); + if (code !== 0) { throw new Error(`Install step failed: ${step.label.trim()}`); } + } + } else { + // Download script, verify checksum, then execute — never pipe curl directly to bash. + const steps = [ + { label: 'Downloading installer...\n', cmd: 'curl -fsSL https://get.openclaw.sh -o /tmp/occ-install.sh' }, + { label: 'Fetching checksum...\n', cmd: 'curl -fsSL https://releases.openclaw.sh/install.sh.sha256 -o /tmp/occ-install.sha256' }, + { label: 'Verifying installer integrity...\n', cmd: 'cd /tmp && sha256sum -c occ-install.sha256' }, + { label: 'Running installer...\n', cmd: 'bash /tmp/occ-install.sh' }, + { label: '', cmd: 'rm -f /tmp/occ-install.sh /tmp/occ-install.sha256' }, + ]; + for (const step of steps) { + if (step.label) { onLog(step.label); } + const code = await this.execStream('bash', ['-c', step.cmd], { timeout: 120_000 }, onLog, onLog); + if (code !== 0 && step.label) { + await this.execStream('bash', ['-c', 'rm -f /tmp/occ-install.sh /tmp/occ-install.sha256'], {}, () => {}, () => {}).catch(() => {}); + throw new Error(`Install step failed: ${step.label.trim()}`); + } + } + } + } + + getConfigPath(): Promise { return Promise.resolve(openClawConfigFile()); } + + async readConfig(): Promise { + const p = openClawConfigFile(); + if (!fs.existsSync(p)) { return {}; } + try { return JSON.parse(fs.readFileSync(p, 'utf-8')) as OpenClawConfig; } catch { return {}; } + } + + async writeConfig(patch: Partial): Promise { + const p = openClawConfigFile(); + fs.mkdirSync(path.dirname(p), { recursive: true }); + let existing: OpenClawConfig = {}; + if (fs.existsSync(p)) { try { existing = JSON.parse(fs.readFileSync(p, 'utf-8')) as OpenClawConfig; } catch { /* ok */ } } + fs.writeFileSync(p, JSON.stringify({ ...existing, ...patch }, null, 2), 'utf-8'); + } + + async gatewayHealthCheck(): Promise { + const p = await this.findOpenClawPath(); + if (!p) { return { state: 'unknown', error: 'CLI not installed' }; } + try { + const r = await this.exec(p, ['gateway', 'status', '--json'], { timeout: 8000 }); + if (r.code === 0) { + try { + const parsed = JSON.parse(r.stdout) as Partial; + return { state: parsed.state ?? 'unknown', port: parsed.port, version: parsed.version, uptime: parsed.uptime }; + } catch { + const out = (r.stdout + r.stderr).toLowerCase(); + return { state: out.includes('running') ? 'running' : 'stopped' }; + } + } + return { state: 'stopped' }; + } catch { return { state: 'error', error: 'Health check failed' }; } + } + + async gatewayStart(onLog: LogFn): Promise { + const p = await this.findOpenClawPath(); + if (!p) { throw new Error('CLI not installed'); } + const code = await this.execStream(p, ['gateway', 'start'], {}, onLog, onLog); + if (code !== 0) { throw new Error(`gateway start exited ${code}`); } + } + + async gatewayStop(onLog: LogFn): Promise { + const p = await this.findOpenClawPath(); + if (!p) { throw new Error('CLI not installed'); } + const code = await this.execStream(p, ['gateway', 'stop'], {}, onLog, onLog); + if (code !== 0) { throw new Error(`gateway stop exited ${code}`); } + } + + async gatewayRestart(onLog: LogFn): Promise { + const p = await this.findOpenClawPath(); + if (!p) { throw new Error('CLI not installed'); } + const code = await this.execStream(p, ['gateway', 'restart'], {}, onLog, onLog); + if (code !== 0) { throw new Error(`gateway restart exited ${code}`); } + } + + async gatewayReboot(onLog: LogFn): Promise { + const p = await this.findOpenClawPath(); + if (p) { + try { + const code = await this.execStream(p, ['gateway', 'reboot'], {}, onLog, onLog); + if (code === 0) return; + } catch { /* fall through to OS-level reboot */ } + } + // Fallback to OS-level reboot + if (process.platform === 'win32') { + onLog('openclaw gateway reboot unavailable — falling back to OS reboot'); + await this.execStream('shutdown', ['/r', '/t', '0'], { windowsHide: true }, onLog, onLog); + } else { + onLog('openclaw gateway reboot unavailable — falling back to sudo reboot'); + await this.execStream('sudo', ['reboot'], {}, onLog, onLog); + } + } + + async runSetup(params: SetupParams, onLog: LogFn): Promise { + const p = await this.findOpenClawPath(); + if (!p) { throw new Error('CLI not installed'); } + // Pass API key via environment variable — never as a CLI argument (visible in ps). + const code = await this.execStream(p, ['onboard', '--provider', params.provider, '--port', params.port], { env: { OPENCLAW_API_KEY: params.apiKey } }, onLog, onLog); + if (code !== 0) { throw new Error(`onboard exited ${code}`); } + } + + gatewayHostPort(): number | undefined { + return undefined; // local host: no port remapping + } + + localStateDir(): string { + return path.join(os.homedir(), '.openclaw'); + } + + buildExecEnv(): Record { + return buildLocalEnv(); + } +} diff --git a/apps/editor/extensions/openclaw/src/hosts/manager.ts b/apps/editor/extensions/openclaw/src/hosts/manager.ts new file mode 100644 index 00000000..03befcca --- /dev/null +++ b/apps/editor/extensions/openclaw/src/hosts/manager.ts @@ -0,0 +1,252 @@ +import * as vscode from 'vscode'; +import type { + HostAdapter, + HostConnection, + HostEntry, + HostStatus, + HostType, + OpenClawCoreAPI, +} from './types'; +import { HostRegistry } from './registry'; + +// ───────────────────────────────────────────── +// HostManager +// Owns adapters, live connections, and the registry. +// Implements OpenClawCoreAPI for export to adapter extensions. +// ───────────────────────────────────────────── + +/** Extension IDs trusted to register adapters without user confirmation. */ +const TRUSTED_ADAPTER_EXTENSIONS = new Set([ + 'openclaw.openclaw-local', + 'openclaw.openclaw-docker', + 'openclaw.openclaw-ssh', + 'openclaw.openclaw-cloud', + 'openclaw.home', +]); + +/** globalState key for user-approved adapter extension IDs. */ +const APPROVED_ADAPTERS_KEY = 'occ.approvedAdapterExtensions'; + +export class HostManager implements OpenClawCoreAPI, vscode.Disposable { + readonly version = '1.0.0'; + + private _adapters = new Map(); + private _connections = new Map(); + private _disposables: vscode.Disposable[] = []; + private _globalState: vscode.Memento | undefined; + private readonly _log: vscode.OutputChannel; + + private readonly _onDidChangeActiveHost = new vscode.EventEmitter(); + readonly onDidChangeActiveHost: vscode.Event = this._onDidChangeActiveHost.event; + + private readonly _onDidChangeHostStatus = new vscode.EventEmitter<{ hostId: string; status: HostStatus }>(); + readonly onDidChangeHostStatus: vscode.Event<{ hostId: string; status: HostStatus }> = this._onDidChangeHostStatus.event; + + private readonly _onDidAddHost = new vscode.EventEmitter(); + readonly onDidAddHost: vscode.Event = this._onDidAddHost.event; + + private readonly _onDidRemoveHost = new vscode.EventEmitter(); + readonly onDidRemoveHost: vscode.Event = this._onDidRemoveHost.event; + + constructor(private readonly registry: HostRegistry, globalState?: vscode.Memento) { + this._globalState = globalState; + this._log = vscode.window.createOutputChannel('OpenClaw Adapters'); + this._disposables.push( + this._log, + registry.onDidChange(() => this._onRegistryChange()), + ); + } + + // ── Adapter registration ────────────────── + + registerHostAdapter(adapter: HostAdapter, extensionId: string): vscode.Disposable { + // Verify the extension actually exists in VS Code + const ext = vscode.extensions.getExtension(extensionId); + if (!ext) { + this._log.appendLine(`[BLOCKED] Adapter "${adapter.type}" from unknown extension "${extensionId}"`); + throw new Error(`Extension "${extensionId}" not found`); + } + + // Check trust + if (!this._isAdapterTrusted(extensionId)) { + this._log.appendLine(`[UNTRUSTED] Adapter "${adapter.type}" from "${extensionId}" — awaiting user approval`); + // Return a no-op disposable; actual registration happens if user approves + void this._promptAdapterApproval(adapter, extensionId); + return new vscode.Disposable(() => { /* no-op until approved */ }); + } + + return this._doRegister(adapter, extensionId); + } + + private _isAdapterTrusted(extensionId: string): boolean { + if (TRUSTED_ADAPTER_EXTENSIONS.has(extensionId)) { return true; } + const approved = this._globalState?.get(APPROVED_ADAPTERS_KEY, []) ?? []; + return approved.includes(extensionId); + } + + private async _promptAdapterApproval(adapter: HostAdapter, extensionId: string): Promise { + const choice = await vscode.window.showWarningMessage( + `Extension "${extensionId}" wants to register as an OpenClaw host adapter (type: ${adapter.type}). Allow?`, + { modal: true }, + 'Allow', + 'Deny', + ); + if (choice === 'Allow') { + // Persist approval + const approved = this._globalState?.get(APPROVED_ADAPTERS_KEY, []) ?? []; + if (!approved.includes(extensionId)) { + approved.push(extensionId); + await this._globalState?.update(APPROVED_ADAPTERS_KEY, approved); + } + this._doRegister(adapter, extensionId); + this._log.appendLine(`[APPROVED] Adapter "${adapter.type}" from "${extensionId}"`); + } else { + this._log.appendLine(`[DENIED] Adapter "${adapter.type}" from "${extensionId}"`); + } + } + + private _doRegister(adapter: HostAdapter, extensionId: string): vscode.Disposable { + this._log.appendLine(`[OK] Adapter "${adapter.type}" registered from "${extensionId}"`); + this._adapters.set(adapter.type, adapter); + this._connectPersistedHosts(adapter.type); + return new vscode.Disposable(() => { + this._adapters.delete(adapter.type); + }); + } + + getAdapter(type: HostType): HostAdapter | undefined { + return this._adapters.get(type); + } + + // ── Host queries ────────────────────────── + + getActiveHost(): HostConnection | undefined { + const id = this.registry.getActiveHostId(); + return this._connections.get(id); + } + + getHost(id: string): HostConnection | undefined { + return this._connections.get(id); + } + + getAllHosts(): HostEntry[] { + return this.registry.getAllHosts(); + } + + async setActiveHost(id: string): Promise { + const prev = this.registry.getActiveHostId(); + if (prev === id) { return; } + this.registry.setActiveHostId(id); + // Ensure connected + await this._ensureConnected(id); + this._onDidChangeActiveHost.fire(this._connections.get(id)); + } + + // ── Connection management ───────────────── + + private async _connectPersistedHosts(type: HostType): Promise { + const entries = this.registry.getAllHosts().filter(h => h.type === type); + for (const entry of entries) { + await this._ensureConnected(entry.id).catch(() => {/* ignore individual failures */}); + } + } + + private async _ensureConnected(id: string): Promise { + if (this._connections.has(id)) { + return this._connections.get(id); + } + const entry = this.registry.getHost(id); + if (!entry) { return undefined; } + const adapter = this._adapters.get(entry.type); + if (!adapter) { return undefined; } + // Hydrate full connection config from SecretStorage + const fullConnection = await this.registry.getHostConnection(id); + if (!fullConnection) { return undefined; } + try { + const conn = await adapter.connect(fullConnection); + this._connections.set(id, conn); + this.registry.touchLastConnected(id); + return conn; + } catch (err) { + this.registry.setHostStatus(id, 'error', String(err)); + this._onDidChangeHostStatus.fire({ hostId: id, status: 'error' }); + return undefined; + } + } + + // ── Wizard / picker ─────────────────────── + + async showHostPicker(): Promise { + const entries = this.registry.getAllHosts(); + const activeId = this.registry.getActiveHostId(); + const items = entries.map(e => ({ + label: e.label, + description: e.type + (e.id === activeId ? ' • active' : ''), + id: e.id, + })); + const pick = await vscode.window.showQuickPick(items, { + placeHolder: 'Select a host', + }); + return pick?.id; + } + + async showAddHostWizard(_type?: HostType): Promise { + // Full wizard lives in the webview panel; this is a lightweight fallback. + vscode.window.showInformationMessage('Use the OpenClaw panel to add hosts.'); + return undefined; + } + + async addHost(entry: Omit): Promise { + const id = `${entry.type}-${Date.now()}`; + const full: HostEntry = { ...entry, id, createdAt: new Date().toISOString() }; + await this.registry.addHost(full); + this._onDidAddHost.fire(full); + await this._ensureConnected(id).catch(() => { /* ignore — caller handles */ }); + return full; + } + + async refreshHost(id: string): Promise { + const conn = this._connections.get(id); + if (!conn) { + await this._ensureConnected(id); + return; + } + try { + const status = await conn.gatewayHealthCheck(); + const hostStatus: HostStatus = + status.state === 'running' ? 'online' : + status.state === 'error' ? 'error' : 'offline'; + this.registry.setHostStatus(id, hostStatus, status.error); + this._onDidChangeHostStatus.fire({ hostId: id, status: hostStatus }); + } catch { + this.registry.setHostStatus(id, 'error', 'Health check failed'); + this._onDidChangeHostStatus.fire({ hostId: id, status: 'error' }); + } + } + + // ── Internal ────────────────────────────── + + private _onRegistryChange(): void { + // Drop connections for removed hosts + for (const [id, conn] of this._connections) { + if (!this.registry.getHost(id)) { + conn.dispose(); + this._connections.delete(id); + } + } + } + + // ── Dispose ─────────────────────────────── + + dispose(): void { + for (const conn of this._connections.values()) { + conn.dispose(); + } + this._connections.clear(); + this._onDidChangeActiveHost.dispose(); + this._onDidChangeHostStatus.dispose(); + this._onDidAddHost.dispose(); + this._onDidRemoveHost.dispose(); + this._disposables.forEach(d => d.dispose()); + } +} diff --git a/apps/editor/extensions/openclaw/src/hosts/registry.ts b/apps/editor/extensions/openclaw/src/hosts/registry.ts new file mode 100644 index 00000000..ebcd70ad --- /dev/null +++ b/apps/editor/extensions/openclaw/src/hosts/registry.ts @@ -0,0 +1,275 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import type { HostConnectionConfig, HostEntry, HostsFile, HostType, HostStatus } from './types'; + +// ───────────────────────────────────────────── +// Paths +// ───────────────────────────────────────────── + +export function getOccDir(): string { + return path.join(os.homedir(), '.occ'); +} + +export function getHostsFilePath(): string { + return path.join(getOccDir(), 'hosts.json'); +} + +export function getHostCacheDir(hostId: string): string { + return path.join(getOccDir(), 'hosts', hostId); +} + +export function getHostCachePath(hostId: string): string { + return path.join(getHostCacheDir(hostId), 'cache.json'); +} + +// ───────────────────────────────────────────── +// Default local host seed +// ───────────────────────────────────────────── + +function makeLocalDefaultEntry(): HostEntry { + return { + id: 'local', + type: 'local' as HostType, + label: 'Local', + connection: { type: 'local' }, + default: true, + createdAt: new Date().toISOString(), + }; +} + +function makeEmptyHostsFile(localEntry: HostEntry): HostsFile { + return { + version: 1, + activeHostId: localEntry.id, + hosts: [localEntry], + }; +} + +// ───────────────────────────────────────────── +// HostRegistry +// ───────────────────────────────────────────── + +/** SecretStorage key prefix for connection configs. */ +const CONN_SECRET_PREFIX = 'occ.host.conn.'; + +/** Returns true if a connection object has sensitive fields beyond just `type`. */ +function hasConnectionDetails(conn: HostConnectionConfig): boolean { + return Object.keys(conn).some(k => k !== 'type'); +} + +/** Returns a stub with only the `type` field, safe for plaintext storage. */ +function stubConnection(conn: HostConnectionConfig): HostConnectionConfig { + return { type: conn.type } as HostConnectionConfig; +} + +export class HostRegistry implements vscode.Disposable { + private _hostsFile: HostsFile | undefined; + private _watcher: fs.FSWatcher | undefined; + private _disposables: vscode.Disposable[] = []; + + private readonly _onDidChange = new vscode.EventEmitter(); + readonly onDidChange: vscode.Event = this._onDidChange.event; + + constructor(private readonly _secrets: vscode.SecretStorage) {} + + // ── Init ────────────────────────────────── + + async init(): Promise { + await this._ensureOccDir(); + await this._loadOrSeed(); + await this._migrateConnectionsToSecrets(); + this._startWatching(); + } + + private async _ensureOccDir(): Promise { + const dir = getOccDir(); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + const hostsDir = path.join(dir, 'hosts'); + if (!fs.existsSync(hostsDir)) { + fs.mkdirSync(hostsDir, { recursive: true }); + } + } + + private async _loadOrSeed(): Promise { + const filePath = getHostsFilePath(); + if (!fs.existsSync(filePath)) { + const seed = makeEmptyHostsFile(makeLocalDefaultEntry()); + this._hostsFile = seed; + this._persist(); + } else { + this._hostsFile = this._readFromDisk(); + // Ensure local default is always present + if (!this._hostsFile.hosts.find(h => h.id === 'local')) { + this._hostsFile.hosts.unshift(makeLocalDefaultEntry()); + if (!this._hostsFile.activeHostId) { + this._hostsFile.activeHostId = 'local'; + } + this._persist(); + } + } + } + + private _readFromDisk(): HostsFile { + try { + const raw = fs.readFileSync(getHostsFilePath(), 'utf-8'); + return JSON.parse(raw) as HostsFile; + } catch { + return makeEmptyHostsFile(makeLocalDefaultEntry()); + } + } + + private _persist(): void { + try { + fs.writeFileSync( + getHostsFilePath(), + JSON.stringify(this._hostsFile, null, 2), + { encoding: 'utf-8', mode: 0o600 }, + ); + } catch (err) { + console.error('[HostRegistry] Failed to persist hosts.json:', err); + } + } + + private _startWatching(): void { + const filePath = getHostsFilePath(); + try { + this._watcher = fs.watch(filePath, (_event) => { + const fresh = this._readFromDisk(); + this._hostsFile = fresh; + this._onDidChange.fire(); + }); + } catch { + // File watcher is best-effort + } + } + + // ── Migration: move plaintext connections → SecretStorage ── + + private async _migrateConnectionsToSecrets(): Promise { + if (!this._hostsFile) { return; } + let migrated = false; + for (const host of this._hostsFile.hosts) { + if (hasConnectionDetails(host.connection)) { + await this._secrets.store( + CONN_SECRET_PREFIX + host.id, + JSON.stringify(host.connection), + ); + host.connection = stubConnection(host.connection); + migrated = true; + } + } + if (migrated) { + this._persist(); + } + } + + // ── Connection secrets ──────────────────── + + /** Retrieve the full connection config from SecretStorage. */ + async getHostConnection(id: string): Promise { + const raw = await this._secrets.get(CONN_SECRET_PREFIX + id); + if (raw) { + try { return JSON.parse(raw) as HostConnectionConfig; } catch { /* fall through */ } + } + // Fallback: return whatever is in the hosts file (e.g. local type stub) + return this._hostsFile?.hosts.find(h => h.id === id)?.connection; + } + + /** Store a connection config in SecretStorage. */ + private async _storeConnectionSecret(id: string, conn: HostConnectionConfig): Promise { + await this._secrets.store(CONN_SECRET_PREFIX + id, JSON.stringify(conn)); + } + + /** Remove a connection config from SecretStorage. */ + private async _deleteConnectionSecret(id: string): Promise { + await this._secrets.delete(CONN_SECRET_PREFIX + id); + } + + // ── Read ────────────────────────────────── + + getAllHosts(): HostEntry[] { + return this._hostsFile?.hosts ?? []; + } + + getHost(id: string): HostEntry | undefined { + return this._hostsFile?.hosts.find(h => h.id === id); + } + + getActiveHostId(): string { + return this._hostsFile?.activeHostId ?? 'local'; + } + + // ── Write ───────────────────────────────── + + async addHost(entry: HostEntry): Promise { + if (!this._hostsFile) { return; } + // Store full connection config in SecretStorage + await this._storeConnectionSecret(entry.id, entry.connection); + // Write only the type stub to hosts.json + const safeEntry: HostEntry = { ...entry, connection: stubConnection(entry.connection) }; + this._hostsFile.hosts = this._hostsFile.hosts.filter(h => h.id !== safeEntry.id); + this._hostsFile.hosts.push(safeEntry); + this._persist(); + this._onDidChange.fire(); + } + + updateHost(id: string, patch: Partial): void { + if (!this._hostsFile) { return; } + const idx = this._hostsFile.hosts.findIndex(h => h.id === id); + if (idx === -1) { return; } + this._hostsFile.hosts[idx] = { ...this._hostsFile.hosts[idx], ...patch }; + this._persist(); + this._onDidChange.fire(); + } + + async removeHost(id: string): Promise { + if (!this._hostsFile || id === 'local') { return; } // local is permanent + this._hostsFile.hosts = this._hostsFile.hosts.filter(h => h.id !== id); + if (this._hostsFile.activeHostId === id) { + this._hostsFile.activeHostId = 'local'; + } + await this._deleteConnectionSecret(id); + this._persist(); + this._onDidChange.fire(); + } + + setActiveHostId(id: string): void { + if (!this._hostsFile) { return; } + if (!this._hostsFile.hosts.find(h => h.id === id)) { return; } + this._hostsFile.activeHostId = id; + this._persist(); + this._onDidChange.fire(); + } + + setHostStatus(id: string, status: HostStatus, error?: string): void { + this.updateHost(id, { + lastStatus: status, + lastError: error, + }); + } + + touchLastConnected(id: string): void { + this.updateHost(id, { lastConnectedAt: new Date().toISOString() }); + } + + // ── Host cache dir ──────────────────────── + + ensureHostCacheDir(hostId: string): void { + const dir = getHostCacheDir(hostId); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + } + + // ── Dispose ─────────────────────────────── + + dispose(): void { + this._watcher?.close(); + this._onDidChange.dispose(); + this._disposables.forEach(d => d.dispose()); + } +} diff --git a/apps/editor/extensions/openclaw/src/hosts/statusbar.ts b/apps/editor/extensions/openclaw/src/hosts/statusbar.ts new file mode 100644 index 00000000..5e3737b0 --- /dev/null +++ b/apps/editor/extensions/openclaw/src/hosts/statusbar.ts @@ -0,0 +1,51 @@ +import * as vscode from 'vscode'; +import type { HostManager } from './manager'; +import type { HostEntry } from './types'; + +// ───────────────────────────────────────────── +// HostStatusBarItem +// Shows the active host name in the status bar. +// Click → host picker quick-pick. +// ───────────────────────────────────────────── + +export class HostStatusBarItem implements vscode.Disposable { + private readonly _item: vscode.StatusBarItem; + private _disposables: vscode.Disposable[] = []; + + constructor(private readonly manager: HostManager) { + this._item = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + 100, + ); + this._item.command = 'openclaw.pickHost'; + this._item.tooltip = 'OpenClaw: active host — click to switch'; + this._disposables.push(this._item); + + this._disposables.push( + manager.onDidChangeActiveHost(() => this._refresh()), + manager.onDidChangeHostStatus(() => this._refresh()), + ); + + this._refresh(); + this._item.show(); + } + + private _refresh(): void { + const activeId = (this.manager as any).registry?.getActiveHostId?.() ?? 'local'; + const hosts: HostEntry[] = this.manager.getAllHosts(); + const entry = hosts.find(h => h.id === activeId); + const label = entry?.label ?? 'Local'; + const status = entry?.lastStatus; + + const icon = status === 'online' ? '$(vm-active)' + : status === 'error' ? '$(vm-connect)' + : status === 'offline' ? '$(vm)' + : '$(vm)'; + + this._item.text = `${icon} ${label}`; + } + + dispose(): void { + this._disposables.forEach(d => d.dispose()); + } +} diff --git a/apps/editor/extensions/openclaw/src/hosts/tree.ts b/apps/editor/extensions/openclaw/src/hosts/tree.ts new file mode 100644 index 00000000..9e2016af --- /dev/null +++ b/apps/editor/extensions/openclaw/src/hosts/tree.ts @@ -0,0 +1,82 @@ +import * as vscode from 'vscode'; +import type { HostManager } from './manager'; +import type { HostEntry } from './types'; + +// ───────────────────────────────────────────── +// HostTreeItem +// ───────────────────────────────────────────── + +class HostTreeItem extends vscode.TreeItem { + constructor(readonly entry: HostEntry, isActive: boolean) { + super(entry.label, vscode.TreeItemCollapsibleState.None); + this.id = `host-${entry.id}`; + this.contextValue = `host-${entry.type}`; + this.description = entry.type; + + const status = entry.lastStatus; + this.iconPath = new vscode.ThemeIcon( + status === 'online' ? 'vm-active' + : status === 'error' ? 'vm-connect' + : 'vm', + isActive ? new vscode.ThemeColor('statusBarItem.prominentBackground') : undefined, + ); + + if (isActive) { + this.label = `${entry.label} ●`; + } + + this.command = { + command: 'openclaw.setActiveHost', + title: 'Switch to host', + arguments: [entry.id], + }; + + this.tooltip = new vscode.MarkdownString( + `**${entry.label}** (${entry.type})\n\n` + + (entry.lastStatus ? `Status: ${entry.lastStatus}\n` : '') + + (entry.lastConnectedAt ? `Last connected: ${entry.lastConnectedAt}` : ''), + ); + } +} + +// ───────────────────────────────────────────── +// HostTreeProvider +// ───────────────────────────────────────────── + +export class HostTreeProvider + implements vscode.TreeDataProvider, vscode.Disposable +{ + private readonly _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = + this._onDidChangeTreeData.event; + + private _disposables: vscode.Disposable[] = []; + + constructor(private readonly manager: HostManager) { + this._disposables.push( + manager.onDidChangeActiveHost(() => this._onDidChangeTreeData.fire()), + manager.onDidChangeHostStatus(() => this._onDidChangeTreeData.fire()), + manager.onDidAddHost(() => this._onDidChangeTreeData.fire()), + manager.onDidRemoveHost(() => this._onDidChangeTreeData.fire()), + ); + } + + getTreeItem(element: HostTreeItem): vscode.TreeItem { + return element; + } + + getChildren(_element?: HostTreeItem): HostTreeItem[] { + const entries = this.manager.getAllHosts(); + const activeId = (this.manager as any).registry?.getActiveHostId?.() ?? 'local'; + return entries.map(e => new HostTreeItem(e, e.id === activeId)); + } + + refresh(): void { + this._onDidChangeTreeData.fire(); + } + + dispose(): void { + this._onDidChangeTreeData.dispose(); + this._disposables.forEach(d => d.dispose()); + } +} diff --git a/apps/editor/extensions/openclaw/src/hosts/types.ts b/apps/editor/extensions/openclaw/src/hosts/types.ts new file mode 100644 index 00000000..80fb5b20 --- /dev/null +++ b/apps/editor/extensions/openclaw/src/hosts/types.ts @@ -0,0 +1,319 @@ +import * as vscode from 'vscode'; + +// ───────────────────────────────────────────── +// Primitive helpers +// ───────────────────────────────────────────── + +export type LogFn = (line: string) => void; + +export interface ExecOpts { + cwd?: string; + env?: Record; + timeout?: number; + /** Bytes to pipe to stdin before closing it */ + stdinData?: string; + windowsHide?: boolean; + /** Run via OS shell (required for .cmd/.bat shims on Windows) */ + shell?: boolean; +} + +export interface ExecResult { + stdout: string; + stderr: string; + code: number; +} + +export interface CliCheckResult { + ok: boolean; + output?: string; + error?: string; + command: string; +} + +// ───────────────────────────────────────────── +// Gateway +// ───────────────────────────────────────────── + +export type GatewayRunState = 'running' | 'stopped' | 'starting' | 'error' | 'unknown'; + +export interface GatewayStatus { + state: GatewayRunState; + port?: number; + version?: string; + uptime?: string; + error?: string; +} + +// ───────────────────────────────────────────── +// OpenClaw config (openclaw.json) +// ───────────────────────────────────────────── + +export interface OpenClawConfig { + gateway?: { port?: number; [key: string]: unknown }; + [key: string]: unknown; +} + +// ───────────────────────────────────────────── +// Setup params (what home.ts passes to runSetup) +// ───────────────────────────────────────────── + +export interface SetupParams { + provider: string; + apiKey: string; + port: string; +} + +// ───────────────────────────────────────────── +// Add-Host wizard config fields +// ───────────────────────────────────────────── + +export interface ConfigField { + id: string; + label: string; + type: 'text' | 'password' | 'number' | 'select' | 'checkbox'; + placeholder?: string; + required?: boolean; + options?: { label: string; value: string }[]; + defaultValue?: string | number | boolean; +} + +export interface ConfigValidationResult { + valid: boolean; + errors?: { fieldId: string; message: string }[]; +} + +export interface TestResult { + success: boolean; + message: string; + details?: { + openclawInstalled?: boolean; + openclawVersion?: string; + gatewayRunning?: boolean; + os?: string; + hostname?: string; + }; +} + +// ───────────────────────────────────────────── +// Discovery +// ───────────────────────────────────────────── + +export interface DiscoveredHost { + suggestedId: string; + suggestedLabel: string; + connection: HostConnectionConfig; + metadata?: Record; +} + +// ───────────────────────────────────────────── +// Connection configs (stored in hosts.json) +// ───────────────────────────────────────────── + +export interface LocalConnection { + type: 'local'; +} + +export interface DockerConnection { + type: 'docker'; + containerId?: string; + containerLabel?: string; + composeService?: string; + composeFile?: string; + dockerHost?: string; + shell?: string; + portMappings?: { gateway?: number }; + /** Host-side directory mounted as /home/node/.openclaw inside the container (e.g. ~/Desktop/occ-state-dir). */ + localMountPath?: string; +} + +export interface SSHConnection { + type: 'ssh'; + host: string; + port?: number; + user: string; + authMethod: 'key' | 'agent' | 'password'; + keyPath?: string; + passphrase?: boolean; + jumpHost?: string; + gatewayPort?: number; + sshConfigHost?: string; +} + +export interface CloudConnection { + type: 'cloud'; + provider: 'moltpod'; + podId: string; + apiEndpoint?: string; +} + +export type HostConnectionConfig = + | LocalConnection + | DockerConnection + | SSHConnection + | CloudConnection; + +// ───────────────────────────────────────────── +// Host entry (one row in hosts.json) +// ───────────────────────────────────────────── + +export type HostType = 'local' | 'docker' | 'ssh' | 'cloud'; +export type HostStatus = 'online' | 'offline' | 'error' | 'unknown'; + +export interface HostEntry { + id: string; + type: HostType; + label: string; + connection: HostConnectionConfig; + configPath?: string; + default?: boolean; + color?: string; + tags?: string[]; + createdAt: string; + lastConnectedAt?: string; + lastStatus?: HostStatus; + lastError?: string; +} + +// ───────────────────────────────────────────── +// hosts.json root schema +// ───────────────────────────────────────────── + +export interface HostsFile { + version: 1; + activeHostId: string; + hosts: HostEntry[]; +} + +// ───────────────────────────────────────────── +// Per-host cache (~/.occ/hosts/{id}/cache.json) +// ───────────────────────────────────────────── + +export interface HostCache { + lastCheckedAt: string; + gateway: { + running: boolean; + version?: string; + uptime?: string; + port?: number; + }; + agents: { + count: number; + names: string[]; + defaultAgent?: string; + }; + channels: { + count: number; + connected: string[]; + }; + system: { + os?: string; + hostname?: string; + openclawVersion?: string; + nodeVersion?: string; + }; +} + +// ───────────────────────────────────────────── +// HostConnection — what every adapter implements +// ───────────────────────────────────────────── + +export interface HostConnection extends vscode.Disposable { + readonly id: string; + readonly type: HostType; + readonly label: string; + + // ── CLI ── + isCliInstalled(): Promise; + getCliVersion(): Promise; + installCli(onLog: LogFn): Promise; + findOpenClawPath(): Promise; + testOpenClawCli(): Promise; + + // ── Process execution ── + exec(cmd: string, args: string[], opts?: ExecOpts): Promise; + execStream( + cmd: string, + args: string[], + opts: ExecOpts, + onData: LogFn, + onError: LogFn, + ): Promise; + + // ── Filesystem ── + readFile(path: string): Promise; + writeFile(path: string, content: string): Promise; + exists(path: string): Promise; + mkdir(path: string): Promise; + stat(path: string): Promise<{ size: number; isDirectory: boolean } | null>; + + // ── OpenClaw config ── + readConfig(): Promise; + writeConfig(patch: Partial): Promise; + getConfigPath(): Promise; + + // ── Gateway ── + gatewayHealthCheck(): Promise; + gatewayStart(onLog: LogFn): Promise; + gatewayStop(onLog: LogFn): Promise; + gatewayRestart(onLog: LogFn): Promise; + gatewayReboot(onLog: LogFn): Promise; + + // ── Full install+onboard ── + runSetup(params: SetupParams, onLog: LogFn): Promise; + + // ── Port override (for tunnelled connections, e.g. Docker) ── + /** Host-side port to poll for gateway health. Overrides the port read from openclaw.json. */ + gatewayHostPort?(): number | undefined; + + // ── Local filesystem paths ── + /** + * Host-side path to the OpenClaw state directory. + * Local: ~/.openclaw Docker: ~/Desktop/occ-state-dir (or whatever is mounted) + */ + localStateDir?(): string; + + // ── Environment ── + buildExecEnv(): Record; +} + +// ───────────────────────────────────────────── +// HostAdapter — implemented by each adapter ext +// ───────────────────────────────────────────── + +export interface HostAdapter { + readonly type: HostType; + readonly displayName: string; + readonly icon: vscode.ThemeIcon; + + discover(): Promise; + connect(config: HostConnectionConfig): Promise; + testConnection(config: HostConnectionConfig): Promise; + getConfigFields(): ConfigField[]; + validateConfig(config: HostConnectionConfig): ConfigValidationResult; +} + +// ───────────────────────────────────────────── +// OpenClawCoreAPI — returned from activate() +// Adapter extensions grab it via getExtension().exports +// ───────────────────────────────────────────── + +export interface OpenClawCoreAPI { + readonly version: string; + + registerHostAdapter(adapter: HostAdapter, extensionId: string): vscode.Disposable; + + getActiveHost(): HostConnection | undefined; + getHost(id: string): HostConnection | undefined; + getAllHosts(): HostEntry[]; + setActiveHost(id: string): Promise; + + readonly onDidChangeActiveHost: vscode.Event; + readonly onDidChangeHostStatus: vscode.Event<{ hostId: string; status: HostStatus }>; + readonly onDidAddHost: vscode.Event; + readonly onDidRemoveHost: vscode.Event; + + showHostPicker(): Promise; + showAddHostWizard(type?: HostType): Promise; + addHost(entry: Omit): Promise; + refreshHost(id: string): Promise; +} diff --git a/apps/editor/extensions/openclaw/src/panels/config-path.ts b/apps/editor/extensions/openclaw/src/panels/config-path.ts index cf7865f1..715c6cce 100644 --- a/apps/editor/extensions/openclaw/src/panels/config-path.ts +++ b/apps/editor/extensions/openclaw/src/panels/config-path.ts @@ -1,6 +1,9 @@ import * as os from "os"; import * as path from "path"; import * as vscode from "vscode"; +import * as dotenv from "dotenv"; + +dotenv.config(); let customPath: string | undefined; const DEFAULT_PATH = path.join(os.homedir(), ".openclaw", "openclaw.json"); diff --git a/apps/editor/extensions/openclaw/src/panels/config.ts b/apps/editor/extensions/openclaw/src/panels/config.ts index ce9eb944..53a90c78 100644 --- a/apps/editor/extensions/openclaw/src/panels/config.ts +++ b/apps/editor/extensions/openclaw/src/panels/config.ts @@ -451,7 +451,7 @@ export class ConfigPanel { - + + + + +

v${version}

+

OCC Home

+

Choose a host to open

+ +
+ + + +
+ + + +`; + } + + private _getHostTypeSelectionHtml(iconUri: string, version: string): string { + return ` + + + + + + + + +

v${version}

+

Welcome to OpenClaw

+

Choose where OpenClaw runs. You can always switch later.

+
+ + + + + + + +
+ + +`; + } + + private _getLoadingHtml(iconUri: string, version: string): string { + return ` + + + + + + + +
+ ${stepperHtml} + ${contentHtml} +
+ + +`; + } + + private _getSetupHtml( isInstalled: boolean, iconUri: string, - occUser: { email: string; picture: string | null; balance_usd: number; api_keys?: { moltpilotKey?: string; occKey?: string } | null } | null = null + occUser: { email: string; picture: string | null; balance_usd: number; api_keys?: { moltpilotKey?: string; occKey?: string } | null } | null = null, + setupFor: 'docker' | 'local' | null = null ): string { // Render user area statically (avoids JS innerHTML escaping issues) let userAreaHtml: string; @@ -1641,6 +2226,12 @@ The binary is already downloaded — do NOT re-download or compile anything.`; min-height: 100vh; padding: 32px 20px 40px; text-align: center; } + /* Focus styles for keyboard navigation */ + button:focus-visible, a:focus-visible { + outline: 2px solid #7c8cf8; + outline-offset: 2px; + } + /* ── Header ── */ .header-bar { position: fixed; top: 12px; right: 12px; z-index: 200; @@ -1662,6 +2253,11 @@ The binary is already downloaded — do NOT re-download or compile anything.`; padding: 4px 10px; border-radius: 6px; cursor: pointer; transition: background 0.15s; } .sign-in-btn:hover { background: rgba(220,40,40,0.16); } + .reset-setup-btn { + font-size: 11px; color: #888; background: transparent; border: none; + cursor: pointer; padding: 4px 8px; border-radius: 4px; + } + .reset-setup-btn:hover { color: #fff; background: rgba(255,255,255,0.06); } .user-popover-wrap { position: relative; } .user-popover { display: none; position: absolute; top: calc(100% + 8px); right: 0; @@ -1861,11 +2457,37 @@ The binary is already downloaded — do NOT re-download or compile anything.`; cursor: pointer; font-family: inherit; transition: background 0.15s; } .modal-confirm:hover { background: #b91c1c; } + /* ── Bootstrap choice cards ── */ + .setup-choice-card { + display: flex; flex-direction: column; align-items: center; gap: 6px; + background: rgba(255,255,255,0.03); border: 1.5px solid rgba(255,255,255,0.1); + border-radius: 12px; padding: 20px 18px; cursor: pointer; font-family: inherit; + transition: border-color 0.15s, background 0.15s; width: 160px; min-height: 110px; + color: #e0e0e0; text-align: center; + } + .setup-choice-card:hover { border-color: rgba(220,40,40,0.6); background: rgba(220,40,40,0.07); } + .setup-choice-card.selected { border-color: #dc2828; background: rgba(220,40,40,0.1); } + .setup-choice-title { font-size: 13px; font-weight: 600; color: #fff; } + .setup-choice-sub { font-size: 10px; color: #666; line-height: 1.4; } + /* ── Doctor checklist ── */ + .doctor-item { + display: flex; align-items: center; gap: 10px; padding: 8px 12px; + background: rgba(255,255,255,0.03); border-radius: 6px; font-size: 12px; color: #ccc; + } + .doctor-icon { width: 16px; text-align: center; flex-shrink: 0; } + .doctor-label { flex: 1; } + .doctor-detail { font-size: 11px; color: #666; } + .doctor-spin { display: inline-block; width: 12px; height: 12px; border: 2px solid #333; border-top-color: #888; border-radius: 50%; animation: spin 0.7s linear infinite; } + + -
${userAreaHtml}
+
+ ${userAreaHtml} + +
@@ -1873,14 +2495,14 @@ The binary is already downloaded — do NOT re-download or compile anything.`;
Follow the steps below to get started
-
-
-
${isInstalled ? '✓' : '1'}
-
Install
OpenClaw
+
+
+
${setupFor === 'docker' ? '✓' : (isInstalled ? '✓' : '1')}
+
${setupFor === 'docker' ? 'Docker
Selected' : 'Install
OpenClaw'}
-
-
2
-
Configure
AI Model
+
+
${setupFor === 'docker' ? '2' : '2'}
+
${setupFor === 'docker' ? 'Provision
Compose' : 'Configure
AI Model'}
3
@@ -1888,44 +2510,195 @@ The binary is already downloaded — do NOT re-download or compile anything.`;
- -
+ +
+ +
How would you like to set up OpenClaw?
+
Choose your installation method. Docker is recommended for a consistent, isolated environment.
+
+ + +
+
+ + + - -