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
3 changes: 2 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"@react-grab/ami",
"@react-grab/cursor",
"@react-grab/opencode",
"@react-grab/cli"
"@react-grab/cli",
"@react-grab/codex"
]
],
"access": "public",
Expand Down
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,56 @@ export default function RootLayout({ children }) {
}
```

### Codex CLI

#### Server Setup

The server runs on port `6567` and interfaces with the Codex CLI. Add to your `package.json`:

```json
{
"scripts": {
"dev": "npx @react-grab/codex@latest && next dev"
}
}
```

#### Client Setup

```html
<script src="//unpkg.com/react-grab/dist/index.global.js"></script>
<!-- add this in the <head> -->
<script src="//unpkg.com/@react-grab/codex/dist/client.global.js"></script>
```

Or using Next.js `Script` component in your `app/layout.tsx`:

```jsx
import Script from "next/script";

export default function RootLayout({ children }) {
return (
<html>
<head>
{process.env.NODE_ENV === "development" && (
<>
<Script
src="//unpkg.com/react-grab/dist/index.global.js"
strategy="beforeInteractive"
/>
<Script
src="//unpkg.com/@react-grab/codex/dist/client.global.js"
strategy="lazyOnload"
/>
</>
)}
</head>
<body>{children}</body>
</html>
);
}
```

### Opencode

#### Server Setup
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"private": true,
"type": "module",
"scripts": {
"build": "turbo run build --filter=react-grab --filter=@react-grab/cursor --filter=@react-grab/claude-code --filter=@react-grab/ami --filter=@react-grab/opencode --filter=@react-grab/cli",
"dev": "turbo dev --filter=react-grab --filter=@react-grab/cursor --filter=@react-grab/claude-code --filter=@react-grab/ami --filter=@react-grab/opencode --filter=@react-grab/cli",
"build": "turbo run build --filter=react-grab --filter=@react-grab/cursor --filter=@react-grab/claude-code --filter=@react-grab/ami --filter=@react-grab/opencode --filter=@react-grab/cli --filter=@react-grab/codex",
"dev": "turbo dev --filter=react-grab --filter=@react-grab/cursor --filter=@react-grab/claude-code --filter=@react-grab/ami --filter=@react-grab/opencode --filter=@react-grab/cli --filter=@react-grab/codex",
"lint": "pnpm --filter react-grab lint",
"lint:fix": "pnpm --filter react-grab lint:fix",
"format": "prettier --write .",
Expand All @@ -19,6 +19,7 @@
},
"devDependencies": {
"@changesets/cli": "^2.27.10",
"@types/node": "20.19.23",
"prettier": "^3.4.2",
"turbo": "^2.3.3"
},
Expand Down
1 change: 1 addition & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Patch Changes

- fix: ux nits
- feat: add Codex agent integration to CLI

## 0.0.70

Expand Down
6 changes: 5 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ npx @react-grab/cli -p pnpm -a claude-code -y
| `--framework` | `-f` | Framework to configure | `next`, `vite`, `webpack` |
| `--package-manager` | `-p` | Package manager to use | `npm`, `yarn`, `pnpm`, `bun` |
| `--router` | `-r` | Next.js router type | `app`, `pages` |
| `--agent` | `-a` | Agent integration to add | `claude-code`, `cursor`, `opencode`, `none` |
| `--agent` | `-a` | Agent integration to add | `claude-code`, `cursor`, `codex`, `opencode`, `none` |
| `--yes` | `-y` | Skip all confirmation prompts | - |
| `--skip-install` | - | Skip package installation (only modify files) | - |
| `--help` | `-h` | Show help | - |
Expand All @@ -56,6 +56,9 @@ npx @react-grab/cli -y
# Next.js App Router with Cursor agent
npx @react-grab/cli -f next -r app -a cursor -y

# Next.js App Router with Codex agent
npx @react-grab/cli -f next -r app -a codex -y

# Vite with Claude Code agent using pnpm
npx @react-grab/cli -f vite -p pnpm -a claude-code -y

Expand All @@ -81,6 +84,7 @@ The CLI can optionally set up agent integrations for:

- **Claude Code** (`-a claude-code`) - Send selected elements to Claude Code
- **Cursor** (`-a cursor`) - Send selected elements to Cursor
- **Codex** (`-a codex`) - Send selected elements to Codex
- **Opencode** (`-a opencode`) - Send selected elements to Opencode

## Manual Installation
Expand Down
11 changes: 9 additions & 2 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const PACKAGE_MANAGER_NAMES: Record<PackageManager, string> = {
const AGENT_NAMES: Record<string, string> = {
"claude-code": "Claude Code",
cursor: "Cursor",
codex: "Codex",
opencode: "Opencode",
};

Expand Down Expand Up @@ -258,7 +259,7 @@ const parseArgs = async (): Promise<CliArgs> => {
.option("agent", {
alias: "a",
type: "string",
choices: ["claude-code", "cursor", "opencode", "none"] as const,
choices: ["claude-code", "cursor", "opencode", "codex", "none"] as const,
description:
"Agent integration to automatically forward selected elements to agent instead of copying to clipboard",
})
Expand All @@ -284,6 +285,10 @@ const parseArgs = async (): Promise<CliArgs> => {
"$0 -a cursor -y",
"Add Cursor agent integration non-interactively",
)
.example(
"$0 -a codex -y",
"Add Codex agent integration non-interactively",
)
.example("$0 -p pnpm -a claude-code", "Use pnpm and add Claude Code agent")
.example(
"$0 --skip-install",
Expand All @@ -293,6 +298,7 @@ const parseArgs = async (): Promise<CliArgs> => {
`${pc.bold("Agent Integrations:")}\n` +
` ${pc.cyan("claude-code")} Connect React Grab to Claude Code\n` +
` ${pc.cyan("cursor")} Connect React Grab to Cursor IDE\n` +
` ${pc.cyan("codex")} Connect React Grab to Codex CLI\n` +
` ${pc.cyan("opencode")} Connect React Grab to Opencode\n\n` +
`${pc.bold("Supported Frameworks:")}\n` +
` ${pc.cyan("next")} Next.js (App Router & Pages Router)\n` +
Expand Down Expand Up @@ -496,6 +502,7 @@ const main = async () => {
const availableAgents = [
{ name: "Claude Code", value: "claude-code" as const },
{ name: "Cursor", value: "cursor" as const },
{ name: "Codex", value: "codex" as const },
{ name: "Opencode", value: "opencode" as const },
].filter((agent) => !projectInfo.installedAgents.includes(agent.value));

Expand All @@ -511,7 +518,7 @@ const main = async () => {
} else {
const wantAgentIntegration = await confirm({
message:
"Do you want to add an agent integration (Claude Code, Cursor, or Opencode)?",
"Do you want to add an agent integration (Claude Code, Cursor, Codex, or Opencode)?",
default: false,
});

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ export const detectReactGrab = (projectRoot: string): boolean => {
return filesToCheck.some(hasReactGrabInFile);
};

const AGENT_PACKAGES = ["@react-grab/claude-code", "@react-grab/cursor", "@react-grab/opencode"];
const AGENT_PACKAGES = ["@react-grab/claude-code", "@react-grab/cursor", "@react-grab/opencode", "@react-grab/codex"];

export const detectUnsupportedFramework = (projectRoot: string): UnsupportedFramework => {
const packageJsonPath = join(projectRoot, "package.json");
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/templates.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type AgentIntegration = "claude-code" | "cursor" | "opencode" | "none";
export type AgentIntegration = "claude-code" | "cursor" | "opencode" | "codex" | "none";

export const NEXT_APP_ROUTER_SCRIPT = `{process.env.NODE_ENV === "development" && (
<Script
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ export const transformProject = (
const AGENT_PREFIXES: Record<string, string> = {
"claude-code": "npx @react-grab/claude-code@latest &&",
cursor: "npx @react-grab/cursor@latest &&",
codex: "npx @react-grab/codex@latest &&",
opencode: "npx @react-grab/opencode@latest &&",
};

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/test/detect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,13 +224,15 @@ describe("detectInstalledAgents", () => {
devDependencies: {
"@react-grab/cursor": "1.0.0",
"@react-grab/claude-code": "1.0.0",
"@react-grab/codex": "1.0.0",
},
})
);

const agents = detectInstalledAgents("/test");
expect(agents).toContain("cursor");
expect(agents).toContain("claude-code");
expect(agents).toContain("codex");
expect(agents).not.toContain("opencode");
});

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe("getPackagesToInstall", () => {
});

it("should handle all agent types", () => {
const agents = ["claude-code", "cursor", "opencode"] as const;
const agents = ["claude-code", "cursor", "opencode", "codex"] as const;

for (const agent of agents) {
const packages = getPackagesToInstall(agent, false);
Expand Down
9 changes: 8 additions & 1 deletion packages/cli/test/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe("Next.js App Router templates", () => {
});

it("should include all agent types correctly", () => {
const agents = ["claude-code", "cursor", "opencode"] as const;
const agents = ["claude-code", "cursor", "opencode", "codex"] as const;

for (const agent of agents) {
const script = NEXT_APP_ROUTER_SCRIPT_WITH_AGENT(agent);
Expand All @@ -54,6 +54,13 @@ describe("Vite templates", () => {
expect(script).toContain("@react-grab/opencode");
});

it("should generate script with codex agent", () => {
const script = VITE_SCRIPT_WITH_AGENT("codex");

expect(script).toContain("react-grab");
expect(script).toContain("@react-grab/codex");
});

it("should return basic script when agent is none", () => {
const script = VITE_SCRIPT_WITH_AGENT("none");

Expand Down
21 changes: 21 additions & 0 deletions packages/cli/test/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,17 @@ describe("previewTransform - Vite", () => {
expect(result.newContent).toContain("@react-grab/opencode");
});

it("should add React Grab with codex agent to index.html", () => {
mockExistsSync.mockImplementation((path) => String(path).endsWith("index.html"));
mockReadFileSync.mockReturnValue(indexContent);

const result = previewTransform("/test", "vite", "unknown", "codex", false);

expect(result.success).toBe(true);
expect(result.newContent).toContain("react-grab");
expect(result.newContent).toContain("@react-grab/codex");
});

it("should add agent to existing React Grab installation", () => {
const indexWithReactGrab = `<!doctype html>
<html lang="en">
Expand Down Expand Up @@ -500,6 +511,16 @@ describe("previewPackageJsonTransform", () => {
expect(result.newContent).toContain("npx @react-grab/opencode@latest &&");
});

it("should add codex prefix to dev script", () => {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(packageJsonContent);

const result = previewPackageJsonTransform("/test", "codex", []);

expect(result.success).toBe(true);
expect(result.newContent).toContain("npx @react-grab/codex@latest &&");
});

it("should skip when agent is none", () => {
const result = previewPackageJsonTransform("/test", "none", []);

Expand Down
1 change: 1 addition & 0 deletions packages/react-grab-claude-code/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["node"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
Expand Down
4 changes: 4 additions & 0 deletions packages/react-grab-codex/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Changelog

## 0.0.67
- Initial Codex bridge package.
24 changes: 24 additions & 0 deletions packages/react-grab-codex/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# @react-grab/codex

Codex CLI bridge for React Grab. It spawns `codex exec --json -`, streams Codex events to SSE, and provides a browser client that plugs into React Grab.

## Getting started

1. Install the Codex CLI and ensure `codex` is on your PATH.
2. Install the package: `pnpm add @react-grab/codex`.
3. Start the local bridge (default port 6567): `npx react-grab-codex`.
4. Include the client in your page or extension:
```ts
import { attachAgent } from "@react-grab/codex/client";
attachAgent();
```

## Options

- `workspace` (default: current working directory) via `--cd`.
- `model` passed to `codex exec`.
- `fullAuto` defaults to `true`; set `false` to disable.
- `yolo` opt-in flag.
- `sandbox`, `profile`, `skipGitRepoCheck`, `config[]`, `outputSchemaPath` forwarded when provided.

Resume is supported through stored React Grab sessions (replays context), not Codex thread resume.
38 changes: 38 additions & 0 deletions packages/react-grab-codex/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@react-grab/codex",
"version": "0.0.67",
"type": "module",
"bin": {
"react-grab-codex": "./dist/cli.js"
},
"exports": {
"./client": {
"types": "./dist/client.d.ts",
"import": "./dist/client.js",
"require": "./dist/client.cjs"
},
"./server": {
"types": "./dist/server.d.ts",
"import": "./dist/server.js",
"require": "./dist/server.cjs"
},
"./dist/*": "./dist/*.js",
"./dist/*.js": "./dist/*.js"
},
"browser": "dist/client.global.js",
"files": [
"dist"
],
"scripts": {
"dev": "tsup --watch",
"build": "rm -rf dist && NODE_ENV=production tsup"
},
"devDependencies": {
"tsup": "^8.4.0"
},
"dependencies": {
"@hono/node-server": "^1.19.6",
"hono": "^4.0.0",
"react-grab": "workspace:*"
}
}
15 changes: 15 additions & 0 deletions packages/react-grab-codex/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const serverPath = join(__dirname, "server.js");

spawn(process.execPath, [serverPath], {
detached: true,
stdio: "ignore",
}).unref();

console.log("[React Grab] Server starting on port 6567...");
Loading