@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,275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
cancelCommand,
|
|
5
|
+
rescueCommand,
|
|
6
|
+
resultCommand,
|
|
7
|
+
reviewCommand,
|
|
8
|
+
setupCommand,
|
|
9
|
+
statusCommand
|
|
10
|
+
} from "./claude-companion.mjs";
|
|
11
|
+
import { renderSetup, renderStatus, renderStoredResult } from "./lib/render.mjs";
|
|
12
|
+
|
|
13
|
+
const PROTOCOL_VERSION = "2024-11-05";
|
|
14
|
+
|
|
15
|
+
const tools = [
|
|
16
|
+
{
|
|
17
|
+
name: "setup",
|
|
18
|
+
description: "Check local Claude Code CLI availability, auth status, and companion state directory.",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
cwd: { type: "string", description: "Workspace directory. Defaults to the MCP server cwd." }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "review",
|
|
28
|
+
description: "Run a read-only Claude Code review over the current git state.",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
cwd: { type: "string" },
|
|
33
|
+
scope: { type: "string", enum: ["working-tree", "branch"], default: "working-tree" },
|
|
34
|
+
base: { type: "string", description: "Base ref when scope is branch." },
|
|
35
|
+
background: { type: "boolean", default: false },
|
|
36
|
+
model: { type: "string", description: "Optional Claude model alias or full model name. Omit to let Claude CLI choose its current default/latest model." },
|
|
37
|
+
effort: { type: "string", enum: ["low", "medium", "high", "xhigh", "max"] },
|
|
38
|
+
maxBudgetUsd: { type: "number" }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "adversarial_review",
|
|
44
|
+
description: "Run a read-only Claude Code review with explicit critique/focus text.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
cwd: { type: "string" },
|
|
49
|
+
focus: { type: "string", description: "Additional review framing or critique focus." },
|
|
50
|
+
scope: { type: "string", enum: ["working-tree", "branch"], default: "working-tree" },
|
|
51
|
+
base: { type: "string" },
|
|
52
|
+
background: { type: "boolean", default: false },
|
|
53
|
+
model: { type: "string", description: "Optional Claude model alias or full model name. Omit to let Claude CLI choose its current default/latest model." },
|
|
54
|
+
effort: { type: "string", enum: ["low", "medium", "high", "xhigh", "max"] },
|
|
55
|
+
maxBudgetUsd: { type: "number" }
|
|
56
|
+
},
|
|
57
|
+
required: ["focus"]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "rescue",
|
|
62
|
+
description: "Delegate an arbitrary task to Claude Code. Defaults to write-capable unless readOnly is true.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
cwd: { type: "string" },
|
|
67
|
+
prompt: { type: "string" },
|
|
68
|
+
readOnly: { type: "boolean", default: false },
|
|
69
|
+
background: { type: "boolean", default: false },
|
|
70
|
+
resume: { type: "boolean", default: false },
|
|
71
|
+
sessionId: { type: "string", description: "With resume=true, resume this Claude session id. With resume=false, create/use this exact new session id." },
|
|
72
|
+
forkSession: { type: "boolean", default: false, description: "Pass through Claude CLI --fork-session when resuming. Behavior depends on the installed Claude CLI." },
|
|
73
|
+
model: { type: "string", description: "Optional Claude model alias or full model name. Omit to let Claude CLI choose its current default/latest model." },
|
|
74
|
+
effort: { type: "string", enum: ["low", "medium", "high", "xhigh", "max"] },
|
|
75
|
+
maxBudgetUsd: { type: "number" },
|
|
76
|
+
permissionMode: { type: "string", enum: ["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"] }
|
|
77
|
+
},
|
|
78
|
+
required: ["prompt"]
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "status",
|
|
83
|
+
description: "List active and recent Claude Code companion jobs for this workspace.",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
cwd: { type: "string" },
|
|
88
|
+
all: { type: "boolean", default: false }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "result",
|
|
94
|
+
description: "Show stored output for a completed Claude Code companion job.",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
cwd: { type: "string" },
|
|
99
|
+
jobId: { type: "string" }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "cancel",
|
|
105
|
+
description: "Cancel an active background Claude Code companion job.",
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: {
|
|
109
|
+
cwd: { type: "string" },
|
|
110
|
+
jobId: { type: "string" }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
let buffer = Buffer.alloc(0);
|
|
117
|
+
let transportMode = null;
|
|
118
|
+
|
|
119
|
+
function encodeMessage(message) {
|
|
120
|
+
const body = Buffer.from(JSON.stringify(message), "utf8");
|
|
121
|
+
return Buffer.concat([
|
|
122
|
+
Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, "utf8"),
|
|
123
|
+
body
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function send(message) {
|
|
128
|
+
if (transportMode === "line") {
|
|
129
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
process.stdout.write(encodeMessage(message));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function textResult(text) {
|
|
136
|
+
return { content: [{ type: "text", text: String(text ?? "") }] };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function errorResult(message) {
|
|
140
|
+
return { content: [{ type: "text", text: String(message ?? "") }], isError: true };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function tryReadMessage() {
|
|
144
|
+
const firstNonWhitespace = buffer.findIndex((byte) => byte !== 0x20 && byte !== 0x09 && byte !== 0x0d && byte !== 0x0a);
|
|
145
|
+
if (firstNonWhitespace > 0) {
|
|
146
|
+
buffer = buffer.slice(firstNonWhitespace);
|
|
147
|
+
}
|
|
148
|
+
if (buffer.length === 0) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (buffer[0] === 0x7b || transportMode === "line") {
|
|
153
|
+
const newline = buffer.indexOf("\n");
|
|
154
|
+
if (newline === -1) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const line = buffer.slice(0, newline).toString("utf8").trim();
|
|
158
|
+
buffer = buffer.slice(newline + 1);
|
|
159
|
+
if (!line) {
|
|
160
|
+
return tryReadMessage();
|
|
161
|
+
}
|
|
162
|
+
transportMode = "line";
|
|
163
|
+
return JSON.parse(line);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const separator = buffer.indexOf("\r\n\r\n");
|
|
167
|
+
if (separator === -1) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const header = buffer.slice(0, separator).toString("utf8");
|
|
171
|
+
const match = /Content-Length:\s*(\d+)/i.exec(header);
|
|
172
|
+
if (!match) {
|
|
173
|
+
buffer = buffer.slice(separator + 4);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
transportMode = "header";
|
|
177
|
+
const length = Number(match[1]);
|
|
178
|
+
const start = separator + 4;
|
|
179
|
+
const end = start + length;
|
|
180
|
+
if (buffer.length < end) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const body = buffer.slice(start, end).toString("utf8");
|
|
184
|
+
buffer = buffer.slice(end);
|
|
185
|
+
return JSON.parse(body);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function callTool(name, args = {}) {
|
|
189
|
+
if (name === "setup") {
|
|
190
|
+
return textResult(renderSetup(await setupCommand(args)));
|
|
191
|
+
}
|
|
192
|
+
if (name === "review") {
|
|
193
|
+
const response = await reviewCommand(args);
|
|
194
|
+
return textResult(response.rendered);
|
|
195
|
+
}
|
|
196
|
+
if (name === "adversarial_review") {
|
|
197
|
+
const response = await reviewCommand({ ...args, adversarial: true });
|
|
198
|
+
return textResult(response.rendered);
|
|
199
|
+
}
|
|
200
|
+
if (name === "rescue") {
|
|
201
|
+
const response = await rescueCommand(args);
|
|
202
|
+
return textResult(response.rendered);
|
|
203
|
+
}
|
|
204
|
+
if (name === "status") {
|
|
205
|
+
return textResult(renderStatus(statusCommand(args)));
|
|
206
|
+
}
|
|
207
|
+
if (name === "result") {
|
|
208
|
+
const response = resultCommand(args);
|
|
209
|
+
return textResult(renderStoredResult(response.job, response.stored));
|
|
210
|
+
}
|
|
211
|
+
if (name === "cancel") {
|
|
212
|
+
const response = cancelCommand(args);
|
|
213
|
+
return textResult(`Cancelled ${response.id}.\n`);
|
|
214
|
+
}
|
|
215
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function handle(message) {
|
|
219
|
+
if (message.id === undefined) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
if (message.method === "initialize") {
|
|
224
|
+
send({
|
|
225
|
+
jsonrpc: "2.0",
|
|
226
|
+
id: message.id,
|
|
227
|
+
result: {
|
|
228
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
229
|
+
capabilities: { tools: {} },
|
|
230
|
+
serverInfo: { name: "claude-code-companion", version: "0.1.0" }
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (message.method === "tools/list") {
|
|
236
|
+
send({ jsonrpc: "2.0", id: message.id, result: { tools } });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (message.method === "tools/call") {
|
|
240
|
+
const result = await callTool(message.params?.name, message.params?.arguments ?? {});
|
|
241
|
+
send({ jsonrpc: "2.0", id: message.id, result });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (message.method === "ping") {
|
|
245
|
+
send({ jsonrpc: "2.0", id: message.id, result: {} });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
send({
|
|
249
|
+
jsonrpc: "2.0",
|
|
250
|
+
id: message.id,
|
|
251
|
+
error: { code: -32601, message: `Method not found: ${message.method}` }
|
|
252
|
+
});
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (message.method === "tools/call") {
|
|
255
|
+
send({ jsonrpc: "2.0", id: message.id, result: errorResult(error instanceof Error ? error.message : String(error)) });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
send({
|
|
259
|
+
jsonrpc: "2.0",
|
|
260
|
+
id: message.id,
|
|
261
|
+
error: { code: -32000, message: error instanceof Error ? error.message : String(error) }
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
process.stdin.on("data", async (chunk) => {
|
|
267
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
268
|
+
while (true) {
|
|
269
|
+
const message = tryReadMessage();
|
|
270
|
+
if (!message) {
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
await handle(message);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function parseArgs(argv, config = {}) {
|
|
2
|
+
const valueOptions = new Set(config.valueOptions ?? []);
|
|
3
|
+
const booleanOptions = new Set(config.booleanOptions ?? []);
|
|
4
|
+
const aliasMap = config.aliasMap ?? {};
|
|
5
|
+
const options = {};
|
|
6
|
+
const positionals = [];
|
|
7
|
+
|
|
8
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
9
|
+
const raw = argv[index];
|
|
10
|
+
if (raw === "--") {
|
|
11
|
+
positionals.push(...argv.slice(index + 1));
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
if (!raw.startsWith("-") || raw === "-") {
|
|
15
|
+
positionals.push(raw);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const withoutPrefix = raw.replace(/^-+/, "");
|
|
20
|
+
const equalsIndex = withoutPrefix.indexOf("=");
|
|
21
|
+
const keyRaw = equalsIndex === -1 ? withoutPrefix : withoutPrefix.slice(0, equalsIndex);
|
|
22
|
+
const key = aliasMap[keyRaw] ?? keyRaw;
|
|
23
|
+
const inlineValue = equalsIndex === -1 ? null : withoutPrefix.slice(equalsIndex + 1);
|
|
24
|
+
|
|
25
|
+
if (booleanOptions.has(key)) {
|
|
26
|
+
options[key] = inlineValue == null ? true : inlineValue !== "false";
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (valueOptions.has(key)) {
|
|
31
|
+
if (inlineValue != null) {
|
|
32
|
+
options[key] = inlineValue;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
index += 1;
|
|
36
|
+
if (index >= argv.length) {
|
|
37
|
+
throw new Error(`Missing value for --${key}.`);
|
|
38
|
+
}
|
|
39
|
+
options[key] = argv[index];
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (inlineValue != null) {
|
|
44
|
+
options[key] = inlineValue;
|
|
45
|
+
} else {
|
|
46
|
+
options[key] = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { options, positionals };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function stringArg(value, fallback = "") {
|
|
54
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
55
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { binaryStatus, spawnCapture } from "./process.mjs";
|
|
2
|
+
|
|
3
|
+
const READ_ONLY_TOOLS = "Read,Glob,Grep,Bash(git *)";
|
|
4
|
+
const EDIT_TOOLS = "Edit,MultiEdit,Write,NotebookEdit";
|
|
5
|
+
|
|
6
|
+
export function getClaudeVersion(cwd) {
|
|
7
|
+
return binaryStatus("claude", ["--version"], { cwd });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getClaudeAuthStatus(cwd) {
|
|
11
|
+
const result = binaryStatus("claude", ["auth", "status"], { cwd, timeoutMs: 8000 });
|
|
12
|
+
let parsed = null;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(result.stdout);
|
|
15
|
+
} catch {
|
|
16
|
+
parsed = null;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
available: result.available || Boolean(result.stdout),
|
|
20
|
+
timedOut: result.detail?.includes("ETIMEDOUT") || result.signal === "SIGTERM",
|
|
21
|
+
status: result.status,
|
|
22
|
+
stdout: result.stdout,
|
|
23
|
+
stderr: result.stderr,
|
|
24
|
+
detail: result.detail,
|
|
25
|
+
parsed
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeEffort(effort) {
|
|
30
|
+
if (!effort) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const value = String(effort).trim();
|
|
34
|
+
const allowed = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
35
|
+
if (!allowed.has(value)) {
|
|
36
|
+
throw new Error(`Unsupported Claude effort "${effort}". Use low, medium, high, xhigh, or max.`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseClaudeJson(stdout) {
|
|
42
|
+
const text = String(stdout ?? "").trim();
|
|
43
|
+
if (!text) {
|
|
44
|
+
return { parsed: null, finalText: "" };
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(text);
|
|
48
|
+
const finalText =
|
|
49
|
+
parsed.result ??
|
|
50
|
+
parsed.response ??
|
|
51
|
+
parsed.message?.content?.map?.((part) => part.text ?? "").join("\n") ??
|
|
52
|
+
parsed.content?.map?.((part) => part.text ?? "").join("\n") ??
|
|
53
|
+
text;
|
|
54
|
+
return { parsed, finalText: String(finalText ?? "").trim() };
|
|
55
|
+
} catch {
|
|
56
|
+
return { parsed: null, finalText: text };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function runClaude(prompt, options = {}) {
|
|
61
|
+
const args = ["-p", prompt, "--output-format", "json"];
|
|
62
|
+
if (options.resume) {
|
|
63
|
+
if (options.sessionId) {
|
|
64
|
+
args.push("--resume", options.sessionId);
|
|
65
|
+
} else {
|
|
66
|
+
args.push("--continue");
|
|
67
|
+
}
|
|
68
|
+
if (options.forkSession) {
|
|
69
|
+
args.push("--fork-session");
|
|
70
|
+
}
|
|
71
|
+
} else if (options.sessionId) {
|
|
72
|
+
args.push("--session-id", options.sessionId);
|
|
73
|
+
}
|
|
74
|
+
if (options.model) {
|
|
75
|
+
args.push("--model", options.model);
|
|
76
|
+
}
|
|
77
|
+
const effort = normalizeEffort(options.effort);
|
|
78
|
+
if (effort) {
|
|
79
|
+
args.push("--effort", effort);
|
|
80
|
+
}
|
|
81
|
+
if (options.maxBudgetUsd) {
|
|
82
|
+
args.push("--max-budget-usd", String(options.maxBudgetUsd));
|
|
83
|
+
}
|
|
84
|
+
if (options.readOnly) {
|
|
85
|
+
args.push("--permission-mode", "plan");
|
|
86
|
+
args.push("--allowedTools", READ_ONLY_TOOLS);
|
|
87
|
+
args.push("--disallowedTools", EDIT_TOOLS);
|
|
88
|
+
} else {
|
|
89
|
+
args.push("--permission-mode", options.permissionMode || "acceptEdits");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const result = await spawnCapture("claude", args, {
|
|
93
|
+
cwd: options.cwd,
|
|
94
|
+
onStart: options.onStart,
|
|
95
|
+
onStdout: options.onStdout,
|
|
96
|
+
onStderr: options.onStderr
|
|
97
|
+
});
|
|
98
|
+
const parsed = parseClaudeJson(result.stdout);
|
|
99
|
+
return {
|
|
100
|
+
...result,
|
|
101
|
+
parsed: parsed.parsed,
|
|
102
|
+
finalText: parsed.finalText,
|
|
103
|
+
sessionId: parsed.parsed?.session_id ?? parsed.parsed?.sessionId ?? null,
|
|
104
|
+
costUsd: parsed.parsed?.total_cost_usd ?? parsed.parsed?.cost_usd ?? null
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
function runGit(cwd, args) {
|
|
4
|
+
const result = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 10000 });
|
|
5
|
+
return {
|
|
6
|
+
ok: result.status === 0,
|
|
7
|
+
stdout: String(result.stdout ?? "").trim(),
|
|
8
|
+
stderr: String(result.stderr ?? "").trim()
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function gitAvailable(cwd) {
|
|
13
|
+
return runGit(cwd, ["rev-parse", "--is-inside-work-tree"]).ok;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function collectGitContext(cwd, options = {}) {
|
|
17
|
+
if (!gitAvailable(cwd)) {
|
|
18
|
+
return {
|
|
19
|
+
available: false,
|
|
20
|
+
label: "workspace files",
|
|
21
|
+
content: "This directory is not inside a git repository. Review the workspace directly."
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const status = runGit(cwd, ["status", "--short", "--untracked-files=all"]).stdout;
|
|
26
|
+
const branch = runGit(cwd, ["branch", "--show-current"]).stdout || "detached";
|
|
27
|
+
const scope = options.scope ?? "working-tree";
|
|
28
|
+
const base = options.base ?? "";
|
|
29
|
+
let diff = "";
|
|
30
|
+
let label = "working tree";
|
|
31
|
+
|
|
32
|
+
if (scope === "branch" && base) {
|
|
33
|
+
diff = runGit(cwd, ["diff", `${base}...HEAD`]).stdout;
|
|
34
|
+
label = `branch diff against ${base}`;
|
|
35
|
+
} else {
|
|
36
|
+
const staged = runGit(cwd, ["diff", "--cached"]).stdout;
|
|
37
|
+
const unstaged = runGit(cwd, ["diff"]).stdout;
|
|
38
|
+
diff = [staged && "Staged diff:\n" + staged, unstaged && "Unstaged diff:\n" + unstaged]
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.join("\n\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
available: true,
|
|
45
|
+
label,
|
|
46
|
+
content: [
|
|
47
|
+
`Branch: ${branch}`,
|
|
48
|
+
`Status:\n${status || "(clean)"}`,
|
|
49
|
+
`Diff:\n${diff || "(no tracked diff; inspect untracked files listed in status if relevant)"}`
|
|
50
|
+
].join("\n\n")
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { appendLog, generateJobId, listJobs, now, readJob, resolveJobLogFile, upsertJob, writeJob } from "./state.mjs";
|
|
2
|
+
import { spawnDetached, terminateProcessTree } from "./process.mjs";
|
|
3
|
+
|
|
4
|
+
export function createJob(cwd, values) {
|
|
5
|
+
const id = generateJobId(values.prefix ?? "claude");
|
|
6
|
+
const logFile = resolveJobLogFile(cwd, id);
|
|
7
|
+
const job = {
|
|
8
|
+
id,
|
|
9
|
+
kind: values.kind ?? "task",
|
|
10
|
+
title: values.title ?? "Claude Task",
|
|
11
|
+
summary: values.summary ?? "",
|
|
12
|
+
cwd,
|
|
13
|
+
status: "queued",
|
|
14
|
+
phase: "queued",
|
|
15
|
+
pid: null,
|
|
16
|
+
logFile,
|
|
17
|
+
readOnly: Boolean(values.readOnly),
|
|
18
|
+
background: Boolean(values.background),
|
|
19
|
+
createdAt: now()
|
|
20
|
+
};
|
|
21
|
+
writeJob(cwd, id, job);
|
|
22
|
+
upsertJob(cwd, job);
|
|
23
|
+
appendLog(logFile, `[${now()}] Queued ${job.title}.\n`);
|
|
24
|
+
return job;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function markJob(cwd, jobId, patch) {
|
|
28
|
+
const existing = readJob(cwd, jobId) ?? { id: jobId, cwd };
|
|
29
|
+
const next = { ...existing, ...patch, updatedAt: now() };
|
|
30
|
+
writeJob(cwd, jobId, next);
|
|
31
|
+
upsertJob(cwd, next);
|
|
32
|
+
return next;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function recentJobs(cwd, includeAll = false) {
|
|
36
|
+
const jobs = listJobs(cwd);
|
|
37
|
+
return includeAll ? jobs : jobs.slice(0, 10);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function findJob(cwd, reference = "") {
|
|
41
|
+
const jobs = listJobs(cwd);
|
|
42
|
+
if (!reference) {
|
|
43
|
+
return jobs[0] ?? null;
|
|
44
|
+
}
|
|
45
|
+
return jobs.find((job) => job.id === reference || job.id.startsWith(reference)) ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function launchWorker(scriptPath, cwd, jobId) {
|
|
49
|
+
return spawnDetached(process.execPath, [scriptPath, "worker", "--cwd", cwd, "--job-id", jobId], { cwd });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function cancelJob(cwd, reference = "") {
|
|
53
|
+
const job = findJob(cwd, reference);
|
|
54
|
+
if (!job) {
|
|
55
|
+
throw new Error(reference ? `No Claude job found for ${reference}.` : "No Claude job found.");
|
|
56
|
+
}
|
|
57
|
+
const killed = terminateProcessTree(job.pid);
|
|
58
|
+
const next = markJob(cwd, job.id, {
|
|
59
|
+
status: "cancelled",
|
|
60
|
+
phase: "cancelled",
|
|
61
|
+
pid: null,
|
|
62
|
+
completedAt: now(),
|
|
63
|
+
errorMessage: "Cancelled by user."
|
|
64
|
+
});
|
|
65
|
+
appendLog(job.logFile, `[${now()}] Cancel requested. killed=${killed}\n`);
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function binaryStatus(binary, args = ["--version"], options = {}) {
|
|
4
|
+
const result = spawnSync(binary, args, {
|
|
5
|
+
cwd: options.cwd ?? process.cwd(),
|
|
6
|
+
encoding: "utf8",
|
|
7
|
+
timeout: options.timeoutMs ?? 5000
|
|
8
|
+
});
|
|
9
|
+
return {
|
|
10
|
+
available: !result.error && result.status === 0,
|
|
11
|
+
status: result.status,
|
|
12
|
+
stdout: String(result.stdout ?? "").trim(),
|
|
13
|
+
stderr: String(result.stderr ?? "").trim(),
|
|
14
|
+
detail: result.error ? result.error.message : null
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function spawnCapture(command, args, options = {}) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const child = spawn(command, args, {
|
|
21
|
+
cwd: options.cwd ?? process.cwd(),
|
|
22
|
+
env: options.env ?? process.env,
|
|
23
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
24
|
+
windowsHide: true
|
|
25
|
+
});
|
|
26
|
+
let stdout = "";
|
|
27
|
+
let stderr = "";
|
|
28
|
+
child.stdout?.on("data", (chunk) => {
|
|
29
|
+
stdout += chunk.toString();
|
|
30
|
+
options.onStdout?.(chunk.toString(), child.pid);
|
|
31
|
+
});
|
|
32
|
+
child.stderr?.on("data", (chunk) => {
|
|
33
|
+
stderr += chunk.toString();
|
|
34
|
+
options.onStderr?.(chunk.toString(), child.pid);
|
|
35
|
+
});
|
|
36
|
+
child.on("error", (error) => {
|
|
37
|
+
resolve({ status: 1, stdout, stderr, error });
|
|
38
|
+
});
|
|
39
|
+
child.on("close", (status, signal) => {
|
|
40
|
+
resolve({ status: status ?? 1, signal, stdout, stderr, error: null });
|
|
41
|
+
});
|
|
42
|
+
options.onStart?.(child.pid);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function spawnDetached(command, args, options = {}) {
|
|
47
|
+
const child = spawn(command, args, {
|
|
48
|
+
cwd: options.cwd ?? process.cwd(),
|
|
49
|
+
env: options.env ?? process.env,
|
|
50
|
+
detached: true,
|
|
51
|
+
stdio: "ignore",
|
|
52
|
+
windowsHide: true
|
|
53
|
+
});
|
|
54
|
+
child.unref();
|
|
55
|
+
return child;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function terminateProcessTree(pid) {
|
|
59
|
+
const numericPid = Number(pid);
|
|
60
|
+
if (!Number.isFinite(numericPid) || numericPid <= 0) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
process.kill(-numericPid, "SIGTERM");
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
try {
|
|
68
|
+
process.kill(numericPid, "SIGTERM");
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { collectGitContext } from "./git.mjs";
|
|
2
|
+
|
|
3
|
+
export function buildReviewPrompt(cwd, options = {}) {
|
|
4
|
+
const context = collectGitContext(cwd, options);
|
|
5
|
+
return [
|
|
6
|
+
"You are Claude Code acting as a read-only reviewer for Codex.",
|
|
7
|
+
"Do not modify files. Do not run commands that write files. Focus on bugs, regressions, UX issues, missing tests, and maintainability risks.",
|
|
8
|
+
"Return findings first, ordered by severity. Include file paths and line references when you can infer them.",
|
|
9
|
+
options.focus ? `Additional focus: ${options.focus}` : "",
|
|
10
|
+
`Review target: ${context.label}`,
|
|
11
|
+
"Local git context:",
|
|
12
|
+
context.content
|
|
13
|
+
]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join("\n\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildRescuePrompt(prompt, options = {}) {
|
|
19
|
+
const mode = options.readOnly
|
|
20
|
+
? "Read-only mode: investigate, reason, and report. Do not edit files."
|
|
21
|
+
: "Write-capable mode: you may edit files in this checkout when needed to satisfy the task.";
|
|
22
|
+
return [mode, "You are Claude Code being called by Codex as a companion agent.", "Task:", prompt]
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.join("\n\n");
|
|
25
|
+
}
|