Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ MAIN_VITE_POSTHOG_HOST=https://us.i.posthog.com
VITE_POSTHOG_KEY=phc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
VITE_POSTHOG_HOST=https://us.i.posthog.com

# Feedback URL (optional - defaults to open source Discord if not set)
# Set this in hosted builds to use a private feedback channel
# OpenCodex link overrides (optional - keep product/community/changelog links OpenCodex-native)
# Community falls back to VITE_FEEDBACK_URL, then the public Discord invite
# VITE_FEEDBACK_URL=https://discord.gg/your-private-invite
# VITE_OPENCODEX_COMMUNITY_URL=https://community.example.com/opencodex
# VITE_OPENCODEX_CHANGELOG_URL=https://product.example.com/opencodex/changelog
# VITE_OPENCODEX_AGENTS_CHANGELOG_URL=https://product.example.com/opencodex/agents/changelog
# VITE_OPENCODEX_CHANGELOG_FEED_URL=https://product.example.com/api/opencodex/changelog?per_page=3

# API URL (optional - defaults to https://21st.dev)
# Only change this if you're running the web app locally
Expand Down
36 changes: 19 additions & 17 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## What is this?

**21st Agents** - A local-first Electron desktop app for AI-powered code assistance. Users create chat sessions linked to local project folders, interact with Claude in Plan or Agent mode, and see real-time tool execution (bash, file edits, web search, etc.).
**OpenCodex** - A local-native Electron desktop coding workstation. Users create chat sessions linked to local project folders, interact through the desktop UI, and see real-time tool execution (bash, file edits, web search, etc.).

## Commands

Expand Down Expand Up @@ -165,8 +165,8 @@ rm -rf ~/Library/Application\ Support/Agents\ Dev/
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -kill -r -domain local -domain system -domain user

# 3. Clear app preferences
defaults delete dev.21st.agents.dev # Dev mode
defaults delete dev.21st.agents # Production
defaults delete dev.opencodex.desktop.dev # Dev mode
defaults delete dev.opencodex.desktop # Production

# 4. Run in dev mode with clean state
cd apps/desktop
Expand All @@ -178,16 +178,16 @@ bun run dev
- **Folder dialog not appearing**: Window focus timing issues on first launch. Fixed by ensuring window focus before showing `dialog.showOpenDialog()`.

**Dev vs Production App:**
- Dev mode uses `twentyfirst-agents-dev://` protocol
- Dev mode uses `opencodex-dev://` protocol
- Dev mode uses separate userData path (`~/Library/Application Support/Agents Dev/`)
- This prevents conflicts between dev and production installs

## Releasing a New Version

### Prerequisites for Notarization

- Keychain profile: `21st-notarize`
- Create with: `xcrun notarytool store-credentials "21st-notarize" --apple-id YOUR_APPLE_ID --team-id YOUR_TEAM_ID`
- Keychain profile: `opencodex-notarize`
- Create with: `xcrun notarytool store-credentials "opencodex-notarize" --apple-id YOUR_APPLE_ID --team-id YOUR_TEAM_ID`

### Release Commands

Expand All @@ -199,7 +199,8 @@ bun run release
bun run build # Compile TypeScript
bun run package:mac # Build & sign macOS app
bun run dist:manifest # Generate latest-mac.yml manifests
./scripts/upload-release-wrangler.sh # Submit notarization & upload to R2 CDN
bun run dist:doctor # Verify build deps plus OPENCODEX_WEB_URL / OPENCODEX_UPDATE_BASE_URL
bun run dist:upload # Print the OpenCodex upload plan for the configured update target
```

### Bump Version Before Release
Expand All @@ -210,30 +211,31 @@ npm version patch --no-git-tag-version # 0.0.27 → 0.0.28

### After Release Script Completes

1. Wait for notarization (2-5 min): `xcrun notarytool history --keychain-profile "21st-notarize"`
1. Wait for notarization (2-5 min): `xcrun notarytool history --keychain-profile "opencodex-notarize"`
2. Staple DMGs: `cd release && xcrun stapler staple *.dmg`
3. Re-upload stapled DMGs to R2 and GitHub (see RELEASE.md for commands)
4. Update changelog: `gh release edit v0.0.X --notes "..."`
5. **Upload manifests (triggers auto-updates!)** — see RELEASE.md
6. Sync to public: `./scripts/sync-to-public.sh`

### Files Uploaded to CDN
### Files Uploaded to Configured Update Target

| File | Purpose |
|------|---------|
| `latest-mac.yml` | Manifest for arm64 auto-updates |
| `latest-mac-x64.yml` | Manifest for Intel auto-updates |
| `1Code-{version}-arm64-mac.zip` | Auto-update payload (arm64) |
| `1Code-{version}-mac.zip` | Auto-update payload (Intel) |
| `1Code-{version}-arm64.dmg` | Manual download (arm64) |
| `1Code-{version}.dmg` | Manual download (Intel) |
| `OpenCodex-{version}-arm64-mac.zip` | Auto-update payload (arm64) |
| `OpenCodex-{version}-mac.zip` | Auto-update payload (Intel) |
| `OpenCodex-{version}-arm64.dmg` | Manual download (arm64) |
| `OpenCodex-{version}.dmg` | Manual download (Intel) |

### Auto-Update Flow

1. App checks `https://cdn.21st.dev/releases/desktop/latest-mac.yml` on startup and when window regains focus (with 1 min cooldown)
2. If version in manifest > current version, shows "Update Available" banner
3. User clicks Download → downloads ZIP in background
4. User clicks "Restart Now" → installs update and restarts
1. Release validation requires both `OPENCODEX_WEB_URL` and `OPENCODEX_UPDATE_BASE_URL`; packaged builds keep `.invalid` placeholders only as a non-ready safety fallback.
2. App checks the configured `OPENCODEX_UPDATE_BASE_URL` target for `latest-mac.yml` on startup and when window regains focus (with 1 min cooldown)
3. If version in manifest > current version, shows "Update Available" banner
4. User clicks Download → downloads ZIP in background
5. User clicks "Restart Now" → installs update and restarts

## Current Status (WIP)

Expand Down
9 changes: 6 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 22 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "21st-desktop",
"name": "opencodex-desktop",
"version": "0.0.72",
"private": true,
"description": "1Code - UI for parallel work with AI agents",
"homepage": "https://21st.dev",
"description": "OpenCodex - local-native desktop coding workstation",
"homepage": "",
"author": {
"name": "21st.dev",
"email": "support@21st.dev"
"name": "OpenCodex",
"email": "support@opencodex.local"
},
"main": "out/main/index.js",
"scripts": {
Expand All @@ -18,14 +18,15 @@
"package:win": "electron-builder --win",
"package:linux": "electron-builder --linux",
"dist": "electron-builder",
"dist:doctor": "node scripts/check-packaging-readiness.mjs",
"dist:manifest": "node scripts/generate-update-manifest.mjs",
"dist:upload": "node scripts/upload-release.mjs",
"claude:download": "node scripts/download-claude-binary.mjs --version=2.1.45",
"claude:download:all": "node scripts/download-claude-binary.mjs --version=2.1.45 --all",
"codex:download": "node scripts/download-codex-binary.mjs --version=0.98.0",
"codex:download:all": "node scripts/download-codex-binary.mjs --version=0.98.0 --all",
"release": "rm -rf release && bun i && bun run claude:download && bun run codex:download && bun run build && bun run package:mac && bun run dist:manifest && ./scripts/upload-release-wrangler.sh",
"release:dev": "rm -rf release && bun run claude:download && bun run codex:download && bun run build && bun run package:mac && rm -rf node_modules && bun i",
"release": "node scripts/clean-paths.mjs release && bun i && bun run dist:doctor && bun run claude:download && bun run codex:download && bun run build && bun run package:mac && bun run dist:manifest && bun run dist:upload",
"release:dev": "node scripts/clean-paths.mjs release node_modules && bun run claude:download && bun run codex:download && bun run build && bun run package:mac && bun i",
"sync:public": "./scripts/sync-to-public.sh",
"icon:generate": "node scripts/generate-icon.mjs",
"db:generate": "drizzle-kit generate",
Expand Down Expand Up @@ -140,14 +141,14 @@
"vite": "^6.3.4"
},
"build": {
"appId": "dev.21st.agents",
"productName": "1Code",
"appId": "dev.opencodex.desktop",
"productName": "OpenCodex",
"npmRebuild": true,
"protocols": [
{
"name": "1Code",
"name": "OpenCodex",
"schemes": [
"twentyfirst-agents"
"opencodex"
]
}
],
Expand All @@ -173,6 +174,14 @@
{
"from": "resources/bin/VERSION",
"to": "bin/VERSION"
},
{
"from": "resources/cli/opencodex",
"to": "cli/opencodex"
},
{
"from": "resources/cli/opencodex.cmd",
"to": "cli/opencodex.cmd"
}
],
"asar": true,
Expand Down Expand Up @@ -207,7 +216,7 @@
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.plist",
"extendInfo": {
"NSMicrophoneUsageDescription": "1Code needs microphone access for voice dictation"
"NSMicrophoneUsageDescription": "OpenCodex needs microphone access for voice dictation"
}
},
"dmg": {
Expand Down Expand Up @@ -253,7 +262,7 @@
},
"publish": {
"provider": "generic",
"url": "https://cdn.21st.dev/releases/desktop"
"url": "${env.OPENCODEX_UPDATE_BASE_URL}"
}
},
"pnpm": {
Expand Down
8 changes: 4 additions & 4 deletions resources/cli/1code → resources/cli/opencodex
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/bash
# 1code CLI launcher
# Opens 1Code app with the specified directory
# OpenCodex CLI launcher
# Opens OpenCodex with the specified directory

# Resolve the directory argument (default to current directory)
DIR="${1:-.}"
Expand All @@ -15,5 +15,5 @@ if [ -z "$DIR" ] || [ ! -d "$DIR" ]; then
exit 1
fi

# Open 1Code app with the directory argument
open -a "1Code" --args "$DIR"
# Open OpenCodex with the directory argument
open -a "OpenCodex" --args "$DIR"
18 changes: 18 additions & 0 deletions resources/cli/opencodex.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@echo off
setlocal
set "DIR=%~1"
if "%DIR%"=="" set "DIR=."

for %%I in ("%DIR%") do set "TARGET_DIR=%%~fI"
if not exist "%TARGET_DIR%\" (
echo Error: Invalid directory
exit /b 1
)

set "APP_EXE=%LOCALAPPDATA%\Programs\OpenCodex\OpenCodex.exe"
if not exist "%APP_EXE%" (
echo Error: OpenCodex.exe not found at "%APP_EXE%"
exit /b 1
)

start "" "%APP_EXE%" "%TARGET_DIR%"
23 changes: 23 additions & 0 deletions scripts/check-packaging-readiness.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env node

import path from "node:path"
import { fileURLToPath } from "node:url"
import { assessPackagingReadiness } from "./release-config.mjs"

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.join(__dirname, "..")
const readiness = assessPackagingReadiness({
env: process.env,
rootDir,
})

if (readiness.ok) {
console.log("OpenCodex packaging readiness: OK")
process.exit(0)
}

console.log("OpenCodex packaging readiness: BLOCKED")
for (const issue of readiness.issues) {
console.log(`- ${issue}`)
}
process.exit(1)
28 changes: 28 additions & 0 deletions scripts/clean-paths.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env node

import fs from "node:fs"
import path from "node:path"
import { fileURLToPath } from "node:url"

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.join(__dirname, "..")
const targets = process.argv.slice(2)

if (targets.length === 0) {
console.error("Provide one or more relative paths to clean.")
process.exit(1)
}

for (const target of targets) {
const normalizedTarget = path.normalize(target)
if (
path.isAbsolute(normalizedTarget) ||
normalizedTarget === ".." ||
normalizedTarget.startsWith(`..${path.sep}`)
) {
console.error(`Refusing to clean unsafe path: ${target}`)
process.exit(1)
}

fs.rmSync(path.join(rootDir, normalizedTarget), { recursive: true, force: true })
}
15 changes: 12 additions & 3 deletions scripts/download-claude-binary.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { fileURLToPath } from "node:url"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const ROOT_DIR = path.join(__dirname, "..")
const BIN_DIR = path.join(ROOT_DIR, "resources", "bin")
const USER_AGENT = "opencodex-desktop-claude-downloader"

// Claude Code distribution base URL
const DIST_BASE =
Expand All @@ -36,10 +37,16 @@ const PLATFORMS = {
/**
* Fetch JSON from URL
*/
function getRequestHeaders() {
return {
"User-Agent": USER_AGENT,
}
}

function fetchJson(url) {
return new Promise((resolve, reject) => {
https
.get(url, (res) => {
.get(url, { headers: getRequestHeaders() }, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
return fetchJson(res.headers.location).then(resolve).catch(reject)
}
Expand All @@ -64,7 +71,7 @@ function downloadFile(url, destPath) {

const request = (url) => {
https
.get(url, (res) => {
.get(url, { headers: getRequestHeaders() }, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
file.close()
fs.unlinkSync(destPath)
Expand Down Expand Up @@ -136,7 +143,9 @@ async function getLatestVersion() {

try {
// Fetch from the same endpoint that install.sh uses
const response = await fetch("https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest")
const response = await fetch("https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest", {
headers: getRequestHeaders(),
})
if (response.ok) {
const version = await response.text()
return version.trim()
Expand Down
2 changes: 1 addition & 1 deletion scripts/download-codex-binary.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const BIN_DIR = path.join(ROOT_DIR, "resources", "bin")

const RELEASE_REPO = "openai/codex"
const RELEASE_TAG_PREFIX = "rust-v"
const USER_AGENT = "21st-desktop-codex-downloader"
const USER_AGENT = "opencodex-desktop-codex-downloader"

const PLATFORMS = {
"darwin-arm64": {
Expand Down
Loading