Skip to content
Merged
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
84 changes: 0 additions & 84 deletions .github/workflows/release-sync.yml

This file was deleted.

2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

- Initial public release of the Claude Code plugin for Codex.
- Includes tracked review, adversarial review, rescue, status, result, cancel, and setup flows.
- Includes Codex hook integration, release CI, and marketplace sync automation.
- Includes Codex hook integration and plugin installer automation.
76 changes: 29 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,74 +31,56 @@ The goal is to stay close to the upstream OpenAI plugin's UX, but Claude Code an
- Claude Code CLI installed and authenticated
- `claude auth login`, or
- `ANTHROPIC_API_KEY` set in the environment
- either the hosted Sendbird Codex marketplace or a local plugin registration

## Install

### Hosted Marketplace Install
### Quick Install

Add this GitHub repository as a repo marketplace in Codex:

```text
https://github.com/sendbird/codex-plugins
```bash
curl -fsSL "https://raw.githubusercontent.com/sendbird/cc-plugin-codex/main/scripts/install.sh" | bash
```

Then install:

```text
cc@sendbird-codex
```
The installer:

This repository, `sendbird/cc-plugin-codex`, is the plugin source repository used for development and releases.
- copies the plugin into `~/.codex/plugins/cc`
- creates or updates `~/.agents/plugins/marketplace.json`
- enables `cc@local-plugins` in `~/.codex/config.toml`
- enables `codex_hooks = true`
- installs Codex hooks and the global `cc-rescue` agent

### Local Development Install
### Update

Clone this repository under `~/plugins/cc`:
Rerun the install command. The installer is safe to run again and will refresh the installed copy in place.

```bash
mkdir -p ~/plugins
git clone https://github.com/sendbird/cc-plugin-codex.git ~/plugins/cc
cd ~/plugins/cc
curl -fsSL "https://raw.githubusercontent.com/sendbird/cc-plugin-codex/main/scripts/install.sh" | bash
```

Add it to your local Codex marketplace file at `~/.agents/plugins/marketplace.json`:

```json
{
"name": "sendbird-local",
"interface": {
"displayName": "Sendbird Local Plugins"
},
"plugins": [
{
"name": "cc",
"source": {
"source": "local",
"path": "./plugins/cc"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_USE"
},
"category": "Coding"
}
]
}
### Uninstall

```bash
curl -fsSL "https://raw.githubusercontent.com/sendbird/cc-plugin-codex/main/scripts/uninstall.sh" | bash
```

The plugin path is relative to the marketplace root, matching the current Codex marketplace spec.
### npx

Install hooks and the global `cc-rescue` agent:
If you install the npm package distribution, the same installer is exposed through the package CLI:

```bash
node scripts/install-hooks.mjs
npx cc-plugin-codex install
npx cc-plugin-codex update
npx cc-plugin-codex uninstall
```

Enable Codex hooks in `~/.codex/config.toml`:
### Manual Install

```toml
[features]
codex_hooks = true
If you want to work from a local checkout instead of the one-shot installer:

```bash
mkdir -p ~/.codex/plugins
git clone https://github.com/sendbird/cc-plugin-codex.git ~/.codex/plugins/cc
cd ~/.codex/plugins/cc
node scripts/local-plugin-install.mjs install --plugin-root ~/.codex/plugins/cc
```

If Claude Code is not installed yet:
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
"type": "git",
"url": "https://github.com/sendbird/cc-plugin-codex.git"
},
"bin": {
"cc-plugin-codex": "./scripts/installer-cli.mjs"
},
"bugs": {
"url": "https://github.com/sendbird/cc-plugin-codex/issues"
},
Expand All @@ -24,10 +27,13 @@
"lint": "eslint .",
"typecheck": "tsc -p tsconfig.json",
"check": "npm run lint && npm run typecheck && npm run test && npm run test:integration && npm run test:e2e",
"install:codex": "node scripts/installer-cli.mjs install",
"setup:git-hooks": "node scripts/setup-git-hooks.mjs",
"test": "node --test tests/*.test.mjs",
"test:integration": "node --test tests/integration/*.test.mjs",
"test:e2e": "node --test tests/e2e/*.test.mjs"
"test:e2e": "node --test tests/e2e/*.test.mjs",
"uninstall:codex": "node scripts/installer-cli.mjs uninstall",
"update:codex": "node scripts/installer-cli.mjs update"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
Expand Down
24 changes: 24 additions & 0 deletions scripts/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash

# Copyright 2026 Sendbird, Inc.
# SPDX-License-Identifier: Apache-2.0

set -euo pipefail

require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Missing required command: $1" >&2
exit 1
fi
}

require_cmd curl
require_cmd tar
require_cmd node

REPO_TARBALL_URL="${CC_PLUGIN_CODEX_TARBALL_URL:-https://github.com/sendbird/cc-plugin-codex/archive/refs/heads/main.tar.gz}"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT

curl -fsSL "$REPO_TARBALL_URL" | tar xz -C "$TMP_DIR" --strip-components=1
node "$TMP_DIR/scripts/installer-cli.mjs" install
136 changes: 136 additions & 0 deletions scripts/installer-cli.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env node

/**
* Copyright 2026 Sendbird, Inc.
* SPDX-License-Identifier: Apache-2.0
*/

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
import { resolveCodexHome } from "./lib/codex-paths.mjs";

const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const PACKAGE_ROOT = path.resolve(SCRIPT_DIR, "..");
const CODEX_HOME = resolveCodexHome();
const INSTALL_DIR = path.join(CODEX_HOME, "plugins", "cc");
const INCLUDED_PATHS = [
".codex-plugin",
"CHANGELOG.md",
"LICENSE",
"NOTICE",
"README.md",
"agents",
"assets",
"hooks",
"internal-skills",
"package.json",
"prompts",
"schemas",
"scripts",
"skills",
];

function usage() {
console.error("Usage: cc-plugin-codex <install|update|uninstall>");
process.exit(1);
}

function parseArgs(argv) {
const [command] = argv;
if (!command || !["install", "update", "uninstall"].includes(command)) {
usage();
}
return { command };
}

function samePath(a, b) {
return path.resolve(a) === path.resolve(b);
}

function ensureEmptyDir(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
fs.mkdirSync(dirPath, { recursive: true });
}

function copyDistribution(sourceRoot, destinationRoot) {
ensureEmptyDir(destinationRoot);
for (const relativePath of INCLUDED_PATHS) {
const sourcePath = path.join(sourceRoot, relativePath);
if (!fs.existsSync(sourcePath)) {
continue;
}
const destinationPath = path.join(destinationRoot, relativePath);
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.cpSync(sourcePath, destinationPath, { recursive: true });
}
}

function stageInstall(sourceRoot, installDir) {
if (samePath(sourceRoot, installDir)) {
return;
}

const stagingParent = path.dirname(installDir);
fs.mkdirSync(stagingParent, { recursive: true });
const stagingDir = fs.mkdtempSync(path.join(stagingParent, ".cc-staging-"));

try {
copyDistribution(sourceRoot, stagingDir);
fs.rmSync(installDir, { recursive: true, force: true });
fs.renameSync(stagingDir, installDir);
} catch (error) {
fs.rmSync(stagingDir, { recursive: true, force: true });
throw error;
}
}

function runLocalInstaller(installDir, command) {
const installerPath = path.join(installDir, "scripts", "local-plugin-install.mjs");
const result = spawnSync(process.execPath, [installerPath, command, "--plugin-root", installDir], {
stdio: "inherit",
env: {
...process.env,
HOME: process.env.HOME || os.homedir(),
USERPROFILE: process.env.USERPROFILE || process.env.HOME || os.homedir(),
},
});

if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}

function installOrUpdate() {
stageInstall(PACKAGE_ROOT, INSTALL_DIR);
runLocalInstaller(INSTALL_DIR, "install");
console.log(`Plugin files installed to ${INSTALL_DIR}`);
}

function uninstall() {
if (fs.existsSync(path.join(INSTALL_DIR, "scripts", "local-plugin-install.mjs"))) {
runLocalInstaller(INSTALL_DIR, "uninstall");
} else if (fs.existsSync(PACKAGE_ROOT)) {
// Uninstall can still run from a package tarball even if the installed tree is gone.
stageInstall(PACKAGE_ROOT, INSTALL_DIR);
runLocalInstaller(INSTALL_DIR, "uninstall");
}

fs.rmSync(INSTALL_DIR, { recursive: true, force: true });
const pluginsDir = path.dirname(INSTALL_DIR);
if (fs.existsSync(pluginsDir) && fs.readdirSync(pluginsDir).length === 0) {
fs.rmdirSync(pluginsDir);
}
console.log(`Plugin files removed from ${INSTALL_DIR}`);
}

const { command } = parseArgs(process.argv.slice(2));

if (command === "install" || command === "update") {
installOrUpdate();
} else {
uninstall();
}
Loading