From 7ac75200a5b768a441afd08ce7233f15290c6879 Mon Sep 17 00:00:00 2001 From: Vladislav Gapurov Date: Thu, 4 Dec 2025 23:22:29 +0100 Subject: [PATCH 01/12] feat: add codex package --- package.json | 4 +- packages/react-grab-codex/CHANGELOG.md | 4 + packages/react-grab-codex/README.md | 24 ++ packages/react-grab-codex/package.json | 38 +++ packages/react-grab-codex/src/cli.ts | 15 + packages/react-grab-codex/src/client.ts | 228 +++++++++++++ packages/react-grab-codex/src/constants.ts | 1 + packages/react-grab-codex/src/server.ts | 352 +++++++++++++++++++++ packages/react-grab-codex/tsconfig.json | 16 + packages/react-grab-codex/tsup.config.ts | 47 +++ pnpm-lock.yaml | 16 + 11 files changed, 743 insertions(+), 2 deletions(-) create mode 100644 packages/react-grab-codex/CHANGELOG.md create mode 100644 packages/react-grab-codex/README.md create mode 100644 packages/react-grab-codex/package.json create mode 100644 packages/react-grab-codex/src/cli.ts create mode 100644 packages/react-grab-codex/src/client.ts create mode 100644 packages/react-grab-codex/src/constants.ts create mode 100644 packages/react-grab-codex/src/server.ts create mode 100644 packages/react-grab-codex/tsconfig.json create mode 100644 packages/react-grab-codex/tsup.config.ts diff --git a/package.json b/package.json index ac529d15..11a325a8 100644 --- a/package.json +++ b/package.json @@ -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 .", diff --git a/packages/react-grab-codex/CHANGELOG.md b/packages/react-grab-codex/CHANGELOG.md new file mode 100644 index 00000000..490ecda4 --- /dev/null +++ b/packages/react-grab-codex/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +## 0.0.67 +- Initial Codex bridge package. \ No newline at end of file diff --git a/packages/react-grab-codex/README.md b/packages/react-grab-codex/README.md new file mode 100644 index 00000000..13c58bcf --- /dev/null +++ b/packages/react-grab-codex/README.md @@ -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. \ No newline at end of file diff --git a/packages/react-grab-codex/package.json b/packages/react-grab-codex/package.json new file mode 100644 index 00000000..8eda8365 --- /dev/null +++ b/packages/react-grab-codex/package.json @@ -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:*" + } +} \ No newline at end of file diff --git a/packages/react-grab-codex/src/cli.ts b/packages/react-grab-codex/src/cli.ts new file mode 100644 index 00000000..8c4ba668 --- /dev/null +++ b/packages/react-grab-codex/src/cli.ts @@ -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..."); \ No newline at end of file diff --git a/packages/react-grab-codex/src/client.ts b/packages/react-grab-codex/src/client.ts new file mode 100644 index 00000000..02f75469 --- /dev/null +++ b/packages/react-grab-codex/src/client.ts @@ -0,0 +1,228 @@ +import type { + AgentContext, + AgentProvider, + AgentSession, + AgentSessionStorage, + init, + ReactGrabAPI, +} from "react-grab/core"; +import { DEFAULT_PORT } from "./constants.js"; + +const DEFAULT_SERVER_URL = `http://localhost:${DEFAULT_PORT}`; +const STORAGE_KEY = "react-grab:agent-sessions"; + +interface CodexAgentOptions { + model?: string; + workspace?: string; + sandbox?: "read-only" | "workspace-write" | "danger-full-access"; + fullAuto?: boolean; + yolo?: boolean; + profile?: string; + skipGitRepoCheck?: boolean; + config?: string[]; + outputSchemaPath?: string; +} + +interface CodexAgentContext extends AgentContext {} + +interface CodexAgentProviderOptions { + serverUrl?: string; + getOptions?: () => Partial; +} + +interface StoredSessions { + [sessionId: string]: AgentSession; +} + +interface SSEEvent { + eventType: string; + data: string; +} + +const parseSSEEvent = (eventBlock: string): SSEEvent => { + let eventType = ""; + let data = ""; + for (const line of eventBlock.split("\n")) { + if (line.startsWith("event:")) { + eventType = line.slice(6).trim(); + } else if (line.startsWith("data:")) { + data = line.slice(5).trim(); + } + } + return { eventType, data }; +}; + +const createSSEStream = (stream: ReadableStream) => { + const iterate = async function* (): AsyncGenerator { + const reader = stream.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + try { + while (true) { + const { done, value } = await reader.read(); + if (value) { + buffer += decoder.decode(value, { stream: true }); + } + + let boundary = buffer.indexOf("\n\n"); + while (boundary !== -1) { + const { eventType, data } = parseSSEEvent(buffer.slice(0, boundary)); + buffer = buffer.slice(boundary + 2); + + if (eventType === "done") { + return; + } + if (eventType === "error") { + throw new Error(data || "Agent error"); + } + if (data) { + yield data; + } + + boundary = buffer.indexOf("\n\n"); + } + + if (done) { + break; + } + } + } finally { + reader.releaseLock(); + } + }; + + return iterate(); +}; + +const streamFromServer = ( + serverUrl: string, + context: CodexAgentContext, + signal: AbortSignal, +) => { + const iterate = async function* (): AsyncGenerator { + const response = await fetch(`${serverUrl}/agent`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(context), + signal, + }); + + if (!response.ok) { + throw new Error(`Server error: ${response.status}`); + } + + if (!response.body) { + throw new Error("No response body"); + } + + const stream = createSSEStream(response.body); + for await (const chunk of stream) { + yield chunk; + } + }; + + return iterate(); +}; + +export const createCodexAgentProvider = ( + providerOptions: CodexAgentProviderOptions = {}, +): AgentProvider => { + const { serverUrl = DEFAULT_SERVER_URL, getOptions } = providerOptions; + + const mergeOptions = (contextOptions: unknown): CodexAgentOptions => { + const merged: CodexAgentOptions = { fullAuto: true }; + + const providerOptionValues = getOptions ? getOptions() : undefined; + if (providerOptionValues && typeof providerOptionValues === "object") { + Object.assign(merged, providerOptionValues); + } + + if (contextOptions && typeof contextOptions === "object") { + Object.assign(merged, contextOptions); + } + + return merged; + }; + + const send = ( + context: CodexAgentContext, + signal: AbortSignal, + ): AsyncIterable => { + const mergedContext: CodexAgentContext = { + ...context, + options: mergeOptions(context.options), + }; + return streamFromServer(serverUrl, mergedContext, signal); + }; + + const resume = ( + sessionId: string, + signal: AbortSignal, + storage: AgentSessionStorage, + ): AsyncIterable => { + const iterate = async function* (): AsyncGenerator { + const savedSessions = storage.getItem(STORAGE_KEY); + if (!savedSessions) { + throw new Error("No sessions to resume"); + } + + const sessionsObject: StoredSessions = JSON.parse(savedSessions); + const session = sessionsObject[sessionId]; + if (!session) { + throw new Error(`Session ${sessionId} not found`); + } + + const sessionContext = session.context; + const mergedContext: CodexAgentContext = { + content: sessionContext.content, + prompt: sessionContext.prompt, + options: mergeOptions(sessionContext.options), + }; + + yield "Resuming..."; + const stream = streamFromServer(serverUrl, mergedContext, signal); + for await (const chunk of stream) { + yield chunk; + } + }; + + return iterate(); + }; + + return { + send, + resume, + supportsResume: true, + }; +}; + +declare global { + interface Window { + __REACT_GRAB__?: ReturnType; + } +} + +export const attachAgent = async () => { + if (typeof window === "undefined") { + return; + } + + const provider = createCodexAgentProvider(); + + const api = window.__REACT_GRAB__; + if (api) { + api.setAgent({ provider, storage: sessionStorage }); + return; + } + + const handleInit = (event: Event & { detail?: ReactGrabAPI }) => { + if (event.detail) { + event.detail.setAgent({ provider, storage: sessionStorage }); + } + }; + + window.addEventListener("react-grab:init", handleInit, { once: true }); +}; + +attachAgent(); \ No newline at end of file diff --git a/packages/react-grab-codex/src/constants.ts b/packages/react-grab-codex/src/constants.ts new file mode 100644 index 00000000..6551e7dd --- /dev/null +++ b/packages/react-grab-codex/src/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_PORT = 6567; \ No newline at end of file diff --git a/packages/react-grab-codex/src/server.ts b/packages/react-grab-codex/src/server.ts new file mode 100644 index 00000000..27d19174 --- /dev/null +++ b/packages/react-grab-codex/src/server.ts @@ -0,0 +1,352 @@ +import { spawn } from "node:child_process"; +import net from "node:net"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { streamSSE } from "hono/streaming"; +import { serve } from "@hono/node-server"; +import type { AgentContext } from "react-grab/core"; +import { DEFAULT_PORT } from "./constants.js"; + +interface CodexAgentOptions { + model?: string; + workspace?: string; + sandbox?: "read-only" | "workspace-write" | "danger-full-access"; + fullAuto?: boolean; + yolo?: boolean; + profile?: string; + skipGitRepoCheck?: boolean; + config?: string[]; + outputSchemaPath?: string; +} + +interface CodexAgentContext extends AgentContext {} + +interface CodexStreamContentBlock { + type?: string; + text?: string; +} + +interface CodexStreamMessage { + content?: CodexStreamContentBlock[]; +} + +interface CodexStreamItem { + type?: string; + status?: string; + text?: string; + content?: CodexStreamContentBlock[]; + command?: string; + output?: string; + path?: string; + summary?: string; +} + +interface CodexStreamError { + message?: string; + type?: string; +} + +interface CodexStreamEvent { + type?: string; + subtype?: string; + message?: CodexStreamMessage; + item?: CodexStreamItem; + result?: string; + error?: CodexStreamError; + is_error?: boolean; +} + +const isCodexStreamEvent = (value: unknown): value is CodexStreamEvent => + typeof value === "object" && value !== null; + +const normalizeStreamEvent = (event: CodexStreamEvent): CodexStreamEvent => { + if (event.type && event.type.includes(".")) { + const [baseType, derivedSubtype] = event.type.split(".", 2); + return { + ...event, + type: baseType, + subtype: event.subtype ?? derivedSubtype, + }; + } + return event; +}; + +const parseStreamLine = (line: string): CodexStreamEvent | null => { + const trimmed = line.trim(); + if (!trimmed) return null; + + try { + const parsed = JSON.parse(trimmed); + if (isCodexStreamEvent(parsed)) { + return normalizeStreamEvent(parsed); + } + return null; + } catch { + return null; + } +}; + +const collectText = (blocks?: CodexStreamContentBlock[]): string => { + if (!blocks) return ""; + return blocks + .filter( + (block) => + block !== undefined && + block.type === "text" && + typeof block.text === "string", + ) + .map((block) => block.text || "") + .join(" ") + .trim(); +}; + +const deriveItemStatus = (item: CodexStreamItem): string => { + if (item.type === "reasoning") { + return item.text || collectText(item.content); + } + if (item.type === "agent_message") { + return item.text || collectText(item.content); + } + if (item.type === "command_execution") { + if (item.status === "started") { + const commandText = item.command || item.text || item.summary || "Command"; + return `Running: ${commandText}`; + } + if (item.status === "completed") { + return "Command finished"; + } + const progress = item.text || collectText(item.content); + if (progress) { + return progress; + } + if (item.status) { + return `Command ${item.status}`; + } + } + if (item.type === "file_change") { + const pathText = + item.path || item.summary || item.text || collectText(item.content); + return pathText ? `Editing: ${pathText}` : "Editing file"; + } + if (item.type === "todo_list") { + const todoText = item.text || collectText(item.content) || "Updated tasks"; + return `Plan updated: ${todoText}`; + } + return item.text || collectText(item.content); +}; + +export const createServer = () => { + const app = new Hono(); + + app.use("/*", cors()); + + app.post("/agent", async (context) => { + const body = await context.req.json(); + const { content, prompt, options } = body; + const fullPrompt = `${prompt}\n\n${content}`; + + return streamSSE(context, async (stream) => { + const codexArgs = ["exec", "--json", "--color", "never", "-"]; + const workspace = options?.workspace || process.cwd(); + + codexArgs.push("--cd", workspace); + + if (options?.model) { + codexArgs.push("--model", options.model); + } + if (options?.sandbox) { + codexArgs.push("--sandbox", options.sandbox); + } + if (options?.fullAuto !== false) { + codexArgs.push("--full-auto"); + } + if (options?.yolo) { + codexArgs.push("--yolo"); + } + if (options?.profile) { + codexArgs.push("--profile", options.profile); + } + if (options?.skipGitRepoCheck) { + codexArgs.push("--skip-git-repo-check"); + } + if (options?.config && Array.isArray(options.config)) { + for (const entry of options.config) { + codexArgs.push("--config", entry); + } + } + if (options?.outputSchemaPath) { + codexArgs.push("--output-schema", options.outputSchemaPath); + } + + try { + await stream.writeSSE({ data: "Planning next moves", event: "status" }); + + const codexProcess = spawn("codex", codexArgs, { + stdio: ["pipe", "pipe", "pipe"], + cwd: workspace, + env: { ...process.env }, + }); + + let buffer = ""; + + const sendError = async (message: string) => { + await stream.writeSSE({ data: `Error: ${message}`, event: "error" }); + }; + + const processEvent = async (event: CodexStreamEvent) => { + if (event.type === "thread" && event.subtype === "started") { + await stream.writeSSE({ + data: "Codex session started", + event: "status", + }); + return; + } + + if (event.type === "turn") { + if (event.subtype === "started") { + await stream.writeSSE({ + data: "Planning next moves", + event: "status", + }); + return; + } + if (event.subtype === "completed") { + await stream.writeSSE({ data: "Turn complete", event: "status" }); + return; + } + if (event.subtype === "failed") { + await sendError(event.result || event.error?.message || "Turn failed"); + return; + } + } + + if (event.item) { + const itemStatus = deriveItemStatus(event.item); + if (itemStatus) { + await stream.writeSSE({ data: itemStatus, event: "status" }); + } + } + + if (event.message) { + const text = collectText(event.message.content); + if (text) { + await stream.writeSSE({ data: text, event: "status" }); + } + } + + if (event.type === "result") { + if (event.subtype === "success") { + await stream.writeSSE({ + data: "Completed successfully", + event: "status", + }); + return; + } + if (event.subtype === "error" || event.is_error || event.error) { + await sendError(event.result || event.error?.message || "Unknown error"); + return; + } + await stream.writeSSE({ data: "Task finished", event: "status" }); + return; + } + + if (event.type === "error" || event.is_error) { + await sendError(event.result || event.error?.message || "Unknown error"); + } + }; + + const processLine = async (line: string) => { + const event = parseStreamLine(line); + if (!event) { + return; + } + await processEvent(event); + }; + + codexProcess.stdout.on("data", async (chunk: Buffer) => { + buffer += chunk.toString(); + + let newlineIndex = buffer.indexOf("\n"); + while (newlineIndex !== -1) { + const line = buffer.slice(0, newlineIndex); + buffer = buffer.slice(newlineIndex + 1); + await processLine(line); + newlineIndex = buffer.indexOf("\n"); + } + }); + + codexProcess.stderr.on("data", (chunk: Buffer) => { + console.error("[codex stderr]:", chunk.toString()); + }); + + codexProcess.stdin.write(fullPrompt); + codexProcess.stdin.end(); + + stream.onAbort(() => { + if (!codexProcess.killed) { + codexProcess.kill(); + } + }); + + await new Promise((resolve, reject) => { + codexProcess.on("close", (code) => { + const finalize = async () => { + if (buffer.trim()) { + await processLine(buffer); + buffer = ""; + } + if (code === 0) { + resolve(); + } else { + reject(new Error(`codex exited with code ${code ?? "unknown"}`)); + } + }; + finalize().catch(reject); + }); + + codexProcess.on("error", (error) => { + reject(error); + }); + }); + + await stream.writeSSE({ data: "", event: "done" }); + } catch (error) { + const message = + error instanceof Error ? error.message : "Unknown error"; + await stream.writeSSE({ data: `Error: ${message}`, event: "error" }); + await stream.writeSSE({ data: "", event: "done" }); + } + }); + }); + + app.get("/health", (context) => { + return context.json({ status: "ok", provider: "codex" }); + }); + + return app; +}; + +const isPortInUse = (port: number): Promise => + new Promise((resolve) => { + const server = net.createServer(); + server.once("error", () => resolve(true)); + server.once("listening", () => { + server.close(); + resolve(false); + }); + server.listen(port); + }); + +export const startServer = async (port: number = DEFAULT_PORT) => { + if (await isPortInUse(port)) { + return; + } + + const app = createServer(); + serve({ fetch: app.fetch, port }); + console.log(`[React Grab] Server started on port ${port}`); +}; + +if (import.meta.url === `file://${process.argv[1]}`) { + startServer(DEFAULT_PORT).catch(console.error); +} \ No newline at end of file diff --git a/packages/react-grab-codex/tsconfig.json b/packages/react-grab-codex/tsconfig.json new file mode 100644 index 00000000..25068207 --- /dev/null +++ b/packages/react-grab-codex/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "noEmit": true + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/react-grab-codex/tsup.config.ts b/packages/react-grab-codex/tsup.config.ts new file mode 100644 index 00000000..6ed04c6d --- /dev/null +++ b/packages/react-grab-codex/tsup.config.ts @@ -0,0 +1,47 @@ +import { defineConfig } from "tsup"; + +export default defineConfig([ + { + entry: { + server: "./src/server.ts", + cli: "./src/cli.ts", + }, + format: ["cjs", "esm"], + dts: true, + clean: false, + splitting: false, + sourcemap: false, + target: "node18", + platform: "node", + treeshake: true, + noExternal: [/.*/], + }, + { + entry: { + client: "./src/client.ts", + }, + format: ["cjs", "esm"], + dts: true, + clean: false, + splitting: false, + sourcemap: false, + target: "esnext", + platform: "browser", + treeshake: true, + }, + { + entry: ["./src/client.ts"], + format: ["iife"], + globalName: "ReactGrabCodex", + outExtension: () => ({ js: ".global.js" }), + dts: false, + clean: false, + minify: process.env.NODE_ENV === "production", + splitting: false, + sourcemap: false, + target: "esnext", + platform: "browser", + treeshake: true, + noExternal: [/.*/], + }, +]); \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 489724d2..2fa6d3d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -232,6 +232,22 @@ importers: specifier: ^8.4.0 version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + packages/react-grab-codex: + dependencies: + '@hono/node-server': + specifier: ^1.19.6 + version: 1.19.6(hono@4.10.7) + hono: + specifier: ^4.0.0 + version: 4.10.7 + react-grab: + specifier: workspace:* + version: link:../react-grab + devDependencies: + tsup: + specifier: ^8.4.0 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + packages/react-grab-cursor: dependencies: '@hono/node-server': From cfadaa8b13915969aa44cc0821ffefcf1895add3 Mon Sep 17 00:00:00 2001 From: Vladislav Gapurov Date: Thu, 4 Dec 2025 23:22:46 +0100 Subject: [PATCH 02/12] chore: update TypeScript configuration and add @types/node dependency --- package.json | 1 + packages/react-grab-claude-code/tsconfig.json | 5 ++- packages/react-grab-codex/tsconfig.json | 1 + packages/react-grab-cursor/tsconfig.json | 5 ++- pnpm-lock.yaml | 31 +++++++++++-------- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 11a325a8..01020737 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ }, "devDependencies": { "@changesets/cli": "^2.27.10", + "@types/node": "20.19.23", "prettier": "^3.4.2", "turbo": "^2.3.3" }, diff --git a/packages/react-grab-claude-code/tsconfig.json b/packages/react-grab-claude-code/tsconfig.json index eb51098d..6ea970b4 100644 --- a/packages/react-grab-claude-code/tsconfig.json +++ b/packages/react-grab-claude-code/tsconfig.json @@ -3,6 +3,7 @@ "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", + "types": ["node"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -10,5 +11,7 @@ "declarationMap": true, "noEmit": true }, - "include": ["src"] + "include": [ + "src" + ] } diff --git a/packages/react-grab-codex/tsconfig.json b/packages/react-grab-codex/tsconfig.json index 25068207..2cbca8ab 100644 --- a/packages/react-grab-codex/tsconfig.json +++ b/packages/react-grab-codex/tsconfig.json @@ -3,6 +3,7 @@ "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", + "types": ["node"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, diff --git a/packages/react-grab-cursor/tsconfig.json b/packages/react-grab-cursor/tsconfig.json index eb51098d..6ea970b4 100644 --- a/packages/react-grab-cursor/tsconfig.json +++ b/packages/react-grab-cursor/tsconfig.json @@ -3,6 +3,7 @@ "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", + "types": ["node"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -10,5 +11,7 @@ "declarationMap": true, "noEmit": true }, - "include": ["src"] + "include": [ + "src" + ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fa6d3d0..1ca501fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@changesets/cli': specifier: ^2.27.10 version: 2.29.7(@types/node@20.19.23) + '@types/node': + specifier: 20.19.23 + version: 20.19.23 prettier: specifier: ^3.4.2 version: 3.6.2 @@ -379,7 +382,7 @@ importers: dependencies: '@vercel/analytics': specifier: ^1.5.0 - version: 1.5.0(next@16.0.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) + version: 1.5.0(next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) lucide-react: specifier: ^0.553.0 version: 0.553.0(react@19.2.1) @@ -388,10 +391,10 @@ importers: version: 12.23.24(react-dom@19.2.1(react@19.2.1))(react@19.2.1) next: specifier: 16.0.7 - version: 16.0.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) nuqs: specifier: ^2.8.1 - version: 2.8.1(next@16.0.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) + version: 2.8.1(next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) pretty-ms: specifier: ^9.3.0 version: 9.3.0 @@ -7552,9 +7555,9 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vercel/analytics@1.5.0(next@16.0.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)': + '@vercel/analytics@1.5.0(next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)': optionalDependencies: - next: 16.0.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 19.2.1 '@vercel/oidc@3.0.3': {} @@ -9605,7 +9608,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.0.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: '@next/env': 16.0.7 '@swc/helpers': 0.5.15 @@ -9613,7 +9616,7 @@ snapshots: postcss: 8.4.31 react: 19.2.1 react-dom: 19.2.1(react@19.2.1) - styled-jsx: 5.1.6(react@19.2.1) + styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.1) optionalDependencies: '@next/swc-darwin-arm64': 16.0.7 '@next/swc-darwin-x64': 16.0.7 @@ -9662,12 +9665,12 @@ snapshots: dependencies: boolbase: 1.0.0 - nuqs@2.8.1(next@16.0.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1): + nuqs@2.8.1(next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1): dependencies: '@standard-schema/spec': 1.0.0 react: 19.2.1 optionalDependencies: - next: 16.0.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) object-assign@4.1.1: {} @@ -10447,15 +10450,17 @@ snapshots: stubborn-utils@1.0.2: {} - styled-jsx@5.1.6(react@19.0.1): + styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.1): dependencies: client-only: 0.0.1 - react: 19.0.1 + react: 19.2.1 + optionalDependencies: + '@babel/core': 7.28.5 - styled-jsx@5.1.6(react@19.2.1): + styled-jsx@5.1.6(react@19.0.1): dependencies: client-only: 0.0.1 - react: 19.2.1 + react: 19.0.1 sucrase@3.35.0: dependencies: From 12537f5a7671973c533357b40209f3a9e536e3f4 Mon Sep 17 00:00:00 2001 From: Vladislav Gapurov Date: Fri, 5 Dec 2025 16:57:18 +0100 Subject: [PATCH 03/12] docs: add Codex CLI integration docs # Conflicts: # README.md --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/README.md b/README.md index 8f6ca756..cb490b4c 100644 --- a/README.md +++ b/README.md @@ -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 + + + +``` + +Or using Next.js `Script` component in your `app/layout.tsx`: + +```jsx +import Script from "next/script"; + +export default function RootLayout({ children }) { + return ( + + + {process.env.NODE_ENV === "development" && ( + <> +