@indianaprado/claude-code-companion 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "claude-code-companion",
3
+ "version": "0.1.0",
4
+ "description": "Use Claude Code from Codex for review, rescue, and delegated tasks.",
5
+ "author": {
6
+ "name": "Local developer"
7
+ },
8
+ "skills": "./skills/",
9
+ "interface": {
10
+ "displayName": "Claude Code Companion",
11
+ "shortDescription": "Delegate Codex work to Claude Code.",
12
+ "longDescription": "A local companion plugin that lets Codex call the installed Claude Code CLI for read-only reviews, adversarial reviews, write-capable rescue tasks, and background jobs.",
13
+ "developerName": "Local developer",
14
+ "category": "Productivity",
15
+ "capabilities": ["Interactive", "Read", "Write"],
16
+ "defaultPrompt": [
17
+ "Ask Claude to review my current changes.",
18
+ "Delegate this UI task to Claude Code.",
19
+ "Check Claude task status."
20
+ ]
21
+ },
22
+ "mcpServers": "./.mcp.json"
23
+ }
package/.mcp.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "mcpServers": {
3
+ "claude-code-companion": {
4
+ "command": "node",
5
+ "args": [
6
+ "./scripts/claude-mcp-server.mjs"
7
+ ]
8
+ }
9
+ }
10
+ }
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # Claude Code Companion
2
+
3
+ Use the installed Claude Code CLI from Codex through a local MCP server.
4
+
5
+ This plugin mirrors the mode-driven shape of `openai/codex-plugin-cc` in reverse:
6
+
7
+ - read-only `review`
8
+ - read-only `adversarial_review`
9
+ - write-capable `rescue`
10
+ - foreground or background execution
11
+ - `status`, `result`, and `cancel` job control
12
+
13
+ ## Requirements
14
+
15
+ - Node.js
16
+ - Claude Code CLI available as `claude`
17
+ - Claude Code authenticated in the same terminal environment
18
+
19
+ ## One-line Codex MCP Install
20
+
21
+ After publishing this package to npm:
22
+
23
+ ```bash
24
+ codex mcp add claude-code-companion npx -y @indianaprado/claude-code-companion
25
+ ```
26
+
27
+ That command adds a global Codex MCP server entry named `claude-code-companion`.
28
+ Codex will start the MCP server with `npx` when needed.
29
+
30
+ For a local unpublished checkout:
31
+
32
+ ```bash
33
+ codex mcp add claude-code-companion node /Users/pranaybindela/Desktop/work/claude-mcp/claude-code-companion/scripts/claude-mcp-server.mjs
34
+ ```
35
+
36
+ Confirm the entry:
37
+
38
+ ```bash
39
+ codex mcp list
40
+ codex mcp get claude-code-companion
41
+ ```
42
+
43
+ ## Publish To npm
44
+
45
+ The package name is scoped because the unscoped `claude-code-companion` name is already taken on npm.
46
+ The package intentionally has no npm dependencies, no install lifecycle scripts, and no external
47
+ package imports. Check that before publishing:
48
+
49
+ ```bash
50
+ npm run supply-chain:check
51
+ ```
52
+
53
+ ```bash
54
+ npm login
55
+ npm publish --access public
56
+ ```
57
+
58
+ Dry-run before publishing:
59
+
60
+ ```bash
61
+ npm publish --dry-run
62
+ ```
63
+
64
+ Check setup:
65
+
66
+ ```bash
67
+ node scripts/claude-companion.mjs setup
68
+ ```
69
+
70
+ ## Manual CLI
71
+
72
+ ```bash
73
+ node scripts/claude-companion.mjs review
74
+ node scripts/claude-companion.mjs adversarial-review "focus on UX and frontend state bugs"
75
+ node scripts/claude-companion.mjs rescue --read-only "investigate why the dashboard is slow"
76
+ node scripts/claude-companion.mjs rescue --resume --session-id <uuid> "continue this Claude session"
77
+ node scripts/claude-companion.mjs rescue --background "implement the mock frontend"
78
+ node scripts/claude-companion.mjs status
79
+ node scripts/claude-companion.mjs result <job-id>
80
+ node scripts/claude-companion.mjs cancel <job-id>
81
+ ```
82
+
83
+ When installed from npm, the helper CLI is available as:
84
+
85
+ ```bash
86
+ npx -y -p @indianaprado/claude-code-companion claude-code-companion-cli setup
87
+ ```
88
+
89
+ ## MCP Tools
90
+
91
+ The bundled MCP server exposes:
92
+
93
+ - `setup`
94
+ - `review`
95
+ - `adversarial_review`
96
+ - `rescue`
97
+ - `status`
98
+ - `result`
99
+ - `cancel`
100
+
101
+ `rescue` is write-capable by default. Pass `readOnly: true` for investigation-only delegation.
102
+
103
+ `model` is optional. When omitted, the plugin does not pass `--model`, so Claude CLI chooses its
104
+ current default/latest model. On this machine, that currently selected `claude-opus-4-5-20251101`.
105
+ When provided, `model` is forwarded to `claude --model`, so aliases such as `sonnet` and full model
106
+ names such as `claude-sonnet-4-5-20250929` work when supported by the installed Claude CLI.
107
+
108
+ Session controls:
109
+
110
+ - `resume: true` resumes the most recent Claude conversation in the current directory.
111
+ - `resume: true` plus `sessionId` resumes that exact Claude session.
112
+ - `sessionId` without `resume` starts or uses that exact session id.
113
+ - `forkSession: true` is passed through to Claude CLI with resume; behavior depends on the installed CLI.
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@indianaprado/claude-code-companion",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Claude Code Companion MCP server for Codex.",
6
+ "type": "module",
7
+ "bin": {
8
+ "claude-code-companion": "scripts/claude-mcp-server.mjs",
9
+ "claude-code-companion-cli": "scripts/claude-companion.mjs"
10
+ },
11
+ "files": [
12
+ ".codex-plugin/",
13
+ ".mcp.json",
14
+ "README.md",
15
+ "scripts/",
16
+ "skills/"
17
+ ],
18
+ "engines": {
19
+ "node": ">=20"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "keywords": [
25
+ "codex",
26
+ "mcp",
27
+ "claude-code",
28
+ "claude",
29
+ "agent"
30
+ ],
31
+ "author": "Local developer",
32
+ "license": "MIT",
33
+ "scripts": {
34
+ "setup": "node scripts/claude-companion.mjs setup",
35
+ "status": "node scripts/claude-companion.mjs status",
36
+ "test:syntax": "find scripts -name '*.mjs' -exec node --check {} \\;",
37
+ "supply-chain:check": "node scripts/audit-package.mjs",
38
+ "test": "npm run supply-chain:check && npm run test:syntax",
39
+ "pack:dry": "npm pack --dry-run"
40
+ }
41
+ }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ const ROOT = path.resolve(new URL("..", import.meta.url).pathname);
7
+ const PACKAGE_JSON = path.join(ROOT, "package.json");
8
+ const FORBIDDEN_DEP_FIELDS = [
9
+ "dependencies",
10
+ "devDependencies",
11
+ "optionalDependencies",
12
+ "peerDependencies",
13
+ "bundledDependencies",
14
+ "bundleDependencies"
15
+ ];
16
+ const FORBIDDEN_LIFECYCLE_SCRIPTS = [
17
+ "preinstall",
18
+ "install",
19
+ "postinstall",
20
+ "prepare",
21
+ "prepublish",
22
+ "prepublishOnly",
23
+ "prepack",
24
+ "postpack"
25
+ ];
26
+
27
+ function fail(message) {
28
+ process.stderr.write(`${message}\n`);
29
+ process.exitCode = 1;
30
+ }
31
+
32
+ function readPackageJson() {
33
+ return JSON.parse(fs.readFileSync(PACKAGE_JSON, "utf8"));
34
+ }
35
+
36
+ function walk(dir) {
37
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
38
+ const files = [];
39
+ for (const entry of entries) {
40
+ const fullPath = path.join(dir, entry.name);
41
+ if (entry.isDirectory()) {
42
+ files.push(...walk(fullPath));
43
+ } else if (entry.isFile() && fullPath.endsWith(".mjs")) {
44
+ files.push(fullPath);
45
+ }
46
+ }
47
+ return files;
48
+ }
49
+
50
+ function extractBareImports(source) {
51
+ const matches = [];
52
+ const patterns = [
53
+ /\bimport\s+[^'"]*from\s+['"]([^'"]+)['"]/g,
54
+ /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g
55
+ ];
56
+ for (const pattern of patterns) {
57
+ for (const match of source.matchAll(pattern)) {
58
+ const specifier = match[1];
59
+ if (!specifier.startsWith("node:") && !specifier.startsWith("./") && !specifier.startsWith("../")) {
60
+ matches.push(specifier);
61
+ }
62
+ }
63
+ }
64
+ return matches;
65
+ }
66
+
67
+ const pkg = readPackageJson();
68
+
69
+ for (const field of FORBIDDEN_DEP_FIELDS) {
70
+ if (pkg[field] && Object.keys(pkg[field]).length > 0) {
71
+ fail(`Forbidden dependency field is populated: ${field}`);
72
+ }
73
+ }
74
+
75
+ for (const scriptName of FORBIDDEN_LIFECYCLE_SCRIPTS) {
76
+ if (pkg.scripts?.[scriptName]) {
77
+ fail(`Forbidden npm lifecycle script present: ${scriptName}`);
78
+ }
79
+ }
80
+
81
+ for (const file of walk(path.join(ROOT, "scripts"))) {
82
+ const bareImports = extractBareImports(fs.readFileSync(file, "utf8"));
83
+ if (bareImports.length > 0) {
84
+ fail(`${path.relative(ROOT, file)} imports external package(s): ${bareImports.join(", ")}`);
85
+ }
86
+ }
87
+
88
+ if (!process.exitCode) {
89
+ process.stdout.write("Supply-chain check passed: no npm dependencies, no install lifecycle scripts, no external package imports.\n");
90
+ }
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import fs from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import { parseArgs } from "./lib/args.mjs";
8
+ import { getClaudeAuthStatus, getClaudeVersion, runClaude } from "./lib/claude.mjs";
9
+ import { binaryStatus } from "./lib/process.mjs";
10
+ import { buildRescuePrompt, buildReviewPrompt } from "./lib/prompts.mjs";
11
+ import { cancelJob, createJob, findJob, launchWorker, markJob, recentJobs } from "./lib/jobs.mjs";
12
+ import { appendLog, now, readJob, resolveStateDir } from "./lib/state.mjs";
13
+ import { renderQueued, renderSetup, renderStatus, renderStoredResult, renderTaskResult } from "./lib/render.mjs";
14
+
15
+ const SCRIPT_PATH = fileURLToPath(import.meta.url);
16
+
17
+ function isDirectRun() {
18
+ if (!process.argv[1]) {
19
+ return false;
20
+ }
21
+ try {
22
+ return fs.realpathSync(process.argv[1]) === fs.realpathSync(SCRIPT_PATH);
23
+ } catch {
24
+ return path.resolve(process.argv[1]) === path.resolve(SCRIPT_PATH);
25
+ }
26
+ }
27
+
28
+ function output(value, asJson = false) {
29
+ process.stdout.write(asJson ? `${JSON.stringify(value, null, 2)}\n` : String(value));
30
+ }
31
+
32
+ function cwdFrom(options) {
33
+ return options.cwd ? path.resolve(process.cwd(), options.cwd) : process.cwd();
34
+ }
35
+
36
+ function summarize(prompt) {
37
+ const text = String(prompt ?? "").replace(/\s+/g, " ").trim();
38
+ return text.length > 96 ? `${text.slice(0, 93)}...` : text;
39
+ }
40
+
41
+ export async function setupCommand(input = {}) {
42
+ const cwd = path.resolve(input.cwd ?? process.cwd());
43
+ const node = binaryStatus("node", ["--version"], { cwd });
44
+ const claude = getClaudeVersion(cwd);
45
+ const auth = getClaudeAuthStatus(cwd);
46
+ const authExplicitlyMissing = auth.parsed?.loggedIn === false;
47
+ return {
48
+ ready: node.available && claude.available && !authExplicitlyMissing,
49
+ node,
50
+ claude,
51
+ auth,
52
+ stateDir: resolveStateDir(cwd)
53
+ };
54
+ }
55
+
56
+ async function executeClaudeJob(cwd, job, request) {
57
+ markJob(cwd, job.id, {
58
+ status: "running",
59
+ phase: "running",
60
+ startedAt: now(),
61
+ request
62
+ });
63
+ const result = await runClaude(request.prompt, {
64
+ cwd,
65
+ readOnly: request.readOnly,
66
+ resume: request.resume,
67
+ sessionId: request.sessionId,
68
+ model: request.model,
69
+ effort: request.effort,
70
+ maxBudgetUsd: request.maxBudgetUsd,
71
+ permissionMode: request.permissionMode,
72
+ onStart: (pid) => {
73
+ markJob(cwd, job.id, { pid, phase: "claude-running" });
74
+ appendLog(job.logFile, `[${now()}] Claude process started pid=${pid}.\n`);
75
+ },
76
+ onStdout: (chunk) => appendLog(job.logFile, chunk),
77
+ onStderr: (chunk) => appendLog(job.logFile, chunk)
78
+ });
79
+ const rendered = renderTaskResult(job, result);
80
+ markJob(cwd, job.id, {
81
+ status: result.status === 0 ? "completed" : "failed",
82
+ phase: result.status === 0 ? "done" : "failed",
83
+ pid: null,
84
+ completedAt: now(),
85
+ sessionId: result.sessionId,
86
+ costUsd: result.costUsd,
87
+ result: {
88
+ status: result.status,
89
+ signal: result.signal,
90
+ stdout: result.stdout,
91
+ stderr: result.stderr,
92
+ finalText: result.finalText,
93
+ parsed: result.parsed
94
+ },
95
+ rendered
96
+ });
97
+ return { job: readJob(cwd, job.id), rendered, result };
98
+ }
99
+
100
+ async function runJobRequest(cwd, request, options = {}) {
101
+ const job = createJob(cwd, {
102
+ prefix: request.kind === "review" ? "review" : "task",
103
+ kind: request.kind,
104
+ title: request.title,
105
+ summary: request.summary,
106
+ readOnly: request.readOnly,
107
+ background: request.background
108
+ });
109
+ markJob(cwd, job.id, { request });
110
+
111
+ if (request.background) {
112
+ const child = launchWorker(SCRIPT_PATH, cwd, job.id);
113
+ markJob(cwd, job.id, { pid: child.pid ?? null, phase: "queued" });
114
+ return { job: readJob(cwd, job.id), rendered: renderQueued(job), queued: true };
115
+ }
116
+
117
+ return executeClaudeJob(cwd, job, request, options);
118
+ }
119
+
120
+ export async function reviewCommand(input = {}) {
121
+ const cwd = path.resolve(input.cwd ?? process.cwd());
122
+ const prompt = buildReviewPrompt(cwd, { scope: input.scope, base: input.base, focus: input.focus });
123
+ return runJobRequest(cwd, {
124
+ kind: input.adversarial ? "adversarial-review" : "review",
125
+ title: input.adversarial ? "Claude Adversarial Review" : "Claude Review",
126
+ summary: input.adversarial ? summarize(input.focus || "Adversarial review") : "Review current git state",
127
+ prompt,
128
+ readOnly: true,
129
+ background: Boolean(input.background),
130
+ model: input.model,
131
+ effort: input.effort,
132
+ maxBudgetUsd: input.maxBudgetUsd
133
+ });
134
+ }
135
+
136
+ export async function rescueCommand(input = {}) {
137
+ const cwd = path.resolve(input.cwd ?? process.cwd());
138
+ if (!input.prompt || !String(input.prompt).trim()) {
139
+ throw new Error("Provide a task prompt for Claude.");
140
+ }
141
+ const readOnly = Boolean(input.readOnly);
142
+ const prompt = buildRescuePrompt(input.prompt, { readOnly });
143
+ return runJobRequest(cwd, {
144
+ kind: "task",
145
+ title: input.resume ? "Claude Resume" : "Claude Task",
146
+ summary: summarize(input.prompt),
147
+ prompt,
148
+ readOnly,
149
+ background: Boolean(input.background),
150
+ resume: Boolean(input.resume),
151
+ sessionId: input.sessionId,
152
+ forkSession: input.forkSession,
153
+ model: input.model,
154
+ effort: input.effort,
155
+ maxBudgetUsd: input.maxBudgetUsd,
156
+ permissionMode: input.permissionMode
157
+ });
158
+ }
159
+
160
+ export function statusCommand(input = {}) {
161
+ const cwd = path.resolve(input.cwd ?? process.cwd());
162
+ return recentJobs(cwd, Boolean(input.all));
163
+ }
164
+
165
+ export function resultCommand(input = {}) {
166
+ const cwd = path.resolve(input.cwd ?? process.cwd());
167
+ const job = findJob(cwd, input.jobId ?? "");
168
+ return { job, stored: job ? readJob(cwd, job.id) : null };
169
+ }
170
+
171
+ export function cancelCommand(input = {}) {
172
+ const cwd = path.resolve(input.cwd ?? process.cwd());
173
+ return cancelJob(cwd, input.jobId ?? "");
174
+ }
175
+
176
+ async function handleWorker(argv) {
177
+ const { options } = parseArgs(argv, {
178
+ valueOptions: ["cwd", "job-id"]
179
+ });
180
+ const cwd = cwdFrom(options);
181
+ const job = readJob(cwd, options["job-id"]);
182
+ if (!job?.request) {
183
+ throw new Error(`No queued Claude job found for ${options["job-id"]}.`);
184
+ }
185
+ await executeClaudeJob(cwd, job, job.request);
186
+ }
187
+
188
+ async function main() {
189
+ const [command, ...argv] = process.argv.slice(2);
190
+ const common = {
191
+ valueOptions: ["cwd", "job-id", "base", "scope", "model", "effort", "max-budget-usd", "session-id", "permission-mode"],
192
+ booleanOptions: ["json", "background", "read-only", "write", "resume", "fresh", "all", "adversarial", "fork-session"]
193
+ };
194
+ const { options, positionals } = parseArgs(argv, common);
195
+ const cwd = cwdFrom(options);
196
+ const asJson = Boolean(options.json);
197
+
198
+ if (command === "worker") {
199
+ await handleWorker(argv);
200
+ return;
201
+ }
202
+ if (command === "setup") {
203
+ const report = await setupCommand({ cwd });
204
+ output(asJson ? report : renderSetup(report), asJson);
205
+ return;
206
+ }
207
+ if (command === "review" || command === "adversarial-review") {
208
+ const response = await reviewCommand({
209
+ cwd,
210
+ scope: options.scope,
211
+ base: options.base,
212
+ focus: positionals.join(" "),
213
+ adversarial: command === "adversarial-review" || options.adversarial,
214
+ background: options.background,
215
+ model: options.model,
216
+ effort: options.effort,
217
+ maxBudgetUsd: options["max-budget-usd"]
218
+ });
219
+ output(asJson ? response : response.rendered, asJson);
220
+ return;
221
+ }
222
+ if (command === "rescue" || command === "task") {
223
+ const response = await rescueCommand({
224
+ cwd,
225
+ prompt: positionals.join(" "),
226
+ readOnly: Boolean(options["read-only"]) && !options.write,
227
+ background: options.background,
228
+ resume: Boolean(options.resume),
229
+ sessionId: options["session-id"],
230
+ forkSession: Boolean(options["fork-session"]),
231
+ model: options.model,
232
+ effort: options.effort,
233
+ maxBudgetUsd: options["max-budget-usd"],
234
+ permissionMode: options["permission-mode"]
235
+ });
236
+ output(asJson ? response : response.rendered, asJson);
237
+ return;
238
+ }
239
+ if (command === "status") {
240
+ const jobs = statusCommand({ cwd, all: options.all });
241
+ output(asJson ? jobs : renderStatus(jobs), asJson);
242
+ return;
243
+ }
244
+ if (command === "result") {
245
+ const response = resultCommand({ cwd, jobId: options["job-id"] ?? positionals[0] });
246
+ output(asJson ? response : renderStoredResult(response.job, response.stored), asJson);
247
+ return;
248
+ }
249
+ if (command === "cancel") {
250
+ const response = cancelCommand({ cwd, jobId: options["job-id"] ?? positionals[0] });
251
+ output(asJson ? response : `Cancelled ${response.id}.\n`, asJson);
252
+ return;
253
+ }
254
+
255
+ throw new Error("Usage: claude-companion.mjs setup|review|adversarial-review|rescue|status|result|cancel");
256
+ }
257
+
258
+ if (isDirectRun()) {
259
+ main().catch((error) => {
260
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
261
+ process.exit(1);
262
+ });
263
+ }