@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,81 @@
1
+ import fs from "node:fs";
2
+
3
+ function elapsed(job) {
4
+ const start = Date.parse(job.startedAt ?? job.createdAt ?? "");
5
+ const end = Date.parse(job.completedAt ?? new Date().toISOString());
6
+ if (!Number.isFinite(start) || !Number.isFinite(end)) {
7
+ return "";
8
+ }
9
+ const seconds = Math.max(0, Math.round((end - start) / 1000));
10
+ return `${seconds}s`;
11
+ }
12
+
13
+ export function renderSetup(report) {
14
+ return [
15
+ "# Claude Code Companion Setup",
16
+ "",
17
+ `Node: ${report.node.available ? "available" : "missing"} ${report.node.stdout || report.node.detail || ""}`.trim(),
18
+ `Claude CLI: ${report.claude.available ? "available" : "missing"} ${report.claude.stdout || report.claude.detail || ""}`.trim(),
19
+ `Claude auth: ${
20
+ report.auth.parsed?.loggedIn
21
+ ? "logged in"
22
+ : report.auth.parsed?.loggedIn === false
23
+ ? "not logged in"
24
+ : "unknown; auth probe timed out or returned no JSON"
25
+ }`,
26
+ `State directory: ${report.stateDir}`,
27
+ "",
28
+ report.ready
29
+ ? "Ready. If auth is shown as unknown, run a tiny `claude -p` command to confirm the CLI can use your logged-in session."
30
+ : "Not ready. Run `claude auth login` if auth is missing, then retry."
31
+ ].join("\n") + "\n";
32
+ }
33
+
34
+ export function renderTaskResult(job, result) {
35
+ const lines = [
36
+ `# ${job.title}`,
37
+ "",
38
+ `Job: ${job.id}`,
39
+ `Status: ${result.status === 0 ? "completed" : "failed"}`,
40
+ `Mode: ${job.readOnly ? "read-only" : "write-capable"}`,
41
+ result.sessionId ? `Claude session: ${result.sessionId}` : "",
42
+ result.costUsd != null ? `Cost: $${result.costUsd}` : "",
43
+ "",
44
+ result.finalText || "(Claude returned no final text.)"
45
+ ].filter(Boolean);
46
+ if (result.stderr?.trim()) {
47
+ lines.push("", "## stderr", "", result.stderr.trim());
48
+ }
49
+ return lines.join("\n") + "\n";
50
+ }
51
+
52
+ export function renderQueued(job) {
53
+ return `${job.title} started in the background as ${job.id}. Use \`status\` or \`result\` with this job id.\n`;
54
+ }
55
+
56
+ export function renderStatus(jobs) {
57
+ if (jobs.length === 0) {
58
+ return "No Claude jobs found for this workspace.\n";
59
+ }
60
+ const rows = ["| Job | Kind | Status | Phase | Mode | Elapsed | Summary |", "| --- | --- | --- | --- | --- | --- | --- |"];
61
+ for (const job of jobs) {
62
+ rows.push(
63
+ `| ${job.id} | ${job.kind ?? ""} | ${job.status ?? ""} | ${job.phase ?? ""} | ${job.readOnly ? "read-only" : "write"} | ${elapsed(job)} | ${(job.summary ?? "").replaceAll("|", "\\|")} |`
64
+ );
65
+ }
66
+ return `${rows.join("\n")}\n`;
67
+ }
68
+
69
+ export function renderStoredResult(job, stored) {
70
+ if (!job || !stored) {
71
+ return "No stored Claude result found.\n";
72
+ }
73
+ if (stored.rendered) {
74
+ return stored.rendered;
75
+ }
76
+ let log = "";
77
+ if (stored.logFile && fs.existsSync(stored.logFile)) {
78
+ log = fs.readFileSync(stored.logFile, "utf8");
79
+ }
80
+ return [`# ${stored.title ?? "Claude Job"}`, "", `Job: ${stored.id}`, `Status: ${stored.status}`, "", log || "(No output stored.)"].join("\n") + "\n";
81
+ }
@@ -0,0 +1,154 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ const STATE_VERSION = 1;
7
+ const MAX_JOBS = 50;
8
+
9
+ function nowIso() {
10
+ return new Date().toISOString();
11
+ }
12
+
13
+ export function resolveWorkspaceRoot(cwd) {
14
+ let current = path.resolve(cwd ?? process.cwd());
15
+ while (true) {
16
+ if (fs.existsSync(path.join(current, ".git"))) {
17
+ return current;
18
+ }
19
+ const parent = path.dirname(current);
20
+ if (parent === current) {
21
+ return path.resolve(cwd ?? process.cwd());
22
+ }
23
+ current = parent;
24
+ }
25
+ }
26
+
27
+ function stateRoot() {
28
+ return (
29
+ process.env.CLAUDE_CODE_COMPANION_DATA ||
30
+ process.env.CODEX_PLUGIN_DATA ||
31
+ process.env.PLUGIN_DATA ||
32
+ path.join(os.tmpdir(), "claude-code-companion")
33
+ );
34
+ }
35
+
36
+ export function resolveStateDir(cwd) {
37
+ const workspaceRoot = resolveWorkspaceRoot(cwd);
38
+ let canonical = workspaceRoot;
39
+ try {
40
+ canonical = fs.realpathSync.native(workspaceRoot);
41
+ } catch {
42
+ canonical = workspaceRoot;
43
+ }
44
+ const slug = (path.basename(workspaceRoot) || "workspace")
45
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
46
+ .replace(/^-+|-+$/g, "") || "workspace";
47
+ const hash = createHash("sha256").update(canonical).digest("hex").slice(0, 16);
48
+ return path.join(stateRoot(), `${slug}-${hash}`);
49
+ }
50
+
51
+ export function resolveStateFile(cwd) {
52
+ return path.join(resolveStateDir(cwd), "state.json");
53
+ }
54
+
55
+ export function resolveJobsDir(cwd) {
56
+ return path.join(resolveStateDir(cwd), "jobs");
57
+ }
58
+
59
+ export function ensureStateDir(cwd) {
60
+ fs.mkdirSync(resolveJobsDir(cwd), { recursive: true });
61
+ }
62
+
63
+ function defaultState() {
64
+ return { version: STATE_VERSION, jobs: [] };
65
+ }
66
+
67
+ export function loadState(cwd) {
68
+ const file = resolveStateFile(cwd);
69
+ if (!fs.existsSync(file)) {
70
+ return defaultState();
71
+ }
72
+ try {
73
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
74
+ return {
75
+ ...defaultState(),
76
+ ...parsed,
77
+ jobs: Array.isArray(parsed.jobs) ? parsed.jobs : []
78
+ };
79
+ } catch {
80
+ return defaultState();
81
+ }
82
+ }
83
+
84
+ export function saveState(cwd, state) {
85
+ ensureStateDir(cwd);
86
+ const jobs = [...(state.jobs ?? [])]
87
+ .sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")))
88
+ .slice(0, MAX_JOBS);
89
+ const nextState = { version: STATE_VERSION, jobs };
90
+ fs.writeFileSync(resolveStateFile(cwd), `${JSON.stringify(nextState, null, 2)}\n`, "utf8");
91
+ return nextState;
92
+ }
93
+
94
+ export function updateState(cwd, mutate) {
95
+ const state = loadState(cwd);
96
+ mutate(state);
97
+ return saveState(cwd, state);
98
+ }
99
+
100
+ export function generateJobId(prefix = "job") {
101
+ return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
102
+ }
103
+
104
+ export function upsertJob(cwd, patch) {
105
+ const timestamp = nowIso();
106
+ return updateState(cwd, (state) => {
107
+ const index = state.jobs.findIndex((job) => job.id === patch.id);
108
+ if (index === -1) {
109
+ state.jobs.unshift({ createdAt: timestamp, updatedAt: timestamp, ...patch });
110
+ return;
111
+ }
112
+ state.jobs[index] = { ...state.jobs[index], ...patch, updatedAt: timestamp };
113
+ });
114
+ }
115
+
116
+ export function listJobs(cwd) {
117
+ return loadState(cwd).jobs;
118
+ }
119
+
120
+ export function resolveJobFile(cwd, jobId) {
121
+ ensureStateDir(cwd);
122
+ return path.join(resolveJobsDir(cwd), `${jobId}.json`);
123
+ }
124
+
125
+ export function resolveJobLogFile(cwd, jobId) {
126
+ ensureStateDir(cwd);
127
+ return path.join(resolveJobsDir(cwd), `${jobId}.log`);
128
+ }
129
+
130
+ export function readJob(cwd, jobId) {
131
+ const file = resolveJobFile(cwd, jobId);
132
+ if (!fs.existsSync(file)) {
133
+ return null;
134
+ }
135
+ return JSON.parse(fs.readFileSync(file, "utf8"));
136
+ }
137
+
138
+ export function writeJob(cwd, jobId, payload) {
139
+ ensureStateDir(cwd);
140
+ fs.writeFileSync(resolveJobFile(cwd, jobId), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
141
+ }
142
+
143
+ export function appendLog(logFile, message) {
144
+ const text = String(message ?? "");
145
+ if (!logFile || !text) {
146
+ return;
147
+ }
148
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
149
+ fs.appendFileSync(logFile, text, "utf8");
150
+ }
151
+
152
+ export function now() {
153
+ return nowIso();
154
+ }
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: claude-code-runtime
3
+ description: Use when Codex should delegate a review, diagnosis, implementation task, or background job to Claude Code through the local Claude Code Companion MCP server.
4
+ ---
5
+
6
+ # Claude Code Runtime
7
+
8
+ Use the `claude-code-companion` MCP tools as a thin runtime bridge to the installed `claude` CLI.
9
+
10
+ ## Tools
11
+
12
+ - `setup`: Check whether Node and Claude Code CLI are available and whether Claude auth appears usable.
13
+ - `review`: Run a read-only Claude review over the current git state.
14
+ - `adversarial_review`: Run a read-only Claude critique with custom focus text.
15
+ - `rescue`: Delegate an arbitrary task to Claude Code. Use `readOnly: true` for investigation-only work; otherwise it is write-capable.
16
+ - `status`: Show active and recent Claude jobs.
17
+ - `result`: Show stored output for a completed job.
18
+ - `cancel`: Terminate an active background job.
19
+
20
+ ## Operating Rules
21
+
22
+ - Keep delegation fluid. Do not impose frontend/backend ownership policy unless the user asks for it.
23
+ - Use `background: true` for open-ended or long-running Claude work; use `status` and `result` later.
24
+ - Use read-only review tools when the user asks for critique, planning, diagnosis, or a second opinion without edits.
25
+ - Use write-capable `rescue` when the user explicitly wants Claude to implement or fix something.
26
+ - Omit `model` when the user wants Claude CLI's current default/latest model. Pass `model` only when the user asks for a specific alias or full model name.
27
+ - Claude runs in the same checkout. Do not assume worktree isolation.
28
+ - Preserve the user's task text. Add only minimal context needed to make the delegation clear.