@indianaprado/claude-code-companion 0.1.0 → 0.1.3

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/README.md CHANGED
@@ -99,6 +99,56 @@ The bundled MCP server exposes:
99
99
  - `cancel`
100
100
 
101
101
  `rescue` is write-capable by default. Pass `readOnly: true` for investigation-only delegation.
102
+ The plugin does not set a default Claude permission mode; pass `permissionMode: "plan"` only when
103
+ you explicitly want Claude's plan mode.
104
+
105
+ Modes only control edit posture:
106
+
107
+ - `normal`: no plugin-imposed permission mode.
108
+ - `no_write`: no plan mode; appends a no-edit instruction and denies Claude edit tools.
109
+ - `plan`: passes Claude `--permission-mode plan`.
110
+ - `auto_accept`: passes Claude `--permission-mode acceptEdits`.
111
+
112
+ The same capability flags are available in every mode:
113
+
114
+ - `tools`
115
+ - `allowedTools`
116
+ - `disallowedTools`
117
+ - `mcpConfig`
118
+ - `strictMcpConfig`
119
+ - `addDir`
120
+
121
+ Use `mcpConfig` to give Claude access to local MCP servers for web/search, DBs, browser tools, or
122
+ other local integrations. Avoid recursively giving Claude this `claude-code-companion` MCP unless you
123
+ explicitly want nested Claude calls.
124
+
125
+ Examples:
126
+
127
+ ```bash
128
+ # Allow web search/fetch in no-write mode.
129
+ node scripts/claude-companion.mjs rescue --mode no_write \
130
+ --allowed-tools WebSearch,WebFetch \
131
+ "Use web search to summarize today's NBA Finals news."
132
+
133
+ # Allow local shell programs in no-write mode.
134
+ node scripts/claude-companion.mjs rescue --mode no_write \
135
+ --allowed-tools Bash \
136
+ "Run redis-cli PING and report the output."
137
+
138
+ # Give Claude local MCP servers.
139
+ node scripts/claude-companion.mjs rescue --mode no_write \
140
+ --mcp-config /path/to/claude-mcp-config.json \
141
+ --allowed-tools mcp__server_name__tool_name \
142
+ "Use the MCP tool and summarize the result."
143
+ ```
144
+
145
+ Important: `no_write` blocks Claude's built-in `Edit` and `Write` tools and tells Claude not to edit.
146
+ If you also grant unrestricted `Bash`, a shell command can still mutate files or external systems.
147
+ For hard no-write isolation with Bash/DB tools, run Claude against read-only credentials or an external
148
+ sandbox.
149
+
150
+ `maxBudgetUsd` is optional. Omit it for no cap. Very small caps are rejected by default because
151
+ Claude Code can spend more than that loading context before producing a useful reply.
102
152
 
103
153
  `model` is optional. When omitted, the plugin does not pass `--model`, so Claude CLI chooses its
104
154
  current default/latest model. On this machine, that currently selected `claude-opus-4-5-20251101`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indianaprado/claude-code-companion",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "description": "Claude Code Companion MCP server for Codex.",
6
6
  "type": "module",
@@ -62,13 +62,21 @@ async function executeClaudeJob(cwd, job, request) {
62
62
  });
63
63
  const result = await runClaude(request.prompt, {
64
64
  cwd,
65
+ mode: request.mode,
65
66
  readOnly: request.readOnly,
66
67
  resume: request.resume,
67
68
  sessionId: request.sessionId,
68
69
  model: request.model,
69
70
  effort: request.effort,
70
71
  maxBudgetUsd: request.maxBudgetUsd,
72
+ allowTinyBudget: request.allowTinyBudget,
71
73
  permissionMode: request.permissionMode,
74
+ tools: request.tools,
75
+ allowedTools: request.allowedTools,
76
+ disallowedTools: request.disallowedTools,
77
+ mcpConfig: request.mcpConfig,
78
+ strictMcpConfig: request.strictMcpConfig,
79
+ addDir: request.addDir,
72
80
  onStart: (pid) => {
73
81
  markJob(cwd, job.id, { pid, phase: "claude-running" });
74
82
  appendLog(job.logFile, `[${now()}] Claude process started pid=${pid}.\n`);
@@ -103,6 +111,7 @@ async function runJobRequest(cwd, request, options = {}) {
103
111
  kind: request.kind,
104
112
  title: request.title,
105
113
  summary: request.summary,
114
+ mode: request.mode,
106
115
  readOnly: request.readOnly,
107
116
  background: request.background
108
117
  });
@@ -125,11 +134,20 @@ export async function reviewCommand(input = {}) {
125
134
  title: input.adversarial ? "Claude Adversarial Review" : "Claude Review",
126
135
  summary: input.adversarial ? summarize(input.focus || "Adversarial review") : "Review current git state",
127
136
  prompt,
137
+ mode: input.mode ?? "no_write",
128
138
  readOnly: true,
129
139
  background: Boolean(input.background),
130
140
  model: input.model,
131
141
  effort: input.effort,
132
- maxBudgetUsd: input.maxBudgetUsd
142
+ maxBudgetUsd: input.maxBudgetUsd,
143
+ allowTinyBudget: input.allowTinyBudget,
144
+ permissionMode: input.permissionMode,
145
+ tools: input.tools,
146
+ allowedTools: input.allowedTools,
147
+ disallowedTools: input.disallowedTools,
148
+ mcpConfig: input.mcpConfig,
149
+ strictMcpConfig: input.strictMcpConfig,
150
+ addDir: input.addDir
133
151
  });
134
152
  }
135
153
 
@@ -138,13 +156,15 @@ export async function rescueCommand(input = {}) {
138
156
  if (!input.prompt || !String(input.prompt).trim()) {
139
157
  throw new Error("Provide a task prompt for Claude.");
140
158
  }
141
- const readOnly = Boolean(input.readOnly);
142
- const prompt = buildRescuePrompt(input.prompt, { readOnly });
159
+ const mode = input.mode ?? (input.readOnly ? "no_write" : null);
160
+ const readOnly = mode === "no_write";
161
+ const prompt = buildRescuePrompt(input.prompt, { mode, readOnly });
143
162
  return runJobRequest(cwd, {
144
163
  kind: "task",
145
164
  title: input.resume ? "Claude Resume" : "Claude Task",
146
165
  summary: summarize(input.prompt),
147
166
  prompt,
167
+ mode,
148
168
  readOnly,
149
169
  background: Boolean(input.background),
150
170
  resume: Boolean(input.resume),
@@ -153,7 +173,14 @@ export async function rescueCommand(input = {}) {
153
173
  model: input.model,
154
174
  effort: input.effort,
155
175
  maxBudgetUsd: input.maxBudgetUsd,
156
- permissionMode: input.permissionMode
176
+ allowTinyBudget: input.allowTinyBudget,
177
+ permissionMode: input.permissionMode,
178
+ tools: input.tools,
179
+ allowedTools: input.allowedTools,
180
+ disallowedTools: input.disallowedTools,
181
+ mcpConfig: input.mcpConfig,
182
+ strictMcpConfig: input.strictMcpConfig,
183
+ addDir: input.addDir
157
184
  });
158
185
  }
159
186
 
@@ -188,8 +215,24 @@ async function handleWorker(argv) {
188
215
  async function main() {
189
216
  const [command, ...argv] = process.argv.slice(2);
190
217
  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"]
218
+ valueOptions: [
219
+ "cwd",
220
+ "job-id",
221
+ "base",
222
+ "scope",
223
+ "model",
224
+ "effort",
225
+ "max-budget-usd",
226
+ "session-id",
227
+ "permission-mode",
228
+ "mode",
229
+ "tools",
230
+ "allowed-tools",
231
+ "disallowed-tools",
232
+ "mcp-config",
233
+ "add-dir"
234
+ ],
235
+ booleanOptions: ["json", "background", "read-only", "write", "resume", "fresh", "all", "adversarial", "fork-session", "allow-tiny-budget", "strict-mcp-config"]
193
236
  };
194
237
  const { options, positionals } = parseArgs(argv, common);
195
238
  const cwd = cwdFrom(options);
@@ -212,9 +255,18 @@ async function main() {
212
255
  focus: positionals.join(" "),
213
256
  adversarial: command === "adversarial-review" || options.adversarial,
214
257
  background: options.background,
258
+ mode: options.mode,
215
259
  model: options.model,
216
260
  effort: options.effort,
217
- maxBudgetUsd: options["max-budget-usd"]
261
+ maxBudgetUsd: options["max-budget-usd"],
262
+ allowTinyBudget: Boolean(options["allow-tiny-budget"]),
263
+ permissionMode: options["permission-mode"],
264
+ tools: options.tools,
265
+ allowedTools: options["allowed-tools"],
266
+ disallowedTools: options["disallowed-tools"],
267
+ mcpConfig: options["mcp-config"],
268
+ strictMcpConfig: Boolean(options["strict-mcp-config"]),
269
+ addDir: options["add-dir"]
218
270
  });
219
271
  output(asJson ? response : response.rendered, asJson);
220
272
  return;
@@ -223,6 +275,7 @@ async function main() {
223
275
  const response = await rescueCommand({
224
276
  cwd,
225
277
  prompt: positionals.join(" "),
278
+ mode: options.mode,
226
279
  readOnly: Boolean(options["read-only"]) && !options.write,
227
280
  background: options.background,
228
281
  resume: Boolean(options.resume),
@@ -231,7 +284,14 @@ async function main() {
231
284
  model: options.model,
232
285
  effort: options.effort,
233
286
  maxBudgetUsd: options["max-budget-usd"],
234
- permissionMode: options["permission-mode"]
287
+ allowTinyBudget: Boolean(options["allow-tiny-budget"]),
288
+ permissionMode: options["permission-mode"],
289
+ tools: options.tools,
290
+ allowedTools: options["allowed-tools"],
291
+ disallowedTools: options["disallowed-tools"],
292
+ mcpConfig: options["mcp-config"],
293
+ strictMcpConfig: Boolean(options["strict-mcp-config"]),
294
+ addDir: options["add-dir"]
235
295
  });
236
296
  output(asJson ? response : response.rendered, asJson);
237
297
  return;
@@ -12,6 +12,31 @@ import { renderSetup, renderStatus, renderStoredResult } from "./lib/render.mjs"
12
12
 
13
13
  const PROTOCOL_VERSION = "2024-11-05";
14
14
 
15
+ const modeSchema = {
16
+ type: "string",
17
+ enum: ["normal", "no_write", "plan", "auto_accept"],
18
+ description: "Optional high-level Claude mode. normal adds no permission defaults; no_write blocks edit tools without plan mode; plan uses Claude plan mode; auto_accept uses acceptEdits."
19
+ };
20
+
21
+ const stringOrArraySchema = (description) => ({
22
+ oneOf: [
23
+ { type: "string" },
24
+ { type: "array", items: { type: "string" } }
25
+ ],
26
+ description
27
+ });
28
+
29
+ const capabilityProperties = {
30
+ mode: modeSchema,
31
+ tools: { type: "string", description: "Optional pass-through for Claude --tools, for example 'default' or 'Bash,Read,Edit'." },
32
+ allowedTools: stringOrArraySchema("Optional pass-through for Claude --allowedTools."),
33
+ disallowedTools: stringOrArraySchema("Optional pass-through for Claude --disallowedTools."),
34
+ mcpConfig: stringOrArraySchema("Optional path(s) or JSON string(s) passed to Claude --mcp-config."),
35
+ strictMcpConfig: { type: "boolean", default: false, description: "Pass --strict-mcp-config so Claude uses only the supplied MCP config." },
36
+ addDir: stringOrArraySchema("Optional additional directory path(s) passed to Claude --add-dir."),
37
+ permissionMode: { type: "string", description: "Optional raw Claude --permission-mode. Prefer mode unless you need a CLI-specific value." }
38
+ };
39
+
15
40
  const tools = [
16
41
  {
17
42
  name: "setup",
@@ -35,7 +60,9 @@ const tools = [
35
60
  background: { type: "boolean", default: false },
36
61
  model: { type: "string", description: "Optional Claude model alias or full model name. Omit to let Claude CLI choose its current default/latest model." },
37
62
  effort: { type: "string", enum: ["low", "medium", "high", "xhigh", "max"] },
38
- maxBudgetUsd: { type: "number" }
63
+ maxBudgetUsd: { type: "number", description: "Optional. Omit for no budget cap. Values below 0.25 are rejected unless allowTinyBudget is true." },
64
+ allowTinyBudget: { type: "boolean", default: false },
65
+ ...capabilityProperties
39
66
  }
40
67
  }
41
68
  },
@@ -52,7 +79,9 @@ const tools = [
52
79
  background: { type: "boolean", default: false },
53
80
  model: { type: "string", description: "Optional Claude model alias or full model name. Omit to let Claude CLI choose its current default/latest model." },
54
81
  effort: { type: "string", enum: ["low", "medium", "high", "xhigh", "max"] },
55
- maxBudgetUsd: { type: "number" }
82
+ maxBudgetUsd: { type: "number", description: "Optional. Omit for no budget cap. Values below 0.25 are rejected unless allowTinyBudget is true." },
83
+ allowTinyBudget: { type: "boolean", default: false },
84
+ ...capabilityProperties
56
85
  },
57
86
  required: ["focus"]
58
87
  }
@@ -72,8 +101,9 @@ const tools = [
72
101
  forkSession: { type: "boolean", default: false, description: "Pass through Claude CLI --fork-session when resuming. Behavior depends on the installed Claude CLI." },
73
102
  model: { type: "string", description: "Optional Claude model alias or full model name. Omit to let Claude CLI choose its current default/latest model." },
74
103
  effort: { type: "string", enum: ["low", "medium", "high", "xhigh", "max"] },
75
- maxBudgetUsd: { type: "number" },
76
- permissionMode: { type: "string", enum: ["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"] }
104
+ maxBudgetUsd: { type: "number", description: "Optional. Omit for no budget cap. Values below 0.25 are rejected unless allowTinyBudget is true." },
105
+ allowTinyBudget: { type: "boolean", default: false },
106
+ ...capabilityProperties
77
107
  },
78
108
  required: ["prompt"]
79
109
  }
@@ -227,7 +257,7 @@ async function handle(message) {
227
257
  result: {
228
258
  protocolVersion: PROTOCOL_VERSION,
229
259
  capabilities: { tools: {} },
230
- serverInfo: { name: "claude-code-companion", version: "0.1.0" }
260
+ serverInfo: { name: "claude-code-companion", version: "0.1.3" }
231
261
  }
232
262
  });
233
263
  return;
@@ -1,7 +1,8 @@
1
1
  import { binaryStatus, spawnCapture } from "./process.mjs";
2
2
 
3
- const READ_ONLY_TOOLS = "Read,Glob,Grep,Bash(git *)";
4
- const EDIT_TOOLS = "Edit,MultiEdit,Write,NotebookEdit";
3
+ const MIN_BUDGET_USD = 0.25;
4
+ const NO_WRITE_DISALLOWED_TOOLS = "Edit,Write";
5
+ const VALID_MODES = new Set(["normal", "no_write", "plan", "auto_accept"]);
5
6
 
6
7
  export function getClaudeVersion(cwd) {
7
8
  return binaryStatus("claude", ["--version"], { cwd });
@@ -38,6 +39,28 @@ function normalizeEffort(effort) {
38
39
  return value;
39
40
  }
40
41
 
42
+ function normalizeMode(mode) {
43
+ if (mode == null || mode === "") {
44
+ return null;
45
+ }
46
+ const value = String(mode).trim();
47
+ if (!VALID_MODES.has(value)) {
48
+ throw new Error(`Unsupported Claude mode "${mode}". Use normal, no_write, plan, or auto_accept.`);
49
+ }
50
+ return value;
51
+ }
52
+
53
+ function appendStringList(args, flag, value) {
54
+ if (value == null || value === false) {
55
+ return;
56
+ }
57
+ const values = Array.isArray(value) ? value : [value];
58
+ const normalized = values.map((entry) => String(entry).trim()).filter(Boolean);
59
+ if (normalized.length > 0) {
60
+ args.push(flag, ...normalized);
61
+ }
62
+ }
63
+
41
64
  function parseClaudeJson(stdout) {
42
65
  const text = String(stdout ?? "").trim();
43
66
  if (!text) {
@@ -59,6 +82,10 @@ function parseClaudeJson(stdout) {
59
82
 
60
83
  export async function runClaude(prompt, options = {}) {
61
84
  const args = ["-p", prompt, "--output-format", "json"];
85
+ if (options.mode && options.permissionMode) {
86
+ throw new Error("Use either mode or permissionMode, not both.");
87
+ }
88
+ const mode = normalizeMode(options.mode ?? (options.readOnly ? "no_write" : null));
62
89
  if (options.resume) {
63
90
  if (options.sessionId) {
64
91
  args.push("--resume", options.sessionId);
@@ -78,15 +105,40 @@ export async function runClaude(prompt, options = {}) {
78
105
  if (effort) {
79
106
  args.push("--effort", effort);
80
107
  }
81
- if (options.maxBudgetUsd) {
82
- args.push("--max-budget-usd", String(options.maxBudgetUsd));
108
+ appendStringList(args, "--add-dir", options.addDir);
109
+ appendStringList(args, "--mcp-config", options.mcpConfig);
110
+ if (options.strictMcpConfig) {
111
+ args.push("--strict-mcp-config");
112
+ }
113
+ if (options.tools) {
114
+ args.push("--tools", String(options.tools));
115
+ }
116
+ if (options.maxBudgetUsd != null) {
117
+ const budget = Number(options.maxBudgetUsd);
118
+ if (!Number.isFinite(budget) || budget <= 0) {
119
+ throw new Error("maxBudgetUsd must be a positive number when provided.");
120
+ }
121
+ if (budget < MIN_BUDGET_USD && !options.allowTinyBudget) {
122
+ throw new Error(
123
+ `maxBudgetUsd ${budget} is too low for Claude Code's startup/context cost. Omit maxBudgetUsd for no cap, use at least ${MIN_BUDGET_USD}, or set allowTinyBudget: true.`
124
+ );
125
+ }
126
+ args.push("--max-budget-usd", String(budget));
83
127
  }
84
- if (options.readOnly) {
128
+ appendStringList(args, "--allowedTools", options.allowedTools);
129
+ appendStringList(
130
+ args,
131
+ "--disallowedTools",
132
+ mode === "no_write"
133
+ ? [NO_WRITE_DISALLOWED_TOOLS, ...(Array.isArray(options.disallowedTools) ? options.disallowedTools : [options.disallowedTools]).filter(Boolean)]
134
+ : options.disallowedTools
135
+ );
136
+ if (mode === "plan") {
85
137
  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");
138
+ } else if (mode === "auto_accept") {
139
+ args.push("--permission-mode", "acceptEdits");
140
+ } else if (options.permissionMode) {
141
+ args.push("--permission-mode", options.permissionMode);
90
142
  }
91
143
 
92
144
  const result = await spawnCapture("claude", args, {
@@ -14,6 +14,7 @@ export function createJob(cwd, values) {
14
14
  phase: "queued",
15
15
  pid: null,
16
16
  logFile,
17
+ mode: values.mode ?? null,
17
18
  readOnly: Boolean(values.readOnly),
18
19
  background: Boolean(values.background),
19
20
  createdAt: now()
@@ -16,9 +16,14 @@ export function buildReviewPrompt(cwd, options = {}) {
16
16
  }
17
17
 
18
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.";
19
+ const mode =
20
+ options.mode === "no_write" || options.readOnly
21
+ ? "No-write mode: investigate, reason, and report. Do not edit files."
22
+ : options.mode === "plan"
23
+ ? "Plan mode requested: analyze and propose a plan without editing files."
24
+ : options.mode === "auto_accept"
25
+ ? "Auto-accept mode requested: you may edit files in this checkout when needed to satisfy the task."
26
+ : "";
22
27
  return [mode, "You are Claude Code being called by Codex as a companion agent.", "Task:", prompt]
23
28
  .filter(Boolean)
24
29
  .join("\n\n");
@@ -37,7 +37,7 @@ export function renderTaskResult(job, result) {
37
37
  "",
38
38
  `Job: ${job.id}`,
39
39
  `Status: ${result.status === 0 ? "completed" : "failed"}`,
40
- `Mode: ${job.readOnly ? "read-only" : "write-capable"}`,
40
+ `Mode: ${job.mode ?? (job.readOnly ? "no_write" : "normal")}`,
41
41
  result.sessionId ? `Claude session: ${result.sessionId}` : "",
42
42
  result.costUsd != null ? `Cost: $${result.costUsd}` : "",
43
43
  "",
@@ -60,7 +60,7 @@ export function renderStatus(jobs) {
60
60
  const rows = ["| Job | Kind | Status | Phase | Mode | Elapsed | Summary |", "| --- | --- | --- | --- | --- | --- | --- |"];
61
61
  for (const job of jobs) {
62
62
  rows.push(
63
- `| ${job.id} | ${job.kind ?? ""} | ${job.status ?? ""} | ${job.phase ?? ""} | ${job.readOnly ? "read-only" : "write"} | ${elapsed(job)} | ${(job.summary ?? "").replaceAll("|", "\\|")} |`
63
+ `| ${job.id} | ${job.kind ?? ""} | ${job.status ?? ""} | ${job.phase ?? ""} | ${job.mode ?? (job.readOnly ? "no_write" : "normal")} | ${elapsed(job)} | ${(job.summary ?? "").replaceAll("|", "\\|")} |`
64
64
  );
65
65
  }
66
66
  return `${rows.join("\n")}\n`;
@@ -24,5 +24,9 @@ Use the `claude-code-companion` MCP tools as a thin runtime bridge to the instal
24
24
  - Use read-only review tools when the user asks for critique, planning, diagnosis, or a second opinion without edits.
25
25
  - Use write-capable `rescue` when the user explicitly wants Claude to implement or fix something.
26
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
+ - Omit `maxBudgetUsd` unless the user explicitly asks for a budget cap.
28
+ - Omit `permissionMode` unless the user explicitly asks for a Claude permission mode such as `plan`.
29
+ - Use `mode: "no_write"` when Claude should be able to investigate normally but must not edit files. This is not plan mode.
30
+ - Capability fields (`tools`, `allowedTools`, `disallowedTools`, `mcpConfig`, `strictMcpConfig`, `addDir`) are available in every mode. Pass them only when the user wants Claude to use extra local tools, MCP servers, DB access, browser/search tools, or extra directories.
27
31
  - Claude runs in the same checkout. Do not assume worktree isolation.
28
32
  - Preserve the user's task text. Add only minimal context needed to make the delegation clear.