@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.
- package/.codex-plugin/plugin.json +23 -0
- package/.mcp.json +10 -0
- package/README.md +113 -0
- package/package.json +41 -0
- package/scripts/audit-package.mjs +90 -0
- package/scripts/claude-companion.mjs +263 -0
- package/scripts/claude-mcp-server.mjs +275 -0
- package/scripts/lib/args.mjs +55 -0
- package/scripts/lib/claude.mjs +106 -0
- package/scripts/lib/git.mjs +52 -0
- package/scripts/lib/jobs.mjs +67 -0
- package/scripts/lib/process.mjs +74 -0
- package/scripts/lib/prompts.mjs +25 -0
- package/scripts/lib/render.mjs +81 -0
- package/scripts/lib/state.mjs +154 -0
- package/skills/claude-code-runtime/SKILL.md +28 -0
|
@@ -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
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
|
+
}
|