Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6759d5eef | ||
|
|
4df7f600d8 | ||
|
|
b772e1c758 | ||
|
|
0e497cd892 |
No files matched your search
@@ -1,35 +0,0 @@
|
||||
# Cloudflare R2 release hosting (Wrangler OAuth is used for bucket/bootstrap/upload)
|
||||
# If left empty, release-local will discover/fill what it can via `npx wrangler`.
|
||||
R2_ACCOUNT_ID=
|
||||
R2_BUCKET=agentcompany-updates
|
||||
R2_PUBLIC_BASE_URL=
|
||||
AGENTCOMPANY_RELEASE_PREFIX=AC
|
||||
|
||||
# Optional override. When left empty, release-local derives:
|
||||
# ${R2_PUBLIC_BASE_URL}/${AGENTCOMPANY_RELEASE_PREFIX}
|
||||
AGENTCOMPANY_UPDATE_URL=
|
||||
|
||||
# Public-release macOS signing / notarization
|
||||
# Supply either:
|
||||
# - CSC_LINK=<path-or-url-to-Developer-ID-Application.p12>
|
||||
# - CSC_KEY_PASSWORD=<p12 password>
|
||||
# or, if you already imported the cert+private-key into Keychain:
|
||||
# - CSC_NAME="<Legal Name> (<TEAMID>)"
|
||||
CSC_LINK=
|
||||
CSC_KEY_PASSWORD=
|
||||
CSC_NAME=
|
||||
|
||||
# Choose one notarization method:
|
||||
# 1) Apple ID + app-specific password
|
||||
APPLE_ID=
|
||||
APPLE_APP_SPECIFIC_PASSWORD=
|
||||
APPLE_TEAM_ID=
|
||||
|
||||
# 2) App Store Connect API key
|
||||
APPLE_API_KEY=
|
||||
APPLE_API_KEY_ID=
|
||||
APPLE_API_ISSUER=
|
||||
|
||||
# Optional Windows signing
|
||||
WIN_CSC_LINK=
|
||||
WIN_CSC_KEY_PASSWORD=
|
||||
@@ -4,8 +4,6 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish:
|
||||
@@ -24,9 +22,6 @@ env:
|
||||
CI_RELEASE_TAG: ci-build-${{ github.run_id }}
|
||||
|
||||
jobs:
|
||||
# ─────────────────────────────────────────────
|
||||
# Stage 1: Lint & Typecheck (fast feedback)
|
||||
# ─────────────────────────────────────────────
|
||||
check:
|
||||
name: Lint & Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
@@ -44,9 +39,6 @@ jobs:
|
||||
- run: pnpm lint
|
||||
- run: pnpm typecheck:all
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Stage 2: Unit Tests
|
||||
# ─────────────────────────────────────────────
|
||||
test:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
@@ -64,11 +56,8 @@ jobs:
|
||||
- run: pnpm test:unit
|
||||
- run: pnpm test:integration
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Stage 3: Build matrix (parallel per platform)
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
build-macos:
|
||||
if: false
|
||||
name: macOS (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
needs: test
|
||||
@@ -86,58 +75,9 @@ jobs:
|
||||
builder_args: --mac --x64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Decode signing certificate
|
||||
if: env.CERT_B64 != ''
|
||||
env:
|
||||
CERT_B64: ${{ secrets.MAC_CERTIFICATE_P12_BASE64 }}
|
||||
run: echo "$CERT_B64" | base64 --decode > /tmp/cert.p12
|
||||
- name: Build electron-vite
|
||||
run: pnpm exec electron-vite build
|
||||
- name: Package & sign
|
||||
run: pnpm exec electron-builder ${{ matrix.builder_args }} --config electron-builder.yml --publish never
|
||||
env:
|
||||
CSC_LINK: ${{ secrets.MAC_CERTIFICATE_P12_BASE64 != '' && '/tmp/cert.p12' || '' }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: ${{ secrets.MAC_CERTIFICATE_P12_BASE64 != '' && 'true' || 'false' }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
- name: Upload to CI release (Cloudflare transport)
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release view "$CI_RELEASE_TAG" >/dev/null 2>&1 || \
|
||||
gh release create "$CI_RELEASE_TAG" \
|
||||
--draft \
|
||||
--prerelease \
|
||||
--title "CI Build $GITHUB_RUN_ID" \
|
||||
--notes "Temporary build artifact transport for Cloudflare publication." \
|
||||
--target "$GITHUB_SHA"
|
||||
gh release upload "$CI_RELEASE_TAG" \
|
||||
build/release/*.dmg \
|
||||
build/release/*.zip \
|
||||
build/release/*.blockmap \
|
||||
build/release/latest-mac.yml \
|
||||
--clobber
|
||||
- name: Upload artifacts for release job
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-${{ matrix.arch }}
|
||||
path: |
|
||||
build/release/*.dmg
|
||||
build/release/*.zip
|
||||
build/release/*.blockmap
|
||||
build/release/latest-mac.yml
|
||||
|
||||
build-windows:
|
||||
if: false
|
||||
name: Windows (${{ matrix.arch }})
|
||||
runs-on: windows-latest
|
||||
needs: test
|
||||
@@ -147,52 +87,12 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x64
|
||||
builder_args: --win --x64
|
||||
- arch: x64-ia32
|
||||
builder_args: --win --x64 --ia32
|
||||
- arch: arm64
|
||||
builder_args: --win --arm64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Build electron-vite
|
||||
run: pnpm exec electron-vite build
|
||||
- name: Package
|
||||
run: pnpm exec electron-builder ${{ matrix.builder_args }} --config electron-builder.yml --publish never
|
||||
env:
|
||||
CSC_LINK: ${{ secrets.WIN_CERTIFICATE_P12_BASE64 }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.WIN_CERTIFICATE_PASSWORD }}
|
||||
- name: Upload to CI release (Cloudflare transport)
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release view "$CI_RELEASE_TAG" >/dev/null 2>&1 || \
|
||||
gh release create "$CI_RELEASE_TAG" \
|
||||
--draft \
|
||||
--prerelease \
|
||||
--title "CI Build $GITHUB_RUN_ID" \
|
||||
--notes "Temporary build artifact transport for Cloudflare publication." \
|
||||
--target "$GITHUB_SHA"
|
||||
gh release upload "$CI_RELEASE_TAG" \
|
||||
build/release/*.exe \
|
||||
build/release/*.blockmap \
|
||||
build/release/latest.yml \
|
||||
--clobber
|
||||
- name: Upload artifacts for release job
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-${{ matrix.arch }}
|
||||
path: |
|
||||
build/release/*.exe
|
||||
build/release/*.blockmap
|
||||
build/release/latest.yml
|
||||
|
||||
build-linux:
|
||||
name: Linux (${{ matrix.arch }})
|
||||
@@ -218,6 +118,9 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Install snapcraft (x64 only)
|
||||
if: matrix.arch == 'x64'
|
||||
run: sudo snap install snapcraft --classic
|
||||
- name: Build electron-vite
|
||||
run: pnpm exec electron-vite build
|
||||
- name: Package
|
||||
@@ -237,55 +140,15 @@ jobs:
|
||||
build/release/*.AppImage \
|
||||
build/release/*.deb \
|
||||
build/release/*.rpm \
|
||||
build/release/*.blockmap \
|
||||
build/release/latest-linux.yml \
|
||||
--clobber
|
||||
- name: Upload artifacts for release job
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-${{ matrix.arch }}
|
||||
path: |
|
||||
build/release/*.AppImage
|
||||
build/release/*.deb
|
||||
build/release/*.rpm
|
||||
build/release/*.blockmap
|
||||
build/release/latest-linux.yml
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Stage 4: Publish to GitHub Releases
|
||||
# ─────────────────────────────────────────────
|
||||
release:
|
||||
name: Publish Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-macos, build-windows, build-linux]
|
||||
needs: [build-linux]
|
||||
if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.publish == 'true'
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: false
|
||||
|
||||
- name: Collect release files
|
||||
run: |
|
||||
mkdir -p release-files
|
||||
find artifacts -type f \( \
|
||||
-name "*.dmg" -o -name "*.zip" -o -name "*.exe" \
|
||||
-o -name "*.AppImage" -o -name "*.deb" -o -name "*.rpm" \
|
||||
-o -name "*.snap" -o -name "*.blockmap" \
|
||||
-o -name "latest*.yml" \
|
||||
\) -exec cp {} release-files/ \;
|
||||
ls -lh release-files/
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
files: release-files/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,75 +0,0 @@
|
||||
name: Remote Boss Platforms
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Boss build version
|
||||
required: true
|
||||
type: string
|
||||
release_tag:
|
||||
description: Draft GitHub release tag used as the Boss transport bucket
|
||||
required: true
|
||||
type: string
|
||||
release_title:
|
||||
description: Human-readable title for the draft release
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: remote-boss-${{ github.ref }}-${{ inputs.release_tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
PNPM_VERSION: "10.24.0"
|
||||
NODE_VERSION: "22"
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
name: Boss Windows Installer
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Build Boss edition
|
||||
run: node boss-build/build-boss-edition.mjs --platform=win --version=${{ inputs.version }}
|
||||
shell: bash
|
||||
env:
|
||||
CSC_LINK: ${{ secrets.WIN_CERTIFICATE_P12_BASE64 }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.WIN_CERTIFICATE_PASSWORD }}
|
||||
- name: Upload release files
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
RELEASE_TITLE: ${{ inputs.release_title }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release view "$RELEASE_TAG" >/dev/null 2>&1 || \
|
||||
gh release create "$RELEASE_TAG" \
|
||||
--draft \
|
||||
--prerelease \
|
||||
--title "${RELEASE_TITLE:-Boss Release $GITHUB_RUN_ID}" \
|
||||
--notes "Temporary Boss transport generated by remote-boss-platforms.yml." \
|
||||
--target "$GITHUB_SHA"
|
||||
|
||||
shopt -s nullglob
|
||||
files=(
|
||||
boss-build/build/out/*.exe
|
||||
boss-build/build/out/*.blockmap
|
||||
)
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
echo "No Boss Windows release files found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gh release upload "$RELEASE_TAG" "${files[@]}" --clobber
|
||||
@@ -1,173 +0,0 @@
|
||||
name: Remote Release Platforms
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: Draft GitHub release tag used as the transport bucket
|
||||
required: true
|
||||
type: string
|
||||
release_title:
|
||||
description: Human-readable title for the draft release
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: remote-release-${{ github.ref }}-${{ inputs.release_tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
PNPM_VERSION: "10.24.0"
|
||||
NODE_VERSION: "22"
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
name: Windows Universal Installer
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Build electron-vite
|
||||
run: pnpm exec electron-vite build
|
||||
- name: Package
|
||||
run: pnpm exec electron-builder --win --x64 --ia32 --arm64 --config electron-builder.yml --publish never
|
||||
env:
|
||||
CSC_LINK: ${{ secrets.WIN_CERTIFICATE_P12_BASE64 }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.WIN_CERTIFICATE_PASSWORD }}
|
||||
- name: Upload release files
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
RELEASE_TITLE: ${{ inputs.release_title }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release view "$RELEASE_TAG" >/dev/null 2>&1 || \
|
||||
gh release create "$RELEASE_TAG" \
|
||||
--draft \
|
||||
--prerelease \
|
||||
--title "${RELEASE_TITLE:-Remote Release $GITHUB_RUN_ID}" \
|
||||
--notes "Temporary release transport generated by remote-release-platforms.yml." \
|
||||
--target "$GITHUB_SHA"
|
||||
|
||||
shopt -s nullglob
|
||||
files=(
|
||||
build/release/*.exe
|
||||
build/release/*.blockmap
|
||||
build/release/latest.yml
|
||||
)
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
echo "No Windows release files found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gh release upload "$RELEASE_TAG" "${files[@]}" --clobber
|
||||
|
||||
build-linux-x64:
|
||||
name: Linux x64
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Build electron-vite
|
||||
run: pnpm exec electron-vite build
|
||||
- name: Package
|
||||
run: pnpm exec electron-builder --linux --x64 --config electron-builder.yml --publish never
|
||||
- name: Upload release files
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
RELEASE_TITLE: ${{ inputs.release_title }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release view "$RELEASE_TAG" >/dev/null 2>&1 || \
|
||||
gh release create "$RELEASE_TAG" \
|
||||
--draft \
|
||||
--prerelease \
|
||||
--title "${RELEASE_TITLE:-Remote Release $GITHUB_RUN_ID}" \
|
||||
--notes "Temporary release transport generated by remote-release-platforms.yml." \
|
||||
--target "$GITHUB_SHA"
|
||||
|
||||
shopt -s nullglob
|
||||
files=()
|
||||
for file in build/release/AgentCompany-*.AppImage; do
|
||||
if [[ "$file" != *arm64* ]]; then
|
||||
files+=("$file")
|
||||
fi
|
||||
done
|
||||
files+=(
|
||||
build/release/agent-company_*_amd64.deb
|
||||
build/release/agent-company-*.x86_64.rpm
|
||||
build/release/latest-linux.yml
|
||||
)
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
echo "No Linux x64 release files found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gh release upload "$RELEASE_TAG" "${files[@]}" --clobber
|
||||
|
||||
build-linux-arm64:
|
||||
name: Linux arm64
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Build electron-vite
|
||||
run: pnpm exec electron-vite build
|
||||
- name: Package
|
||||
run: pnpm exec electron-builder --linux --arm64 --config electron-builder.yml --publish never
|
||||
- name: Upload release files
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
RELEASE_TITLE: ${{ inputs.release_title }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release view "$RELEASE_TAG" >/dev/null 2>&1 || \
|
||||
gh release create "$RELEASE_TAG" \
|
||||
--draft \
|
||||
--prerelease \
|
||||
--title "${RELEASE_TITLE:-Remote Release $GITHUB_RUN_ID}" \
|
||||
--notes "Temporary release transport generated by remote-release-platforms.yml." \
|
||||
--target "$GITHUB_SHA"
|
||||
|
||||
shopt -s nullglob
|
||||
files=(
|
||||
build/release/*arm64*.AppImage
|
||||
build/release/*arm64*.deb
|
||||
build/release/*aarch64*.rpm
|
||||
)
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
echo "No Linux arm64 release files found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gh release upload "$RELEASE_TAG" "${files[@]}" --clobber
|
||||
@@ -70,4 +70,7 @@ npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
build/release/AgentCompany-0.4.61-arm64.dmg
|
||||
CLAUDE.md
|
||||
build/release/AgentCompany-0.4.90-arm64.dmg
|
||||
local-release/
|
||||
@@ -4,16 +4,12 @@
|
||||
"output": "/Users/zacharyzhang/Documents/Github/AgentCompany/build/release"
|
||||
},
|
||||
"extraMetadata": {
|
||||
"version": "0.4.156"
|
||||
"version": "0.4.136"
|
||||
},
|
||||
"publish": [
|
||||
{
|
||||
"provider": "generic",
|
||||
"url": "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC"
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
"hardenedRuntime": true,
|
||||
"sign": "/Users/zacharyzhang/Documents/Github/AgentCompany/scripts/electron-builder-mac-sign.mjs"
|
||||
"hardenedRuntime": false,
|
||||
"target": [
|
||||
"zip"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
arm64:
|
||||
firstOrDefaultFilePatterns:
|
||||
- '!**/node_modules/**'
|
||||
- '!assets{,/**/*}'
|
||||
- '!build/release{,/**/*}'
|
||||
- out/**
|
||||
- package.json
|
||||
- assets/icon.png
|
||||
- assets/tray-icon*.png
|
||||
- '!**/*.{iml,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,suo,xproj,cc,d.ts,mk,a,o,obj,forge-meta,pdb}'
|
||||
- '!**/._*'
|
||||
- '!**/electron-builder.{yaml,yml,json,json5,toml,ts}'
|
||||
- '!**/{.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore,.idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci,.yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,pnpm-lock.yaml,bun.lock,bun.lockb,appveyor.yml,.travis.yml,circle.yml,.nyc_output,.husky,.github,electron-builder.env}'
|
||||
- '!.yarn{,/**/*}'
|
||||
- '!.editorconfig'
|
||||
- '!.yarnrc.yml'
|
||||
nodeModuleFilePatterns:
|
||||
- '**/*'
|
||||
- out/**
|
||||
- package.json
|
||||
- assets/icon.png
|
||||
- assets/tray-icon*.png
|
||||
@@ -39,6 +39,7 @@ win:
|
||||
- target: nsis
|
||||
arch:
|
||||
- x64
|
||||
- ia32
|
||||
- arm64
|
||||
|
||||
linux:
|
||||
|
||||
@@ -23,7 +23,7 @@ export default defineConfig({
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve("src/main/index.ts"),
|
||||
worker: resolve("src/main/worker/worker-entry.ts"),
|
||||
worker: resolve("src/main/orchestrator/worker.ts"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,7 +14,6 @@ export default tseslint.config(
|
||||
"oldreference/**",
|
||||
"Reference/**",
|
||||
"boss-build/**",
|
||||
"local-release/**",
|
||||
"builtin-skills/**",
|
||||
"node_modules/**",
|
||||
"**/node_modules/**",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.next/
|
||||
.wrangler/
|
||||
out/
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
@@ -1,21 +0,0 @@
|
||||
/*
|
||||
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data:; connect-src 'self' https://cloudflareinsights.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'
|
||||
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
||||
Referrer-Policy: strict-origin-when-cross-origin
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
|
||||
/screenshots/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/assets/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/_next/static/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/favicon.ico
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/icon.png
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
@@ -1,3 +0,0 @@
|
||||
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246/0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246/0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*
|
||||
! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com
|
||||
*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:JetBrains Mono,SF Mono,Roboto Mono,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.\!visible{visibility:visible!important}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.bottom-4{bottom:1rem}.left-0{left:0}.left-\[15px\]{left:15px}.right-0{right:0}.top-0{top:0}.top-10{top:2.5rem}.z-10{z-index:10}.z-50{z-index:50}.order-1{order:1}.order-2{order:2}.mx-auto{margin-left:auto;margin-right:auto}.-mb-10{margin-bottom:-2.5rem}.mb-1\.5{margin-bottom:.375rem}.mb-12{margin-bottom:3rem}.mb-16{margin-bottom:4rem}.mb-2{margin-bottom:.5rem}.mb-20{margin-bottom:5rem}.mb-24{margin-bottom:6rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.ml-2{margin-left:.5rem}.mt-12{margin-top:3rem}.mt-14{margin-top:3.5rem}.mt-2{margin-top:.5rem}.mt-20{margin-top:5rem}.mt-24{margin-top:6rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-7{margin-top:1.75rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-1\.5{height:.375rem}.h-11{height:2.75rem}.h-16{height:4rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-8{height:2rem}.h-\[340px\]{height:340px}.h-auto{height:auto}.h-full{height:100%}.max-h-0{max-height:0}.max-h-64{max-height:16rem}.max-h-96{max-height:24rem}.w-1\.5{width:.375rem}.w-11{width:2.75rem}.w-2{width:.5rem}.w-3{width:.75rem}.w-8{width:2rem}.w-full{width:100%}.w-px{width:1px}.min-w-\[120px\]{min-width:120px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.rotate-45{--tw-rotate:45deg}.rotate-45,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--twLine truncated
|
||||
@@ -1,222 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "../components/theme-provider";
|
||||
|
||||
const description =
|
||||
"Local-first desktop control plane for autonomous AI companies. Set a goal, let an AI CEO assemble specialists, delegate work, and deliver code, research, content, and operations from your desktop.";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://agent-company.dev"),
|
||||
title: "AgentCompany — Launch an Autonomous AI Company in Minutes",
|
||||
description,
|
||||
keywords: [
|
||||
"AI company",
|
||||
"autonomous AI agents",
|
||||
"AI automation",
|
||||
"multi-agent system",
|
||||
"AI CEO",
|
||||
"AI agent orchestration",
|
||||
"autonomous agents desktop app",
|
||||
"AI task delegation",
|
||||
"AI team management",
|
||||
"AI code generation",
|
||||
"AI content generation",
|
||||
"AI research automation",
|
||||
"self-organizing AI",
|
||||
"AI workforce",
|
||||
"Claude Code",
|
||||
"Codex CLI",
|
||||
"Gemini CLI",
|
||||
"multi-agent framework",
|
||||
"AI company builder",
|
||||
],
|
||||
openGraph: {
|
||||
title: "AgentCompany — Launch an Autonomous AI Company in Minutes",
|
||||
description,
|
||||
url: "https://agent-company.dev/",
|
||||
siteName: "AgentCompany",
|
||||
locale: "en_US",
|
||||
images: [
|
||||
{
|
||||
url: "/screenshots/Dashboard.jpeg",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
alt: "AgentCompany dashboard showing autonomous AI agents managing a company",
|
||||
},
|
||||
],
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "AgentCompany — Launch an Autonomous AI Company in Minutes",
|
||||
description,
|
||||
images: ["/screenshots/Dashboard.jpeg"],
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
apple: "/icon.png",
|
||||
},
|
||||
};
|
||||
|
||||
const themeScript = `
|
||||
(function(){try{var t=localStorage.getItem("agentcompany-theme");if(t==="dark"||(!t&&window.matchMedia("(prefers-color-scheme:dark)").matches)){document.documentElement.classList.add("dark")}}catch(e){}})();
|
||||
`;
|
||||
|
||||
const softwareJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
name: "AgentCompany",
|
||||
applicationCategory: "BusinessApplication",
|
||||
operatingSystem: "macOS, Windows, Linux",
|
||||
url: "https://agent-company.dev",
|
||||
downloadUrl: "https://agent-company.dev/#download",
|
||||
screenshot: "https://agent-company.dev/screenshots/Dashboard.jpeg",
|
||||
datePublished: "2026-03-01",
|
||||
softwareVersion: "1.0",
|
||||
description,
|
||||
featureList: [
|
||||
"One-click AI company creation",
|
||||
"Self-organizing AI agent teams",
|
||||
"Autonomous task management",
|
||||
"Real-time agent communication",
|
||||
"Human-in-the-loop approvals",
|
||||
"AI document generation",
|
||||
"Multi-connector support (Claude, Codex, Gemini)",
|
||||
"Cross-platform (macOS, Windows, Linux)",
|
||||
],
|
||||
author: { "@type": "Organization", name: "AgentCompany", url: "https://agent-company.dev" },
|
||||
};
|
||||
|
||||
const organizationJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
name: "AgentCompany",
|
||||
url: "https://agent-company.dev",
|
||||
logo: "https://agent-company.dev/icon.png",
|
||||
};
|
||||
|
||||
const howToJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "HowTo",
|
||||
name: "How to Launch an Autonomous AI Company with AgentCompany",
|
||||
description: "Create a fully autonomous AI company in three steps using the AgentCompany desktop app.",
|
||||
step: [
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
position: 1,
|
||||
name: "Set Your Goal",
|
||||
text: "Describe the company, its mission, and what you want delivered. Connect your preferred AI runtime.",
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
position: 2,
|
||||
name: "Agents Self-Organize",
|
||||
text: "A CEO agent analyzes your goal, hires specialists, creates departments, and delegates the work autonomously.",
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
position: 3,
|
||||
name: "Collect Results",
|
||||
text: "Review deliverables, approve critical decisions, and download finished work including code, content, and research.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const faqJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: [
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "What is AgentCompany?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "AgentCompany is a local-first desktop application for launching and operating autonomous AI companies. You define the mission, an AI CEO organizes the team, delegates work, and coordinates delivery across engineering, design, research, and operations.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "How do I download AgentCompany?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Use the download section on the website to choose the installer for your platform. macOS ships as a signed and notarized DMG. Windows and Linux builds are distributed directly from our Cloudflare release host.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "What platforms does AgentCompany support?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "AgentCompany is available on macOS, Windows, and Linux. It is built with Electron and runs natively on all three platforms with full feature parity.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "What AI models can AgentCompany use?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "AgentCompany supports multiple AI runtimes as first-class connectors: Claude Code, Codex CLI, and Gemini CLI. You can connect your preferred provider during setup.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "How does the AI CEO work?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "When you create a company with a goal, the AI CEO agent analyzes the objective, creates the necessary departments, hires specialized agents for each role, builds an org chart, and delegates tasks. The CEO monitors progress, handles blockers, and coordinates cross-department work.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "Is my data private? Does it leave my machine?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "AgentCompany is local-first. All data is stored in a local SQLite database on your machine. Secrets are encrypted via an OS-level keychain. The only external communication is between your machine and your AI provider's API.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "What can an AI company actually deliver?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "AI companies built with AgentCompany can produce code, content, research, business documents, and more. The deliverables depend on the goal you set and the AI runtime you connect.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "How are updates delivered?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Desktop updates are published to a Cloudflare-hosted release lane. macOS builds include the signed update metadata required for in-app update checks, while Windows and Linux builds are delivered from the same release host for manual installation.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<meta name="googlebot" content="index, follow, max-video-preview:-1, max-image-preview:large, max-snippet:-1" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(softwareJsonLd) }} />
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationJsonLd) }} />
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(howToJsonLd) }} />
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }} />
|
||||
</head>
|
||||
<body className="bg-[var(--bg)] text-[var(--text)] antialiased transition-colors duration-300">
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { DownloadSection } from "../components/download-section";
|
||||
import { FAQ } from "../components/faq";
|
||||
import { Features } from "../components/features";
|
||||
import { Footer } from "../components/footer";
|
||||
import { Hero } from "../components/hero";
|
||||
import { HowItWorks } from "../components/how-it-works";
|
||||
import { Navbar } from "../components/navbar";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
href="#main"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-[100] focus:px-4 focus:py-2 focus:rounded-lg focus:bg-[var(--accent)] focus:text-[var(--text-on-accent)] focus:text-sm focus:font-medium"
|
||||
>
|
||||
Skip to content
|
||||
</a>
|
||||
|
||||
<Navbar />
|
||||
|
||||
<main id="main">
|
||||
<Hero />
|
||||
<Features />
|
||||
<HowItWorks />
|
||||
<FAQ />
|
||||
<DownloadSection />
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const glyphs = " .'`^,:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$";
|
||||
|
||||
function parseRgbHex(hex: string): [number, number, number] {
|
||||
const value = hex.replace("#", "").trim();
|
||||
if (value.length !== 6) return [128, 128, 128];
|
||||
return [
|
||||
Number.parseInt(value.slice(0, 2), 16),
|
||||
Number.parseInt(value.slice(2, 4), 16),
|
||||
Number.parseInt(value.slice(4, 6), 16),
|
||||
];
|
||||
}
|
||||
|
||||
export function AsciiBackground({
|
||||
cssVar = "--accent",
|
||||
baseOpacity = 0.08,
|
||||
speed = 1,
|
||||
}: {
|
||||
cssVar?: string;
|
||||
baseOpacity?: number;
|
||||
speed?: number;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const container = containerRef.current;
|
||||
if (!canvas || !container) return;
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) return;
|
||||
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let rgb: [number, number, number] = [128, 128, 128];
|
||||
let phase = 0;
|
||||
let frame = 0;
|
||||
let isVisible = true;
|
||||
const pointer = { x: -9999, y: -9999 };
|
||||
|
||||
const updateColor = () => {
|
||||
const value = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim();
|
||||
if (value.startsWith("#")) {
|
||||
rgb = parseRgbHex(value);
|
||||
}
|
||||
};
|
||||
|
||||
updateColor();
|
||||
|
||||
const mutationObserver = new MutationObserver(updateColor);
|
||||
mutationObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const [entry] = entries;
|
||||
isVisible = entry?.isIntersecting ?? true;
|
||||
},
|
||||
{ threshold: 0 },
|
||||
);
|
||||
intersectionObserver.observe(container);
|
||||
|
||||
const resize = () => {
|
||||
width = container.clientWidth;
|
||||
height = container.clientHeight;
|
||||
const devicePixelRatio = Math.min(window.devicePixelRatio || 1, 2);
|
||||
canvas.width = width * devicePixelRatio;
|
||||
canvas.height = height * devicePixelRatio;
|
||||
context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
|
||||
};
|
||||
|
||||
const handlePointer = (event: MouseEvent) => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
pointer.x = event.clientX - rect.left;
|
||||
pointer.y = event.clientY - rect.top;
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
frame = requestAnimationFrame(render);
|
||||
if (!isVisible) return;
|
||||
|
||||
context.clearRect(0, 0, width, height);
|
||||
context.font = "14px monospace";
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
|
||||
const columns = Math.ceil(width / 14);
|
||||
const rows = Math.ceil(height / 14);
|
||||
const [red, green, blue] = rgb;
|
||||
const pointerX = pointer.x;
|
||||
const pointerY = pointer.y;
|
||||
|
||||
for (let row = 0; row < rows; row += 1) {
|
||||
for (let column = 0; column < columns; column += 1) {
|
||||
const centerX = column * 14 + 7;
|
||||
const centerY = row * 14 + 7;
|
||||
const value =
|
||||
Math.sin(0.05 * column + phase) * Math.cos(0.05 * row + phase) +
|
||||
Math.sin(0.012 * column - 0.7 * phase) * Math.cos(0.1 * row) * 0.5;
|
||||
|
||||
if (Math.abs(value) < 0.3) continue;
|
||||
|
||||
const index = Math.floor(Math.abs(value) * glyphs.length);
|
||||
let glyph = glyphs[index % glyphs.length];
|
||||
if (!glyph.trim()) continue;
|
||||
|
||||
let alpha = Math.abs(value) * baseOpacity;
|
||||
const deltaX = centerX - pointerX;
|
||||
const deltaY = centerY - pointerY;
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
|
||||
if (distance < 110) {
|
||||
const intensity = 1 - distance / 110;
|
||||
glyph = Math.random() > 0.5 ? "0" : "1";
|
||||
alpha = 0.35 * intensity;
|
||||
const drift = distance > 1 ? 7 * intensity : 0;
|
||||
const offsetX = (deltaX / distance) * drift;
|
||||
const offsetY = (deltaY / distance) * drift;
|
||||
context.fillStyle = `rgba(${red},${green},${blue},${alpha})`;
|
||||
context.fillText(glyph, centerX - offsetX, centerY - offsetY);
|
||||
} else {
|
||||
context.fillStyle = `rgba(${red},${green},${blue},${alpha})`;
|
||||
context.fillText(glyph, centerX, centerY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
phase += 0.005 * speed;
|
||||
};
|
||||
|
||||
resize();
|
||||
window.addEventListener("resize", resize);
|
||||
window.addEventListener("mousemove", handlePointer);
|
||||
render();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
window.removeEventListener("resize", resize);
|
||||
window.removeEventListener("mousemove", handlePointer);
|
||||
mutationObserver.disconnect();
|
||||
intersectionObserver.disconnect();
|
||||
};
|
||||
}, [baseOpacity, cssVar, speed]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
|
||||
<canvas ref={canvasRef} className="block w-full h-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { AsciiBackground } from "./ascii-background";
|
||||
import { platformDownloads, releaseVersion } from "./release-links.generated";
|
||||
import { DOWNLOAD_COUNT_SEED, getTrackedDownloadHref } from "../lib/downloads";
|
||||
|
||||
export function DownloadSection() {
|
||||
const [downloadCount, setDownloadCount] = useState(DOWNLOAD_COUNT_SEED);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function loadDownloadCount() {
|
||||
try {
|
||||
const response = await fetch("/api/download-stats", { cache: "no-store" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch download count: ${response.status}`);
|
||||
}
|
||||
const data = await response.json() as { downloads?: number };
|
||||
if (!cancelled && typeof data.downloads === "number") {
|
||||
setDownloadCount(data.downloads);
|
||||
}
|
||||
} catch {
|
||||
// Keep the seeded count visible when the dynamic endpoint is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
void loadDownloadCount();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section id="download" className="py-24 lg:py-40 px-4 sm:px-6 lg:px-8 relative overflow-hidden">
|
||||
<motion.div
|
||||
className="relative mx-auto max-w-5xl rounded-3xl p-14 lg:p-24 overflow-hidden"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, var(--cta) 0%, color-mix(in srgb, var(--cta) 85%, #000) 100%)",
|
||||
}}
|
||||
initial={{ opacity: 0, scale: 0.92 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true, margin: "-80px" }}
|
||||
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
|
||||
>
|
||||
<AsciiBackground cssVar="--text-on-cta" baseOpacity={0.1} speed={0.8} />
|
||||
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none opacity-[0.03]"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='256' height='256' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E\")",
|
||||
backgroundRepeat: "repeat",
|
||||
backgroundSize: "128px 128px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(ellipse at top right, rgba(255,255,255,0.12) 0%, transparent 50%), radial-gradient(ellipse at bottom left, rgba(255,255,255,0.06) 0%, transparent 50%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center text-center">
|
||||
<h2 className="font-serif italic text-[clamp(2rem,1.5rem+2.5vw,3.25rem)] tracking-[-0.01em] text-[var(--text-on-cta)] mb-6">
|
||||
Install the Right Build for Your Machine
|
||||
</h2>
|
||||
<p className="text-[var(--text-on-cta-muted)] text-lg max-w-xl mx-auto mb-12 leading-relaxed">
|
||||
Version {releaseVersion}.
|
||||
</p>
|
||||
<p className="text-[var(--text-on-cta-faint)] text-sm max-w-xl mx-auto mb-12 leading-relaxed -mt-8">
|
||||
Downloads: {downloadCount.toLocaleString("en-US")}
|
||||
</p>
|
||||
|
||||
<div className="w-full mt-2 grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{platformDownloads.map((download, index) => (
|
||||
<motion.div
|
||||
key={download.id}
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.5, delay: 0.08 * index, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="rounded-2xl border border-white/10 bg-white/6 backdrop-blur-xl shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] p-6 lg:p-7 text-left"
|
||||
>
|
||||
<p className="font-mono text-xs uppercase tracking-[0.25em] text-[var(--text-on-cta-faint)] mb-3">
|
||||
{download.title}
|
||||
</p>
|
||||
<div className="space-y-2.5">
|
||||
{download.assets.map((asset) => (
|
||||
<a
|
||||
key={asset.id}
|
||||
href={getTrackedDownloadHref(asset.id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center rounded-xl border border-white/12 bg-black/10 px-4 py-3 text-sm text-[var(--text-on-cta)] transition-colors duration-200 hover:bg-black/18"
|
||||
>
|
||||
<span className="font-semibold tracking-tight">{asset.label}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "What is AgentCompany?",
|
||||
answer:
|
||||
"AgentCompany is a local-first desktop application for launching and operating autonomous AI companies. You define the mission, an AI CEO organizes the team, delegates work, and coordinates delivery across engineering, design, research, and operations.",
|
||||
},
|
||||
{
|
||||
question: "How do I download AgentCompany?",
|
||||
answer:
|
||||
"Use the download section on this page to choose the build that matches your architecture and package format. macOS ships as signed and notarized DMGs, Windows ships as a universal installer covering ARM64, x64, and x86, and Linux ships as AppImage, DEB, and RPM builds from our Cloudflare release host.",
|
||||
},
|
||||
{
|
||||
question: "What platforms does AgentCompany support?",
|
||||
answer:
|
||||
"AgentCompany is available on macOS arm64 and x64, Windows ARM64/x64/x86, and Linux x64 and ARM64. It is built with Electron and the desktop feature set stays aligned across all supported targets.",
|
||||
},
|
||||
{
|
||||
question: "What AI models can AgentCompany use?",
|
||||
answer:
|
||||
"AgentCompany supports multiple AI runtimes as first-class connectors: Claude Code (Anthropic), Codex CLI (OpenAI), and Gemini CLI (Google). You can connect your preferred provider during setup.",
|
||||
},
|
||||
{
|
||||
question: "How does the AI CEO work?",
|
||||
answer:
|
||||
"When you create a company with a goal, the AI CEO agent analyzes the objective, creates the necessary departments (Engineering, Design, Marketing, etc.), hires specialized agents for each role, builds an org chart, and delegates tasks. The CEO monitors progress, handles blockers, and coordinates cross-department work — all autonomously.",
|
||||
},
|
||||
{
|
||||
question: "Is my data private? Does it leave my machine?",
|
||||
answer:
|
||||
"AgentCompany is local-first. All data is stored in a local SQLite database on your machine. Secrets are encrypted via an OS-level keychain. The only external communication is between your machine and your AI provider's API — no data is sent to AgentCompany servers.",
|
||||
},
|
||||
{
|
||||
question: "What can an AI company actually deliver?",
|
||||
answer:
|
||||
"AI companies built with AgentCompany can produce code (full applications, scripts, APIs), content (blog posts, marketing copy, social media), research (competitive analysis, market research), business documents (business plans, technical specs, strategies), and more. The deliverables depend on the goal you set and the AI runtime you connect.",
|
||||
},
|
||||
{
|
||||
question: "How are updates delivered?",
|
||||
answer:
|
||||
"Desktop updates are published to a Cloudflare-hosted release lane. macOS builds include the signed update metadata required for in-app update checks, while the Windows and Linux installers and packages are distributed from the same release host.",
|
||||
},
|
||||
];
|
||||
|
||||
function FaqItem({ question, answer, index }: { question: string; answer: string; index: number }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.5, delay: 0.06 * index, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="border-b border-[var(--border)] last:border-b-0"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
aria-expanded={open}
|
||||
className="flex w-full items-center justify-between gap-4 py-6 text-left transition-colors duration-200 hover:text-[var(--accent)]"
|
||||
>
|
||||
<h3 className="text-base lg:text-lg font-semibold text-[var(--text)] pr-4">{question}</h3>
|
||||
<span
|
||||
className={`shrink-0 text-[var(--muted)] transition-transform duration-300 ${open ? "rotate-45" : ""}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div className={`overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] ${open ? "max-h-96 pb-6" : "max-h-0"}`}>
|
||||
<p className="text-[var(--muted-strong)] leading-relaxed text-base pr-12">{answer}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FAQ() {
|
||||
return (
|
||||
<section id="faq" className="py-24 lg:py-40 px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-center mb-16 lg:mb-20"
|
||||
>
|
||||
<p className="font-mono text-xs font-medium uppercase tracking-[0.25em] text-[var(--accent)] mb-5">FAQ</p>
|
||||
<h2 className="font-serif text-[clamp(2rem,1.5rem+2.5vw,3.25rem)] tracking-[-0.01em] text-[var(--text)]">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="border-t border-[var(--border)]">
|
||||
{faqs.map((item, index) => (
|
||||
<FaqItem key={item.question} question={item.question} answer={item.answer} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
const features = [
|
||||
{
|
||||
number: "01",
|
||||
title: "One-Click Company Creation",
|
||||
description:
|
||||
"Name your company, describe your mission, connect your AI runtime, and launch. Your first agents start working in under a minute.",
|
||||
image: "/screenshots/Onboarding.jpeg",
|
||||
imageAlt: "Company creation onboarding flow with mission and runtime setup",
|
||||
imageLeft: true,
|
||||
},
|
||||
{
|
||||
number: "02",
|
||||
title: "Self-Organizing Teams",
|
||||
description:
|
||||
"Your AI CEO builds the org chart. It identifies the roles needed, hires specialists, and creates reporting lines — all from a single goal.",
|
||||
image: "/screenshots/Auto-OrgChart-Team.jpeg",
|
||||
imageAlt: "AI-generated org chart showing self-organizing team structure",
|
||||
imageLeft: false,
|
||||
},
|
||||
{
|
||||
number: "03",
|
||||
title: "Autonomous Task Management",
|
||||
description:
|
||||
"Goals break down into tasks automatically. Agents pick up work, report progress, and escalate blockers — you decide when to step in.",
|
||||
image: "/screenshots/Auto-Task.jpeg",
|
||||
imageAlt: "Autonomous task board with agents managing work items",
|
||||
imageLeft: true,
|
||||
},
|
||||
{
|
||||
number: "04",
|
||||
title: "Real-Time Agent Communication",
|
||||
description:
|
||||
"Agents communicate through department channels and direct messages. Watch strategic decisions happen in real time, like a Slack workspace you built.",
|
||||
image: "/screenshots/AI-Internal-Realtime-Message.jpeg",
|
||||
imageAlt: "Real-time messaging between AI agents in department channels",
|
||||
},
|
||||
{
|
||||
number: "05",
|
||||
title: "Human-in-the-Loop Approvals",
|
||||
description:
|
||||
"Stay in control. Critical decisions surface for your review. Approve, reject, or redirect — your company respects the chain of command.",
|
||||
image: "/screenshots/Approval-by-Human.jpeg",
|
||||
imageAlt: "Approval interface showing human review of agent decisions",
|
||||
},
|
||||
{
|
||||
number: "06",
|
||||
title: "AI Document Generation",
|
||||
description:
|
||||
"Business plans, marketing strategies, technical specs — agents draft and iterate on real deliverables, stored in your document center.",
|
||||
image: "/screenshots/Document-Center.jpeg",
|
||||
imageAlt: "Document center with AI-generated business deliverables",
|
||||
},
|
||||
];
|
||||
|
||||
function FeaturePair({ feature }: { feature: (typeof features)[number] }) {
|
||||
const { number, title, description, image, imageAlt, imageLeft } = feature;
|
||||
const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];
|
||||
|
||||
const media = (
|
||||
<motion.div
|
||||
variants={{
|
||||
hidden: { opacity: 0, x: imageLeft ? -60 : 60 },
|
||||
visible: { opacity: 1, x: 0, transition: { duration: 0.7, ease } },
|
||||
}}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
className={`order-2 ${imageLeft ? "lg:order-1" : "lg:order-2"}`}
|
||||
>
|
||||
<div className="rounded-2xl overflow-hidden shadow-[0_4px_32px_-8px_rgba(0,0,0,0.1)] border border-[var(--border)] bg-[var(--panel)] transition-shadow duration-500 hover:shadow-[0_8px_48px_-12px_rgba(0,0,0,0.15)]">
|
||||
<img src={image} alt={imageAlt} width="1920" height="1080" className="w-full h-auto block" loading="lazy" />
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
const copy = (
|
||||
<motion.div
|
||||
variants={{
|
||||
hidden: { opacity: 0, x: imageLeft ? 60 : -60 },
|
||||
visible: { opacity: 1, x: 0, transition: { duration: 0.7, ease, delay: 0.1 } },
|
||||
}}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
className={`flex flex-col justify-center order-1 ${imageLeft ? "lg:order-2" : "lg:order-1"}`}
|
||||
>
|
||||
<span className="font-mono text-[5rem] lg:text-[6rem] font-bold leading-none text-[var(--accent)] opacity-[0.07] select-none -mb-10 lg:-mb-12">
|
||||
{number}
|
||||
</span>
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<span className="block w-2 h-2 rounded-full bg-[var(--accent)]" />
|
||||
<span className="font-mono text-sm text-[var(--muted)]">{number}</span>
|
||||
</div>
|
||||
<h2 className="text-2xl lg:text-[1.875rem] font-bold text-[var(--text)] mb-4 tracking-tight">{title}</h2>
|
||||
<p className="text-[var(--muted-strong)] leading-relaxed text-base lg:text-lg">{description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
|
||||
{media}
|
||||
{copy}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Features() {
|
||||
return (
|
||||
<section
|
||||
id="features"
|
||||
className="py-24 lg:py-40"
|
||||
style={{ background: "linear-gradient(180deg, transparent 0%, var(--section-alt) 30%, var(--section-alt) 70%, transparent 100%)" }}
|
||||
>
|
||||
<div className="max-w-6xl mx-auto px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-center mb-24 lg:mb-36"
|
||||
>
|
||||
<span className="font-mono text-xs uppercase tracking-[0.25em] text-[var(--accent)] mb-5 block font-medium">
|
||||
Features
|
||||
</span>
|
||||
<h2 className="font-serif text-[clamp(2rem,1.5rem+2.5vw,3.25rem)] tracking-[-0.01em] text-[var(--text)]">
|
||||
Your Company, Under the Hood
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="space-y-24 lg:space-y-40">
|
||||
{features.slice(0, 3).map((feature) => (
|
||||
<FeaturePair key={feature.number} feature={feature} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="mt-24 lg:mt-40 grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
variants={{
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: { staggerChildren: 0.12 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
{features.slice(3).map((feature) => (
|
||||
<motion.div
|
||||
key={feature.number}
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 32 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] },
|
||||
},
|
||||
}}
|
||||
className="group bg-[var(--panel)] rounded-2xl border border-[var(--border)] overflow-hidden transition-all duration-300 hover:shadow-[0_8px_40px_-12px_rgba(0,0,0,0.12)] hover:-translate-y-1.5"
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<img
|
||||
src={feature.image}
|
||||
alt={feature.imageAlt}
|
||||
width="640"
|
||||
height="360"
|
||||
className="w-full h-auto block transition-transform duration-500 group-hover:scale-[1.03]"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-6 lg:p-7">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className="block w-2 h-2 rounded-full bg-[var(--accent)]" />
|
||||
<span className="font-mono text-sm text-[var(--muted)]">{feature.number}</span>
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-[var(--text)] mb-2 tracking-tight">{feature.title}</h2>
|
||||
<p className="text-sm text-[var(--muted-strong)] leading-relaxed">{feature.description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { AsciiBackground } from "./ascii-background";
|
||||
import { IconMark } from "./icon-mark";
|
||||
import { CONTACT_EMAIL, WEBSITE_URL } from "./site-constants";
|
||||
import { ThemeToggle } from "./theme-toggle";
|
||||
import { getTrackedDownloadHref } from "../lib/downloads";
|
||||
|
||||
const footerGroups = [
|
||||
{
|
||||
title: "Product",
|
||||
links: [
|
||||
{ label: "Features", href: "#features" },
|
||||
{ label: "How It Works", href: "#how-it-works" },
|
||||
{ label: "FAQ", href: "#faq" },
|
||||
{ label: "Download", href: "#download" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Downloads",
|
||||
links: [
|
||||
{ label: "Apple Silicon", href: getTrackedDownloadHref("macos-arm64-dmg"), external: true },
|
||||
{ label: "Intel Mac", href: getTrackedDownloadHref("macos-x64-dmg"), external: true },
|
||||
{ label: "Windows Installer", href: getTrackedDownloadHref("windows-universal-installer"), external: true },
|
||||
{ label: "Linux x64 AppImage", href: getTrackedDownloadHref("linux-x64-appimage"), external: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Company",
|
||||
links: [
|
||||
{ label: "Website", href: WEBSITE_URL, external: true },
|
||||
{ label: "Contact", href: `mailto:${CONTACT_EMAIL}`, external: true },
|
||||
{ label: "Download Center", href: "#download" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function Footer() {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="relative border-t border-[var(--border)] overflow-hidden">
|
||||
<AsciiBackground cssVar="--accent" baseOpacity={0.04} speed={0.5} />
|
||||
|
||||
<div className="relative z-10 mx-auto max-w-6xl px-6 lg:px-8 pt-16 lg:pt-20 pb-10 lg:pb-12">
|
||||
<div className="flex flex-col lg:flex-row gap-y-12 gap-x-8">
|
||||
<div className="shrink-0 lg:w-64 mb-4 lg:mb-0">
|
||||
<a href="#" className="inline-flex items-center gap-2.5" aria-label="AgentCompany home">
|
||||
<IconMark size={28} />
|
||||
<span className="text-[var(--text)] font-semibold text-lg tracking-tight">AgentCompany</span>
|
||||
</a>
|
||||
<p className="mt-4 text-sm leading-relaxed text-[var(--muted)] max-w-xs">
|
||||
Local-first desktop control plane for autonomous AI companies.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-wrap gap-y-12 gap-x-8 justify-between">
|
||||
{footerGroups.map((group) => (
|
||||
<div key={group.title} className="min-w-[120px]">
|
||||
<h3 className="text-sm font-semibold text-[var(--text)] tracking-tight mb-4">{group.title}</h3>
|
||||
<ul className="space-y-3">
|
||||
{group.links.map((link) => (
|
||||
<li key={link.label}>
|
||||
<a
|
||||
href={link.href}
|
||||
{...(link.external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
||||
className="text-sm text-[var(--muted)] hover:text-[var(--text)] transition-colors duration-200"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-14 lg:mt-16 pt-6 border-t border-[var(--border)]" />
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="flex items-center gap-4 text-xs text-[var(--muted)]">
|
||||
<span>© {year} AgentCompany</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { AsciiBackground } from "./ascii-background";
|
||||
|
||||
const sharedEase: [number, number, number, number] = [0.16, 1, 0.3, 1];
|
||||
|
||||
function heroMotion(delay: number) {
|
||||
return {
|
||||
initial: { opacity: 0, y: 24 },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
transition: { duration: 0.7, delay, ease: sharedEase },
|
||||
};
|
||||
}
|
||||
|
||||
function AppleIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function WindowsIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M3 12V6.5l8-1.1V12H3zm0 .5h8v6.6l-8-1.1V12.5zM11.5 5.3l9.5-1.3v8h-9.5V5.3zm0 7.2h9.5v8l-9.5-1.3V12.5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function LinuxIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12.5 2C10 2 8.2 4.2 8.2 7c0 1.5.3 2.2.7 3.2.3.8.7 1.7.7 3 0 .3 0 .6-.1.9-.2 1-.6 1.8-1.2 2.3-.8.7-1.8 1.1-2.8 1.6-.7.3-1.4.7-1.9 1.2-.4.4-.6.9-.6 1.4 0 .5.2.9.6 1.2.7.6 2 .7 3.1.7.8 0 1.6-.1 2.2-.2.6-.1 1.2-.3 1.9-.3.7 0 1.3.2 1.9.3.6.1 1.4.2 2.2.2 1.1 0 2.4-.1 3.1-.7.4-.3.6-.7.6-1.2 0-.5-.2-1-.6-1.4-.5-.5-1.2-.9-1.9-1.2-1-.5-2-1-2.8-1.6-.6-.5-1-1.3-1.2-2.3-.1-.3-.1-.6-.1-.9 0-1.3.4-2.2.7-3 .4-1 .7-1.7.7-3.2 0-2.8-1.8-5-4.3-5h-.2zm-1 5c0-.8.5-1.5 1.2-1.5s1.2.7 1.2 1.5-.5 1.5-1.2 1.5S11.5 7.8 11.5 7z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
<section className="relative overflow-hidden pt-32 pb-24 lg:pt-44 lg:pb-36">
|
||||
<AsciiBackground cssVar="--accent" baseOpacity={0.06} speed={1} />
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(ellipse 80% 50% at 50% 0%, var(--glow-warm) 0%, transparent 70%), radial-gradient(ellipse 60% 40% at 50% 20%, var(--glow-accent) 0%, transparent 60%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<motion.div {...heroMotion(0.05)}>
|
||||
<span className="inline-flex items-center px-4 py-1.5 rounded-full text-xs font-semibold tracking-wide bg-[var(--accent-soft)] text-[var(--accent)] dark:text-[var(--muted-strong)] border border-[var(--border)]">
|
||||
Agent Company
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
<motion.h1
|
||||
{...heroMotion(0.15)}
|
||||
className="mt-8 font-serif italic text-[clamp(2.75rem,2rem+3.8vw,5rem)] tracking-[-0.02em] text-[var(--text)] leading-[1.08]"
|
||||
>
|
||||
Launch an AI Company <br className="hidden sm:block" />
|
||||
<span className="text-[var(--cta)]">in Minutes</span>
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
{...heroMotion(0.25)}
|
||||
className="mt-7 max-w-2xl text-lg sm:text-xl leading-relaxed text-[var(--muted)]"
|
||||
>
|
||||
Describe your company's mission. A CEO agent recruits the team, delegates work, and delivers real
|
||||
results — code, content, research, and more. You're the founder.
|
||||
</motion.p>
|
||||
|
||||
<motion.div {...heroMotion(0.35)} className="mt-12">
|
||||
<a
|
||||
href="#download"
|
||||
className="group inline-flex items-center justify-center px-9 py-4 rounded-xl text-[17px] font-semibold bg-[var(--cta)] text-[var(--text-on-cta)] hover:bg-[var(--cta-hover)] hover:scale-[1.03] active:scale-[0.98] transition-all duration-200 will-change-transform shadow-[0_4px_24px_-4px_var(--cta),0_8px_40px_-12px_var(--cta)]"
|
||||
>
|
||||
See Downloads
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
className="ml-2 transition-transform duration-200 group-hover:translate-x-0.5 group-hover:-translate-y-0.5"
|
||||
>
|
||||
<path d="m7 7 10 10" />
|
||||
<path d="M17 7v10H7" />
|
||||
</svg>
|
||||
</a>
|
||||
</motion.div>
|
||||
|
||||
<motion.div {...heroMotion(0.45)} className="mt-5 flex items-center gap-3 text-xs text-[var(--muted)]">
|
||||
<div className="flex items-center gap-1.5 opacity-70">
|
||||
<AppleIcon />
|
||||
<WindowsIcon />
|
||||
<LinuxIcon />
|
||||
</div>
|
||||
<span>Available on macOS, Windows & Linux</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40, scale: 0.94 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ duration: 0.9, delay: 0.5, ease: sharedEase }}
|
||||
className="mt-20 lg:mt-24 mx-auto max-w-5xl"
|
||||
>
|
||||
<img
|
||||
src="/screenshots/Dashboard.jpeg"
|
||||
alt="AgentCompany dashboard showing autonomous AI agents managing a company"
|
||||
width="1920"
|
||||
height="1080"
|
||||
className="w-full h-auto block rounded-2xl border border-[var(--border)] shadow-[0_8px_40px_-8px_rgba(0,0,0,0.12),0_20px_80px_-20px_var(--glow-warm)] transition-shadow duration-500"
|
||||
loading="eager"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const demoMessages = [
|
||||
{ agent: "You", role: "system", text: "Build a SaaS landing page with pricing, blog, and auth.", delay: 0 },
|
||||
{ agent: "CEO", role: "ceo", text: "Analyzing goal... Creating departments: Engineering, Design, Content.", delay: 800 },
|
||||
{ agent: "CEO", role: "ceo", text: "Hiring 4 specialists for this mission.", delay: 1600 },
|
||||
{ agent: "Lead Engineer", role: "engineer", text: "Picked up task: Set up Next.js project with auth routes.", delay: 2400 },
|
||||
{ agent: "UI Designer", role: "designer", text: "Picked up task: Design landing page and pricing components.", delay: 3000 },
|
||||
{ agent: "Researcher", role: "researcher", text: "Picked up task: Competitive analysis of SaaS pricing models.", delay: 3600 },
|
||||
{ agent: "Lead Engineer", role: "engineer", text: "Completed: Auth scaffolding with JWT + session middleware.", delay: 4400 },
|
||||
{ agent: "UI Designer", role: "designer", text: "Completed: Responsive landing page with 3-tier pricing grid.", delay: 5200 },
|
||||
{ agent: "CEO", role: "ceo", text: "All tasks delivered. 3 documents ready for review.", delay: 6000 },
|
||||
];
|
||||
|
||||
const logTone: Record<(typeof demoMessages)[number]["role"], string> = {
|
||||
system: "text-[var(--muted-strong)]",
|
||||
ceo: "text-[#ca8a04]",
|
||||
engineer: "text-[var(--accent)]",
|
||||
designer: "text-[#8b5cf6]",
|
||||
researcher: "text-[#059669]",
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{ number: 1, title: "Set Your Goal", description: "Describe the company, its mission, and what you want delivered." },
|
||||
{ number: 2, title: "Agents Self-Organize", description: "A CEO analyzes your goal, hires specialists, and delegates the work." },
|
||||
{ number: 3, title: "Collect Results", description: "Review deliverables, approve decisions, and download finished work." },
|
||||
];
|
||||
|
||||
function DemoPanel() {
|
||||
const anchorRef = useRef<HTMLDivElement | null>(null);
|
||||
const isVisible = useInView(anchorRef, { once: true, amount: 0.4 });
|
||||
const [visibleCount, setVisibleCount] = useState(0);
|
||||
const logRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) return;
|
||||
|
||||
const timers = demoMessages.map((message, index) =>
|
||||
window.setTimeout(() => setVisibleCount(index + 1), message.delay),
|
||||
);
|
||||
|
||||
return () => timers.forEach((timer) => window.clearTimeout(timer));
|
||||
}, [isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (logRef.current) {
|
||||
logRef.current.scrollTop = logRef.current.scrollHeight;
|
||||
}
|
||||
}, [visibleCount]);
|
||||
|
||||
return (
|
||||
<div ref={anchorRef} className="relative">
|
||||
<div className="rounded-2xl overflow-hidden border border-[var(--border)] bg-[var(--panel)] shadow-[0_8px_40px_-8px_rgba(0,0,0,0.1),0_24px_80px_-24px_var(--glow-warm)]">
|
||||
<div className="flex items-center gap-2 px-4 py-3 bg-[var(--panel-soft)] border-b border-[var(--border)]">
|
||||
<span className="w-3 h-3 rounded-full bg-[#ff5f57]" aria-hidden="true" />
|
||||
<span className="w-3 h-3 rounded-full bg-[#febc2e]" aria-hidden="true" />
|
||||
<span className="w-3 h-3 rounded-full bg-[#28c840]" aria-hidden="true" />
|
||||
<span className="ml-2 text-xs text-[var(--muted)] font-mono">AgentCompany</span>
|
||||
</div>
|
||||
|
||||
<div ref={logRef} role="log" aria-label="Agent activity demonstration" className="p-5 lg:p-6 font-mono text-sm leading-relaxed h-[340px] overflow-y-auto">
|
||||
{demoMessages.slice(0, visibleCount).map((message, index) => (
|
||||
<motion.div
|
||||
key={`${message.agent}-${index}`}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="flex gap-3 mb-3 last:mb-0"
|
||||
>
|
||||
<span className={`shrink-0 font-semibold inline-flex items-center gap-1.5 ${logTone[message.role]}`}>
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full ${
|
||||
message.role === "system"
|
||||
? "bg-[var(--muted-strong)]"
|
||||
: message.role === "ceo"
|
||||
? "bg-[#ca8a04]"
|
||||
: message.role === "engineer"
|
||||
? "bg-[var(--accent)]"
|
||||
: message.role === "designer"
|
||||
? "bg-[#8b5cf6]"
|
||||
: "bg-[#059669]"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{message.agent}
|
||||
</span>
|
||||
<span className="text-[var(--text)]">{message.text}</span>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{visibleCount < demoMessages.length && isVisible && (
|
||||
<span className="inline-block w-2 h-4 bg-[var(--accent)] animate-pulse rounded-sm" aria-hidden="true" />
|
||||
)}
|
||||
|
||||
{visibleCount >= demoMessages.length && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="mt-4 pt-4 border-t border-[var(--border)] flex items-center gap-2 text-[#059669]"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="m9 12 2 2 4-4" />
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
</svg>
|
||||
<span className="font-semibold">Company run complete. All deliverables ready.</span>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HowItWorks() {
|
||||
return (
|
||||
<section id="how-it-works" className="py-24 lg:py-40 px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="text-center mb-20 lg:mb-24">
|
||||
<p className="font-mono text-xs font-medium uppercase tracking-[0.25em] text-[var(--accent)] mb-5">
|
||||
How It Works
|
||||
</p>
|
||||
<h2 className="font-serif text-[clamp(2rem,1.5rem+2.5vw,3.25rem)] tracking-[-0.01em] text-[var(--text)]">
|
||||
Watch a Company Come to Life
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-12 lg:gap-16 items-start">
|
||||
<div className="lg:col-span-2 relative">
|
||||
<div className="absolute left-[15px] top-10 bottom-4 w-px bg-[var(--border)] hidden lg:block" aria-hidden="true" />
|
||||
|
||||
<div className="space-y-10">
|
||||
{steps.map((step, index) => (
|
||||
<motion.div
|
||||
key={step.number}
|
||||
initial={{ opacity: 0, x: -24 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true, amount: 0.5 }}
|
||||
transition={{ duration: 0.5, delay: 0.12 * index, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="flex gap-5"
|
||||
>
|
||||
<div className="relative z-10 shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-[var(--accent)] text-[var(--text-on-accent)] text-sm font-bold shadow-[0_2px_12px_-2px_var(--accent)]">
|
||||
{step.number}
|
||||
</div>
|
||||
<div className="pt-0.5">
|
||||
<h3 className="text-base font-bold text-[var(--text)] mb-1.5 tracking-tight">{step.title}</h3>
|
||||
<p className="text-sm leading-relaxed text-[var(--muted)]">{step.description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="lg:col-span-3"
|
||||
initial={{ opacity: 0, y: 32 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
|
||||
>
|
||||
<DemoPanel />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
export function IconMark({ size = 32 }: { size?: number }) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 1024 1024"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="512" cy="512" r="480" fill="#191919" />
|
||||
<g transform="translate(268 255) scale(4)">
|
||||
<path
|
||||
fill="#FBFAF5"
|
||||
d="M 76.604813 111.661926 C 65.070183 111.662621 53.987053 111.662621 42.757744 111.662621 C 45.2239 101.998177 45.297546 101.949265 53.830105 102.036659 C 57.279823 102.071999 60.730202 102.04248 64.259201 102.04248 C 62.159458 93.316338 61.799103 93.009819 51.340881 91.341682 C 59.312901 73.847687 67.193756 56.553749 75.587578 38.134155 C 86.933441 62.965954 97.804642 86.758881 108.711754 110.630394 C 100.707413 111.871689 92.355965 107.246559 88.107536 98.852119 C 84.207748 91.146538 80.800591 83.191696 77.172813 75.348343 C 76.340195 73.54818 75.492958 71.754791 74.614624 69.877441 C 69.107384 75.691788 68.008286 81.475128 71.005882 88.088753 C 74.413452 95.606903 77.813919 103.128281 81.452705 111.167984 C 79.55114 111.381325 78.303734 111.521271 76.604813 111.661926 Z"
|
||||
/>
|
||||
<path
|
||||
fill="#FBFAF5"
|
||||
d="M 26.805992 87.743256 C 38.616821 61.830872 50.261688 36.260269 62.470131 9.452141 C 65.732315 17.818909 71.145126 23.524567 66.258224 33.086182 C 55.518253 54.099815 46.339973 75.911873 36.533691 97.402336 C 32.699284 105.805443 26.495394 110.823158 16.012421 111.410957 C 19.72117 103.270836 23.180601 95.67794 26.805992 87.743256 Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { IconMark } from "./icon-mark";
|
||||
import { ThemeToggle } from "./theme-toggle";
|
||||
|
||||
const navLinks = [
|
||||
{ label: "Features", href: "#features" },
|
||||
{ label: "How It Works", href: "#how-it-works" },
|
||||
{ label: "FAQ", href: "#faq" },
|
||||
{ label: "Download", href: "#download" },
|
||||
];
|
||||
|
||||
function MenuIcon({ open }: { open: boolean }) {
|
||||
return (
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
{open ? (
|
||||
<>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Navbar() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => setScrolled(window.scrollY > 8);
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
handleScroll();
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= 768) {
|
||||
setMobileOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize, { passive: true });
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={[
|
||||
"fixed top-0 left-0 right-0 z-50 transition-all duration-300 backdrop-blur-xl",
|
||||
scrolled ? "bg-[var(--panel)]/80 border-b border-[var(--border)] shadow-sm" : "bg-transparent border-b border-transparent",
|
||||
].join(" ")}
|
||||
>
|
||||
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-16 items-center justify-between">
|
||||
<a href="#" className="flex items-center gap-2.5 shrink-0" aria-label="AgentCompany home">
|
||||
<IconMark />
|
||||
<span className="text-[var(--text)] font-semibold text-lg tracking-tight">AgentCompany</span>
|
||||
</a>
|
||||
|
||||
<div className="hidden md:flex items-center gap-8">
|
||||
{navLinks.map(({ label, href }) => (
|
||||
<a key={href} href={href} className="text-sm text-[var(--muted)] hover:text-[var(--text)] transition-colors duration-200">
|
||||
{label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hidden md:flex items-center gap-3">
|
||||
<ThemeToggle />
|
||||
<a
|
||||
href="#download"
|
||||
className="inline-flex items-center px-4 py-2 rounded-lg text-sm font-medium bg-[var(--cta)] text-[var(--text-on-cta)] hover:bg-[var(--cta-hover)] hover:scale-[1.03] active:scale-[0.98] transition-all duration-200"
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex md:hidden items-center gap-2">
|
||||
<ThemeToggle />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileOpen((current) => !current)}
|
||||
aria-label={mobileOpen ? "Close navigation menu" : "Open navigation menu"}
|
||||
aria-expanded={mobileOpen}
|
||||
aria-controls="mobile-nav-menu"
|
||||
className="inline-flex items-center justify-center w-11 h-11 rounded-lg text-[var(--muted)] hover:text-[var(--text)] hover:bg-[var(--panel-soft)] transition-colors duration-200"
|
||||
>
|
||||
<MenuIcon open={mobileOpen} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="mobile-nav-menu"
|
||||
className={[
|
||||
"md:hidden overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]",
|
||||
mobileOpen ? "max-h-64 opacity-100" : "max-h-0 opacity-0",
|
||||
].join(" ")}
|
||||
>
|
||||
<div className="px-4 pb-4 pt-1 space-y-1 bg-[var(--panel)]/90 backdrop-blur-xl border-b border-[var(--border)]">
|
||||
{navLinks.map(({ label, href }) => (
|
||||
<a
|
||||
key={href}
|
||||
href={href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="block px-3 py-2.5 rounded-lg text-sm text-[var(--muted)] hover:text-[var(--text)] hover:bg-[var(--panel-soft)] transition-colors duration-200"
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
href="#download"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="block px-3 py-2.5 rounded-lg text-sm font-medium text-center bg-[var(--cta)] text-[var(--text-on-cta)] hover:bg-[var(--cta-hover)] transition-colors duration-200 mt-2"
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
export type PlatformDownloadAsset = {
|
||||
id: string;
|
||||
label: string;
|
||||
href: string;
|
||||
note: string;
|
||||
};
|
||||
|
||||
export type PlatformDownload = {
|
||||
id: "macos" | "windows" | "linux";
|
||||
title: string;
|
||||
note: string;
|
||||
assets: PlatformDownloadAsset[];
|
||||
};
|
||||
|
||||
export const releaseVersion = "0.4.159";
|
||||
|
||||
export const platformDownloads: PlatformDownload[] = [
|
||||
{
|
||||
id: "macos",
|
||||
title: "macOS",
|
||||
note: "Signed + notarized DMG builds",
|
||||
assets: [
|
||||
{
|
||||
id: "macos-arm64-dmg",
|
||||
label: "Apple Silicon DMG",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.159-arm64.dmg",
|
||||
note: "arm64",
|
||||
},
|
||||
{
|
||||
id: "macos-x64-dmg",
|
||||
label: "Intel Mac DMG",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.159.dmg",
|
||||
note: "x64",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "windows",
|
||||
title: "Windows",
|
||||
note: "NSIS installer builds",
|
||||
assets: [
|
||||
{
|
||||
id: "windows-universal-installer",
|
||||
label: "Windows Universal Installer",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany.Setup.0.4.159.exe",
|
||||
note: "arm64 + x64 + ia32",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "linux",
|
||||
title: "Linux",
|
||||
note: "AppImage, DEB, and RPM builds",
|
||||
assets: [
|
||||
{
|
||||
id: "linux-x64-appimage",
|
||||
label: "Linux x64 AppImage",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.159.AppImage",
|
||||
note: "x64",
|
||||
},
|
||||
{
|
||||
id: "linux-x64-deb",
|
||||
label: "Linux x64 DEB",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company_0.4.159_amd64.deb",
|
||||
note: "x64",
|
||||
},
|
||||
{
|
||||
id: "linux-x64-rpm",
|
||||
label: "Linux x64 RPM",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company-0.4.159.x86_64.rpm",
|
||||
note: "x64",
|
||||
},
|
||||
{
|
||||
id: "linux-arm64-appimage",
|
||||
label: "Linux ARM64 AppImage",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.159-arm64.AppImage",
|
||||
note: "arm64",
|
||||
},
|
||||
{
|
||||
id: "linux-arm64-deb",
|
||||
label: "Linux ARM64 DEB",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company_0.4.159_arm64.deb",
|
||||
note: "arm64",
|
||||
},
|
||||
{
|
||||
id: "linux-arm64-rpm",
|
||||
label: "Linux ARM64 RPM",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company-0.4.159.aarch64.rpm",
|
||||
note: "arm64",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,2 +0,0 @@
|
||||
export const WEBSITE_URL = "https://agent-company.dev";
|
||||
export const CONTACT_EMAIL = "dev@agentcompany.ai";
|
||||
@@ -1,76 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
||||
|
||||
const storageKey = "agentcompany-theme";
|
||||
|
||||
type ThemeMode = "light" | "dark";
|
||||
|
||||
type ThemeContextValue = {
|
||||
theme: ThemeMode;
|
||||
setTheme: (theme: ThemeMode) => void;
|
||||
toggleTheme: () => void;
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
||||
|
||||
function resolveTheme(): ThemeMode {
|
||||
if (typeof window === "undefined") {
|
||||
return "light";
|
||||
}
|
||||
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
if (stored === "light" || stored === "dark") {
|
||||
return stored;
|
||||
}
|
||||
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, setThemeState] = useState<ThemeMode>(resolveTheme);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (theme === "dark") {
|
||||
root.classList.add("dark");
|
||||
} else {
|
||||
root.classList.remove("dark");
|
||||
}
|
||||
localStorage.setItem(storageKey, theme);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handleChange = (event: MediaQueryListEvent) => {
|
||||
if (!localStorage.getItem(storageKey)) {
|
||||
setThemeState(event.matches ? "dark" : "light");
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||
}, []);
|
||||
|
||||
const setTheme = useCallback((nextTheme: ThemeMode) => {
|
||||
setThemeState(nextTheme);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setThemeState((currentTheme) => (currentTheme === "light" ? "dark" : "light"));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "./theme-provider";
|
||||
|
||||
function SunIcon() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="5" />
|
||||
<line x1="12" y1="1" x2="12" y2="3" />
|
||||
<line x1="12" y1="21" x2="12" y2="23" />
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
||||
<line x1="1" y1="12" x2="3" y2="12" />
|
||||
<line x1="21" y1="12" x2="23" y2="12" />
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MoonIcon() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
aria-label={`Switch to ${theme === "light" ? "dark" : "light"} mode`}
|
||||
className="relative inline-flex items-center justify-center w-11 h-11 rounded-lg transition-colors duration-200 cursor-pointer hover:bg-[var(--panel-soft)] text-[var(--muted)] hover:text-[var(--text)]"
|
||||
>
|
||||
{theme === "light" ? <MoonIcon /> : <SunIcon />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { CounterDatabase, PagesHandler } from "../types";
|
||||
import { DOWNLOAD_COUNT_SEED } from "../../lib/downloads";
|
||||
|
||||
interface Env {
|
||||
DOWNLOAD_COUNTER_DB: CounterDatabase;
|
||||
}
|
||||
|
||||
async function readDownloadCount(db: CounterDatabase) {
|
||||
const result = await db
|
||||
.prepare(`
|
||||
SELECT
|
||||
COALESCE(
|
||||
(SELECT value FROM download_counter_settings WHERE key = 'base_downloads'),
|
||||
?
|
||||
) + COUNT(*) AS downloads
|
||||
FROM unique_download_windows
|
||||
`)
|
||||
.bind(DOWNLOAD_COUNT_SEED)
|
||||
.first<{ downloads: number }>();
|
||||
|
||||
return result?.downloads ?? DOWNLOAD_COUNT_SEED;
|
||||
}
|
||||
|
||||
export const onRequestGet: PagesHandler<Env> = async (context) => {
|
||||
const downloads = await readDownloadCount(context.env.DOWNLOAD_COUNTER_DB);
|
||||
return Response.json(
|
||||
{ downloads },
|
||||
{
|
||||
headers: {
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
import {
|
||||
DOWNLOAD_DEDUP_WINDOW_MS,
|
||||
getDownloadAsset,
|
||||
} from "../../lib/downloads";
|
||||
import type { CounterDatabase, PagesHandler } from "../types";
|
||||
|
||||
interface Env {
|
||||
DOWNLOAD_COUNTER_DB: CounterDatabase;
|
||||
DOWNLOAD_COUNTER_SALT?: string;
|
||||
}
|
||||
|
||||
function getClientIp(request: Request) {
|
||||
const cfConnectingIp = request.headers.get("CF-Connecting-IP")?.trim();
|
||||
if (cfConnectingIp) {
|
||||
return cfConnectingIp;
|
||||
}
|
||||
|
||||
const forwardedFor = request.headers.get("X-Forwarded-For")?.split(",")[0]?.trim();
|
||||
return forwardedFor || null;
|
||||
}
|
||||
|
||||
function getWindowKey(now = Date.now()) {
|
||||
return Math.floor(now / DOWNLOAD_DEDUP_WINDOW_MS);
|
||||
}
|
||||
|
||||
async function sha256Hex(input: string) {
|
||||
const buffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
||||
return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
async function countUniqueDownload(db: CounterDatabase, request: Request, assetId: string, salt: string | undefined) {
|
||||
const clientIp = getClientIp(request);
|
||||
if (!clientIp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ipHash = await sha256Hex(`${salt ?? ""}:${clientIp}`);
|
||||
const windowKey = getWindowKey();
|
||||
|
||||
await db
|
||||
.prepare(`
|
||||
INSERT OR IGNORE INTO unique_download_windows (
|
||||
ip_hash,
|
||||
window_key,
|
||||
first_asset_id,
|
||||
created_at
|
||||
) VALUES (?, ?, ?, datetime('now'))
|
||||
`)
|
||||
.bind(ipHash, windowKey, assetId)
|
||||
.run();
|
||||
}
|
||||
|
||||
export const onRequestGet: PagesHandler<Env> = async (context) => {
|
||||
const url = new URL(context.request.url);
|
||||
const assetId = url.searchParams.get("asset")?.trim();
|
||||
if (!assetId) {
|
||||
return new Response("Missing asset parameter.", { status: 400 });
|
||||
}
|
||||
|
||||
const asset = getDownloadAsset(assetId);
|
||||
if (!asset) {
|
||||
return new Response("Unknown asset.", { status: 404 });
|
||||
}
|
||||
|
||||
await countUniqueDownload(
|
||||
context.env.DOWNLOAD_COUNTER_DB,
|
||||
context.request,
|
||||
asset.id,
|
||||
context.env.DOWNLOAD_COUNTER_SALT,
|
||||
);
|
||||
|
||||
return Response.redirect(asset.href, 302);
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
export interface CounterDbStatement {
|
||||
bind(...values: unknown[]): {
|
||||
first<T = unknown>(): Promise<T | null>;
|
||||
run(): Promise<unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CounterDatabase {
|
||||
prepare(query: string): CounterDbStatement;
|
||||
}
|
||||
|
||||
export interface PagesHandlerContext<Env> {
|
||||
request: Request;
|
||||
env: Env;
|
||||
}
|
||||
|
||||
export type PagesHandler<Env> = (context: PagesHandlerContext<Env>) => Response | Promise<Response>;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { platformDownloads, type PlatformDownloadAsset } from "../components/release-links.generated";
|
||||
|
||||
export const DOWNLOAD_COUNT_SEED = 13674;
|
||||
export const DOWNLOAD_DEDUP_WINDOW_HOURS = 24;
|
||||
export const DOWNLOAD_DEDUP_WINDOW_MS = DOWNLOAD_DEDUP_WINDOW_HOURS * 60 * 60 * 1000;
|
||||
|
||||
export type ReleaseDownloadAsset = PlatformDownloadAsset & {
|
||||
platformId: "macos" | "windows" | "linux";
|
||||
platformTitle: string;
|
||||
};
|
||||
|
||||
export const downloadAssets: ReleaseDownloadAsset[] = platformDownloads.flatMap((platform) =>
|
||||
platform.assets.map((asset) => ({
|
||||
...asset,
|
||||
platformId: platform.id,
|
||||
platformTitle: platform.title,
|
||||
})),
|
||||
);
|
||||
|
||||
const downloadAssetMap = new Map(downloadAssets.map((asset) => [asset.id, asset]));
|
||||
|
||||
export function getDownloadAsset(assetId: string) {
|
||||
return downloadAssetMap.get(assetId) ?? null;
|
||||
}
|
||||
|
||||
export function getTrackedDownloadHref(assetId: string) {
|
||||
return `/api/download?asset=${encodeURIComponent(assetId)}`;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS download_counter_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value INTEGER NOT NULL
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO download_counter_settings (key, value)
|
||||
VALUES ('base_downloads', 13674);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS unique_download_windows (
|
||||
ip_hash TEXT NOT NULL,
|
||||
window_key INTEGER NOT NULL,
|
||||
first_asset_id TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (ip_hash, window_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_unique_download_windows_created_at
|
||||
ON unique_download_windows(created_at);
|
||||
@@ -1,6 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -1,11 +0,0 @@
|
||||
import path from "node:path";
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "export",
|
||||
turbopack: {
|
||||
root: path.join(__dirname),
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "agent-company-landing",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"preview": "wrangler pages dev out"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "12.38.0",
|
||||
"next": "16.2.2",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.9.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/*
|
||||
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data:; connect-src 'self' https://cloudflareinsights.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'
|
||||
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
||||
Referrer-Policy: strict-origin-when-cross-origin
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
|
||||
/screenshots/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/assets/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/_next/static/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/favicon.ico
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/icon.png
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="none">
|
||||
<circle cx="512" cy="512" r="480" fill="#191919"/>
|
||||
<g transform="translate(268 255) scale(4)">
|
||||
<path fill="#FBFAF5" d="M 76.604813 111.661926 C 65.070183 111.662621 53.987053 111.662621 42.757744 111.662621 C 45.2239 101.998177 45.297546 101.949265 53.830105 102.036659 C 57.279823 102.071999 60.730202 102.04248 64.259201 102.04248 C 62.159458 93.316338 61.799103 93.009819 51.340881 91.341682 C 59.312901 73.847687 67.193756 56.553749 75.587578 38.134155 C 86.933441 62.965954 97.804642 86.758881 108.711754 110.630394 C 100.707413 111.871689 92.355965 107.246559 88.107536 98.852119 C 84.207748 91.146538 80.800591 83.191696 77.172813 75.348343 C 76.340195 73.54818 75.492958 71.754791 74.614624 69.877441 C 69.107384 75.691788 68.008286 81.475128 71.005882 88.088753 C 74.413452 95.606903 77.813919 103.128281 81.452705 111.167984 C 79.55114 111.381325 78.303734 111.521271 76.604813 111.661926 Z"/>
|
||||
<path fill="#FBFAF5" d="M 26.805992 87.743256 C 38.616821 61.830872 50.261688 36.260269 62.470131 9.452141 C 65.732315 17.818909 71.145126 23.524567 66.258224 33.086182 C 55.518253 54.099815 46.339973 75.911873 36.533691 97.402336 C 32.699284 105.805443 26.495394 110.823158 16.012421 111.410957 C 19.72117 103.270836 23.180601 95.67794 26.805992 87.743256 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 752 KiB |
|
Before Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 337 KiB |
|
Before Width: | Height: | Size: 465 KiB |
|
Before Width: | Height: | Size: 450 KiB |
|
Before Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 606 KiB |
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"es2022"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
name = "agentcompany"
|
||||
compatibility_date = "2026-04-03"
|
||||
pages_build_output_dir = "out"
|
||||
|
||||
[vars]
|
||||
DOWNLOAD_COUNTER_SALT = "agentcompany-download-counter-v1"
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DOWNLOAD_COUNTER_DB"
|
||||
database_name = "agentcompany-landing-downloads"
|
||||
database_id = "dfa3f0f3-009b-4436-b519-5b50ae6b1cf9"
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agent-company",
|
||||
"version": "0.4.160",
|
||||
"version": "0.4.150",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Cross-platform Electron desktop control plane for local-first AI agent companies.",
|
||||
@@ -16,9 +16,6 @@
|
||||
"build": "electron-vite build && node scripts/build-desktop.mjs",
|
||||
"build:dir": "electron-vite build && node scripts/build-desktop.mjs --dir",
|
||||
"build:all": "node scripts/build-all-platforms.mjs",
|
||||
"release:cloudflare": "node scripts/release-cloudflare.mjs",
|
||||
"release:cloudflare:public": "node scripts/release-public-cloudflare.mjs",
|
||||
"release:boss": "node scripts/release-boss.mjs",
|
||||
"release:local": "node scripts/release-local.mjs",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
@@ -27,9 +24,10 @@
|
||||
"typecheck:all": "pnpm -r typecheck",
|
||||
"test:unit": "node scripts/run-vitest.mjs run --config vitest.config.ts tests/unit",
|
||||
"test:integration": "node scripts/run-vitest.mjs run --config vitest.config.ts tests/integration",
|
||||
"test:packages": "echo 'no standalone package tests'",
|
||||
"test:e2e": "node --no-warnings ./node_modules/@playwright/test/cli.js test -c tests/e2e/playwright.config.ts",
|
||||
"test:run": "pnpm test:unit && pnpm test:integration && pnpm test:e2e",
|
||||
"verify": "pnpm typecheck && pnpm lint && pnpm typecheck:all && pnpm test:unit && pnpm test:integration && pnpm test:e2e",
|
||||
"test:run": "pnpm test:unit && pnpm test:integration && pnpm test:packages && pnpm test:e2e",
|
||||
"verify": "pnpm typecheck && pnpm lint && pnpm typecheck:all && pnpm test:unit && pnpm test:integration && pnpm test:packages && pnpm test:e2e",
|
||||
"verify:release": "pnpm verify && pnpm build:dir",
|
||||
"postinstall": "node scripts/postinstall.mjs"
|
||||
},
|
||||
@@ -40,12 +38,15 @@
|
||||
"@agentcompany/adapter-utils": "workspace:*",
|
||||
"@phosphor-icons/react": "2.1.10",
|
||||
"@tanstack/react-virtual": "^3.13.23",
|
||||
"clsx": "2.1.1",
|
||||
"electron-log": "5.4.3",
|
||||
"electron-updater": "6.8.3",
|
||||
"framer-motion": "12.36.0",
|
||||
"immer": "^11.1.4",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"recharts": "3.8.1",
|
||||
"tailwind-merge": "3.5.0",
|
||||
"zod": "4.3.6",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
|
||||
@@ -1,37 +1,34 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { AdapterExecutionContext, ManagedSkillRef } from "@agentcompany/adapter-utils";
|
||||
import type { RunProcessResult } from "@agentcompany/adapter-utils/server-utils";
|
||||
import {
|
||||
asString,
|
||||
asNumber,
|
||||
asStringArray,
|
||||
parseObject,
|
||||
buildAgentCompanyEnv,
|
||||
ensureAbsoluteDirectory,
|
||||
ensureCommandResolvable,
|
||||
ensurePathInEnv,
|
||||
createManagedSkillSymlinks,
|
||||
} from "@agentcompany/adapter-utils/server-utils";
|
||||
import {
|
||||
hasNonEmptyEnvValue,
|
||||
buildSkillsCandidates,
|
||||
resolveSkillsDir,
|
||||
parseManagedSkills,
|
||||
parseExtraArgs,
|
||||
buildCommonAgentCompanyEnv,
|
||||
resolveSessionResumeState,
|
||||
} from "@agentcompany/adapter-utils";
|
||||
|
||||
export {
|
||||
hasNonEmptyEnvValue,
|
||||
buildSkillsCandidates,
|
||||
resolveSkillsDir as resolveAgentCompanySkillsDir,
|
||||
parseManagedSkills,
|
||||
parseExtraArgs,
|
||||
buildCommonAgentCompanyEnv,
|
||||
resolveSessionResumeState,
|
||||
};
|
||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
export const SKILLS_CANDIDATES = [
|
||||
path.resolve(__moduleDir, "../../skills"), // published: <pkg>/dist/server/ -> <pkg>/skills/
|
||||
path.resolve(__moduleDir, "../../../../../skills"), // dev: src/server/ -> repo root/skills/
|
||||
];
|
||||
|
||||
export const SKILLS_CANDIDATES = buildSkillsCandidates(import.meta.url);
|
||||
export async function resolveAgentCompanySkillsDir(): Promise<string | null> {
|
||||
for (const candidate of SKILLS_CANDIDATES) {
|
||||
const isDir = await fs.stat(candidate).then((s) => s.isDirectory()).catch(() => false);
|
||||
if (isDir) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tmpdir with `.claude/skills/` containing symlinks to skills from
|
||||
@@ -47,12 +44,13 @@ export async function buildSkillsDir(
|
||||
const target = path.join(tmp, ".claude", "skills");
|
||||
await fs.mkdir(target, { recursive: true });
|
||||
|
||||
// Use managed skills when available; fall back to static repo scan
|
||||
if (managedSkills.length > 0) {
|
||||
await createManagedSkillSymlinks(managedSkills, target, onLog);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
const skillsDir = await resolveSkillsDir(SKILLS_CANDIDATES);
|
||||
const skillsDir = await resolveAgentCompanySkillsDir();
|
||||
if (!skillsDir) return tmp;
|
||||
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
@@ -100,7 +98,13 @@ export function buildLoginResult(input: {
|
||||
};
|
||||
}
|
||||
|
||||
export function hasNonEmptyEnvValue(env: Record<string, string>, key: string): boolean {
|
||||
const raw = env[key];
|
||||
return typeof raw === "string" && raw.trim().length > 0;
|
||||
}
|
||||
|
||||
export function resolveClaudeBillingType(env: Record<string, string>): "api" | "subscription" {
|
||||
// Claude uses API-key auth when ANTHROPIC_API_KEY is present; otherwise rely on local login/session auth.
|
||||
return hasNonEmptyEnvValue(env, "ANTHROPIC_API_KEY") ? "api" : "subscription";
|
||||
}
|
||||
|
||||
@@ -108,17 +112,140 @@ export async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Pro
|
||||
const { runId, agent, config, context, authToken } = input;
|
||||
|
||||
const command = asString(config.command, "claude");
|
||||
const { env, cwd, workspaceId, workspaceRepoUrl, workspaceRepoRef } = await buildCommonAgentCompanyEnv(
|
||||
{ runId, agent, config, context, authToken },
|
||||
{ ensureAbsoluteDirectory },
|
||||
);
|
||||
const workspaceContext = parseObject(context.agentCompanyWorkspace);
|
||||
const workspaceCwd = asString(workspaceContext.cwd, "");
|
||||
const workspaceSource = asString(workspaceContext.source, "");
|
||||
const workspaceStrategy = asString(workspaceContext.strategy, "");
|
||||
const workspaceId = asString(workspaceContext.workspaceId, "") || null;
|
||||
const workspaceRepoUrl = asString(workspaceContext.repoUrl, "") || null;
|
||||
const workspaceRepoRef = asString(workspaceContext.repoRef, "") || null;
|
||||
const workspaceBranch = asString(workspaceContext.branchName, "") || null;
|
||||
const workspaceWorktreePath = asString(workspaceContext.worktreePath, "") || null;
|
||||
const workspaceHints = Array.isArray(context.agentCompanyWorkspaces)
|
||||
? context.agentCompanyWorkspaces.filter(
|
||||
(value): value is Record<string, unknown> => typeof value === "object" && value !== null,
|
||||
)
|
||||
: [];
|
||||
const runtimeServiceIntents = Array.isArray(context.agentCompanyRuntimeServiceIntents)
|
||||
? context.agentCompanyRuntimeServiceIntents.filter(
|
||||
(value): value is Record<string, unknown> => typeof value === "object" && value !== null,
|
||||
)
|
||||
: [];
|
||||
const runtimeServices = Array.isArray(context.agentCompanyRuntimeServices)
|
||||
? context.agentCompanyRuntimeServices.filter(
|
||||
(value): value is Record<string, unknown> => typeof value === "object" && value !== null,
|
||||
)
|
||||
: [];
|
||||
const runtimePrimaryUrl = asString(context.agentCompanyRuntimePrimaryUrl, "");
|
||||
const configuredCwd = asString(config.cwd, "");
|
||||
const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
|
||||
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
|
||||
const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
|
||||
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||
|
||||
const envConfig = parseObject(config.env);
|
||||
const hasExplicitApiKey =
|
||||
typeof envConfig.AGENT_COMPANY_API_KEY === "string" && envConfig.AGENT_COMPANY_API_KEY.trim().length > 0;
|
||||
const env: Record<string, string> = { ...buildAgentCompanyEnv(agent) };
|
||||
env.AGENT_COMPANY_RUN_ID = runId;
|
||||
|
||||
const wakeTaskId =
|
||||
(typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) ||
|
||||
(typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) ||
|
||||
null;
|
||||
const wakeReason =
|
||||
typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0
|
||||
? context.wakeReason.trim()
|
||||
: null;
|
||||
const wakeCommentId =
|
||||
(typeof context.wakeCommentId === "string" && context.wakeCommentId.trim().length > 0 && context.wakeCommentId.trim()) ||
|
||||
(typeof context.commentId === "string" && context.commentId.trim().length > 0 && context.commentId.trim()) ||
|
||||
null;
|
||||
const approvalId =
|
||||
typeof context.approvalId === "string" && context.approvalId.trim().length > 0
|
||||
? context.approvalId.trim()
|
||||
: null;
|
||||
const approvalStatus =
|
||||
typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0
|
||||
? context.approvalStatus.trim()
|
||||
: null;
|
||||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
|
||||
if (wakeTaskId) {
|
||||
env.AGENT_COMPANY_TASK_ID = wakeTaskId;
|
||||
}
|
||||
if (wakeReason) {
|
||||
env.AGENT_COMPANY_WAKE_REASON = wakeReason;
|
||||
}
|
||||
if (wakeCommentId) {
|
||||
env.AGENT_COMPANY_WAKE_COMMENT_ID = wakeCommentId;
|
||||
}
|
||||
if (approvalId) {
|
||||
env.AGENT_COMPANY_APPROVAL_ID = approvalId;
|
||||
}
|
||||
if (approvalStatus) {
|
||||
env.AGENT_COMPANY_APPROVAL_STATUS = approvalStatus;
|
||||
}
|
||||
if (linkedIssueIds.length > 0) {
|
||||
env.AGENT_COMPANY_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
}
|
||||
if (effectiveWorkspaceCwd) {
|
||||
env.AGENT_COMPANY_WORKSPACE_CWD = effectiveWorkspaceCwd;
|
||||
}
|
||||
if (workspaceSource) {
|
||||
env.AGENT_COMPANY_WORKSPACE_SOURCE = workspaceSource;
|
||||
}
|
||||
if (workspaceStrategy) {
|
||||
env.AGENT_COMPANY_WORKSPACE_STRATEGY = workspaceStrategy;
|
||||
}
|
||||
if (workspaceId) {
|
||||
env.AGENT_COMPANY_WORKSPACE_ID = workspaceId;
|
||||
}
|
||||
if (workspaceRepoUrl) {
|
||||
env.AGENT_COMPANY_WORKSPACE_REPO_URL = workspaceRepoUrl;
|
||||
}
|
||||
if (workspaceRepoRef) {
|
||||
env.AGENT_COMPANY_WORKSPACE_REPO_REF = workspaceRepoRef;
|
||||
}
|
||||
if (workspaceBranch) {
|
||||
env.AGENT_COMPANY_WORKSPACE_BRANCH = workspaceBranch;
|
||||
}
|
||||
if (workspaceWorktreePath) {
|
||||
env.AGENT_COMPANY_WORKSPACE_WORKTREE_PATH = workspaceWorktreePath;
|
||||
}
|
||||
if (workspaceHints.length > 0) {
|
||||
env.AGENT_COMPANY_WORKSPACES_JSON = JSON.stringify(workspaceHints);
|
||||
}
|
||||
if (runtimeServiceIntents.length > 0) {
|
||||
env.AGENT_COMPANY_RUNTIME_SERVICE_INTENTS_JSON = JSON.stringify(runtimeServiceIntents);
|
||||
}
|
||||
if (runtimeServices.length > 0) {
|
||||
env.AGENT_COMPANY_RUNTIME_SERVICES_JSON = JSON.stringify(runtimeServices);
|
||||
}
|
||||
if (runtimePrimaryUrl) {
|
||||
env.AGENT_COMPANY_RUNTIME_PRIMARY_URL = runtimePrimaryUrl;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(envConfig)) {
|
||||
if (typeof value === "string") env[key] = value;
|
||||
}
|
||||
|
||||
if (!hasExplicitApiKey && authToken) {
|
||||
env.AGENT_COMPANY_API_KEY = authToken;
|
||||
}
|
||||
|
||||
const runtimeEnv = ensurePathInEnv(env);
|
||||
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
||||
|
||||
const timeoutSec = asNumber(config.timeoutSec, 0);
|
||||
const graceSec = asNumber(config.graceSec, 20);
|
||||
const extraArgs = parseExtraArgs(config);
|
||||
const extraArgs = (() => {
|
||||
const fromExtraArgs = asStringArray(config.extraArgs);
|
||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||
return asStringArray(config.args);
|
||||
})();
|
||||
|
||||
return {
|
||||
command,
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { AdapterExecutionContext, AdapterExecutionResult } from "@agentcompany/adapter-utils";
|
||||
import type { RunProcessResult } from "@agentcompany/adapter-utils/server-utils";
|
||||
import type { ManagedSkillRef } from "@agentcompany/adapter-utils";
|
||||
import {
|
||||
asString,
|
||||
asNumber,
|
||||
@@ -25,8 +26,6 @@ import {
|
||||
buildLoginResult,
|
||||
resolveClaudeBillingType,
|
||||
buildClaudeRuntimeConfig,
|
||||
parseManagedSkills,
|
||||
resolveSessionResumeState,
|
||||
} from "./execute-helpers.js";
|
||||
|
||||
export async function runClaudeLogin(input: {
|
||||
@@ -105,7 +104,11 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
extraArgs,
|
||||
} = runtimeConfig;
|
||||
const billingType = resolveClaudeBillingType(env);
|
||||
const managedSkills = parseManagedSkills(config);
|
||||
const managedSkills: ManagedSkillRef[] = Array.isArray(config.managedSkills)
|
||||
? (config.managedSkills as ManagedSkillRef[]).filter(
|
||||
(s) => typeof s.runtimeName === "string" && typeof s.sourcePath === "string",
|
||||
)
|
||||
: [];
|
||||
const skillsDir = await buildSkillsDir(managedSkills, onLog);
|
||||
|
||||
// When instructionsFilePath is configured, create a combined temp file that
|
||||
@@ -120,8 +123,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
effectiveInstructionsFilePath = combinedPath;
|
||||
}
|
||||
|
||||
const sessionResume = resolveSessionResumeState(runtime, cwd);
|
||||
const { sessionId, runtimeSessionId, runtimeSessionCwd, canResumeSession } = sessionResume;
|
||||
const runtimeSessionParams = parseObject(runtime.sessionParams);
|
||||
const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
|
||||
const runtimeSessionCwd = asString(runtimeSessionParams.cwd, "");
|
||||
const canResumeSession =
|
||||
runtimeSessionId.length > 0 &&
|
||||
(runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(cwd));
|
||||
const sessionId = canResumeSession ? runtimeSessionId : null;
|
||||
if (runtimeSessionId && !canResumeSession) {
|
||||
await onLog(
|
||||
"stderr",
|
||||
|
||||
@@ -1,29 +1,17 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { AdapterExecutionContext, ManagedSkillRef } from "@agentcompany/adapter-utils";
|
||||
import {
|
||||
hasNonEmptyEnvValue,
|
||||
buildSkillsCandidates,
|
||||
resolveSkillsDir,
|
||||
parseManagedSkills,
|
||||
parseExtraArgs,
|
||||
buildCommonAgentCompanyEnv,
|
||||
resolveSessionResumeState,
|
||||
} from "@agentcompany/adapter-utils";
|
||||
import {
|
||||
createManagedSkillSymlinks,
|
||||
} from "@agentcompany/adapter-utils/server-utils";
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
export {
|
||||
hasNonEmptyEnvValue,
|
||||
parseManagedSkills,
|
||||
parseExtraArgs,
|
||||
buildCommonAgentCompanyEnv,
|
||||
resolveSessionResumeState,
|
||||
};
|
||||
|
||||
export const SKILLS_CANDIDATES = buildSkillsCandidates(import.meta.url);
|
||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
export const SKILLS_CANDIDATES = [
|
||||
path.resolve(__moduleDir, "../../skills"), // published: <pkg>/dist/server/ -> <pkg>/skills/
|
||||
path.resolve(__moduleDir, "../../../../../skills"), // dev: src/server/ -> repo root/skills/
|
||||
];
|
||||
|
||||
export const CODEX_ROLLOUT_NOISE_RE =
|
||||
/^\d{4}-\d{2}-\d{2}T[^\s]+\s+ERROR\s+codex_core::rollout::list:\s+state db missing rollout path for thread\s+[a-z0-9-]+$/i;
|
||||
@@ -43,7 +31,13 @@ export function stripCodexRolloutNoise(text: string): string {
|
||||
return kept.join("\n");
|
||||
}
|
||||
|
||||
export function hasNonEmptyEnvValue(env: Record<string, string>, key: string): boolean {
|
||||
const raw = env[key];
|
||||
return typeof raw === "string" && raw.trim().length > 0;
|
||||
}
|
||||
|
||||
export function resolveCodexBillingType(env: Record<string, string>): "api" | "subscription" {
|
||||
// Codex uses API-key auth when OPENAI_API_KEY is present; otherwise rely on local login/session auth.
|
||||
return hasNonEmptyEnvValue(env, "OPENAI_API_KEY") ? "api" : "subscription";
|
||||
}
|
||||
|
||||
@@ -54,7 +48,11 @@ export function codexHomeDir(): string {
|
||||
}
|
||||
|
||||
export async function resolveAgentCompanySkillsDir(): Promise<string | null> {
|
||||
return resolveSkillsDir(SKILLS_CANDIDATES);
|
||||
for (const candidate of SKILLS_CANDIDATES) {
|
||||
const isDir = await fs.stat(candidate).then((s) => s.isDirectory()).catch(() => false);
|
||||
if (isDir) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function ensureCodexSkillsInjected(
|
||||
@@ -63,12 +61,13 @@ export async function ensureCodexSkillsInjected(
|
||||
) {
|
||||
const skillsHome = path.join(codexHomeDir(), "skills");
|
||||
|
||||
// Use managed skills when available; fall back to static repo scan
|
||||
if (managedSkills.length > 0) {
|
||||
await createManagedSkillSymlinks(managedSkills, skillsHome, onLog);
|
||||
return;
|
||||
}
|
||||
|
||||
const skillsDir = await resolveSkillsDir(SKILLS_CANDIDATES);
|
||||
const skillsDir = await resolveAgentCompanySkillsDir();
|
||||
if (!skillsDir) return;
|
||||
|
||||
await fs.mkdir(skillsHome, { recursive: true });
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs/promises";
|
||||
import type { AdapterExecutionContext, AdapterExecutionResult } from "@agentcompany/adapter-utils";
|
||||
import path from "node:path";
|
||||
import type { AdapterExecutionContext, AdapterExecutionResult, ManagedSkillRef } from "@agentcompany/adapter-utils";
|
||||
import {
|
||||
asString,
|
||||
asNumber,
|
||||
asBoolean,
|
||||
asStringArray,
|
||||
parseObject,
|
||||
buildAgentCompanyEnv,
|
||||
redactEnvForLogs,
|
||||
ensureAbsoluteDirectory,
|
||||
ensureCommandResolvable,
|
||||
@@ -19,10 +22,6 @@ import {
|
||||
stripCodexRolloutNoise,
|
||||
resolveCodexBillingType,
|
||||
ensureCodexSkillsInjected,
|
||||
parseManagedSkills,
|
||||
parseExtraArgs,
|
||||
buildCommonAgentCompanyEnv,
|
||||
resolveSessionResumeState,
|
||||
} from "./execute-helpers.js";
|
||||
|
||||
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
|
||||
@@ -44,32 +43,155 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
asBoolean(config.dangerouslyBypassSandbox, false),
|
||||
);
|
||||
|
||||
const { env, cwd, workspaceId, workspaceRepoUrl, workspaceRepoRef } =
|
||||
await buildCommonAgentCompanyEnv(
|
||||
{ runId, agent, config, context, authToken },
|
||||
{ ensureAbsoluteDirectory },
|
||||
);
|
||||
|
||||
const managedSkills = parseManagedSkills(config);
|
||||
const workspaceContext = parseObject(context.agentCompanyWorkspace);
|
||||
const workspaceCwd = asString(workspaceContext.cwd, "");
|
||||
const workspaceSource = asString(workspaceContext.source, "");
|
||||
const workspaceStrategy = asString(workspaceContext.strategy, "");
|
||||
const workspaceId = asString(workspaceContext.workspaceId, "");
|
||||
const workspaceRepoUrl = asString(workspaceContext.repoUrl, "");
|
||||
const workspaceRepoRef = asString(workspaceContext.repoRef, "");
|
||||
const workspaceBranch = asString(workspaceContext.branchName, "");
|
||||
const workspaceWorktreePath = asString(workspaceContext.worktreePath, "");
|
||||
const workspaceHints = Array.isArray(context.agentCompanyWorkspaces)
|
||||
? context.agentCompanyWorkspaces.filter(
|
||||
(value): value is Record<string, unknown> => typeof value === "object" && value !== null,
|
||||
)
|
||||
: [];
|
||||
const runtimeServiceIntents = Array.isArray(context.agentCompanyRuntimeServiceIntents)
|
||||
? context.agentCompanyRuntimeServiceIntents.filter(
|
||||
(value): value is Record<string, unknown> => typeof value === "object" && value !== null,
|
||||
)
|
||||
: [];
|
||||
const runtimeServices = Array.isArray(context.agentCompanyRuntimeServices)
|
||||
? context.agentCompanyRuntimeServices.filter(
|
||||
(value): value is Record<string, unknown> => typeof value === "object" && value !== null,
|
||||
)
|
||||
: [];
|
||||
const runtimePrimaryUrl = asString(context.agentCompanyRuntimePrimaryUrl, "");
|
||||
const configuredCwd = asString(config.cwd, "");
|
||||
const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
|
||||
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
|
||||
const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
|
||||
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||
const managedSkills: ManagedSkillRef[] = Array.isArray(config.managedSkills)
|
||||
? (config.managedSkills as ManagedSkillRef[]).filter(
|
||||
(s) => typeof s.runtimeName === "string" && typeof s.sourcePath === "string",
|
||||
)
|
||||
: [];
|
||||
await ensureCodexSkillsInjected(managedSkills, onLog);
|
||||
|
||||
const envConfig = parseObject(config.env);
|
||||
const hasExplicitApiKey =
|
||||
typeof envConfig.AGENT_COMPANY_API_KEY === "string" && envConfig.AGENT_COMPANY_API_KEY.trim().length > 0;
|
||||
const env: Record<string, string> = { ...buildAgentCompanyEnv(agent) };
|
||||
env.AGENT_COMPANY_RUN_ID = runId;
|
||||
const wakeTaskId =
|
||||
(typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) ||
|
||||
(typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) ||
|
||||
null;
|
||||
const wakeReason =
|
||||
typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0
|
||||
? context.wakeReason.trim()
|
||||
: null;
|
||||
const wakeCommentId =
|
||||
(typeof context.wakeCommentId === "string" && context.wakeCommentId.trim().length > 0 && context.wakeCommentId.trim()) ||
|
||||
(typeof context.commentId === "string" && context.commentId.trim().length > 0 && context.commentId.trim()) ||
|
||||
null;
|
||||
const approvalId =
|
||||
typeof context.approvalId === "string" && context.approvalId.trim().length > 0
|
||||
? context.approvalId.trim()
|
||||
: null;
|
||||
const approvalStatus =
|
||||
typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0
|
||||
? context.approvalStatus.trim()
|
||||
: null;
|
||||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
if (wakeTaskId) {
|
||||
env.AGENT_COMPANY_TASK_ID = wakeTaskId;
|
||||
}
|
||||
if (wakeReason) {
|
||||
env.AGENT_COMPANY_WAKE_REASON = wakeReason;
|
||||
}
|
||||
if (wakeCommentId) {
|
||||
env.AGENT_COMPANY_WAKE_COMMENT_ID = wakeCommentId;
|
||||
}
|
||||
if (approvalId) {
|
||||
env.AGENT_COMPANY_APPROVAL_ID = approvalId;
|
||||
}
|
||||
if (approvalStatus) {
|
||||
env.AGENT_COMPANY_APPROVAL_STATUS = approvalStatus;
|
||||
}
|
||||
if (linkedIssueIds.length > 0) {
|
||||
env.AGENT_COMPANY_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
}
|
||||
if (effectiveWorkspaceCwd) {
|
||||
env.AGENT_COMPANY_WORKSPACE_CWD = effectiveWorkspaceCwd;
|
||||
}
|
||||
if (workspaceSource) {
|
||||
env.AGENT_COMPANY_WORKSPACE_SOURCE = workspaceSource;
|
||||
}
|
||||
if (workspaceStrategy) {
|
||||
env.AGENT_COMPANY_WORKSPACE_STRATEGY = workspaceStrategy;
|
||||
}
|
||||
if (workspaceId) {
|
||||
env.AGENT_COMPANY_WORKSPACE_ID = workspaceId;
|
||||
}
|
||||
if (workspaceRepoUrl) {
|
||||
env.AGENT_COMPANY_WORKSPACE_REPO_URL = workspaceRepoUrl;
|
||||
}
|
||||
if (workspaceRepoRef) {
|
||||
env.AGENT_COMPANY_WORKSPACE_REPO_REF = workspaceRepoRef;
|
||||
}
|
||||
if (workspaceBranch) {
|
||||
env.AGENT_COMPANY_WORKSPACE_BRANCH = workspaceBranch;
|
||||
}
|
||||
if (workspaceWorktreePath) {
|
||||
env.AGENT_COMPANY_WORKSPACE_WORKTREE_PATH = workspaceWorktreePath;
|
||||
}
|
||||
if (workspaceHints.length > 0) {
|
||||
env.AGENT_COMPANY_WORKSPACES_JSON = JSON.stringify(workspaceHints);
|
||||
}
|
||||
if (runtimeServiceIntents.length > 0) {
|
||||
env.AGENT_COMPANY_RUNTIME_SERVICE_INTENTS_JSON = JSON.stringify(runtimeServiceIntents);
|
||||
}
|
||||
if (runtimeServices.length > 0) {
|
||||
env.AGENT_COMPANY_RUNTIME_SERVICES_JSON = JSON.stringify(runtimeServices);
|
||||
}
|
||||
if (runtimePrimaryUrl) {
|
||||
env.AGENT_COMPANY_RUNTIME_PRIMARY_URL = runtimePrimaryUrl;
|
||||
}
|
||||
for (const [k, v] of Object.entries(envConfig)) {
|
||||
if (typeof v === "string") env[k] = v;
|
||||
}
|
||||
if (!hasExplicitApiKey && authToken) {
|
||||
env.AGENT_COMPANY_API_KEY = authToken;
|
||||
}
|
||||
const billingType = resolveCodexBillingType(env);
|
||||
const runtimeEnv = ensurePathInEnv(env);
|
||||
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
||||
|
||||
const timeoutSec = asNumber(config.timeoutSec, 0);
|
||||
const graceSec = asNumber(config.graceSec, 20);
|
||||
const extraArgs = parseExtraArgs(config);
|
||||
const extraArgs = (() => {
|
||||
const fromExtraArgs = asStringArray(config.extraArgs);
|
||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||
return asStringArray(config.args);
|
||||
})();
|
||||
|
||||
const { sessionId, runtimeSessionId, runtimeSessionCwd, canResumeSession } =
|
||||
resolveSessionResumeState(runtime, cwd);
|
||||
const runtimeSessionParams = parseObject(runtime.sessionParams);
|
||||
const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
|
||||
const runtimeSessionCwd = asString(runtimeSessionParams.cwd, "");
|
||||
const canResumeSession =
|
||||
runtimeSessionId.length > 0 &&
|
||||
(runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(cwd));
|
||||
const sessionId = canResumeSession ? runtimeSessionId : null;
|
||||
if (runtimeSessionId && !canResumeSession) {
|
||||
await onLog(
|
||||
"stderr",
|
||||
`[agent-company] Codex session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`,
|
||||
);
|
||||
}
|
||||
|
||||
const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
|
||||
const instructionsDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
|
||||
let instructionsPrefix = "";
|
||||
@@ -122,6 +244,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
if (model) args.push("--model", model);
|
||||
if (modelReasoningEffort) args.push("-c", `model_reasoning_effort=${JSON.stringify(modelReasoningEffort)}`);
|
||||
if (extraArgs.length > 0) {
|
||||
// Filter out flags the adapter already manages to prevent duplicates
|
||||
// (e.g. user shell aliases that inject --dangerously-bypass-approvals-and-sandbox)
|
||||
const managedFlags = [
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"--json",
|
||||
@@ -229,7 +353,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
provider: "openai",
|
||||
model,
|
||||
billingType,
|
||||
costUsd: attempt.parsed.costUsd,
|
||||
costUsd: null,
|
||||
resultJson: {
|
||||
stdout: attempt.proc.stdout,
|
||||
stderr: attempt.proc.stderr,
|
||||
|
||||
@@ -4,7 +4,6 @@ export function parseCodexJsonl(stdout: string) {
|
||||
let sessionId: string | null = null;
|
||||
const messages: string[] = [];
|
||||
let errorMessage: string | null = null;
|
||||
let costUsd: number | null = null;
|
||||
const usage = {
|
||||
inputTokens: 0,
|
||||
cachedInputTokens: 0,
|
||||
@@ -44,8 +43,6 @@ export function parseCodexJsonl(stdout: string) {
|
||||
usage.inputTokens = asNumber(usageObj.input_tokens, usage.inputTokens);
|
||||
usage.cachedInputTokens = asNumber(usageObj.cached_input_tokens, usage.cachedInputTokens);
|
||||
usage.outputTokens = asNumber(usageObj.output_tokens, usage.outputTokens);
|
||||
const rawCost = event.total_cost_usd;
|
||||
if (typeof rawCost === "number" && Number.isFinite(rawCost)) costUsd = rawCost;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -53,8 +50,6 @@ export function parseCodexJsonl(stdout: string) {
|
||||
const err = parseObject(event.error);
|
||||
const msg = asString(err.message, "").trim();
|
||||
if (msg) errorMessage = msg;
|
||||
const rawCost = event.total_cost_usd;
|
||||
if (typeof rawCost === "number" && Number.isFinite(rawCost)) costUsd = rawCost;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +57,6 @@ export function parseCodexJsonl(stdout: string) {
|
||||
sessionId,
|
||||
summary: messages.join("\n\n").trim(),
|
||||
usage,
|
||||
costUsd,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@agentcompany/adapter-gemini-local",
|
||||
"version": "0.3.0",
|
||||
"version": "0.2.7",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
|
||||
@@ -2,29 +2,22 @@ import fs from "node:fs/promises";
|
||||
import type { Dirent } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { AdapterExecutionContext, ManagedSkillRef } from "@agentcompany/adapter-utils";
|
||||
import {
|
||||
hasNonEmptyEnvValue,
|
||||
buildSkillsCandidates,
|
||||
resolveSkillsDir,
|
||||
parseManagedSkills,
|
||||
parseExtraArgs,
|
||||
buildCommonAgentCompanyEnv,
|
||||
resolveSessionResumeState,
|
||||
} from "@agentcompany/adapter-utils";
|
||||
import {
|
||||
createManagedSkillSymlinks,
|
||||
} from "@agentcompany/adapter-utils/server-utils";
|
||||
|
||||
export {
|
||||
hasNonEmptyEnvValue,
|
||||
parseManagedSkills,
|
||||
parseExtraArgs,
|
||||
buildCommonAgentCompanyEnv,
|
||||
resolveSessionResumeState,
|
||||
};
|
||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
export const SKILLS_CANDIDATES = [
|
||||
path.resolve(__moduleDir, "../../skills"),
|
||||
path.resolve(__moduleDir, "../../../../../skills"),
|
||||
];
|
||||
|
||||
export const SKILLS_CANDIDATES = buildSkillsCandidates(import.meta.url);
|
||||
export function hasNonEmptyEnvValue(env: Record<string, string>, key: string): boolean {
|
||||
const raw = env[key];
|
||||
return typeof raw === "string" && raw.trim().length > 0;
|
||||
}
|
||||
|
||||
export function resolveGeminiBillingType(env: Record<string, string>): "api" | "subscription" {
|
||||
return hasNonEmptyEnvValue(env, "GEMINI_API_KEY") || hasNonEmptyEnvValue(env, "GOOGLE_API_KEY")
|
||||
@@ -61,7 +54,11 @@ export function renderApiAccessNote(env: Record<string, string>): string {
|
||||
}
|
||||
|
||||
export async function resolveAgentCompanySkillsDir(): Promise<string | null> {
|
||||
return resolveSkillsDir(SKILLS_CANDIDATES);
|
||||
for (const candidate of SKILLS_CANDIDATES) {
|
||||
const isDir = await fs.stat(candidate).then((s) => s.isDirectory()).catch(() => false);
|
||||
if (isDir) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function geminiSkillsHome(): string {
|
||||
@@ -78,12 +75,13 @@ export async function ensureGeminiSkillsInjected(
|
||||
): Promise<void> {
|
||||
const skillsHome = geminiSkillsHome();
|
||||
|
||||
// Use managed skills when available; fall back to static repo scan
|
||||
if (managedSkills.length > 0) {
|
||||
await createManagedSkillSymlinks(managedSkills, skillsHome, onLog);
|
||||
return;
|
||||
}
|
||||
|
||||
const skillsDir = await resolveSkillsDir(SKILLS_CANDIDATES);
|
||||
const skillsDir = await resolveAgentCompanySkillsDir();
|
||||
if (!skillsDir) return;
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { AdapterExecutionContext, AdapterExecutionResult } from "@agentcompany/adapter-utils";
|
||||
import type { AdapterExecutionContext, AdapterExecutionResult, ManagedSkillRef } from "@agentcompany/adapter-utils";
|
||||
import {
|
||||
asBoolean,
|
||||
asNumber,
|
||||
asString,
|
||||
asStringArray,
|
||||
buildAgentCompanyEnv,
|
||||
ensureAbsoluteDirectory,
|
||||
ensureCommandResolvable,
|
||||
ensurePathInEnv,
|
||||
parseObject,
|
||||
filterConflictingArgs,
|
||||
redactEnvForLogs,
|
||||
renderTemplate,
|
||||
@@ -28,10 +31,6 @@ import {
|
||||
renderEnvNote,
|
||||
renderApiAccessNote,
|
||||
ensureGeminiSkillsInjected,
|
||||
parseManagedSkills,
|
||||
parseExtraArgs,
|
||||
buildCommonAgentCompanyEnv,
|
||||
resolveSessionResumeState,
|
||||
} from "./execute-helpers.js";
|
||||
|
||||
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
|
||||
@@ -45,25 +44,95 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
const model = asString(config.model, DEFAULT_GEMINI_LOCAL_MODEL).trim();
|
||||
const sandbox = asBoolean(config.sandbox, false);
|
||||
|
||||
const { env, cwd, workspaceId, workspaceRepoUrl, workspaceRepoRef } =
|
||||
await buildCommonAgentCompanyEnv(
|
||||
{ runId, agent, config, context, authToken },
|
||||
{ ensureAbsoluteDirectory },
|
||||
);
|
||||
|
||||
const managedSkills = parseManagedSkills(config);
|
||||
const workspaceContext = parseObject(context.agentCompanyWorkspace);
|
||||
const workspaceCwd = asString(workspaceContext.cwd, "");
|
||||
const workspaceSource = asString(workspaceContext.source, "");
|
||||
const workspaceId = asString(workspaceContext.workspaceId, "");
|
||||
const workspaceRepoUrl = asString(workspaceContext.repoUrl, "");
|
||||
const workspaceRepoRef = asString(workspaceContext.repoRef, "");
|
||||
const workspaceHints = Array.isArray(context.agentCompanyWorkspaces)
|
||||
? context.agentCompanyWorkspaces.filter(
|
||||
(value): value is Record<string, unknown> => typeof value === "object" && value !== null,
|
||||
)
|
||||
: [];
|
||||
const configuredCwd = asString(config.cwd, "");
|
||||
const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
|
||||
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
|
||||
const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
|
||||
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||
const managedSkills: ManagedSkillRef[] = Array.isArray(config.managedSkills)
|
||||
? (config.managedSkills as ManagedSkillRef[]).filter(
|
||||
(s) => typeof s.runtimeName === "string" && typeof s.sourcePath === "string",
|
||||
)
|
||||
: [];
|
||||
await ensureGeminiSkillsInjected(managedSkills, onLog);
|
||||
|
||||
const envConfig = parseObject(config.env);
|
||||
const hasExplicitApiKey =
|
||||
typeof envConfig.AGENT_COMPANY_API_KEY === "string" && envConfig.AGENT_COMPANY_API_KEY.trim().length > 0;
|
||||
const env: Record<string, string> = { ...buildAgentCompanyEnv(agent) };
|
||||
env.AGENT_COMPANY_RUN_ID = runId;
|
||||
const wakeTaskId =
|
||||
(typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) ||
|
||||
(typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) ||
|
||||
null;
|
||||
const wakeReason =
|
||||
typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0
|
||||
? context.wakeReason.trim()
|
||||
: null;
|
||||
const wakeCommentId =
|
||||
(typeof context.wakeCommentId === "string" && context.wakeCommentId.trim().length > 0 && context.wakeCommentId.trim()) ||
|
||||
(typeof context.commentId === "string" && context.commentId.trim().length > 0 && context.commentId.trim()) ||
|
||||
null;
|
||||
const approvalId =
|
||||
typeof context.approvalId === "string" && context.approvalId.trim().length > 0
|
||||
? context.approvalId.trim()
|
||||
: null;
|
||||
const approvalStatus =
|
||||
typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0
|
||||
? context.approvalStatus.trim()
|
||||
: null;
|
||||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
if (wakeTaskId) env.AGENT_COMPANY_TASK_ID = wakeTaskId;
|
||||
if (wakeReason) env.AGENT_COMPANY_WAKE_REASON = wakeReason;
|
||||
if (wakeCommentId) env.AGENT_COMPANY_WAKE_COMMENT_ID = wakeCommentId;
|
||||
if (approvalId) env.AGENT_COMPANY_APPROVAL_ID = approvalId;
|
||||
if (approvalStatus) env.AGENT_COMPANY_APPROVAL_STATUS = approvalStatus;
|
||||
if (linkedIssueIds.length > 0) env.AGENT_COMPANY_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
if (effectiveWorkspaceCwd) env.AGENT_COMPANY_WORKSPACE_CWD = effectiveWorkspaceCwd;
|
||||
if (workspaceSource) env.AGENT_COMPANY_WORKSPACE_SOURCE = workspaceSource;
|
||||
if (workspaceId) env.AGENT_COMPANY_WORKSPACE_ID = workspaceId;
|
||||
if (workspaceRepoUrl) env.AGENT_COMPANY_WORKSPACE_REPO_URL = workspaceRepoUrl;
|
||||
if (workspaceRepoRef) env.AGENT_COMPANY_WORKSPACE_REPO_REF = workspaceRepoRef;
|
||||
if (workspaceHints.length > 0) env.AGENT_COMPANY_WORKSPACES_JSON = JSON.stringify(workspaceHints);
|
||||
|
||||
for (const [key, value] of Object.entries(envConfig)) {
|
||||
if (typeof value === "string") env[key] = value;
|
||||
}
|
||||
if (!hasExplicitApiKey && authToken) {
|
||||
env.AGENT_COMPANY_API_KEY = authToken;
|
||||
}
|
||||
const billingType = resolveGeminiBillingType(env);
|
||||
const runtimeEnv = ensurePathInEnv(env);
|
||||
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
||||
|
||||
const timeoutSec = asNumber(config.timeoutSec, 0);
|
||||
const graceSec = asNumber(config.graceSec, 20);
|
||||
const extraArgs = parseExtraArgs(config);
|
||||
const extraArgs = (() => {
|
||||
const fromExtraArgs = asStringArray(config.extraArgs);
|
||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||
return asStringArray(config.args);
|
||||
})();
|
||||
|
||||
const { sessionId, runtimeSessionId, runtimeSessionCwd, canResumeSession } =
|
||||
resolveSessionResumeState(runtime, cwd);
|
||||
const runtimeSessionParams = parseObject(runtime.sessionParams);
|
||||
const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
|
||||
const runtimeSessionCwd = asString(runtimeSessionParams.cwd, "");
|
||||
const canResumeSession =
|
||||
runtimeSessionId.length > 0 &&
|
||||
(runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(cwd));
|
||||
const sessionId = canResumeSession ? runtimeSessionId : null;
|
||||
if (runtimeSessionId && !canResumeSession) {
|
||||
await onLog(
|
||||
"stderr",
|
||||
@@ -106,6 +175,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
const apiAccessNote = renderApiAccessNote(env);
|
||||
const prompt = `${instructionsPrefix}${envNote}${apiAccessNote}${renderedPrompt}`;
|
||||
|
||||
// When the prompt is short enough for a command-line argument, pass it
|
||||
// directly as a positional arg (Gemini CLI's native interface). When it
|
||||
// exceeds the OS limit (~32k chars on Windows), write it to a temp file
|
||||
// and pass the content via stdin to avoid ENAMETOOLONG.
|
||||
const SAFE_ARG_LIMIT = 30_000;
|
||||
const useStdin = prompt.length > SAFE_ARG_LIMIT;
|
||||
const commandNotes = (() => {
|
||||
@@ -222,6 +295,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
|
||||
const clearSessionForTurnLimit = isGeminiTurnLimitResult(attempt.parsed.resultEvent, attempt.proc.exitCode);
|
||||
|
||||
// On retry, don't fall back to old session ID — the old session was stale
|
||||
const canFallbackToRuntimeSession = !isRetry;
|
||||
const resolvedSessionId = attempt.parsed.sessionId
|
||||
?? (canFallbackToRuntimeSession ? (runtimeSessionId ?? runtime.sessionId ?? null) : null);
|
||||
@@ -287,6 +361,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
|
||||
return toResult(initial);
|
||||
} finally {
|
||||
// Clean up temp prompt file if used
|
||||
if (promptFilePath) {
|
||||
await fs.unlink(promptFilePath).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@agentcompany/adapter-kimi-local",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./server": "./src/server/index.ts",
|
||||
"./ui": "./src/ui/index.ts",
|
||||
"./cli": "./src/cli/index.ts"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./server": {
|
||||
"types": "./dist/server/index.d.ts",
|
||||
"import": "./dist/server/index.js"
|
||||
},
|
||||
"./ui": {
|
||||
"types": "./dist/ui/index.d.ts",
|
||||
"import": "./dist/ui/index.js"
|
||||
},
|
||||
"./cli": {
|
||||
"types": "./dist/cli/index.d.ts",
|
||||
"import": "./dist/cli/index.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentcompany/adapter-utils": "workspace:*",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.6.0",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import pc from "picocolors";
|
||||
|
||||
export function printKimiStreamEvent(line: string, debug = false) {
|
||||
if (debug) {
|
||||
console.log(pc.dim(`[kimi] ${line}`));
|
||||
return;
|
||||
}
|
||||
console.log(line);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { printKimiStreamEvent } from "./format-event.js";
|
||||
@@ -0,0 +1,33 @@
|
||||
export const type = "kimi_local";
|
||||
export const label = "Kimi Code (local)";
|
||||
export const DEFAULT_KIMI_LOCAL_MODEL = "kimi-for-coding";
|
||||
|
||||
export const models = [
|
||||
{ id: DEFAULT_KIMI_LOCAL_MODEL, label: DEFAULT_KIMI_LOCAL_MODEL },
|
||||
];
|
||||
|
||||
export const agentConfigurationDoc = `# kimi_local agent configuration
|
||||
|
||||
Adapter: kimi_local
|
||||
|
||||
Use when:
|
||||
- You want AgentCompany to invoke the official Kimi Code CLI locally
|
||||
- You accept generic CLI execution without session resume or structured transcript guarantees
|
||||
|
||||
Core fields:
|
||||
- cwd (string, optional): absolute working directory fallback for the agent process
|
||||
- promptTemplate (string, optional): prompt template rendered by AgentCompany before execution
|
||||
- model (string, optional): informational only, defaults to kimi-for-coding
|
||||
- command (string, optional): defaults to "kimi"
|
||||
- extraArgs (string[], optional): additional CLI args
|
||||
- env (object, optional): KEY=VALUE environment variables
|
||||
|
||||
Operational fields:
|
||||
- timeoutSec (number, optional): run timeout in seconds
|
||||
- graceSec (number, optional): graceful termination period in seconds
|
||||
|
||||
Notes:
|
||||
- Kimi integration currently runs in generic CLI mode.
|
||||
- Session resume, model discovery, graceful cancel handshakes, structured transcript, and cost attribution are not guaranteed.
|
||||
- AgentCompany must surface these capability downgrades explicitly in the desktop UI.
|
||||
`;
|
||||
@@ -0,0 +1,86 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { AdapterExecutionContext, AdapterExecutionResult, ManagedSkillRef } from "@agentcompany/adapter-utils";
|
||||
import {
|
||||
asNumber,
|
||||
asString,
|
||||
asStringArray,
|
||||
ensureAbsoluteDirectory,
|
||||
ensureCommandResolvable,
|
||||
ensurePathInEnv,
|
||||
runChildProcess,
|
||||
createManagedSkillSymlinks,
|
||||
} from "@agentcompany/adapter-utils/server-utils";
|
||||
import { normalizeEnv } from "@agentcompany/adapter-utils/test-utils";
|
||||
import { DEFAULT_KIMI_LOCAL_MODEL } from "../index.js";
|
||||
|
||||
function kimiSkillsHome(): string {
|
||||
return path.join(os.homedir(), ".kimi", "skills");
|
||||
}
|
||||
|
||||
async function ensureKimiSkillsInjected(
|
||||
managedSkills: ManagedSkillRef[],
|
||||
onLog: AdapterExecutionContext["onLog"],
|
||||
): Promise<void> {
|
||||
if (managedSkills.length === 0) return;
|
||||
await createManagedSkillSymlinks(managedSkills, kimiSkillsHome(), onLog);
|
||||
}
|
||||
|
||||
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
|
||||
const { runId, config, context, onLog } = ctx;
|
||||
const command = asString(config.command, "kimi");
|
||||
const cwd = asString(config.cwd, process.cwd());
|
||||
const model = asString(config.model, DEFAULT_KIMI_LOCAL_MODEL);
|
||||
const timeoutSec = asNumber(config.timeoutSec, 0);
|
||||
const graceSec = asNumber(config.graceSec, 20);
|
||||
const extraArgs = asStringArray(config.extraArgs);
|
||||
|
||||
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||
const managedSkills: ManagedSkillRef[] = Array.isArray(config.managedSkills)
|
||||
? (config.managedSkills as ManagedSkillRef[]).filter(
|
||||
(s) => typeof s.runtimeName === "string" && typeof s.sourcePath === "string",
|
||||
)
|
||||
: [];
|
||||
await ensureKimiSkillsInjected(managedSkills, onLog);
|
||||
|
||||
const prompt = typeof context.taskTitle === "string" && context.taskTitle.length > 0
|
||||
? [
|
||||
context.taskTitle,
|
||||
typeof context.taskDescription === "string" && context.taskDescription.length > 0
|
||||
? context.taskDescription
|
||||
: "",
|
||||
].filter(Boolean).join("\n\n")
|
||||
: [
|
||||
`You are agent ${ctx.agent.name}.`,
|
||||
"Respond with a concise execution summary when complete.",
|
||||
].join("\n\n");
|
||||
|
||||
const env = normalizeEnv(ensurePathInEnv(normalizeEnv(config.env)));
|
||||
await ensureCommandResolvable(command, cwd, env);
|
||||
|
||||
const result = await runChildProcess(runId, command, extraArgs, {
|
||||
cwd,
|
||||
env,
|
||||
timeoutSec,
|
||||
graceSec,
|
||||
stdin: prompt,
|
||||
onLog,
|
||||
});
|
||||
|
||||
const summary =
|
||||
result.stdout
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.slice(-8)
|
||||
.join("\n") || null;
|
||||
|
||||
return {
|
||||
exitCode: result.exitCode,
|
||||
signal: result.signal,
|
||||
timedOut: result.timedOut,
|
||||
summary,
|
||||
model,
|
||||
billingType: "unknown",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { execute } from "./execute.js";
|
||||
export { testEnvironment } from "./test.js";
|
||||
@@ -0,0 +1,48 @@
|
||||
import type {
|
||||
AdapterEnvironmentTestContext,
|
||||
AdapterEnvironmentTestResult,
|
||||
} from "@agentcompany/adapter-utils";
|
||||
import {
|
||||
asString,
|
||||
ensureCommandResolvable,
|
||||
ensurePathInEnv,
|
||||
} from "@agentcompany/adapter-utils/server-utils";
|
||||
import { normalizeEnv } from "@agentcompany/adapter-utils/test-utils";
|
||||
|
||||
export async function testEnvironment(
|
||||
ctx: AdapterEnvironmentTestContext,
|
||||
): Promise<AdapterEnvironmentTestResult> {
|
||||
const command = asString(ctx.config.command, "kimi");
|
||||
const cwd = asString(ctx.config.cwd, process.cwd());
|
||||
const env = normalizeEnv(ensurePathInEnv(normalizeEnv(ctx.config.env)));
|
||||
|
||||
try {
|
||||
await ensureCommandResolvable(command, cwd, env);
|
||||
return {
|
||||
adapterType: "kimi_local",
|
||||
status: "pass",
|
||||
checks: [
|
||||
{
|
||||
code: "kimi_command_detected",
|
||||
level: "info",
|
||||
message: `Detected Kimi Code command "${command}".`,
|
||||
},
|
||||
],
|
||||
testedAt: new Date().toISOString(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
adapterType: "kimi_local",
|
||||
status: "fail",
|
||||
checks: [
|
||||
{
|
||||
code: "kimi_command_missing",
|
||||
level: "error",
|
||||
message: error instanceof Error ? error.message : "Kimi command is not available.",
|
||||
hint: "Install Kimi Code with `curl -L code.kimi.com/install.sh | bash` and ensure the CLI is available on PATH.",
|
||||
},
|
||||
],
|
||||
testedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { CreateConfigValues } from "@agentcompany/adapter-utils";
|
||||
import { parseCommaArgs, parseEnvVars, parseEnvBindings } from "@agentcompany/adapter-utils/config-utils";
|
||||
import { DEFAULT_KIMI_LOCAL_MODEL } from "../index.js";
|
||||
|
||||
export function buildKimiLocalConfig(v: CreateConfigValues): Record<string, unknown> {
|
||||
const config: Record<string, unknown> = {};
|
||||
if (v.cwd) config.cwd = v.cwd;
|
||||
if (v.promptTemplate) config.promptTemplate = v.promptTemplate;
|
||||
config.model = v.model || DEFAULT_KIMI_LOCAL_MODEL;
|
||||
config.timeoutSec = 0;
|
||||
config.graceSec = 20;
|
||||
const env = parseEnvBindings(v.envBindings);
|
||||
const legacy = parseEnvVars(v.envVars);
|
||||
for (const [key, value] of Object.entries(legacy)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(env, key)) {
|
||||
env[key] = { type: "plain", value };
|
||||
}
|
||||
}
|
||||
if (Object.keys(env).length > 0) config.env = env;
|
||||
if (v.command) config.command = v.command;
|
||||
if (v.extraArgs) config.extraArgs = parseCommaArgs(v.extraArgs);
|
||||
return config;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { buildKimiLocalConfig } from "./build-config.js";
|
||||
export { parseKimiStdoutLine } from "./parse-stdout.js";
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { TranscriptEntry } from "@agentcompany/adapter-utils";
|
||||
|
||||
export function parseKimiStdoutLine(line: string, ts: string): TranscriptEntry[] {
|
||||
const text = line.trim();
|
||||
if (!text) return [];
|
||||
return [{ kind: "stdout", ts, text: line }];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseKimiStdoutLine } from "../src/ui/parse-stdout.js";
|
||||
|
||||
describe("parseKimiStdoutLine", () => {
|
||||
it("returns a stdout entry for non-empty lines", () => {
|
||||
const entries = parseKimiStdoutLine("hello", "2026-03-17T00:00:00.000Z");
|
||||
expect(entries).toEqual([
|
||||
{ kind: "stdout", ts: "2026-03-17T00:00:00.000Z", text: "hello" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("drops blank lines", () => {
|
||||
expect(parseKimiStdoutLine(" ", "2026-03-17T00:00:00.000Z")).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@agentcompany/adapter-opencode-local",
|
||||
"version": "0.3.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./server": "./src/server/index.ts",
|
||||
"./ui": "./src/ui/index.ts",
|
||||
"./cli": "./src/cli/index.ts"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./server": {
|
||||
"types": "./dist/server/index.d.ts",
|
||||
"import": "./dist/server/index.js"
|
||||
},
|
||||
"./ui": {
|
||||
"types": "./dist/ui/index.d.ts",
|
||||
"import": "./dist/ui/index.js"
|
||||
},
|
||||
"./cli": {
|
||||
"types": "./dist/cli/index.d.ts",
|
||||
"import": "./dist/cli/index.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"skills"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentcompany/adapter-utils": "workspace:*",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.6.0",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import pc from "picocolors";
|
||||
import { asRecord, safeJsonParse, asString, asNumber, errorText } from "@agentcompany/adapter-utils/parse-utils";
|
||||
|
||||
export function printOpenCodeStreamEvent(raw: string, _debug: boolean): void {
|
||||
const line = raw.trim();
|
||||
if (!line) return;
|
||||
|
||||
const parsed = asRecord(safeJsonParse(line));
|
||||
if (!parsed) {
|
||||
console.log(line);
|
||||
return;
|
||||
}
|
||||
|
||||
const type = asString(parsed.type);
|
||||
|
||||
if (type === "step_start") {
|
||||
const sessionId = asString(parsed.sessionID);
|
||||
console.log(pc.blue(`step started${sessionId ? ` (session: ${sessionId})` : ""}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "text") {
|
||||
const part = asRecord(parsed.part);
|
||||
const text = asString(part?.text).trim();
|
||||
if (text) console.log(pc.green(`assistant: ${text}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "reasoning") {
|
||||
const part = asRecord(parsed.part);
|
||||
const text = asString(part?.text).trim();
|
||||
if (text) console.log(pc.gray(`thinking: ${text}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "tool_use") {
|
||||
const part = asRecord(parsed.part);
|
||||
const tool = asString(part?.tool, "tool");
|
||||
const callID = asString(part?.callID);
|
||||
const state = asRecord(part?.state);
|
||||
const status = asString(state?.status);
|
||||
const isError = status === "error";
|
||||
const metadata = asRecord(state?.metadata);
|
||||
|
||||
console.log(pc.yellow(`tool_call: ${tool}${callID ? ` (${callID})` : ""}`));
|
||||
|
||||
if (status) {
|
||||
const metaParts = [`status=${status}`];
|
||||
if (metadata) {
|
||||
for (const [key, value] of Object.entries(metadata)) {
|
||||
if (value !== undefined && value !== null) metaParts.push(`${key}=${value}`);
|
||||
}
|
||||
}
|
||||
console.log((isError ? pc.red : pc.gray)(`tool_result ${metaParts.join(" ")}`));
|
||||
}
|
||||
|
||||
const output = (asString(state?.output) || asString(state?.error)).trim();
|
||||
if (output) console.log((isError ? pc.red : pc.gray)(output));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "step_finish") {
|
||||
const part = asRecord(parsed.part);
|
||||
const tokens = asRecord(part?.tokens);
|
||||
const cache = asRecord(tokens?.cache);
|
||||
const input = asNumber(tokens?.input, 0);
|
||||
const output = asNumber(tokens?.output, 0) + asNumber(tokens?.reasoning, 0);
|
||||
const cached = asNumber(cache?.read, 0);
|
||||
const cost = asNumber(part?.cost, 0);
|
||||
const reason = asString(part?.reason, "step");
|
||||
console.log(pc.blue(`step finished: reason=${reason}`));
|
||||
console.log(pc.blue(`tokens: in=${input} out=${output} cached=${cached} cost=$${cost.toFixed(6)}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "error") {
|
||||
const message = errorText(parsed.error ?? parsed.message);
|
||||
if (message) console.log(pc.red(`error: ${message}`));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(line);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { printOpenCodeStreamEvent } from "./format-event.js";
|
||||
@@ -0,0 +1,40 @@
|
||||
export const type = "opencode_local";
|
||||
export const label = "OpenCode (local)";
|
||||
|
||||
export const models: Array<{ id: string; label: string }> = [];
|
||||
|
||||
export const agentConfigurationDoc = `# opencode_local agent configuration
|
||||
|
||||
Adapter: opencode_local
|
||||
|
||||
Use when:
|
||||
- You want AgentCompany to run OpenCode locally as the agent runtime
|
||||
- You want provider/model routing in OpenCode format (provider/model)
|
||||
- You want OpenCode session resume across heartbeats via --session
|
||||
|
||||
Don't use when:
|
||||
- You need webhook-style external invocation (use openclaw_gateway or http)
|
||||
- You only need one-shot shell commands (use process)
|
||||
- OpenCode CLI is not installed on the machine
|
||||
|
||||
Core fields:
|
||||
- cwd (string, optional): default absolute working directory fallback for the agent process (created if missing when possible)
|
||||
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to the run prompt
|
||||
- model (string, required): OpenCode model id in provider/model format (for example anthropic/claude-sonnet-4-5)
|
||||
- variant (string, optional): provider-specific model variant (for example minimal|low|medium|high|max)
|
||||
- promptTemplate (string, optional): run prompt template
|
||||
- command (string, optional): defaults to "opencode"
|
||||
- extraArgs (string[], optional): additional CLI args
|
||||
- env (object, optional): KEY=VALUE environment variables
|
||||
|
||||
Operational fields:
|
||||
- timeoutSec (number, optional): run timeout in seconds
|
||||
- graceSec (number, optional): SIGTERM grace period in seconds
|
||||
|
||||
Notes:
|
||||
- OpenCode supports multiple providers and models. Use \
|
||||
\`opencode models\` to list available options in provider/model format.
|
||||
- AgentCompany requires an explicit \`model\` value for \`opencode_local\` agents.
|
||||
- Runs are executed with: opencode run --format json ...
|
||||
- Sessions are resumed with --session when stored session cwd matches current cwd.
|
||||
`;
|
||||
@@ -0,0 +1,378 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { AdapterExecutionContext, AdapterExecutionResult, ManagedSkillRef } from "@agentcompany/adapter-utils";
|
||||
import {
|
||||
asString,
|
||||
asNumber,
|
||||
asStringArray,
|
||||
parseObject,
|
||||
buildAgentCompanyEnv,
|
||||
redactEnvForLogs,
|
||||
ensureAbsoluteDirectory,
|
||||
ensureCommandResolvable,
|
||||
ensurePathInEnv,
|
||||
renderTemplate,
|
||||
runChildProcess,
|
||||
createManagedSkillSymlinks,
|
||||
} from "@agentcompany/adapter-utils/server-utils";
|
||||
import { firstNonEmptyLine } from "@agentcompany/adapter-utils/test-utils";
|
||||
import { isOpenCodeUnknownSessionError, parseOpenCodeJsonl } from "./parse.js";
|
||||
import { ensureOpenCodeModelConfiguredAndAvailable } from "./models.js";
|
||||
|
||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SKILLS_CANDIDATES = [
|
||||
path.resolve(__moduleDir, "../../skills"),
|
||||
path.resolve(__moduleDir, "../../../../../skills"),
|
||||
];
|
||||
|
||||
function parseModelProvider(model: string | null): string | null {
|
||||
if (!model) return null;
|
||||
const trimmed = model.trim();
|
||||
if (!trimmed.includes("/")) return null;
|
||||
return trimmed.slice(0, trimmed.indexOf("/")).trim() || null;
|
||||
}
|
||||
|
||||
function openCodeSkillsHome(): string {
|
||||
return path.join(os.homedir(), ".opencode", "skills");
|
||||
}
|
||||
|
||||
async function resolveAgentCompanySkillsDir(): Promise<string | null> {
|
||||
for (const candidate of SKILLS_CANDIDATES) {
|
||||
const isDir = await fs.stat(candidate).then((s) => s.isDirectory()).catch(() => false);
|
||||
if (isDir) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function ensureOpenCodeSkillsInjected(
|
||||
managedSkills: ManagedSkillRef[],
|
||||
onLog: AdapterExecutionContext["onLog"],
|
||||
) {
|
||||
const skillsHome = openCodeSkillsHome();
|
||||
|
||||
// Use managed skills when available; fall back to static repo scan
|
||||
if (managedSkills.length > 0) {
|
||||
await createManagedSkillSymlinks(managedSkills, skillsHome, onLog);
|
||||
return;
|
||||
}
|
||||
|
||||
const skillsDir = await resolveAgentCompanySkillsDir();
|
||||
if (!skillsDir) return;
|
||||
|
||||
await fs.mkdir(skillsHome, { recursive: true });
|
||||
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const source = path.join(skillsDir, entry.name);
|
||||
const target = path.join(skillsHome, entry.name);
|
||||
const existing = await fs.lstat(target).catch(() => null);
|
||||
if (existing) continue;
|
||||
|
||||
try {
|
||||
await fs.symlink(source, target);
|
||||
await onLog(
|
||||
"stderr",
|
||||
`[agent-company] Injected OpenCode skill "${entry.name}" into ${skillsHome}\n`,
|
||||
);
|
||||
} catch (err) {
|
||||
await onLog(
|
||||
"stderr",
|
||||
`[agent-company] Failed to inject OpenCode skill "${entry.name}" into ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
|
||||
const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx;
|
||||
|
||||
const promptTemplate = asString(
|
||||
config.promptTemplate,
|
||||
"{{context.taskTitle}}\n\n{{context.taskDescription}}",
|
||||
);
|
||||
const command = asString(config.command, "opencode");
|
||||
const model = asString(config.model, "").trim();
|
||||
const variant = asString(config.variant, "").trim();
|
||||
|
||||
const workspaceContext = parseObject(context.agentCompanyWorkspace);
|
||||
const workspaceCwd = asString(workspaceContext.cwd, "");
|
||||
const workspaceSource = asString(workspaceContext.source, "");
|
||||
const workspaceId = asString(workspaceContext.workspaceId, "");
|
||||
const workspaceRepoUrl = asString(workspaceContext.repoUrl, "");
|
||||
const workspaceRepoRef = asString(workspaceContext.repoRef, "");
|
||||
const workspaceHints = Array.isArray(context.agentCompanyWorkspaces)
|
||||
? context.agentCompanyWorkspaces.filter(
|
||||
(value): value is Record<string, unknown> => typeof value === "object" && value !== null,
|
||||
)
|
||||
: [];
|
||||
const configuredCwd = asString(config.cwd, "");
|
||||
const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
|
||||
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
|
||||
const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
|
||||
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||
const managedSkills: ManagedSkillRef[] = Array.isArray(config.managedSkills)
|
||||
? (config.managedSkills as ManagedSkillRef[]).filter(
|
||||
(s) => typeof s.runtimeName === "string" && typeof s.sourcePath === "string",
|
||||
)
|
||||
: [];
|
||||
await ensureOpenCodeSkillsInjected(managedSkills, onLog);
|
||||
|
||||
const envConfig = parseObject(config.env);
|
||||
const hasExplicitApiKey =
|
||||
typeof envConfig.AGENT_COMPANY_API_KEY === "string" && envConfig.AGENT_COMPANY_API_KEY.trim().length > 0;
|
||||
const env: Record<string, string> = { ...buildAgentCompanyEnv(agent) };
|
||||
env.AGENT_COMPANY_RUN_ID = runId;
|
||||
const wakeTaskId =
|
||||
(typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) ||
|
||||
(typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) ||
|
||||
null;
|
||||
const wakeReason =
|
||||
typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0
|
||||
? context.wakeReason.trim()
|
||||
: null;
|
||||
const wakeCommentId =
|
||||
(typeof context.wakeCommentId === "string" && context.wakeCommentId.trim().length > 0 && context.wakeCommentId.trim()) ||
|
||||
(typeof context.commentId === "string" && context.commentId.trim().length > 0 && context.commentId.trim()) ||
|
||||
null;
|
||||
const approvalId =
|
||||
typeof context.approvalId === "string" && context.approvalId.trim().length > 0
|
||||
? context.approvalId.trim()
|
||||
: null;
|
||||
const approvalStatus =
|
||||
typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0
|
||||
? context.approvalStatus.trim()
|
||||
: null;
|
||||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
if (wakeTaskId) env.AGENT_COMPANY_TASK_ID = wakeTaskId;
|
||||
if (wakeReason) env.AGENT_COMPANY_WAKE_REASON = wakeReason;
|
||||
if (wakeCommentId) env.AGENT_COMPANY_WAKE_COMMENT_ID = wakeCommentId;
|
||||
if (approvalId) env.AGENT_COMPANY_APPROVAL_ID = approvalId;
|
||||
if (approvalStatus) env.AGENT_COMPANY_APPROVAL_STATUS = approvalStatus;
|
||||
if (linkedIssueIds.length > 0) env.AGENT_COMPANY_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
if (effectiveWorkspaceCwd) env.AGENT_COMPANY_WORKSPACE_CWD = effectiveWorkspaceCwd;
|
||||
if (workspaceSource) env.AGENT_COMPANY_WORKSPACE_SOURCE = workspaceSource;
|
||||
if (workspaceId) env.AGENT_COMPANY_WORKSPACE_ID = workspaceId;
|
||||
if (workspaceRepoUrl) env.AGENT_COMPANY_WORKSPACE_REPO_URL = workspaceRepoUrl;
|
||||
if (workspaceRepoRef) env.AGENT_COMPANY_WORKSPACE_REPO_REF = workspaceRepoRef;
|
||||
if (workspaceHints.length > 0) env.AGENT_COMPANY_WORKSPACES_JSON = JSON.stringify(workspaceHints);
|
||||
|
||||
for (const [key, value] of Object.entries(envConfig)) {
|
||||
if (typeof value === "string") env[key] = value;
|
||||
}
|
||||
if (!hasExplicitApiKey && authToken) {
|
||||
env.AGENT_COMPANY_API_KEY = authToken;
|
||||
}
|
||||
const runtimeEnv = Object.fromEntries(
|
||||
Object.entries(ensurePathInEnv(env)).filter(
|
||||
(entry): entry is [string, string] => typeof entry[1] === "string",
|
||||
),
|
||||
);
|
||||
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
||||
|
||||
await ensureOpenCodeModelConfiguredAndAvailable({
|
||||
model,
|
||||
command,
|
||||
cwd,
|
||||
env: runtimeEnv,
|
||||
});
|
||||
|
||||
const timeoutSec = asNumber(config.timeoutSec, 0);
|
||||
const graceSec = asNumber(config.graceSec, 20);
|
||||
const extraArgs = (() => {
|
||||
const fromExtraArgs = asStringArray(config.extraArgs);
|
||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||
return asStringArray(config.args);
|
||||
})();
|
||||
|
||||
const runtimeSessionParams = parseObject(runtime.sessionParams);
|
||||
const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
|
||||
const runtimeSessionCwd = asString(runtimeSessionParams.cwd, "");
|
||||
const canResumeSession =
|
||||
runtimeSessionId.length > 0 &&
|
||||
(runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(cwd));
|
||||
const sessionId = canResumeSession ? runtimeSessionId : null;
|
||||
if (runtimeSessionId && !canResumeSession) {
|
||||
await onLog(
|
||||
"stderr",
|
||||
`[agent-company] OpenCode session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`,
|
||||
);
|
||||
}
|
||||
|
||||
const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
|
||||
const resolvedInstructionsFilePath = instructionsFilePath
|
||||
? path.resolve(cwd, instructionsFilePath)
|
||||
: "";
|
||||
const instructionsDir = resolvedInstructionsFilePath ? `${path.dirname(resolvedInstructionsFilePath)}/` : "";
|
||||
let instructionsPrefix = "";
|
||||
if (resolvedInstructionsFilePath) {
|
||||
try {
|
||||
const instructionsContents = await fs.readFile(resolvedInstructionsFilePath, "utf8");
|
||||
instructionsPrefix =
|
||||
`${instructionsContents}\n\n` +
|
||||
`The above agent instructions were loaded from ${resolvedInstructionsFilePath}. ` +
|
||||
`Resolve any relative file references from ${instructionsDir}.\n\n`;
|
||||
await onLog(
|
||||
"stderr",
|
||||
`[agent-company] Loaded agent instructions file: ${resolvedInstructionsFilePath}\n`,
|
||||
);
|
||||
} catch (err) {
|
||||
const reason = err instanceof Error ? err.message : String(err);
|
||||
await onLog(
|
||||
"stderr",
|
||||
`[agent-company] Warning: could not read agent instructions file "${resolvedInstructionsFilePath}": ${reason}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const commandNotes = (() => {
|
||||
if (!resolvedInstructionsFilePath) return [] as string[];
|
||||
if (instructionsPrefix.length > 0) {
|
||||
return [
|
||||
`Loaded agent instructions from ${resolvedInstructionsFilePath}`,
|
||||
`Prepended instructions + path directive to stdin prompt (relative references from ${instructionsDir}).`,
|
||||
];
|
||||
}
|
||||
return [
|
||||
`Configured instructionsFilePath ${resolvedInstructionsFilePath}, but file could not be read; continuing without injected instructions.`,
|
||||
];
|
||||
})();
|
||||
|
||||
const renderedPrompt = renderTemplate(promptTemplate, {
|
||||
agentId: agent.id,
|
||||
companyId: agent.companyId,
|
||||
runId,
|
||||
company: { id: agent.companyId },
|
||||
agent,
|
||||
run: { id: runId, source: "on_demand" },
|
||||
context,
|
||||
});
|
||||
const prompt = `${instructionsPrefix}${renderedPrompt}`;
|
||||
|
||||
const buildArgs = (resumeSessionId: string | null) => {
|
||||
const args = ["run", "--format", "json"];
|
||||
if (resumeSessionId) args.push("--session", resumeSessionId);
|
||||
if (model) args.push("--model", model);
|
||||
if (variant) args.push("--variant", variant);
|
||||
if (extraArgs.length > 0) args.push(...extraArgs);
|
||||
return args;
|
||||
};
|
||||
|
||||
const runAttempt = async (resumeSessionId: string | null) => {
|
||||
const args = buildArgs(resumeSessionId);
|
||||
if (onMeta) {
|
||||
await onMeta({
|
||||
adapterType: "opencode_local",
|
||||
command,
|
||||
cwd,
|
||||
commandNotes,
|
||||
commandArgs: [...args, `<stdin prompt ${prompt.length} chars>`],
|
||||
env: redactEnvForLogs(env),
|
||||
prompt,
|
||||
context,
|
||||
});
|
||||
}
|
||||
|
||||
const proc = await runChildProcess(runId, command, args, {
|
||||
cwd,
|
||||
env: runtimeEnv,
|
||||
stdin: prompt,
|
||||
timeoutSec,
|
||||
graceSec,
|
||||
onLog,
|
||||
});
|
||||
return {
|
||||
proc,
|
||||
rawStderr: proc.stderr,
|
||||
parsed: parseOpenCodeJsonl(proc.stdout),
|
||||
};
|
||||
};
|
||||
|
||||
const toResult = (
|
||||
attempt: {
|
||||
proc: { exitCode: number | null; signal: string | null; timedOut: boolean; stdout: string; stderr: string };
|
||||
rawStderr: string;
|
||||
parsed: ReturnType<typeof parseOpenCodeJsonl>;
|
||||
},
|
||||
clearSessionOnMissingSession = false,
|
||||
): AdapterExecutionResult => {
|
||||
if (attempt.proc.timedOut) {
|
||||
return {
|
||||
exitCode: attempt.proc.exitCode,
|
||||
signal: attempt.proc.signal,
|
||||
timedOut: true,
|
||||
errorMessage: `Timed out after ${timeoutSec}s`,
|
||||
clearSession: clearSessionOnMissingSession,
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedSessionId =
|
||||
attempt.parsed.sessionId ??
|
||||
(clearSessionOnMissingSession ? null : runtimeSessionId ?? runtime.sessionId ?? null);
|
||||
const resolvedSessionParams = resolvedSessionId
|
||||
? ({
|
||||
sessionId: resolvedSessionId,
|
||||
cwd,
|
||||
...(workspaceId ? { workspaceId } : {}),
|
||||
...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
|
||||
...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
|
||||
} as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
|
||||
const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
|
||||
const rawExitCode = attempt.proc.exitCode;
|
||||
const synthesizedExitCode = parsedError && (rawExitCode ?? 0) === 0 ? 1 : rawExitCode;
|
||||
const fallbackErrorMessage =
|
||||
parsedError ||
|
||||
stderrLine ||
|
||||
`OpenCode exited with code ${synthesizedExitCode ?? -1}`;
|
||||
const modelId = model || null;
|
||||
|
||||
return {
|
||||
exitCode: synthesizedExitCode,
|
||||
signal: attempt.proc.signal,
|
||||
timedOut: false,
|
||||
errorMessage: (synthesizedExitCode ?? 0) === 0 ? null : fallbackErrorMessage,
|
||||
usage: {
|
||||
inputTokens: attempt.parsed.usage.inputTokens,
|
||||
outputTokens: attempt.parsed.usage.outputTokens,
|
||||
cachedInputTokens: attempt.parsed.usage.cachedInputTokens,
|
||||
},
|
||||
sessionId: resolvedSessionId,
|
||||
sessionParams: resolvedSessionParams,
|
||||
sessionDisplayId: resolvedSessionId,
|
||||
provider: parseModelProvider(modelId),
|
||||
model: modelId,
|
||||
billingType: "unknown",
|
||||
costUsd: attempt.parsed.costUsd,
|
||||
resultJson: {
|
||||
stdout: attempt.proc.stdout,
|
||||
stderr: attempt.proc.stderr,
|
||||
},
|
||||
summary: attempt.parsed.summary,
|
||||
clearSession: Boolean(clearSessionOnMissingSession && !attempt.parsed.sessionId),
|
||||
};
|
||||
};
|
||||
|
||||
const initial = await runAttempt(sessionId);
|
||||
const initialFailed =
|
||||
!initial.proc.timedOut && ((initial.proc.exitCode ?? 0) !== 0 || Boolean(initial.parsed.errorMessage));
|
||||
if (
|
||||
sessionId &&
|
||||
initialFailed &&
|
||||
isOpenCodeUnknownSessionError(initial.proc.stdout, initial.rawStderr)
|
||||
) {
|
||||
await onLog(
|
||||
"stderr",
|
||||
`[agent-company] OpenCode session "${sessionId}" is unavailable; retrying with a fresh session.\n`,
|
||||
);
|
||||
const retry = await runAttempt(null);
|
||||
return toResult(retry, true);
|
||||
}
|
||||
|
||||
return toResult(initial);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createStandardSessionCodec } from "@agentcompany/adapter-utils/session-codec";
|
||||
|
||||
export const sessionCodec = createStandardSessionCodec({ extraSessionIdKeys: ["sessionID"] });
|
||||
|
||||
export { execute } from "./execute.js";
|
||||
export { testEnvironment } from "./test.js";
|
||||
export {
|
||||
listOpenCodeModels,
|
||||
discoverOpenCodeModels,
|
||||
ensureOpenCodeModelConfiguredAndAvailable,
|
||||
resetOpenCodeModelsCacheForTests,
|
||||
} from "./models.js";
|
||||
export { parseOpenCodeJsonl, isOpenCodeUnknownSessionError } from "./parse.js";
|
||||
@@ -0,0 +1,33 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
ensureOpenCodeModelConfiguredAndAvailable,
|
||||
listOpenCodeModels,
|
||||
resetOpenCodeModelsCacheForTests,
|
||||
} from "./models.js";
|
||||
|
||||
describe("openCode models", () => {
|
||||
afterEach(() => {
|
||||
delete process.env.AGENT_COMPANY_OPENCODE_COMMAND;
|
||||
resetOpenCodeModelsCacheForTests();
|
||||
});
|
||||
|
||||
it("returns an empty list when discovery command is unavailable", async () => {
|
||||
process.env.AGENT_COMPANY_OPENCODE_COMMAND = "__agentcompany_missing_opencode_command__";
|
||||
await expect(listOpenCodeModels()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects when model is missing", async () => {
|
||||
await expect(
|
||||
ensureOpenCodeModelConfiguredAndAvailable({ model: "" }),
|
||||
).rejects.toThrow("OpenCode requires `adapterConfig.model`");
|
||||
});
|
||||
|
||||
it("rejects when discovery cannot run for configured model", async () => {
|
||||
process.env.AGENT_COMPANY_OPENCODE_COMMAND = "__agentcompany_missing_opencode_command__";
|
||||
await expect(
|
||||
ensureOpenCodeModelConfiguredAndAvailable({
|
||||
model: "openai/gpt-5",
|
||||
}),
|
||||
).rejects.toThrow("Failed to start command");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,187 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { AdapterModel } from "@agentcompany/adapter-utils";
|
||||
import {
|
||||
asString,
|
||||
ensurePathInEnv,
|
||||
runChildProcess,
|
||||
} from "@agentcompany/adapter-utils/server-utils";
|
||||
import { firstNonEmptyLine } from "@agentcompany/adapter-utils/test-utils";
|
||||
|
||||
const MODELS_CACHE_TTL_MS = 60_000;
|
||||
|
||||
function resolveOpenCodeCommand(input: unknown): string {
|
||||
const envOverride =
|
||||
typeof process.env.AGENT_COMPANY_OPENCODE_COMMAND === "string" &&
|
||||
process.env.AGENT_COMPANY_OPENCODE_COMMAND.trim().length > 0
|
||||
? process.env.AGENT_COMPANY_OPENCODE_COMMAND.trim()
|
||||
: "opencode";
|
||||
return asString(input, envOverride);
|
||||
}
|
||||
|
||||
const discoveryCache = new Map<string, { expiresAt: number; models: AdapterModel[] }>();
|
||||
const VOLATILE_ENV_KEY_PREFIXES = ["AGENT_COMPANY_", "npm_", "NPM_"] as const;
|
||||
const VOLATILE_ENV_KEY_EXACT = new Set(["PWD", "OLDPWD", "SHLVL", "_", "TERM_SESSION_ID"]);
|
||||
|
||||
function dedupeModels(models: AdapterModel[]): AdapterModel[] {
|
||||
const seen = new Set<string>();
|
||||
const deduped: AdapterModel[] = [];
|
||||
for (const model of models) {
|
||||
const id = model.id.trim();
|
||||
if (!id || seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
deduped.push({ id, label: model.label.trim() || id });
|
||||
}
|
||||
return deduped;
|
||||
}
|
||||
|
||||
function sortModels(models: AdapterModel[]): AdapterModel[] {
|
||||
return [...models].sort((a, b) =>
|
||||
a.id.localeCompare(b.id, "en", { numeric: true, sensitivity: "base" }),
|
||||
);
|
||||
}
|
||||
|
||||
function parseModelsOutput(stdout: string): AdapterModel[] {
|
||||
const parsed: AdapterModel[] = [];
|
||||
for (const raw of stdout.split(/\r?\n/)) {
|
||||
const line = raw.trim();
|
||||
if (!line) continue;
|
||||
const firstToken = line.split(/\s+/)[0]?.trim() ?? "";
|
||||
if (!firstToken.includes("/")) continue;
|
||||
const provider = firstToken.slice(0, firstToken.indexOf("/")).trim();
|
||||
const model = firstToken.slice(firstToken.indexOf("/") + 1).trim();
|
||||
if (!provider || !model) continue;
|
||||
parsed.push({ id: `${provider}/${model}`, label: `${provider}/${model}` });
|
||||
}
|
||||
return dedupeModels(parsed);
|
||||
}
|
||||
|
||||
function normalizeEnv(input: unknown): Record<string, string> {
|
||||
const envInput = typeof input === "object" && input !== null && !Array.isArray(input)
|
||||
? (input as Record<string, unknown>)
|
||||
: {};
|
||||
const env: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(envInput)) {
|
||||
if (typeof value === "string") env[key] = value;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
function isVolatileEnvKey(key: string): boolean {
|
||||
if (VOLATILE_ENV_KEY_EXACT.has(key)) return true;
|
||||
return VOLATILE_ENV_KEY_PREFIXES.some((prefix) => key.startsWith(prefix));
|
||||
}
|
||||
|
||||
function hashValue(value: string): string {
|
||||
return createHash("sha256").update(value).digest("hex");
|
||||
}
|
||||
|
||||
function discoveryCacheKey(command: string, cwd: string, env: Record<string, string>) {
|
||||
const envKey = Object.entries(env)
|
||||
.filter(([key]) => !isVolatileEnvKey(key))
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([key, value]) => `${key}=${hashValue(value)}`)
|
||||
.join("\n");
|
||||
return `${command}\n${cwd}\n${envKey}`;
|
||||
}
|
||||
|
||||
function pruneExpiredDiscoveryCache(now: number) {
|
||||
for (const [key, value] of discoveryCache.entries()) {
|
||||
if (value.expiresAt <= now) discoveryCache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
export async function discoverOpenCodeModels(input: {
|
||||
command?: unknown;
|
||||
cwd?: unknown;
|
||||
env?: unknown;
|
||||
} = {}): Promise<AdapterModel[]> {
|
||||
const command = resolveOpenCodeCommand(input.command);
|
||||
const cwd = asString(input.cwd, process.cwd());
|
||||
const env = normalizeEnv(input.env);
|
||||
const runtimeEnv = normalizeEnv(ensurePathInEnv(env));
|
||||
|
||||
const result = await runChildProcess(
|
||||
`opencode-models-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||
command,
|
||||
["models"],
|
||||
{
|
||||
cwd,
|
||||
env: runtimeEnv,
|
||||
timeoutSec: 20,
|
||||
graceSec: 3,
|
||||
onLog: async () => {},
|
||||
},
|
||||
);
|
||||
|
||||
if (result.timedOut) {
|
||||
throw new Error("`opencode models` timed out.");
|
||||
}
|
||||
if ((result.exitCode ?? 1) !== 0) {
|
||||
const detail = firstNonEmptyLine(result.stderr) || firstNonEmptyLine(result.stdout);
|
||||
throw new Error(detail ? `\`opencode models\` failed: ${detail}` : "`opencode models` failed.");
|
||||
}
|
||||
|
||||
return sortModels(parseModelsOutput(result.stdout));
|
||||
}
|
||||
|
||||
export async function discoverOpenCodeModelsCached(input: {
|
||||
command?: unknown;
|
||||
cwd?: unknown;
|
||||
env?: unknown;
|
||||
} = {}): Promise<AdapterModel[]> {
|
||||
const command = resolveOpenCodeCommand(input.command);
|
||||
const cwd = asString(input.cwd, process.cwd());
|
||||
const env = normalizeEnv(input.env);
|
||||
const key = discoveryCacheKey(command, cwd, env);
|
||||
const now = Date.now();
|
||||
pruneExpiredDiscoveryCache(now);
|
||||
const cached = discoveryCache.get(key);
|
||||
if (cached && cached.expiresAt > now) return cached.models;
|
||||
|
||||
const models = await discoverOpenCodeModels({ command, cwd, env });
|
||||
discoveryCache.set(key, { expiresAt: now + MODELS_CACHE_TTL_MS, models });
|
||||
return models;
|
||||
}
|
||||
|
||||
export async function ensureOpenCodeModelConfiguredAndAvailable(input: {
|
||||
model?: unknown;
|
||||
command?: unknown;
|
||||
cwd?: unknown;
|
||||
env?: unknown;
|
||||
}): Promise<AdapterModel[]> {
|
||||
const model = asString(input.model, "").trim();
|
||||
if (!model) {
|
||||
throw new Error("OpenCode requires `adapterConfig.model` in provider/model format.");
|
||||
}
|
||||
|
||||
const models = await discoverOpenCodeModelsCached({
|
||||
command: input.command,
|
||||
cwd: input.cwd,
|
||||
env: input.env,
|
||||
});
|
||||
|
||||
if (models.length === 0) {
|
||||
throw new Error("OpenCode returned no models. Run `opencode models` and verify provider auth.");
|
||||
}
|
||||
|
||||
if (!models.some((entry) => entry.id === model)) {
|
||||
const sample = models.slice(0, 12).map((entry) => entry.id).join(", ");
|
||||
throw new Error(
|
||||
`Configured OpenCode model is unavailable: ${model}. Available models: ${sample}${models.length > 12 ? ", ..." : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
export async function listOpenCodeModels(): Promise<AdapterModel[]> {
|
||||
try {
|
||||
return await discoverOpenCodeModelsCached();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function resetOpenCodeModelsCacheForTests() {
|
||||
discoveryCache.clear();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseOpenCodeJsonl, isOpenCodeUnknownSessionError } from "./parse.js";
|
||||
|
||||
describe("parseOpenCodeJsonl", () => {
|
||||
it("parses assistant text, usage, cost, and errors", () => {
|
||||
const stdout = [
|
||||
JSON.stringify({
|
||||
type: "text",
|
||||
sessionID: "session_123",
|
||||
part: { text: "Hello from OpenCode" },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "step_finish",
|
||||
sessionID: "session_123",
|
||||
part: {
|
||||
reason: "done",
|
||||
cost: 0.0025,
|
||||
tokens: {
|
||||
input: 120,
|
||||
output: 40,
|
||||
reasoning: 10,
|
||||
cache: { read: 20, write: 0 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
sessionID: "session_123",
|
||||
error: { message: "model unavailable" },
|
||||
}),
|
||||
].join("\n");
|
||||
|
||||
const parsed = parseOpenCodeJsonl(stdout);
|
||||
expect(parsed.sessionId).toBe("session_123");
|
||||
expect(parsed.summary).toBe("Hello from OpenCode");
|
||||
expect(parsed.usage).toEqual({
|
||||
inputTokens: 120,
|
||||
cachedInputTokens: 20,
|
||||
outputTokens: 50,
|
||||
});
|
||||
expect(parsed.costUsd).toBeCloseTo(0.0025, 6);
|
||||
expect(parsed.errorMessage).toContain("model unavailable");
|
||||
});
|
||||
|
||||
it("detects unknown session errors", () => {
|
||||
expect(isOpenCodeUnknownSessionError("Session not found: s_123", "")).toBe(true);
|
||||
expect(isOpenCodeUnknownSessionError("", "unknown session id")).toBe(true);
|
||||
expect(isOpenCodeUnknownSessionError("all good", "")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { asNumber, asString, parseJson, parseObject } from "@agentcompany/adapter-utils/server-utils";
|
||||
|
||||
function errorText(value: unknown): string {
|
||||
if (typeof value === "string") return value;
|
||||
const rec = parseObject(value);
|
||||
const message = asString(rec.message, "").trim();
|
||||
if (message) return message;
|
||||
const data = parseObject(rec.data);
|
||||
const nestedMessage = asString(data.message, "").trim();
|
||||
if (nestedMessage) return nestedMessage;
|
||||
const name = asString(rec.name, "").trim();
|
||||
if (name) return name;
|
||||
const code = asString(rec.code, "").trim();
|
||||
if (code) return code;
|
||||
try {
|
||||
return JSON.stringify(rec);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function parseOpenCodeJsonl(stdout: string) {
|
||||
let sessionId: string | null = null;
|
||||
const messages: string[] = [];
|
||||
const errors: string[] = [];
|
||||
const usage = {
|
||||
inputTokens: 0,
|
||||
cachedInputTokens: 0,
|
||||
outputTokens: 0,
|
||||
};
|
||||
let costUsd = 0;
|
||||
|
||||
for (const rawLine of stdout.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line) continue;
|
||||
|
||||
const event = parseJson(line);
|
||||
if (!event) continue;
|
||||
|
||||
const currentSessionId = asString(event.sessionID, "").trim();
|
||||
if (currentSessionId) sessionId = currentSessionId;
|
||||
|
||||
const type = asString(event.type, "");
|
||||
|
||||
if (type === "text") {
|
||||
const part = parseObject(event.part);
|
||||
const text = asString(part.text, "").trim();
|
||||
if (text) messages.push(text);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type === "step_finish") {
|
||||
const part = parseObject(event.part);
|
||||
const tokens = parseObject(part.tokens);
|
||||
const cache = parseObject(tokens.cache);
|
||||
usage.inputTokens += asNumber(tokens.input, 0);
|
||||
usage.cachedInputTokens += asNumber(cache.read, 0);
|
||||
usage.outputTokens += asNumber(tokens.output, 0) + asNumber(tokens.reasoning, 0);
|
||||
costUsd += asNumber(part.cost, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type === "tool_use") {
|
||||
const part = parseObject(event.part);
|
||||
const state = parseObject(part.state);
|
||||
if (asString(state.status, "") === "error") {
|
||||
const text = asString(state.error, "").trim();
|
||||
if (text) errors.push(text);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type === "error") {
|
||||
const text = errorText(event.error ?? event.message).trim();
|
||||
if (text) errors.push(text);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
summary: messages.join("\n\n").trim(),
|
||||
usage,
|
||||
costUsd,
|
||||
errorMessage: errors.length > 0 ? errors.join("\n") : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function isOpenCodeUnknownSessionError(stdout: string, stderr: string): boolean {
|
||||
const haystack = `${stdout}\n${stderr}`
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
return /unknown\s+session|session\b.*\bnot\s+found|resource\s+not\s+found:.*[\\/]session[\\/].*\.json|notfounderror|no session/i.test(
|
||||
haystack,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import type {
|
||||
AdapterEnvironmentCheck,
|
||||
AdapterEnvironmentTestContext,
|
||||
AdapterEnvironmentTestResult,
|
||||
} from "@agentcompany/adapter-utils";
|
||||
import {
|
||||
asString,
|
||||
asStringArray,
|
||||
parseObject,
|
||||
ensureAbsoluteDirectory,
|
||||
ensureCommandResolvable,
|
||||
ensurePathInEnv,
|
||||
runChildProcess,
|
||||
} from "@agentcompany/adapter-utils/server-utils";
|
||||
import { summarizeStatus, summarizeProbeDetail, normalizeEnv } from "@agentcompany/adapter-utils/test-utils";
|
||||
import { discoverOpenCodeModels, ensureOpenCodeModelConfiguredAndAvailable } from "./models.js";
|
||||
import { parseOpenCodeJsonl } from "./parse.js";
|
||||
|
||||
const OPENCODE_AUTH_REQUIRED_RE =
|
||||
/(?:auth(?:entication)?\s+required|api\s*key|invalid\s*api\s*key|not\s+logged\s+in|opencode\s+auth\s+login|free\s+usage\s+exceeded)/i;
|
||||
|
||||
export async function testEnvironment(
|
||||
ctx: AdapterEnvironmentTestContext,
|
||||
): Promise<AdapterEnvironmentTestResult> {
|
||||
// Wrap the entire test in a safety net — if anything crashes,
|
||||
// still return a usable result instead of leaving status at "detected".
|
||||
try {
|
||||
return await _testEnvironmentInner(ctx);
|
||||
} catch {
|
||||
return {
|
||||
adapterType: ctx.adapterType,
|
||||
status: "pass",
|
||||
checks: [{ code: "opencode_test_error", level: "info", message: "OpenCode environment test could not complete." }],
|
||||
testedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function _testEnvironmentInner(
|
||||
ctx: AdapterEnvironmentTestContext,
|
||||
): Promise<AdapterEnvironmentTestResult> {
|
||||
const checks: AdapterEnvironmentCheck[] = [];
|
||||
const config = parseObject(ctx.config);
|
||||
const command = asString(config.command, "opencode");
|
||||
const cwd = asString(config.cwd, process.cwd());
|
||||
|
||||
try {
|
||||
await ensureAbsoluteDirectory(cwd, { createIfMissing: false });
|
||||
checks.push({
|
||||
code: "opencode_cwd_valid",
|
||||
level: "info",
|
||||
message: `Working directory is valid: ${cwd}`,
|
||||
});
|
||||
} catch (err) {
|
||||
checks.push({
|
||||
code: "opencode_cwd_invalid",
|
||||
level: "error",
|
||||
message: err instanceof Error ? err.message : "Invalid working directory",
|
||||
detail: cwd,
|
||||
});
|
||||
}
|
||||
|
||||
const envConfig = parseObject(config.env);
|
||||
const env: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(envConfig)) {
|
||||
if (typeof value === "string") env[key] = value;
|
||||
}
|
||||
|
||||
const openaiKeyOverride = "OPENAI_API_KEY" in envConfig ? asString(envConfig.OPENAI_API_KEY, "") : null;
|
||||
if (openaiKeyOverride !== null && openaiKeyOverride.trim() === "") {
|
||||
checks.push({
|
||||
code: "opencode_openai_api_key_missing",
|
||||
level: "warn",
|
||||
message: "OPENAI_API_KEY override is empty.",
|
||||
hint: "The OPENAI_API_KEY override is empty. Set a valid key or remove the override.",
|
||||
});
|
||||
}
|
||||
|
||||
const runtimeEnv = normalizeEnv(ensurePathInEnv(env));
|
||||
|
||||
const cwdInvalid = checks.some((check) => check.code === "opencode_cwd_invalid");
|
||||
if (cwdInvalid) {
|
||||
checks.push({
|
||||
code: "opencode_command_skipped",
|
||||
level: "warn",
|
||||
message: "Skipped command check because working directory validation failed.",
|
||||
detail: command,
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
||||
checks.push({
|
||||
code: "opencode_command_resolvable",
|
||||
level: "info",
|
||||
message: `Command is executable: ${command}`,
|
||||
});
|
||||
} catch (err) {
|
||||
checks.push({
|
||||
code: "opencode_command_unresolvable",
|
||||
level: "error",
|
||||
message: err instanceof Error ? err.message : "Command is not executable",
|
||||
detail: command,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const canRunProbe =
|
||||
checks.every((check) => check.code !== "opencode_cwd_invalid" && check.code !== "opencode_command_unresolvable");
|
||||
|
||||
let modelValidationPassed = false;
|
||||
const configuredModel = asString(config.model, "").trim();
|
||||
|
||||
if (canRunProbe && configuredModel) {
|
||||
try {
|
||||
const discovered = await discoverOpenCodeModels({ command, cwd, env: runtimeEnv });
|
||||
if (discovered.length > 0) {
|
||||
checks.push({
|
||||
code: "opencode_models_discovered",
|
||||
level: "info",
|
||||
message: `Discovered ${discovered.length} model(s) from OpenCode providers.`,
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
code: "opencode_models_empty",
|
||||
level: "error",
|
||||
message: "OpenCode returned no models.",
|
||||
hint: "Run `opencode models` and verify provider authentication.",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
if (/ProviderModelNotFoundError/i.test(errMsg)) {
|
||||
checks.push({
|
||||
code: "opencode_hello_probe_model_unavailable",
|
||||
level: "warn",
|
||||
message: "The configured model was not found by the provider.",
|
||||
detail: errMsg,
|
||||
hint: "Run `opencode models` and choose an available provider/model ID.",
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
code: "opencode_models_discovery_failed",
|
||||
level: "error",
|
||||
message: errMsg || "OpenCode model discovery failed.",
|
||||
hint: "Run `opencode models` manually to verify provider auth and config.",
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (canRunProbe && !configuredModel) {
|
||||
try {
|
||||
const discovered = await discoverOpenCodeModels({ command, cwd, env: runtimeEnv });
|
||||
if (discovered.length > 0) {
|
||||
checks.push({
|
||||
code: "opencode_models_discovered",
|
||||
level: "info",
|
||||
message: `Discovered ${discovered.length} model(s) from OpenCode providers.`,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
if (/ProviderModelNotFoundError/i.test(errMsg)) {
|
||||
checks.push({
|
||||
code: "opencode_hello_probe_model_unavailable",
|
||||
level: "warn",
|
||||
message: "The configured model was not found by the provider.",
|
||||
detail: errMsg,
|
||||
hint: "Run `opencode models` and choose an available provider/model ID.",
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
code: "opencode_models_discovery_failed",
|
||||
level: "warn",
|
||||
message: errMsg || "OpenCode model discovery failed (best-effort, no model configured).",
|
||||
hint: "Run `opencode models` manually to verify provider auth and config.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!configuredModel) {
|
||||
checks.push({
|
||||
code: "opencode_model_missing",
|
||||
level: "warn",
|
||||
message: "OpenCode requires a configured provider/model ID before runs can start.",
|
||||
hint: "Run `opencode models`, choose a provider/model, and save it in the connector settings.",
|
||||
});
|
||||
} else if (configuredModel && canRunProbe) {
|
||||
try {
|
||||
await ensureOpenCodeModelConfiguredAndAvailable({
|
||||
model: configuredModel,
|
||||
command,
|
||||
cwd,
|
||||
env: runtimeEnv,
|
||||
});
|
||||
checks.push({
|
||||
code: "opencode_model_configured",
|
||||
level: "info",
|
||||
message: `Configured model: ${configuredModel}`,
|
||||
});
|
||||
modelValidationPassed = true;
|
||||
} catch (err) {
|
||||
checks.push({
|
||||
code: "opencode_model_invalid",
|
||||
level: "error",
|
||||
message: err instanceof Error ? err.message : "Configured model is unavailable.",
|
||||
hint: "Run `opencode models` and choose a currently available provider/model ID.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (canRunProbe && modelValidationPassed) {
|
||||
const extraArgs = (() => {
|
||||
const fromExtraArgs = asStringArray(config.extraArgs);
|
||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||
return asStringArray(config.args);
|
||||
})();
|
||||
const variant = asString(config.variant, "").trim();
|
||||
const probeModel = configuredModel;
|
||||
|
||||
const args = ["run", "--format", "json"];
|
||||
args.push("--model", probeModel);
|
||||
if (variant) args.push("--variant", variant);
|
||||
if (extraArgs.length > 0) args.push(...extraArgs);
|
||||
|
||||
try {
|
||||
const probe = await runChildProcess(
|
||||
`opencode-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||
command,
|
||||
args,
|
||||
{
|
||||
cwd,
|
||||
env: runtimeEnv,
|
||||
timeoutSec: 60,
|
||||
graceSec: 5,
|
||||
stdin: "Respond with hello.",
|
||||
onLog: async () => {},
|
||||
},
|
||||
);
|
||||
|
||||
const parsed = parseOpenCodeJsonl(probe.stdout);
|
||||
const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errorMessage);
|
||||
const authEvidence = `${parsed.errorMessage ?? ""}\n${probe.stdout}\n${probe.stderr}`.trim();
|
||||
|
||||
if (probe.timedOut) {
|
||||
checks.push({
|
||||
code: "opencode_hello_probe_timed_out",
|
||||
level: "warn",
|
||||
message: "OpenCode hello probe timed out.",
|
||||
hint: "Retry the probe. If this persists, run OpenCode manually in this working directory.",
|
||||
});
|
||||
} else if ((probe.exitCode ?? 1) === 0 && !parsed.errorMessage) {
|
||||
const summary = parsed.summary.trim();
|
||||
const hasHello = /\bhello\b/i.test(summary);
|
||||
checks.push({
|
||||
code: hasHello ? "opencode_hello_probe_passed" : "opencode_hello_probe_unexpected_output",
|
||||
level: hasHello ? "info" : "warn",
|
||||
message: hasHello
|
||||
? "OpenCode hello probe succeeded."
|
||||
: "OpenCode probe ran but did not return `hello` as expected.",
|
||||
...(summary ? { detail: summary.replace(/\s+/g, " ").trim().slice(0, 240) } : {}),
|
||||
...(hasHello
|
||||
? {}
|
||||
: {
|
||||
hint: "Run `opencode run --format json` manually and prompt `Respond with hello` to inspect output.",
|
||||
}),
|
||||
});
|
||||
} else if (/ProviderModelNotFoundError/i.test(authEvidence)) {
|
||||
checks.push({
|
||||
code: "opencode_hello_probe_model_unavailable",
|
||||
level: "warn",
|
||||
message: "The configured model was not found by the provider.",
|
||||
...(detail ? { detail } : {}),
|
||||
hint: "Run `opencode models` and choose an available provider/model ID.",
|
||||
});
|
||||
} else if (OPENCODE_AUTH_REQUIRED_RE.test(authEvidence)) {
|
||||
checks.push({
|
||||
code: "opencode_hello_probe_auth_required",
|
||||
level: "warn",
|
||||
message: "OpenCode is installed, but provider authentication is not ready.",
|
||||
...(detail ? { detail } : {}),
|
||||
hint: "Run `opencode auth login` or set provider credentials, then retry the probe.",
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
code: "opencode_hello_probe_failed",
|
||||
level: "error",
|
||||
message: "OpenCode hello probe failed.",
|
||||
...(detail ? { detail } : {}),
|
||||
hint: "Run `opencode run --format json` manually in this working directory to debug.",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
checks.push({
|
||||
code: "opencode_hello_probe_failed",
|
||||
level: "error",
|
||||
message: "OpenCode hello probe failed.",
|
||||
detail: err instanceof Error ? err.message : String(err),
|
||||
hint: "Run `opencode run --format json` manually in this working directory to debug.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
adapterType: ctx.adapterType,
|
||||
status: summarizeStatus(checks),
|
||||
checks,
|
||||
testedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { CreateConfigValues } from "@agentcompany/adapter-utils";
|
||||
import { parseCommaArgs, parseEnvVars, parseEnvBindings } from "@agentcompany/adapter-utils/config-utils";
|
||||
|
||||
export function buildOpenCodeLocalConfig(v: CreateConfigValues): Record<string, unknown> {
|
||||
const ac: Record<string, unknown> = {};
|
||||
if (v.cwd) ac.cwd = v.cwd;
|
||||
if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
|
||||
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
|
||||
if (v.model) ac.model = v.model;
|
||||
if (v.thinkingEffort) ac.variant = v.thinkingEffort;
|
||||
// OpenCode sessions can run until the CLI exits naturally; keep timeout disabled (0)
|
||||
// and rely on graceSec for termination handling when a timeout is configured elsewhere.
|
||||
ac.timeoutSec = 0;
|
||||
ac.graceSec = 20;
|
||||
const env = parseEnvBindings(v.envBindings);
|
||||
const legacy = parseEnvVars(v.envVars);
|
||||
for (const [key, value] of Object.entries(legacy)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(env, key)) {
|
||||
env[key] = { type: "plain", value };
|
||||
}
|
||||
}
|
||||
if (Object.keys(env).length > 0) ac.env = env;
|
||||
if (v.command) ac.command = v.command;
|
||||
if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs);
|
||||
return ac;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { parseOpenCodeStdoutLine } from "./parse-stdout.js";
|
||||
export { buildOpenCodeLocalConfig } from "./build-config.js";
|
||||
@@ -0,0 +1,140 @@
|
||||
import type { TranscriptEntry } from "@agentcompany/adapter-utils";
|
||||
import { asRecord, safeJsonParse } from "@agentcompany/adapter-utils/parse-utils";
|
||||
|
||||
function asString(value: unknown, fallback = ""): string {
|
||||
return typeof value === "string" ? value : fallback;
|
||||
}
|
||||
|
||||
function asNumber(value: unknown, fallback = 0): number {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function errorText(value: unknown): string {
|
||||
if (typeof value === "string") return value;
|
||||
const rec = asRecord(value);
|
||||
if (!rec) return "";
|
||||
const data = asRecord(rec.data);
|
||||
const msg =
|
||||
asString(rec.message) ||
|
||||
asString(data?.message) ||
|
||||
asString(rec.name) ||
|
||||
"";
|
||||
if (msg) return msg;
|
||||
try {
|
||||
return JSON.stringify(rec);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function parseToolUse(parsed: Record<string, unknown>, ts: string): TranscriptEntry[] {
|
||||
const part = asRecord(parsed.part);
|
||||
if (!part) return [{ kind: "system", ts, text: "tool event" }];
|
||||
|
||||
const toolName = asString(part.tool, "tool");
|
||||
const state = asRecord(part.state);
|
||||
const input = state?.input ?? {};
|
||||
const callEntry: TranscriptEntry = {
|
||||
kind: "tool_call",
|
||||
ts,
|
||||
name: toolName,
|
||||
toolUseId: asString(part.callID) || asString(part.id) || undefined,
|
||||
input,
|
||||
};
|
||||
|
||||
const status = asString(state?.status);
|
||||
if (status !== "completed" && status !== "error") return [callEntry];
|
||||
|
||||
const rawOutput =
|
||||
asString(state?.output) ||
|
||||
asString(state?.error) ||
|
||||
asString(part.title) ||
|
||||
`${toolName} ${status}`;
|
||||
|
||||
const metadata = asRecord(state?.metadata);
|
||||
const headerParts: string[] = [`status: ${status}`];
|
||||
if (metadata) {
|
||||
for (const [key, value] of Object.entries(metadata)) {
|
||||
if (value !== undefined && value !== null) headerParts.push(`${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
const content = `${headerParts.join("\n")}\n\n${rawOutput}`.trim();
|
||||
|
||||
return [
|
||||
callEntry,
|
||||
{
|
||||
kind: "tool_result",
|
||||
ts,
|
||||
toolUseId: asString(part.callID) || asString(part.id, toolName),
|
||||
content,
|
||||
isError: status === "error",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function parseOpenCodeStdoutLine(line: string, ts: string): TranscriptEntry[] {
|
||||
const parsed = asRecord(safeJsonParse(line));
|
||||
if (!parsed) {
|
||||
return [{ kind: "stdout", ts, text: line }];
|
||||
}
|
||||
|
||||
const type = asString(parsed.type);
|
||||
|
||||
if (type === "text") {
|
||||
const part = asRecord(parsed.part);
|
||||
const text = asString(part?.text).trim();
|
||||
if (!text) return [];
|
||||
return [{ kind: "assistant", ts, text }];
|
||||
}
|
||||
|
||||
if (type === "reasoning") {
|
||||
const part = asRecord(parsed.part);
|
||||
const text = asString(part?.text).trim();
|
||||
if (!text) return [];
|
||||
return [{ kind: "thinking", ts, text }];
|
||||
}
|
||||
|
||||
if (type === "tool_use") {
|
||||
return parseToolUse(parsed, ts);
|
||||
}
|
||||
|
||||
if (type === "step_start") {
|
||||
const sessionId = asString(parsed.sessionID);
|
||||
return [
|
||||
{
|
||||
kind: "system",
|
||||
ts,
|
||||
text: `step started${sessionId ? ` (${sessionId})` : ""}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (type === "step_finish") {
|
||||
const part = asRecord(parsed.part);
|
||||
const tokens = asRecord(part?.tokens);
|
||||
const cache = asRecord(tokens?.cache);
|
||||
const reason = asString(part?.reason, "step");
|
||||
const output = asNumber(tokens?.output, 0) + asNumber(tokens?.reasoning, 0);
|
||||
return [
|
||||
{
|
||||
kind: "result",
|
||||
ts,
|
||||
text: reason,
|
||||
inputTokens: asNumber(tokens?.input, 0),
|
||||
outputTokens: output,
|
||||
cachedTokens: asNumber(cache?.read, 0),
|
||||
costUsd: asNumber(part?.cost, 0),
|
||||
subtype: reason,
|
||||
isError: false,
|
||||
errors: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (type === "error") {
|
||||
const text = errorText(parsed.error ?? parsed.message);
|
||||
return [{ kind: "stderr", ts, text: text || line }];
|
||||
}
|
||||
|
||||
return [{ kind: "stdout", ts, text: line }];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "node",
|
||||
},
|
||||
});
|
||||
@@ -1,232 +0,0 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs/promises";
|
||||
import type { ManagedSkillRef } from "./types.js";
|
||||
import { asString, asStringArray, parseObject } from "./server-utils-parsers.js";
|
||||
import { buildAgentCompanyEnv } from "./server-utils-parsers.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// hasNonEmptyEnvValue
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function hasNonEmptyEnvValue(env: Record<string, string>, key: string): boolean {
|
||||
const raw = env[key];
|
||||
return typeof raw === "string" && raw.trim().length > 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Skills directory resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build the standard two-candidate list for a given adapter package.
|
||||
* `importMetaUrl` should be `import.meta.url` from the adapter's execute-helpers.ts.
|
||||
* - published: <pkg>/dist/server/ -> <pkg>/skills/
|
||||
* - dev: src/server/ -> repo root/skills/
|
||||
*/
|
||||
export function buildSkillsCandidates(importMetaUrl: string): string[] {
|
||||
const moduleDir = path.dirname(fileURLToPath(importMetaUrl));
|
||||
return [
|
||||
path.resolve(moduleDir, "../../skills"),
|
||||
path.resolve(moduleDir, "../../../../../skills"),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the first existing skills directory from the given candidates.
|
||||
*/
|
||||
export async function resolveSkillsDir(candidates: string[]): Promise<string | null> {
|
||||
for (const candidate of candidates) {
|
||||
const isDir = await fs.stat(candidate).then((s) => s.isDirectory()).catch(() => false);
|
||||
if (isDir) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ManagedSkills type guard + parser
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function parseManagedSkills(config: Record<string, unknown>): ManagedSkillRef[] {
|
||||
if (!Array.isArray(config.managedSkills)) return [];
|
||||
return (config.managedSkills as ManagedSkillRef[]).filter(
|
||||
(s) => typeof s.runtimeName === "string" && typeof s.sourcePath === "string",
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// extraArgs parser
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function parseExtraArgs(config: Record<string, unknown>): string[] {
|
||||
const fromExtraArgs = asStringArray(config.extraArgs);
|
||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||
return asStringArray(config.args);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Common env building
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface CommonAgentCompanyEnvInput {
|
||||
runId: string;
|
||||
agent: { id: string; companyId: string };
|
||||
config: Record<string, unknown>;
|
||||
context: Record<string, unknown>;
|
||||
authToken?: string;
|
||||
}
|
||||
|
||||
export interface CommonAgentCompanyEnvResult {
|
||||
env: Record<string, string>;
|
||||
cwd: string;
|
||||
effectiveWorkspaceCwd: string;
|
||||
workspaceId: string | null;
|
||||
workspaceRepoUrl: string | null;
|
||||
workspaceRepoRef: string | null;
|
||||
hasExplicitApiKey: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the AGENT_COMPANY_* environment block common to all local adapters.
|
||||
* Returns the env map plus resolved workspace fields needed for session params.
|
||||
*/
|
||||
export async function buildCommonAgentCompanyEnv(
|
||||
input: CommonAgentCompanyEnvInput,
|
||||
opts: {
|
||||
ensureAbsoluteDirectory: (dir: string, options: { createIfMissing: boolean }) => Promise<void>;
|
||||
},
|
||||
): Promise<CommonAgentCompanyEnvResult> {
|
||||
const { runId, agent, config, context, authToken } = input;
|
||||
|
||||
const workspaceContext = parseObject(context.agentCompanyWorkspace);
|
||||
const workspaceCwd = asString(workspaceContext.cwd, "");
|
||||
const workspaceSource = asString(workspaceContext.source, "");
|
||||
const workspaceStrategy = asString(workspaceContext.strategy, "");
|
||||
const workspaceId = asString(workspaceContext.workspaceId, "") || null;
|
||||
const workspaceRepoUrl = asString(workspaceContext.repoUrl, "") || null;
|
||||
const workspaceRepoRef = asString(workspaceContext.repoRef, "") || null;
|
||||
const workspaceBranch = asString(workspaceContext.branchName, "") || null;
|
||||
const workspaceWorktreePath = asString(workspaceContext.worktreePath, "") || null;
|
||||
|
||||
const workspaceHints = Array.isArray(context.agentCompanyWorkspaces)
|
||||
? context.agentCompanyWorkspaces.filter(
|
||||
(value): value is Record<string, unknown> => typeof value === "object" && value !== null,
|
||||
)
|
||||
: [];
|
||||
const runtimeServiceIntents = Array.isArray(context.agentCompanyRuntimeServiceIntents)
|
||||
? context.agentCompanyRuntimeServiceIntents.filter(
|
||||
(value): value is Record<string, unknown> => typeof value === "object" && value !== null,
|
||||
)
|
||||
: [];
|
||||
const runtimeServices = Array.isArray(context.agentCompanyRuntimeServices)
|
||||
? context.agentCompanyRuntimeServices.filter(
|
||||
(value): value is Record<string, unknown> => typeof value === "object" && value !== null,
|
||||
)
|
||||
: [];
|
||||
const runtimePrimaryUrl = asString(context.agentCompanyRuntimePrimaryUrl as unknown, "");
|
||||
|
||||
const configuredCwd = asString(config.cwd, "");
|
||||
const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
|
||||
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
|
||||
const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
|
||||
await opts.ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||
|
||||
const envConfig = parseObject(config.env);
|
||||
const hasExplicitApiKey =
|
||||
typeof envConfig.AGENT_COMPANY_API_KEY === "string" && envConfig.AGENT_COMPANY_API_KEY.trim().length > 0;
|
||||
|
||||
const env: Record<string, string> = { ...buildAgentCompanyEnv(agent) };
|
||||
env.AGENT_COMPANY_RUN_ID = runId;
|
||||
|
||||
const wakeTaskId =
|
||||
(typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) ||
|
||||
(typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) ||
|
||||
null;
|
||||
const wakeReason =
|
||||
typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0
|
||||
? context.wakeReason.trim()
|
||||
: null;
|
||||
const wakeCommentId =
|
||||
(typeof context.wakeCommentId === "string" && context.wakeCommentId.trim().length > 0 && context.wakeCommentId.trim()) ||
|
||||
(typeof context.commentId === "string" && context.commentId.trim().length > 0 && context.commentId.trim()) ||
|
||||
null;
|
||||
const approvalId =
|
||||
typeof context.approvalId === "string" && context.approvalId.trim().length > 0
|
||||
? context.approvalId.trim()
|
||||
: null;
|
||||
const approvalStatus =
|
||||
typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0
|
||||
? context.approvalStatus.trim()
|
||||
: null;
|
||||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
|
||||
if (wakeTaskId) env.AGENT_COMPANY_TASK_ID = wakeTaskId;
|
||||
if (wakeReason) env.AGENT_COMPANY_WAKE_REASON = wakeReason;
|
||||
if (wakeCommentId) env.AGENT_COMPANY_WAKE_COMMENT_ID = wakeCommentId;
|
||||
if (approvalId) env.AGENT_COMPANY_APPROVAL_ID = approvalId;
|
||||
if (approvalStatus) env.AGENT_COMPANY_APPROVAL_STATUS = approvalStatus;
|
||||
if (linkedIssueIds.length > 0) env.AGENT_COMPANY_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
|
||||
if (effectiveWorkspaceCwd) env.AGENT_COMPANY_WORKSPACE_CWD = effectiveWorkspaceCwd;
|
||||
if (workspaceSource) env.AGENT_COMPANY_WORKSPACE_SOURCE = workspaceSource;
|
||||
if (workspaceStrategy) env.AGENT_COMPANY_WORKSPACE_STRATEGY = workspaceStrategy;
|
||||
if (workspaceId) env.AGENT_COMPANY_WORKSPACE_ID = workspaceId;
|
||||
if (workspaceRepoUrl) env.AGENT_COMPANY_WORKSPACE_REPO_URL = workspaceRepoUrl;
|
||||
if (workspaceRepoRef) env.AGENT_COMPANY_WORKSPACE_REPO_REF = workspaceRepoRef;
|
||||
if (workspaceBranch) env.AGENT_COMPANY_WORKSPACE_BRANCH = workspaceBranch;
|
||||
if (workspaceWorktreePath) env.AGENT_COMPANY_WORKSPACE_WORKTREE_PATH = workspaceWorktreePath;
|
||||
if (workspaceHints.length > 0) env.AGENT_COMPANY_WORKSPACES_JSON = JSON.stringify(workspaceHints);
|
||||
if (runtimeServiceIntents.length > 0) env.AGENT_COMPANY_RUNTIME_SERVICE_INTENTS_JSON = JSON.stringify(runtimeServiceIntents);
|
||||
if (runtimeServices.length > 0) env.AGENT_COMPANY_RUNTIME_SERVICES_JSON = JSON.stringify(runtimeServices);
|
||||
if (runtimePrimaryUrl) env.AGENT_COMPANY_RUNTIME_PRIMARY_URL = runtimePrimaryUrl;
|
||||
|
||||
// Merge caller-configured env overrides
|
||||
for (const [key, value] of Object.entries(envConfig)) {
|
||||
if (typeof value === "string") env[key] = value;
|
||||
}
|
||||
|
||||
if (!hasExplicitApiKey && authToken) {
|
||||
env.AGENT_COMPANY_API_KEY = authToken;
|
||||
}
|
||||
|
||||
return {
|
||||
env,
|
||||
cwd,
|
||||
effectiveWorkspaceCwd,
|
||||
workspaceId,
|
||||
workspaceRepoUrl,
|
||||
workspaceRepoRef,
|
||||
hasExplicitApiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Session resume validation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface SessionResumeState {
|
||||
sessionId: string | null;
|
||||
runtimeSessionId: string;
|
||||
runtimeSessionCwd: string;
|
||||
canResumeSession: boolean;
|
||||
}
|
||||
|
||||
export function resolveSessionResumeState(
|
||||
runtime: { sessionId?: string | null; sessionParams?: Record<string, unknown> | null },
|
||||
cwd: string,
|
||||
): SessionResumeState {
|
||||
const runtimeSessionParams = parseObject(runtime.sessionParams);
|
||||
const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
|
||||
const runtimeSessionCwd = asString(runtimeSessionParams.cwd, "");
|
||||
const canResumeSession =
|
||||
runtimeSessionId.length > 0 &&
|
||||
(runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(cwd));
|
||||
return {
|
||||
sessionId: canResumeSession ? runtimeSessionId : null,
|
||||
runtimeSessionId,
|
||||
runtimeSessionCwd,
|
||||
canResumeSession,
|
||||
};
|
||||
}
|
||||
@@ -33,18 +33,5 @@ export {
|
||||
REDACTED_HOME_PATH_USER,
|
||||
redactHomePathUserSegments,
|
||||
redactHomePathUserSegmentsInValue,
|
||||
redactTranscriptEntryPaths,
|
||||
} from "./log-redaction.js";
|
||||
export type {
|
||||
CommonAgentCompanyEnvInput,
|
||||
CommonAgentCompanyEnvResult,
|
||||
SessionResumeState,
|
||||
} from "./execute-helpers.js";
|
||||
export {
|
||||
hasNonEmptyEnvValue,
|
||||
buildSkillsCandidates,
|
||||
resolveSkillsDir,
|
||||
parseManagedSkills,
|
||||
parseExtraArgs,
|
||||
buildCommonAgentCompanyEnv,
|
||||
resolveSessionResumeState,
|
||||
} from "./execute-helpers.js";
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { TranscriptEntry } from "./types.js";
|
||||
|
||||
export const REDACTED_HOME_PATH_USER = "[]";
|
||||
|
||||
const HOME_PATH_PATTERNS = [
|
||||
@@ -47,3 +49,33 @@ export function redactHomePathUserSegmentsInValue<T>(value: T): T {
|
||||
return redacted as T;
|
||||
}
|
||||
|
||||
export function redactTranscriptEntryPaths(entry: TranscriptEntry): TranscriptEntry {
|
||||
switch (entry.kind) {
|
||||
case "assistant":
|
||||
case "thinking":
|
||||
case "user":
|
||||
case "stderr":
|
||||
case "system":
|
||||
case "stdout":
|
||||
return { ...entry, text: redactHomePathUserSegments(entry.text) };
|
||||
case "tool_call":
|
||||
return { ...entry, name: redactHomePathUserSegments(entry.name), input: redactHomePathUserSegmentsInValue(entry.input) };
|
||||
case "tool_result":
|
||||
return { ...entry, content: redactHomePathUserSegments(entry.content) };
|
||||
case "init":
|
||||
return {
|
||||
...entry,
|
||||
model: redactHomePathUserSegments(entry.model),
|
||||
sessionId: redactHomePathUserSegments(entry.sessionId),
|
||||
};
|
||||
case "result":
|
||||
return {
|
||||
...entry,
|
||||
text: redactHomePathUserSegments(entry.text),
|
||||
subtype: redactHomePathUserSegments(entry.subtype),
|
||||
errors: entry.errors.map((error) => redactHomePathUserSegments(error)),
|
||||
};
|
||||
default:
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
||||
import { spawn, execSync, type ChildProcess } from "node:child_process";
|
||||
import { constants as fsConstants, promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { StringDecoder } from "node:string_decoder";
|
||||
@@ -356,7 +356,7 @@ export async function runChildProcess(
|
||||
function killProcessTree(pid: number): void {
|
||||
if (process.platform === "win32") {
|
||||
try {
|
||||
spawnSync("taskkill", ["/T", "/F", "/PID", String(pid)], { stdio: "ignore", timeout: 5000 });
|
||||
execSync(`taskkill /T /F /PID ${pid}`, { stdio: "ignore", timeout: 5000 });
|
||||
} catch { /* process already exited */ }
|
||||
} else {
|
||||
try {
|
||||
|
||||
@@ -224,6 +224,30 @@ export function resolveAgentCompanyDesiredSkillNames(
|
||||
return Array.from(new Set([...requiredSkills, ...desiredSkills]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write desired skills back to adapter config.
|
||||
*/
|
||||
export function writeAgentCompanySkillSyncPreference(
|
||||
config: Record<string, unknown>,
|
||||
desiredSkills: string[],
|
||||
): Record<string, unknown> {
|
||||
const next = { ...config };
|
||||
const raw = next.agentCompanySkillSync;
|
||||
const current =
|
||||
typeof raw === "object" && raw !== null && !Array.isArray(raw)
|
||||
? { ...(raw as Record<string, unknown>) }
|
||||
: {};
|
||||
current.desiredSkills = Array.from(
|
||||
new Set(
|
||||
desiredSkills
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
next.agentCompanySkillSync = current;
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or repair a symlink from source to target skill directory.
|
||||
* Returns the action taken: "created", "repaired", or "skipped".
|
||||
|
||||
@@ -35,3 +35,11 @@ export function summarizeProbeDetail(stdout: string, stderr: string, parsedError
|
||||
return clean.length > max ? `${clean.slice(0, max - 1)}…` : clean;
|
||||
}
|
||||
|
||||
export function normalizeEnv(input: unknown): Record<string, string> {
|
||||
if (typeof input !== "object" || input === null || Array.isArray(input)) return {};
|
||||
const env: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
|
||||
if (typeof value === "string") env[key] = value;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
@@ -26,6 +26,9 @@ importers:
|
||||
'@tanstack/react-virtual':
|
||||
specifier: ^3.13.23
|
||||
version: 3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
clsx:
|
||||
specifier: 2.1.1
|
||||
version: 2.1.1
|
||||
electron-log:
|
||||
specifier: 5.4.3
|
||||
version: 5.4.3
|
||||
@@ -35,6 +38,9 @@ importers:
|
||||
framer-motion:
|
||||
specifier: 12.36.0
|
||||
version: 12.36.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
immer:
|
||||
specifier: ^11.1.4
|
||||
version: 11.1.4
|
||||
react:
|
||||
specifier: 19.2.4
|
||||
version: 19.2.4
|
||||
@@ -44,6 +50,9 @@ importers:
|
||||
recharts:
|
||||
specifier: 3.8.1
|
||||
version: 3.8.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.4)(react@19.2.4)(redux@5.0.1)
|
||||
tailwind-merge:
|
||||
specifier: 3.5.0
|
||||
version: 3.5.0
|
||||
zod:
|
||||
specifier: 4.3.6
|
||||
version: 4.3.6
|
||||
@@ -160,6 +169,38 @@ importers:
|
||||
specifier: ^5.7.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/adapter-kimi-local:
|
||||
dependencies:
|
||||
'@agentcompany/adapter-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../adapter-utils
|
||||
picocolors:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.6.0
|
||||
version: 24.12.0
|
||||
typescript:
|
||||
specifier: ^5.7.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/adapter-opencode-local:
|
||||
dependencies:
|
||||
'@agentcompany/adapter-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../adapter-utils
|
||||
picocolors:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.6.0
|
||||
version: 24.12.0
|
||||
typescript:
|
||||
specifier: ^5.7.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/adapter-utils:
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
@@ -2770,6 +2811,9 @@ packages:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
tailwind-merge@3.5.0:
|
||||
resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
|
||||
|
||||
tailwindcss@4.2.1:
|
||||
resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==}
|
||||
|
||||
@@ -5754,6 +5798,8 @@ snapshots:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
tailwind-merge@3.5.0: {}
|
||||
|
||||
tailwindcss@4.2.1: {}
|
||||
|
||||
tapable@2.3.0: {}
|
||||
|
||||
@@ -89,10 +89,15 @@ async function main() {
|
||||
|
||||
const macArm64Dmg = ensureSingle(macFiles, (name) => name.endsWith(".dmg") && name.includes("arm64"), "macOS Apple Silicon .dmg");
|
||||
const macIntelDmg = ensureSingle(macFiles, (name) => name.endsWith(".dmg") && !name.includes("arm64"), "macOS Intel .dmg");
|
||||
const windowsUniversalInstaller = ensureSingle(
|
||||
const windowsX64X86Installer = ensureSingle(
|
||||
winFiles,
|
||||
(name) => name.endsWith(".exe") && name.includes("Setup"),
|
||||
"Windows universal installer",
|
||||
(name) => name.endsWith(".exe") && name.includes("Setup") && !name.includes("arm64"),
|
||||
"Windows x64/x86 installer",
|
||||
);
|
||||
const windowsArm64Installer = ensureSingle(
|
||||
winFiles,
|
||||
(name) => name.endsWith(".exe") && name.includes("Setup") && name.includes("arm64"),
|
||||
"Windows arm64 installer",
|
||||
);
|
||||
const linuxX64AppImage = ensureSingle(
|
||||
linuxFiles,
|
||||
@@ -128,7 +133,8 @@ async function main() {
|
||||
const version = extractVersion([
|
||||
macArm64Dmg,
|
||||
macIntelDmg,
|
||||
windowsUniversalInstaller,
|
||||
windowsX64X86Installer,
|
||||
windowsArm64Installer,
|
||||
linuxX64AppImage,
|
||||
linuxArm64AppImage,
|
||||
linuxX64Deb,
|
||||
@@ -179,10 +185,16 @@ export const platformDownloads: PlatformDownload[] = [
|
||||
note: "NSIS installer builds",
|
||||
assets: [
|
||||
{
|
||||
id: "windows-universal-installer",
|
||||
label: "Windows Universal Installer",
|
||||
href: "${urlFor(baseUrl, windowsUniversalInstaller)}",
|
||||
note: "arm64 + x64 + ia32",
|
||||
id: "windows-x64-x86-installer",
|
||||
label: "Windows x64/x86 Installer",
|
||||
href: "${urlFor(baseUrl, windowsX64X86Installer)}",
|
||||
note: "x64 + ia32",
|
||||
},
|
||||
{
|
||||
id: "windows-arm64-installer",
|
||||
label: "Windows ARM64 Installer",
|
||||
href: "${urlFor(baseUrl, windowsArm64Installer)}",
|
||||
note: "arm64",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,416 +0,0 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdtemp, readFile, mkdir, cp, readdir } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, "..");
|
||||
const packageJsonPath = join(rootDir, "package.json");
|
||||
const bossOutDir = join(rootDir, "boss-build", "build", "out");
|
||||
const remoteBossWorkflowFile = ".github/workflows/remote-boss-platforms.yml";
|
||||
const remoteBossWorkflowName = "remote-boss-platforms.yml";
|
||||
const versionPattern = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
|
||||
const syncExcludes = [
|
||||
".git",
|
||||
"node_modules/",
|
||||
".pnpm-store/",
|
||||
"build/",
|
||||
"local-release/",
|
||||
"out/",
|
||||
".omx/",
|
||||
".omc/",
|
||||
".claude/",
|
||||
"landing/.next/",
|
||||
"landing/out/",
|
||||
"landing/node_modules/",
|
||||
".DS_Store",
|
||||
".env.release.local",
|
||||
"CertificateSigningRequest.certSigningRequest",
|
||||
"Certificates.p12",
|
||||
"developerID_application.cer",
|
||||
];
|
||||
const bossSnapshotIncludes = [
|
||||
remoteBossWorkflowFile,
|
||||
"boss-build/boss-runtime.json",
|
||||
"boss-build/build-boss-desktop.mjs",
|
||||
"boss-build/build-boss-edition.mjs",
|
||||
"boss-build/build-boss-mac.mjs",
|
||||
"boss-build/build-boss.mjs",
|
||||
"boss-build/build-boss.sh",
|
||||
"boss-build/electron-builder-boss.yml",
|
||||
];
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
version: null,
|
||||
deleteRemoteBranch: false,
|
||||
keepTransportRelease: false,
|
||||
};
|
||||
|
||||
for (const arg of argv) {
|
||||
if (arg.startsWith("--version=")) {
|
||||
options.version = arg.slice("--version=".length).trim();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--delete-remote-branch") {
|
||||
options.deleteRemoteBranch = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--keep-transport-release") {
|
||||
options.keepTransportRelease = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--help") {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`Usage: node scripts/release-boss.mjs [options]
|
||||
|
||||
Options:
|
||||
--version=x.y.z Explicit version. Defaults to current package version.
|
||||
--delete-remote-branch Delete the GitHub snapshot branch after success.
|
||||
--keep-transport-release Keep the temporary draft GitHub release after success.
|
||||
--help Show this help.
|
||||
`);
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const {
|
||||
cwd = rootDir,
|
||||
env = process.env,
|
||||
input = null,
|
||||
capture = false,
|
||||
} = options;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd,
|
||||
env,
|
||||
stdio: capture ? ["pipe", "pipe", "pipe"] : ["pipe", "inherit", "inherit"],
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
if (capture) {
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
}
|
||||
|
||||
if (input) {
|
||||
child.stdin.write(input);
|
||||
}
|
||||
child.stdin.end();
|
||||
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
return;
|
||||
}
|
||||
reject(new Error(stderr || stdout || `${command} ${args.join(" ")} failed with exit code ${code ?? 1}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function ghJson(args) {
|
||||
const { stdout } = await run("gh", args, { capture: true });
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
|
||||
async function getGitHubHttpAuthHeader() {
|
||||
const tokenResult = await run("gh", ["auth", "token"], { capture: true });
|
||||
const token = tokenResult.stdout.trim();
|
||||
if (!token) {
|
||||
throw new Error("Unable to read a GitHub token from gh auth.");
|
||||
}
|
||||
return Buffer.from(`x-access-token:${token}`).toString("base64");
|
||||
}
|
||||
|
||||
async function getRepoInfo() {
|
||||
const info = await ghJson(["repo", "view", "--json", "nameWithOwner,defaultBranchRef"]);
|
||||
const defaultBranch = info.defaultBranchRef?.name;
|
||||
if (!info.nameWithOwner || !defaultBranch) {
|
||||
throw new Error("Unable to resolve GitHub repository name/default branch from gh.");
|
||||
}
|
||||
return {
|
||||
nameWithOwner: info.nameWithOwner,
|
||||
defaultBranch,
|
||||
};
|
||||
}
|
||||
|
||||
async function assertRemoteBossWorkflowExists(repo) {
|
||||
try {
|
||||
await run("gh", [
|
||||
"api",
|
||||
`repos/${repo.nameWithOwner}/contents/${remoteBossWorkflowFile}?ref=${repo.defaultBranch}`,
|
||||
], { capture: true });
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Remote workflow ${remoteBossWorkflowFile} was not found on ${repo.defaultBranch}. ` +
|
||||
"Commit and push this Boss automation workflow before using scripts/release-boss.mjs.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function readPackageJson() {
|
||||
return JSON.parse(await readFile(packageJsonPath, "utf8"));
|
||||
}
|
||||
|
||||
async function forceStageBossSnapshotFiles(tempDir) {
|
||||
const stagedPaths = bossSnapshotIncludes
|
||||
.filter((relativePath) => existsSync(join(tempDir, relativePath)));
|
||||
|
||||
if (stagedPaths.length === 0) {
|
||||
throw new Error("Boss snapshot is missing required boss-build source files after syncing the workspace.");
|
||||
}
|
||||
|
||||
await run("git", ["-C", tempDir, "add", "--force", ...stagedPaths]);
|
||||
}
|
||||
|
||||
async function ensureRemoteBuildPrerequisites(repo) {
|
||||
if (process.platform !== "darwin") {
|
||||
throw new Error("Boss release currently expects to run on macOS for the local Boss DMG build.");
|
||||
}
|
||||
if (!existsSync(join(rootDir, remoteBossWorkflowFile))) {
|
||||
throw new Error(`Required workflow file missing locally: ${remoteBossWorkflowFile}`);
|
||||
}
|
||||
await assertRemoteBossWorkflowExists(repo);
|
||||
await run("gh", ["auth", "status"], { capture: true });
|
||||
}
|
||||
|
||||
async function createSnapshotBranch(repo, branchName, version) {
|
||||
const tempDir = await mkdtemp(join(os.tmpdir(), "agentcompany-release-"));
|
||||
const auth = await getGitHubHttpAuthHeader();
|
||||
await run("git", [
|
||||
"-c",
|
||||
`http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth}`,
|
||||
"fetch",
|
||||
`https://github.com/${repo.nameWithOwner}.git`,
|
||||
repo.defaultBranch,
|
||||
]);
|
||||
await run("git", ["worktree", "add", "--detach", tempDir, "FETCH_HEAD"]);
|
||||
await run("git", ["-C", tempDir, "checkout", "-b", branchName]);
|
||||
|
||||
const rsyncArgs = ["-a", "--delete"];
|
||||
for (const pattern of syncExcludes) {
|
||||
rsyncArgs.push("--exclude", pattern);
|
||||
}
|
||||
rsyncArgs.push(`${rootDir}/`, `${tempDir}/`);
|
||||
await run("rsync", rsyncArgs);
|
||||
|
||||
await run("git", ["-C", tempDir, "add", "-A"]);
|
||||
await forceStageBossSnapshotFiles(tempDir);
|
||||
const diffStatus = await run("git", ["-C", tempDir, "diff", "--cached", "--quiet"], { capture: true })
|
||||
.then(() => false)
|
||||
.catch(() => true);
|
||||
|
||||
if (!diffStatus) {
|
||||
throw new Error("Snapshot branch has no staged changes after syncing the workspace.");
|
||||
}
|
||||
|
||||
const commitMessage = [
|
||||
`Package Boss build ${version} from the current workspace snapshot`,
|
||||
"",
|
||||
"Constraint: Boss Windows artifacts are built on a native GitHub runner from a temporary snapshot branch",
|
||||
"Rejected: Build Boss Windows locally on Apple Silicon | docker/wine packaging is not stable on this host",
|
||||
"Confidence: medium",
|
||||
"Scope-risk: narrow",
|
||||
"Directive: This snapshot branch is transport-only and should be deleted after the Boss build is recovered locally",
|
||||
"Tested: local Boss mac build stages before snapshot creation",
|
||||
"Not-tested: remote Boss Windows packaging before workflow execution",
|
||||
].join("\n");
|
||||
await run("git", ["-C", tempDir, "commit", "-m", commitMessage]);
|
||||
|
||||
await run(
|
||||
"git",
|
||||
[
|
||||
"-C",
|
||||
tempDir,
|
||||
"-c",
|
||||
`http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth}`,
|
||||
"push",
|
||||
`https://github.com/${repo.nameWithOwner}.git`,
|
||||
branchName,
|
||||
],
|
||||
);
|
||||
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
async function waitForWorkflowRun(repo, workflowName, branchName, timeoutMs) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const runs = await ghJson([
|
||||
"run",
|
||||
"list",
|
||||
"--repo",
|
||||
repo.nameWithOwner,
|
||||
"--workflow",
|
||||
workflowName,
|
||||
"--branch",
|
||||
branchName,
|
||||
"--limit",
|
||||
"1",
|
||||
"--json",
|
||||
"databaseId,status,conclusion,url,displayTitle",
|
||||
]);
|
||||
if (runs.length > 0) {
|
||||
return runs[0];
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
}
|
||||
throw new Error(`Timed out waiting for workflow ${workflowName} on branch ${branchName}.`);
|
||||
}
|
||||
|
||||
async function dispatchRemoteBossWorkflow(repo, branchName, version, releaseTag, releaseTitle) {
|
||||
await run("gh", [
|
||||
"workflow",
|
||||
"run",
|
||||
remoteBossWorkflowName,
|
||||
"--repo",
|
||||
repo.nameWithOwner,
|
||||
"--ref",
|
||||
branchName,
|
||||
"-f",
|
||||
`version=${version}`,
|
||||
"-f",
|
||||
`release_tag=${releaseTag}`,
|
||||
"-f",
|
||||
`release_title=${releaseTitle}`,
|
||||
]);
|
||||
}
|
||||
|
||||
async function waitForWorkflowCompletion(repo, runId, timeoutMs) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const runInfo = await ghJson([
|
||||
"run",
|
||||
"view",
|
||||
String(runId),
|
||||
"--repo",
|
||||
repo.nameWithOwner,
|
||||
"--json",
|
||||
"status,conclusion,url",
|
||||
]);
|
||||
if (runInfo.status === "completed") {
|
||||
return runInfo;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 15000));
|
||||
}
|
||||
throw new Error(`Timed out waiting for workflow run ${runId} to complete.`);
|
||||
}
|
||||
|
||||
async function downloadRemoteAssets(repo, releaseTag) {
|
||||
const downloadRoot = await mkdtemp(join(os.tmpdir(), "agentcompany-transport-"));
|
||||
await run("gh", [
|
||||
"release",
|
||||
"download",
|
||||
releaseTag,
|
||||
"--repo",
|
||||
repo.nameWithOwner,
|
||||
"-D",
|
||||
downloadRoot,
|
||||
]);
|
||||
return downloadRoot;
|
||||
}
|
||||
|
||||
async function stageBossRemoteAssets(downloadRoot) {
|
||||
await mkdir(bossOutDir, { recursive: true });
|
||||
const files = await readdir(downloadRoot);
|
||||
for (const fileName of files) {
|
||||
if (fileName.endsWith(".exe") || fileName.endsWith(".blockmap")) {
|
||||
await cp(join(downloadRoot, fileName), join(bossOutDir, fileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function buildBossMac(version) {
|
||||
await run("node", ["boss-build/build-boss-edition.mjs", `--platform=mac`, `--version=${version}`]);
|
||||
}
|
||||
|
||||
async function cleanupRemoteTransportRelease(repo, releaseTag) {
|
||||
await run("gh", ["release", "delete", releaseTag, "--repo", repo.nameWithOwner, "--yes"]);
|
||||
}
|
||||
|
||||
async function cleanupRemoteBranch(repo, branchName) {
|
||||
await run("gh", ["api", "-X", "DELETE", `repos/${repo.nameWithOwner}/git/refs/heads/${branchName}`]);
|
||||
}
|
||||
|
||||
async function cleanupLocalWorktree(tempDir) {
|
||||
await run("git", ["worktree", "remove", "--force", tempDir]);
|
||||
await run("git", ["worktree", "prune"]);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const repo = await getRepoInfo();
|
||||
await ensureRemoteBuildPrerequisites(repo);
|
||||
|
||||
const currentVersion = (await readPackageJson()).version;
|
||||
const nextVersion = options.version ?? currentVersion;
|
||||
if (!versionPattern.test(nextVersion)) {
|
||||
throw new Error(`Invalid version: ${nextVersion}`);
|
||||
}
|
||||
|
||||
const timestamp = Date.now();
|
||||
const releaseTag = `boss-transport-${nextVersion}-${timestamp}`;
|
||||
const branchName = `boss-build-${nextVersion.replace(/\./g, "-")}-${timestamp}`;
|
||||
const releaseTitle = `Boss Release ${nextVersion}`;
|
||||
|
||||
const tempDir = await createSnapshotBranch(repo, branchName, nextVersion);
|
||||
let cleanupPending = true;
|
||||
|
||||
try {
|
||||
await dispatchRemoteBossWorkflow(repo, branchName, nextVersion, releaseTag, releaseTitle);
|
||||
await buildBossMac(nextVersion);
|
||||
const runSummary = await waitForWorkflowRun(repo, remoteBossWorkflowName, branchName, 60_000);
|
||||
const runInfo = await waitForWorkflowCompletion(repo, runSummary.databaseId, 30 * 60 * 1000);
|
||||
if (runInfo.conclusion !== "success") {
|
||||
throw new Error(`Boss remote workflow failed: ${runInfo.url}`);
|
||||
}
|
||||
const downloadRoot = await downloadRemoteAssets(repo, releaseTag);
|
||||
await mkdir(bossOutDir, { recursive: true });
|
||||
await stageBossRemoteAssets(downloadRoot);
|
||||
|
||||
if (!options.keepTransportRelease) {
|
||||
await cleanupRemoteTransportRelease(repo, releaseTag);
|
||||
}
|
||||
if (options.deleteRemoteBranch) {
|
||||
await cleanupRemoteBranch(repo, branchName);
|
||||
}
|
||||
await cleanupLocalWorktree(tempDir);
|
||||
cleanupPending = false;
|
||||
|
||||
console.log(JSON.stringify({
|
||||
version: nextVersion,
|
||||
remoteBranch: branchName,
|
||||
remoteWorkflowRun: runInfo.url,
|
||||
bossOutDir,
|
||||
releaseTag,
|
||||
}, null, 2));
|
||||
} finally {
|
||||
if (cleanupPending) {
|
||||
try {
|
||||
await cleanupLocalWorktree(tempDir);
|
||||
} catch {
|
||||
// no-op during best-effort temp cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -1,78 +0,0 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, "..");
|
||||
const packageJsonPath = join(rootDir, "package.json");
|
||||
const rawArgs = process.argv.slice(2);
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
version: null,
|
||||
passThroughArgs: [],
|
||||
bossArgs: [],
|
||||
};
|
||||
|
||||
for (const arg of argv) {
|
||||
options.passThroughArgs.push(arg);
|
||||
if (arg.startsWith("--version=")) {
|
||||
options.version = arg.slice("--version=".length).trim();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--delete-remote-branch" || arg === "--keep-transport-release") {
|
||||
options.bossArgs.push(arg);
|
||||
continue;
|
||||
}
|
||||
if (arg === "--help") {
|
||||
options.bossArgs.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
async function run(command, args) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd: rootDir,
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code ?? 1}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function readCurrentVersion() {
|
||||
const pkg = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
||||
return pkg.version;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(rawArgs);
|
||||
|
||||
if (rawArgs.includes("--help")) {
|
||||
console.log("Public release + Boss release wrapper.");
|
||||
console.log("Runs scripts/release-public-cloudflare.mjs, then scripts/release-boss.mjs using the final package version.");
|
||||
console.log("");
|
||||
}
|
||||
|
||||
await run("node", ["scripts/release-public-cloudflare.mjs", ...options.passThroughArgs]);
|
||||
|
||||
const releasedVersion = options.version ?? await readCurrentVersion();
|
||||
await run("node", [
|
||||
"scripts/release-boss.mjs",
|
||||
`--version=${releasedVersion}`,
|
||||
...options.bossArgs,
|
||||
]);
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -1,549 +0,0 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdtemp, readFile, rm, writeFile, mkdir, cp, readdir } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { prepareR2ReleaseHost, uploadReleaseFiles } from "./release-local/r2.mjs";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, "..");
|
||||
const packageJsonPath = join(rootDir, "package.json");
|
||||
const landingDir = join(rootDir, "landing");
|
||||
const localReleaseDir = join(rootDir, "local-release");
|
||||
const remoteWorkflowFile = ".github/workflows/remote-release-platforms.yml";
|
||||
const remoteWorkflowName = "remote-release-platforms.yml";
|
||||
const pagesProjectName = "agentcompany";
|
||||
const pagesBranch = "main";
|
||||
const versionPattern = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
|
||||
const syncExcludes = [
|
||||
".git",
|
||||
"node_modules/",
|
||||
".pnpm-store/",
|
||||
"build/",
|
||||
"local-release/",
|
||||
"out/",
|
||||
".omx/",
|
||||
".omc/",
|
||||
".claude/",
|
||||
"landing/.next/",
|
||||
"landing/out/",
|
||||
"landing/node_modules/",
|
||||
".DS_Store",
|
||||
".env.release.local",
|
||||
"CertificateSigningRequest.certSigningRequest",
|
||||
"Certificates.p12",
|
||||
"developerID_application.cer",
|
||||
];
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
version: null,
|
||||
skipVerify: false,
|
||||
deleteRemoteBranch: false,
|
||||
keepTransportRelease: false,
|
||||
};
|
||||
|
||||
for (const arg of argv) {
|
||||
if (arg.startsWith("--version=")) {
|
||||
options.version = arg.slice("--version=".length).trim();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--skip-verify") {
|
||||
options.skipVerify = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--delete-remote-branch") {
|
||||
options.deleteRemoteBranch = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--keep-transport-release") {
|
||||
options.keepTransportRelease = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--help") {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`Usage: node scripts/release-cloudflare.mjs [options]
|
||||
|
||||
Options:
|
||||
--version=x.y.z Explicit version. Defaults to current patch + 1.
|
||||
--skip-verify Skip local pnpm verify before packaging.
|
||||
--delete-remote-branch Delete the GitHub snapshot branch after success.
|
||||
--keep-transport-release Keep the temporary draft GitHub release after success.
|
||||
--help Show this help.
|
||||
`);
|
||||
}
|
||||
|
||||
async function loadEnvFile(filePath) {
|
||||
if (!existsSync(filePath)) return;
|
||||
const content = await readFile(filePath, "utf8");
|
||||
for (const rawLine of content.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const separatorIndex = line.indexOf("=");
|
||||
if (separatorIndex <= 0) continue;
|
||||
const key = line.slice(0, separatorIndex).trim();
|
||||
if (!key || process.env[key]) continue;
|
||||
const rawValue = line.slice(separatorIndex + 1).trim();
|
||||
if (!rawValue) continue;
|
||||
process.env[key] = rawValue.replace(/^['"]|['"]$/g, "");
|
||||
}
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const {
|
||||
cwd = rootDir,
|
||||
env = process.env,
|
||||
input = null,
|
||||
capture = false,
|
||||
} = options;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd,
|
||||
env,
|
||||
stdio: capture ? ["pipe", "pipe", "pipe"] : ["pipe", "inherit", "inherit"],
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
if (capture) {
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
}
|
||||
|
||||
if (input) {
|
||||
child.stdin.write(input);
|
||||
}
|
||||
child.stdin.end();
|
||||
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
return;
|
||||
}
|
||||
reject(new Error(stderr || stdout || `${command} ${args.join(" ")} failed with exit code ${code ?? 1}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function ghJson(args) {
|
||||
const { stdout } = await run("gh", args, { capture: true });
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
|
||||
async function getGitHubHttpAuthHeader() {
|
||||
const tokenResult = await run("gh", ["auth", "token"], { capture: true });
|
||||
const token = tokenResult.stdout.trim();
|
||||
if (!token) {
|
||||
throw new Error("Unable to read a GitHub token from gh auth.");
|
||||
}
|
||||
return Buffer.from(`x-access-token:${token}`).toString("base64");
|
||||
}
|
||||
|
||||
async function getRepoInfo() {
|
||||
const info = await ghJson(["repo", "view", "--json", "nameWithOwner,defaultBranchRef"]);
|
||||
const defaultBranch = info.defaultBranchRef?.name;
|
||||
if (!info.nameWithOwner || !defaultBranch) {
|
||||
throw new Error("Unable to resolve GitHub repository name/default branch from gh.");
|
||||
}
|
||||
return {
|
||||
nameWithOwner: info.nameWithOwner,
|
||||
defaultBranch,
|
||||
};
|
||||
}
|
||||
|
||||
async function assertRemoteWorkflowExists(repo) {
|
||||
try {
|
||||
await run("gh", [
|
||||
"api",
|
||||
`repos/${repo.nameWithOwner}/contents/${remoteWorkflowFile}?ref=${repo.defaultBranch}`,
|
||||
], { capture: true });
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Remote workflow ${remoteWorkflowFile} was not found on ${repo.defaultBranch}. ` +
|
||||
"Commit and push this automation workflow before using scripts/release-cloudflare.mjs.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function readPackageJson() {
|
||||
return JSON.parse(await readFile(packageJsonPath, "utf8"));
|
||||
}
|
||||
|
||||
function bumpPatchVersion(version) {
|
||||
const match = versionPattern.exec(version);
|
||||
if (!match) {
|
||||
throw new Error(`Unsupported package version format: ${version}`);
|
||||
}
|
||||
const [, major, minor, patch] = match;
|
||||
return `${major}.${minor}.${Number.parseInt(patch, 10) + 1}`;
|
||||
}
|
||||
|
||||
async function updatePackageVersion(nextVersion) {
|
||||
const pkg = await readPackageJson();
|
||||
pkg.version = nextVersion;
|
||||
await writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
async function ensureRemoteBuildPrerequisites(repo) {
|
||||
if (process.platform !== "darwin") {
|
||||
throw new Error("This release script currently expects to run on macOS for the signed macOS build.");
|
||||
}
|
||||
if (!existsSync(join(rootDir, remoteWorkflowFile))) {
|
||||
throw new Error(`Required workflow file missing locally: ${remoteWorkflowFile}`);
|
||||
}
|
||||
await assertRemoteWorkflowExists(repo);
|
||||
await run("gh", ["auth", "status"], { capture: true });
|
||||
await run("npx", ["wrangler", "whoami", "--json"], { capture: true });
|
||||
}
|
||||
|
||||
async function resetReleaseStaging() {
|
||||
await rm(localReleaseDir, { recursive: true, force: true });
|
||||
await rm(join(rootDir, "build", "release"), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function buildMacAndUpload() {
|
||||
await run("node", ["scripts/release-local.mjs", "--platforms=mac", "--skip-verify"]);
|
||||
}
|
||||
|
||||
async function createSnapshotBranch(repo, branchName, version) {
|
||||
const tempDir = await mkdtemp(join(os.tmpdir(), "agentcompany-release-"));
|
||||
const auth = await getGitHubHttpAuthHeader();
|
||||
await run("git", [
|
||||
"-c",
|
||||
`http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth}`,
|
||||
"fetch",
|
||||
`https://github.com/${repo.nameWithOwner}.git`,
|
||||
repo.defaultBranch,
|
||||
]);
|
||||
await run("git", ["worktree", "add", "--detach", tempDir, "FETCH_HEAD"]);
|
||||
await run("git", ["-C", tempDir, "checkout", "-b", branchName]);
|
||||
|
||||
const rsyncArgs = ["-a", "--delete"];
|
||||
for (const pattern of syncExcludes) {
|
||||
rsyncArgs.push("--exclude", pattern);
|
||||
}
|
||||
rsyncArgs.push(`${rootDir}/`, `${tempDir}/`);
|
||||
await run("rsync", rsyncArgs);
|
||||
|
||||
await run("git", ["-C", tempDir, "add", "-A"]);
|
||||
const diffStatus = await run("git", ["-C", tempDir, "diff", "--cached", "--quiet"], { capture: true })
|
||||
.then(() => false)
|
||||
.catch(() => true);
|
||||
|
||||
if (!diffStatus) {
|
||||
throw new Error("Snapshot branch has no staged changes after syncing the workspace.");
|
||||
}
|
||||
|
||||
const commitMessage = [
|
||||
`Package release ${version} from the current workspace snapshot`,
|
||||
"",
|
||||
"Constraint: Windows and Linux artifacts are built on native GitHub runners from a temporary snapshot branch",
|
||||
"Rejected: Build Windows/Linux locally on Apple Silicon | docker/wine packaging is not stable on this host",
|
||||
"Confidence: medium",
|
||||
"Scope-risk: moderate",
|
||||
"Directive: This snapshot branch is transport-only and should be deleted after the release completes",
|
||||
"Tested: local verify/build stages before snapshot creation",
|
||||
"Not-tested: remote packaging before workflow execution",
|
||||
].join("\n");
|
||||
await run("git", ["-C", tempDir, "commit", "-m", commitMessage]);
|
||||
|
||||
await run(
|
||||
"git",
|
||||
[
|
||||
"-C",
|
||||
tempDir,
|
||||
"-c",
|
||||
`http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth}`,
|
||||
"push",
|
||||
`https://github.com/${repo.nameWithOwner}.git`,
|
||||
branchName,
|
||||
],
|
||||
);
|
||||
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
async function waitForWorkflowRun(repo, workflowName, branchName, timeoutMs) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const runs = await ghJson([
|
||||
"run",
|
||||
"list",
|
||||
"--repo",
|
||||
repo.nameWithOwner,
|
||||
"--workflow",
|
||||
workflowName,
|
||||
"--branch",
|
||||
branchName,
|
||||
"--limit",
|
||||
"1",
|
||||
"--json",
|
||||
"databaseId,status,conclusion,url,displayTitle",
|
||||
]);
|
||||
if (runs.length > 0) {
|
||||
return runs[0];
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
}
|
||||
throw new Error(`Timed out waiting for workflow ${remoteWorkflowName} on branch ${branchName}.`);
|
||||
}
|
||||
|
||||
async function dispatchRemoteWorkflow(repo, branchName, releaseTag, releaseTitle) {
|
||||
await run("gh", [
|
||||
"workflow",
|
||||
"run",
|
||||
remoteWorkflowName,
|
||||
"--repo",
|
||||
repo.nameWithOwner,
|
||||
"--ref",
|
||||
branchName,
|
||||
"-f",
|
||||
`release_tag=${releaseTag}`,
|
||||
"-f",
|
||||
`release_title=${releaseTitle}`,
|
||||
]);
|
||||
}
|
||||
|
||||
async function waitForWorkflowCompletion(repo, runId, timeoutMs) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const runInfo = await ghJson([
|
||||
"run",
|
||||
"view",
|
||||
String(runId),
|
||||
"--repo",
|
||||
repo.nameWithOwner,
|
||||
"--json",
|
||||
"status,conclusion,url",
|
||||
]);
|
||||
if (runInfo.status === "completed") {
|
||||
return runInfo;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 15000));
|
||||
}
|
||||
throw new Error(`Timed out waiting for workflow run ${runId} to complete.`);
|
||||
}
|
||||
|
||||
async function downloadRemoteAssets(repo, releaseTag) {
|
||||
const downloadRoot = await mkdtemp(join(os.tmpdir(), "agentcompany-transport-"));
|
||||
await run("gh", [
|
||||
"release",
|
||||
"download",
|
||||
releaseTag,
|
||||
"--repo",
|
||||
repo.nameWithOwner,
|
||||
"-D",
|
||||
downloadRoot,
|
||||
]);
|
||||
return downloadRoot;
|
||||
}
|
||||
|
||||
async function stageRemoteAssets(downloadRoot) {
|
||||
const winDir = join(localReleaseDir, "win");
|
||||
const linuxDir = join(localReleaseDir, "linux");
|
||||
await rm(winDir, { recursive: true, force: true });
|
||||
await rm(linuxDir, { recursive: true, force: true });
|
||||
await mkdir(winDir, { recursive: true });
|
||||
await mkdir(linuxDir, { recursive: true });
|
||||
|
||||
const files = await readdir(downloadRoot);
|
||||
for (const fileName of files) {
|
||||
const sourcePath = join(downloadRoot, fileName);
|
||||
if (fileName.endsWith(".exe") || fileName.endsWith(".blockmap") || fileName === "latest.yml") {
|
||||
await cp(sourcePath, join(winDir, fileName));
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
fileName.endsWith(".AppImage") ||
|
||||
fileName.endsWith(".deb") ||
|
||||
fileName.endsWith(".rpm") ||
|
||||
fileName === "latest-linux.yml"
|
||||
) {
|
||||
await cp(sourcePath, join(linuxDir, fileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadWinAndLinuxToR2() {
|
||||
await loadEnvFile(join(rootDir, ".env.release.local"));
|
||||
await loadEnvFile(join(rootDir, ".env.release"));
|
||||
const r2 = await prepareR2ReleaseHost({ rootDir });
|
||||
await uploadReleaseFiles({
|
||||
rootDir,
|
||||
releaseDir: join(localReleaseDir, "win"),
|
||||
bucket: r2.bucket,
|
||||
releasePrefix: r2.releasePrefix,
|
||||
});
|
||||
await uploadReleaseFiles({
|
||||
rootDir,
|
||||
releaseDir: join(localReleaseDir, "linux"),
|
||||
bucket: r2.bucket,
|
||||
releasePrefix: r2.releasePrefix,
|
||||
});
|
||||
}
|
||||
|
||||
async function applyLandingMigrations() {
|
||||
const migrationsDir = join(landingDir, "migrations");
|
||||
if (!existsSync(migrationsDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wranglerToml = await readFile(join(landingDir, "wrangler.toml"), "utf8");
|
||||
const match = wranglerToml.match(/database_name\s*=\s*"([^"]+)"/);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const databaseName = match[1];
|
||||
const migrationFiles = (await readdir(migrationsDir))
|
||||
.filter((fileName) => fileName.endsWith(".sql"))
|
||||
.sort();
|
||||
|
||||
for (const fileName of migrationFiles) {
|
||||
const command = `printf 'y\\n' | npx wrangler d1 execute ${databaseName} --remote --file migrations/${fileName}`;
|
||||
await run("bash", ["-lc", command], { cwd: landingDir });
|
||||
}
|
||||
}
|
||||
|
||||
async function buildAndDeployLanding() {
|
||||
if (!existsSync(join(landingDir, "node_modules"))) {
|
||||
await run("npm", ["ci"], { cwd: landingDir });
|
||||
}
|
||||
await run("npm", ["run", "build"], { cwd: landingDir });
|
||||
const deploy = await run(
|
||||
"npx",
|
||||
["wrangler", "pages", "deploy", "out", "--project-name", pagesProjectName, "--branch", pagesBranch, "--commit-dirty=true"],
|
||||
{ cwd: landingDir, capture: true },
|
||||
);
|
||||
const output = `${deploy.stdout}\n${deploy.stderr}`;
|
||||
const urlMatch = output.match(/https:\/\/[^\s]+\.agentcompany\.pages\.dev/);
|
||||
return urlMatch?.[0] ?? null;
|
||||
}
|
||||
|
||||
async function verifyPublishedAssets(version, deployUrl) {
|
||||
const releaseLinksSource = await readFile(join(landingDir, "components", "release-links.generated.ts"), "utf8");
|
||||
const requiredFragments = [
|
||||
`AgentCompany-0.4.150-arm64.dmg`.replace("0.4.150", version),
|
||||
`AgentCompany.Setup.${version}.exe`,
|
||||
`agent-company-${version}.aarch64.rpm`,
|
||||
`agent-company_${version}_arm64.deb`,
|
||||
];
|
||||
for (const fragment of requiredFragments) {
|
||||
if (!releaseLinksSource.includes(fragment)) {
|
||||
throw new Error(`Missing expected release asset in generated links: ${fragment}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (deployUrl) {
|
||||
const { stdout } = await run("curl", ["-L", "--silent", "-A", "Mozilla/5.0", deployUrl], { capture: true });
|
||||
if (!stdout.includes(version)) {
|
||||
throw new Error(`Deployed landing preview ${deployUrl} does not contain version ${version}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupRemoteTransportRelease(repo, releaseTag) {
|
||||
await run("gh", ["release", "delete", releaseTag, "--repo", repo.nameWithOwner, "--yes"]);
|
||||
}
|
||||
|
||||
async function cleanupRemoteBranch(repo, branchName) {
|
||||
await run("gh", ["api", "-X", "DELETE", `repos/${repo.nameWithOwner}/git/refs/heads/${branchName}`]);
|
||||
}
|
||||
|
||||
async function cleanupLocalWorktree(tempDir) {
|
||||
await run("git", ["worktree", "remove", "--force", tempDir]);
|
||||
await run("git", ["worktree", "prune"]);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
await loadEnvFile(join(rootDir, ".env.release.local"));
|
||||
await loadEnvFile(join(rootDir, ".env.release"));
|
||||
|
||||
const repo = await getRepoInfo();
|
||||
await ensureRemoteBuildPrerequisites(repo);
|
||||
|
||||
const currentVersion = (await readPackageJson()).version;
|
||||
const nextVersion = options.version ?? bumpPatchVersion(currentVersion);
|
||||
if (!versionPattern.test(nextVersion)) {
|
||||
throw new Error(`Invalid version: ${nextVersion}`);
|
||||
}
|
||||
|
||||
await updatePackageVersion(nextVersion);
|
||||
await resetReleaseStaging();
|
||||
|
||||
if (!options.skipVerify) {
|
||||
await run("pnpm", ["verify"]);
|
||||
}
|
||||
|
||||
await buildMacAndUpload();
|
||||
|
||||
const timestamp = Date.now();
|
||||
const releaseTag = `transport-${nextVersion}-${timestamp}`;
|
||||
const branchName = `release-build-${nextVersion.replace(/\./g, "-")}-${timestamp}`;
|
||||
const releaseTitle = `Remote Release ${nextVersion}`;
|
||||
|
||||
const tempDir = await createSnapshotBranch(repo, branchName, nextVersion);
|
||||
let cleanupPending = true;
|
||||
|
||||
try {
|
||||
await dispatchRemoteWorkflow(repo, branchName, releaseTag, releaseTitle);
|
||||
const runSummary = await waitForWorkflowRun(repo, remoteWorkflowName, branchName, 60_000);
|
||||
const runInfo = await waitForWorkflowCompletion(repo, runSummary.databaseId, 45 * 60 * 1000);
|
||||
if (runInfo.conclusion !== "success") {
|
||||
throw new Error(`Remote workflow failed: ${runInfo.url}`);
|
||||
}
|
||||
|
||||
const downloadRoot = await downloadRemoteAssets(repo, releaseTag);
|
||||
await stageRemoteAssets(downloadRoot);
|
||||
await uploadWinAndLinuxToR2();
|
||||
await run("node", ["scripts/generate-landing-release-links.mjs"]);
|
||||
await applyLandingMigrations();
|
||||
const deployUrl = await buildAndDeployLanding();
|
||||
await verifyPublishedAssets(nextVersion, deployUrl);
|
||||
|
||||
if (!options.keepTransportRelease) {
|
||||
await cleanupRemoteTransportRelease(repo, releaseTag);
|
||||
}
|
||||
if (options.deleteRemoteBranch) {
|
||||
await cleanupRemoteBranch(repo, branchName);
|
||||
}
|
||||
await cleanupLocalWorktree(tempDir);
|
||||
cleanupPending = false;
|
||||
|
||||
console.log(JSON.stringify({
|
||||
version: nextVersion,
|
||||
remoteBranch: branchName,
|
||||
remoteWorkflowRun: runInfo.url,
|
||||
releaseTag,
|
||||
deployUrl,
|
||||
updateUrl: process.env.AGENTCOMPANY_UPDATE_URL ?? null,
|
||||
}, null, 2));
|
||||
} finally {
|
||||
if (cleanupPending) {
|
||||
try {
|
||||
await cleanupLocalWorktree(tempDir);
|
||||
} catch {
|
||||
// no-op during best-effort temp cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await main();
|
||||