claude-octopus 1.0.1 → 1.0.2

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
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="assets/claude-octopus-icon.svg" alt="Claude Octopus" width="200" />
3
+ </p>
4
+
1
5
  # Claude Octopus
2
6
 
3
7
  One brain, many arms.
@@ -120,23 +124,27 @@ In factory-only mode, no query tools are registered — just the wizard. This ke
120
124
 
121
125
  Each non-factory instance exposes:
122
126
 
123
- | Tool | Purpose |
124
- |------|---------|
125
- | `<name>` | Send a task to the agent, get a response + `session_id` |
126
- | `<name>_reply` | Continue a previous conversation by `session_id` |
127
+ | Tool | Purpose |
128
+ | -------------- | ------------------------------------------------------- |
129
+ | `<name>` | Send a task to the agent, get a response + `session_id` |
130
+ | `<name>_reply` | Continue a previous conversation by `session_id` |
127
131
 
128
132
  Per-invocation parameters (override server defaults):
129
133
 
130
- | Parameter | Description |
131
- |-----------|-------------|
132
- | `prompt` | The task or question (required) |
133
- | `cwd` | Working directory override |
134
- | `model` | Model override |
135
- | `allowedTools` | Tool whitelist (intersects with server default) |
136
- | `disallowedTools` | Tool blacklist (unions with server default) |
137
- | `maxTurns` | Max conversation turns |
138
- | `maxBudgetUsd` | Max spend in USD |
139
- | `systemPrompt` | Additional prompt (appended to server default) |
134
+ | Parameter | Description |
135
+ | ----------------- | ----------------------------------------------- |
136
+ | `prompt` | The task or question (required) |
137
+ | `cwd` | Working directory override |
138
+ | `model` | Model override |
139
+ | `tools` | Restrict available tools (intersects with server restriction) |
140
+ | `disallowedTools` | Block additional tools (unions with server blacklist) |
141
+ | `additionalDirs` | Extra directories the agent can access |
142
+ | `plugins` | Additional plugin paths to load |
143
+ | `effort` | Thinking effort (`low`, `medium`, `high`, `max`) |
144
+ | `permissionMode` | Permission mode (can only tighten, never loosen) |
145
+ | `maxTurns` | Max conversation turns |
146
+ | `maxBudgetUsd` | Max spend in USD |
147
+ | `systemPrompt` | Additional prompt (appended to server default) |
140
148
 
141
149
  ## Configuration
142
150
 
@@ -144,50 +152,50 @@ All configuration is via environment variables in `.mcp.json`. Every env var is
144
152
 
145
153
  ### Identity
146
154
 
147
- | Env Var | Description | Default |
148
- |---------|-------------|---------|
149
- | `CLAUDE_TOOL_NAME` | Tool name prefix (`<name>` and `<name>_reply`) | `claude_code` |
150
- | `CLAUDE_DESCRIPTION` | Tool description shown to the host AI | generic |
151
- | `CLAUDE_SERVER_NAME` | MCP server name in protocol handshake | `claude-octopus` |
152
- | `CLAUDE_FACTORY_ONLY` | Only expose the factory wizard tool | `false` |
155
+ | Env Var | Description | Default |
156
+ | --------------------- | ---------------------------------------------- | ---------------- |
157
+ | `CLAUDE_TOOL_NAME` | Tool name prefix (`<name>` and `<name>_reply`) | `claude_code` |
158
+ | `CLAUDE_DESCRIPTION` | Tool description shown to the host AI | generic |
159
+ | `CLAUDE_SERVER_NAME` | MCP server name in protocol handshake | `claude-octopus` |
160
+ | `CLAUDE_FACTORY_ONLY` | Only expose the factory wizard tool | `false` |
153
161
 
154
162
  ### Agent
155
163
 
156
- | Env Var | Description | Default |
157
- |---------|-------------|---------|
158
- | `CLAUDE_MODEL` | Model (`sonnet`, `opus`, `haiku`, or full ID) | SDK default |
159
- | `CLAUDE_CWD` | Working directory | `process.cwd()` |
160
- | `CLAUDE_PERMISSION_MODE` | `default`, `acceptEdits`, `bypassPermissions`, `plan` | `default` |
161
- | `CLAUDE_ALLOWED_TOOLS` | Comma-separated tool whitelist | all |
162
- | `CLAUDE_DISALLOWED_TOOLS` | Comma-separated tool blacklist | none |
163
- | `CLAUDE_MAX_TURNS` | Max conversation turns | unlimited |
164
- | `CLAUDE_MAX_BUDGET_USD` | Max spend per invocation | unlimited |
165
- | `CLAUDE_EFFORT` | `low`, `medium`, `high`, `max` | SDK default |
164
+ | Env Var | Description | Default |
165
+ | ------------------------- | ----------------------------------------------------- | --------------- |
166
+ | `CLAUDE_MODEL` | Model (`sonnet`, `opus`, `haiku`, or full ID) | SDK default |
167
+ | `CLAUDE_CWD` | Working directory | `process.cwd()` |
168
+ | `CLAUDE_PERMISSION_MODE` | `default`, `acceptEdits`, `bypassPermissions`, `plan` | `default` |
169
+ | `CLAUDE_ALLOWED_TOOLS` | Comma-separated tool restriction (available tools) | all |
170
+ | `CLAUDE_DISALLOWED_TOOLS` | Comma-separated tool blacklist | none |
171
+ | `CLAUDE_MAX_TURNS` | Max conversation turns | unlimited |
172
+ | `CLAUDE_MAX_BUDGET_USD` | Max spend per invocation | unlimited |
173
+ | `CLAUDE_EFFORT` | `low`, `medium`, `high`, `max` | SDK default |
166
174
 
167
175
  ### Prompts
168
176
 
169
- | Env Var | Description |
170
- |---------|-------------|
171
- | `CLAUDE_SYSTEM_PROMPT` | Replaces the default Claude Code system prompt |
177
+ | Env Var | Description |
178
+ | ---------------------- | ------------------------------------------------------ |
179
+ | `CLAUDE_SYSTEM_PROMPT` | Replaces the default Claude Code system prompt |
172
180
  | `CLAUDE_APPEND_PROMPT` | Appended to the default prompt (usually what you want) |
173
181
 
174
182
  ### Advanced
175
183
 
176
- | Env Var | Description |
177
- |---------|-------------|
178
- | `CLAUDE_ADDITIONAL_DIRS` | Extra directories to grant access (comma-separated) |
179
- | `CLAUDE_PLUGINS` | Local plugin paths (comma-separated) |
180
- | `CLAUDE_MCP_SERVERS` | MCP servers for the inner agent (JSON) |
184
+ | Env Var | Description |
185
+ | ------------------------ | -------------------------------------------------------- |
186
+ | `CLAUDE_ADDITIONAL_DIRS` | Extra directories to grant access (comma-separated) |
187
+ | `CLAUDE_PLUGINS` | Local plugin paths (comma-separated) |
188
+ | `CLAUDE_MCP_SERVERS` | MCP servers for the inner agent (JSON) |
181
189
  | `CLAUDE_PERSIST_SESSION` | `true`/`false` — enable session resume (default: `true`) |
182
- | `CLAUDE_SETTING_SOURCES` | Settings to load: `user`, `project`, `local` |
183
- | `CLAUDE_SETTINGS` | Path to settings JSON or inline JSON |
184
- | `CLAUDE_BETAS` | Beta features (comma-separated) |
190
+ | `CLAUDE_SETTING_SOURCES` | Settings to load: `user`, `project`, `local` |
191
+ | `CLAUDE_SETTINGS` | Path to settings JSON or inline JSON |
192
+ | `CLAUDE_BETAS` | Beta features (comma-separated) |
185
193
 
186
194
  ### Authentication
187
195
 
188
- | Env Var | Description | Default |
189
- |---------|-------------|---------|
190
- | `ANTHROPIC_API_KEY` | Anthropic API key for this agent | inherited from parent |
196
+ | Env Var | Description | Default |
197
+ | ------------------------- | -------------------------------------- | --------------------- |
198
+ | `ANTHROPIC_API_KEY` | Anthropic API key for this agent | inherited from parent |
191
199
  | `CLAUDE_CODE_OAUTH_TOKEN` | Claude Code OAuth token for this agent | inherited from parent |
192
200
 
193
201
  Leave both unset to inherit auth from the parent process. Set one per agent to use a different account or billing source.
@@ -196,10 +204,10 @@ Lists accept JSON arrays when values contain commas: `["path,with,comma", "/norm
196
204
 
197
205
  ## Security
198
206
 
199
- - **Permission mode defaults to `default`** — tool executions prompt for approval unless you explicitly set `bypassPermissions`.
200
- - **`cwd` overrides are confined** — per-invocation `cwd` must be a descendant of the server-level base directory. Traversal attempts are silently ignored.
201
- - **Tool restrictions narrow, never widen** — per-invocation `allowedTools` intersects with the server whitelist (can only remove tools, not add). `disallowedTools` unions (can only block more).
202
- - **`_reply` tool respects persistence** — not registered when `CLAUDE_PERSIST_SESSION=false`.
207
+ - **Permission mode defaults to ****`default`** — tool executions prompt for approval unless you explicitly set `bypassPermissions`.
208
+ - **`cwd` overrides preserve agent knowledge** — when the host overrides `cwd`, the agent's configured base directory is automatically added to `additionalDirectories` so it retains access to its own context.
209
+ - **Tool restrictions narrow, never widen** — per-invocation `tools` intersects with the server restriction (can only remove tools, not add). `disallowedTools` unions (can only block more).
210
+ - **`_reply`**** tool respects persistence** — not registered when `CLAUDE_PERSIST_SESSION=false`.
203
211
 
204
212
  ## Architecture
205
213
 
@@ -232,14 +240,14 @@ Lists accept JSON arrays when values contain commas: `["path,with,comma", "/norm
232
240
 
233
241
  ## How It Compares
234
242
 
235
- | Feature | [`claude mcp serve`](https://code.claude.com/docs/en/mcp) | [claude-code-mcp](https://github.com/steipete/claude-code-mcp) | **Claude Octopus** |
236
- |---------|--------------------|--------------------------|--------------------|
237
- | Approach | Built-in | CLI wrapping | Agent SDK |
238
- | Exposes | 16 raw tools | 1 prompt tool | 1 prompt + reply |
239
- | Multi-instance | No | No | Yes |
240
- | Per-instance config | No | No | Yes (18 env vars) |
241
- | Factory wizard | No | No | Yes |
242
- | Session continuity | No | No | Yes |
243
+ | Feature | `` | [claude-code-mcp](https://github.com/steipete/claude-code-mcp) | **Claude Octopus** |
244
+ | ------------------- | ------------ | -------------------------------------------------------------- | ------------------ |
245
+ | Approach | Built-in | CLI wrapping | Agent SDK |
246
+ | Exposes | 16 raw tools | 1 prompt tool | 1 prompt + reply |
247
+ | Multi-instance | No | No | Yes |
248
+ | Per-instance config | No | No | Yes (18 env vars) |
249
+ | Factory wizard | No | No | Yes |
250
+ | Session continuity | No | No | Yes |
243
251
 
244
252
  ## Development
245
253
 
@@ -247,7 +255,7 @@ Lists accept JSON arrays when values contain commas: `["path,with,comma", "/norm
247
255
  pnpm install
248
256
  pnpm build # compile TypeScript
249
257
  pnpm test # run tests (vitest)
250
- pnpm test:coverage # 100% coverage
258
+ pnpm test:coverage # coverage report
251
259
  ```
252
260
 
253
261
  ## License
Binary file
@@ -0,0 +1,50 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" shape-rendering="crispEdges">
2
+ <rect x="7" y="6" width="18" height="1" fill="#C56F52"/>
3
+ <rect x="7" y="7" width="18" height="1" fill="#C56F52"/>
4
+ <rect x="7" y="8" width="18" height="1" fill="#C56F52"/>
5
+ <rect x="7" y="9" width="18" height="1" fill="#C56F52"/>
6
+ <rect x="7" y="10" width="4" height="1" fill="#C56F52"/>
7
+ <rect x="13" y="10" width="6" height="1" fill="#C56F52"/>
8
+ <rect x="21" y="10" width="4" height="1" fill="#C56F52"/>
9
+ <rect x="7" y="11" width="4" height="1" fill="#C56F52"/>
10
+ <rect x="13" y="11" width="6" height="1" fill="#C56F52"/>
11
+ <rect x="21" y="11" width="4" height="1" fill="#C56F52"/>
12
+ <rect x="5" y="12" width="22" height="1" fill="#C56F52"/>
13
+ <rect x="5" y="13" width="22" height="1" fill="#C56F52"/>
14
+ <rect x="3" y="14" width="26" height="1" fill="#C56F52"/>
15
+ <rect x="3" y="15" width="26" height="1" fill="#C56F52"/>
16
+ <rect x="3" y="16" width="26" height="1" fill="#C56F52"/>
17
+ <rect x="3" y="17" width="26" height="1" fill="#C56F52"/>
18
+ <rect x="3" y="18" width="2" height="1" fill="#C56F52"/>
19
+ <rect x="7" y="18" width="18" height="1" fill="#C56F52"/>
20
+ <rect x="27" y="18" width="2" height="1" fill="#C56F52"/>
21
+ <rect x="3" y="19" width="2" height="1" fill="#C56F52"/>
22
+ <rect x="7" y="19" width="18" height="1" fill="#C56F52"/>
23
+ <rect x="27" y="19" width="2" height="1" fill="#C56F52"/>
24
+ <rect x="3" y="20" width="2" height="1" fill="#C56F52"/>
25
+ <rect x="7" y="20" width="18" height="1" fill="#C56F52"/>
26
+ <rect x="27" y="20" width="2" height="1" fill="#C56F52"/>
27
+ <rect x="3" y="21" width="2" height="1" fill="#C56F52"/>
28
+ <rect x="7" y="21" width="18" height="1" fill="#C56F52"/>
29
+ <rect x="27" y="21" width="2" height="1" fill="#C56F52"/>
30
+ <rect x="7" y="22" width="2" height="1" fill="#C56F52"/>
31
+ <rect x="11" y="22" width="2" height="1" fill="#C56F52"/>
32
+ <rect x="15" y="22" width="2" height="1" fill="#C56F52"/>
33
+ <rect x="19" y="22" width="2" height="1" fill="#C56F52"/>
34
+ <rect x="23" y="22" width="2" height="1" fill="#C56F52"/>
35
+ <rect x="7" y="23" width="2" height="1" fill="#C56F52"/>
36
+ <rect x="11" y="23" width="2" height="1" fill="#C56F52"/>
37
+ <rect x="15" y="23" width="2" height="1" fill="#C56F52"/>
38
+ <rect x="19" y="23" width="2" height="1" fill="#C56F52"/>
39
+ <rect x="23" y="23" width="2" height="1" fill="#C56F52"/>
40
+ <rect x="7" y="24" width="2" height="1" fill="#C56F52"/>
41
+ <rect x="11" y="24" width="2" height="1" fill="#C56F52"/>
42
+ <rect x="15" y="24" width="2" height="1" fill="#C56F52"/>
43
+ <rect x="19" y="24" width="2" height="1" fill="#C56F52"/>
44
+ <rect x="23" y="24" width="2" height="1" fill="#C56F52"/>
45
+ <rect x="7" y="25" width="2" height="1" fill="#C56F52"/>
46
+ <rect x="11" y="25" width="2" height="1" fill="#C56F52"/>
47
+ <rect x="15" y="25" width="2" height="1" fill="#C56F52"/>
48
+ <rect x="19" y="25" width="2" height="1" fill="#C56F52"/>
49
+ <rect x="23" y="25" width="2" height="1" fill="#C56F52"/>
50
+ </svg>
package/dist/config.js CHANGED
@@ -16,9 +16,9 @@ export function buildBaseOptions() {
16
16
  const model = envStr("CLAUDE_MODEL");
17
17
  if (model)
18
18
  opts.model = model;
19
- const allowed = envList("CLAUDE_ALLOWED_TOOLS");
20
- if (allowed)
21
- opts.allowedTools = allowed;
19
+ const tools = envList("CLAUDE_ALLOWED_TOOLS");
20
+ if (tools)
21
+ opts.tools = tools;
22
22
  const disallowed = envList("CLAUDE_DISALLOWED_TOOLS");
23
23
  if (disallowed)
24
24
  opts.disallowedTools = disallowed;
@@ -62,16 +62,17 @@ export function buildBaseOptions() {
62
62
  }
63
63
  const settings = envStr("CLAUDE_SETTINGS");
64
64
  if (settings) {
65
- if (settings.startsWith("{")) {
65
+ const trimmed = settings.trim();
66
+ if (trimmed.startsWith("{")) {
66
67
  try {
67
- opts.settings = JSON.parse(settings);
68
+ opts.settings = JSON.parse(trimmed);
68
69
  }
69
- catch {
70
- opts.settings = settings;
70
+ catch (e) {
71
+ console.error(`claude-octopus: invalid CLAUDE_SETTINGS JSON: ${e instanceof Error ? e.message : e}`);
71
72
  }
72
73
  }
73
74
  else {
74
- opts.settings = settings;
75
+ opts.settings = trimmed;
75
76
  }
76
77
  }
77
78
  const betas = envList("CLAUDE_BETAS");
package/dist/constants.js CHANGED
@@ -23,8 +23,8 @@ export const OPTION_CATALOG = [
23
23
  {
24
24
  key: "allowedTools",
25
25
  envVar: "CLAUDE_ALLOWED_TOOLS",
26
- label: "Allowed tools",
27
- hint: "Only these tools are available (comma-separated)",
26
+ label: "Available tools",
27
+ hint: "Restrict agent to only these tools (comma-separated)",
28
28
  example: '"Read,Grep,Glob" for read-only; "Bash,Read,Write,Edit,Grep,Glob" for full access',
29
29
  },
30
30
  {
package/dist/index.js CHANGED
@@ -10,15 +10,13 @@
10
10
  */
11
11
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
- import { fileURLToPath } from "node:url";
14
- import { dirname, resolve } from "node:path";
13
+ import { createRequire } from "node:module";
15
14
  import { envStr, envBool, sanitizeToolName } from "./lib.js";
16
15
  import { buildBaseOptions } from "./config.js";
17
16
  import { registerQueryTools } from "./tools/query.js";
18
17
  import { registerFactoryTool } from "./tools/factory.js";
19
- const __filename = fileURLToPath(import.meta.url);
20
- const __dirname = dirname(__filename);
21
- const SERVER_ENTRY = resolve(__dirname, "index.js");
18
+ const require = createRequire(import.meta.url);
19
+ const { version: PKG_VERSION } = require("../package.json");
22
20
  // ── Configuration ──────────────────────────────────────────────────
23
21
  const BASE_OPTIONS = buildBaseOptions();
24
22
  const TOOL_NAME = sanitizeToolName(envStr("CLAUDE_TOOL_NAME") || "claude_code");
@@ -33,12 +31,12 @@ const DEFAULT_DESCRIPTION = [
33
31
  ].join(" ");
34
32
  const TOOL_DESCRIPTION = envStr("CLAUDE_DESCRIPTION") || DEFAULT_DESCRIPTION;
35
33
  // ── Server ─────────────────────────────────────────────────────────
36
- const server = new McpServer({ name: SERVER_NAME, version: "1.0.0" });
34
+ const server = new McpServer({ name: SERVER_NAME, version: PKG_VERSION });
37
35
  if (!FACTORY_ONLY) {
38
36
  registerQueryTools(server, BASE_OPTIONS, TOOL_NAME, TOOL_DESCRIPTION);
39
37
  }
40
38
  if (FACTORY_ONLY) {
41
- registerFactoryTool(server, SERVER_ENTRY);
39
+ registerFactoryTool(server);
42
40
  }
43
41
  // ── Start ──────────────────────────────────────────────────────────
44
42
  async function main() {
package/dist/lib.d.ts CHANGED
@@ -9,10 +9,16 @@ export declare function envJson<T>(key: string, env?: Record<string, string | un
9
9
  export declare const MAX_TOOL_NAME_LEN: number;
10
10
  export declare function sanitizeToolName(raw: string): string;
11
11
  export declare function isDescendantPath(requested: string, baseCwd: string): boolean;
12
- export declare function mergeAllowedTools(serverList: string[] | undefined, callList: string[]): string[];
12
+ export declare function mergeTools(serverList: string[] | undefined, callList: string[]): string[];
13
13
  export declare function mergeDisallowedTools(serverList: string[] | undefined, callList: string[]): string[];
14
14
  export declare const VALID_PERM_MODES: Set<string>;
15
15
  export declare function validatePermissionMode(mode: string): string;
16
+ /**
17
+ * Narrow permission mode: returns the stricter of base and override.
18
+ * Callers can tighten permissions but never loosen them.
19
+ * Returns base unchanged if override is invalid or less strict.
20
+ */
21
+ export declare function narrowPermissionMode(base: string, override: string): string;
16
22
  export declare function deriveServerName(description: string): string;
17
23
  export declare function deriveToolName(name: string): string;
18
24
  export declare function serializeArrayEnv(val: unknown[]): string;
package/dist/lib.js CHANGED
@@ -69,7 +69,7 @@ export function isDescendantPath(requested, baseCwd) {
69
69
  return normalReq.startsWith(baseWithSep);
70
70
  }
71
71
  // ── Tool restriction merging ───────────────────────────────────────
72
- export function mergeAllowedTools(serverList, callList) {
72
+ export function mergeTools(serverList, callList) {
73
73
  if (serverList?.length) {
74
74
  const serverSet = new Set(serverList);
75
75
  return callList.filter((t) => serverSet.has(t));
@@ -87,11 +87,30 @@ export const VALID_PERM_MODES = new Set([
87
87
  "bypassPermissions",
88
88
  "plan",
89
89
  "dontAsk",
90
- "auto",
91
90
  ]);
92
91
  export function validatePermissionMode(mode) {
93
92
  return VALID_PERM_MODES.has(mode) ? mode : "default";
94
93
  }
94
+ // Strictness order: most permissive → most restrictive
95
+ const PERM_STRICTNESS = {
96
+ bypassPermissions: 0,
97
+ acceptEdits: 1,
98
+ default: 2,
99
+ plan: 3,
100
+ dontAsk: 4,
101
+ };
102
+ /**
103
+ * Narrow permission mode: returns the stricter of base and override.
104
+ * Callers can tighten permissions but never loosen them.
105
+ * Returns base unchanged if override is invalid or less strict.
106
+ */
107
+ export function narrowPermissionMode(base, override) {
108
+ if (!VALID_PERM_MODES.has(override))
109
+ return base;
110
+ const baseLevel = PERM_STRICTNESS[base] ?? 2;
111
+ const overrideLevel = PERM_STRICTNESS[override] ?? 2;
112
+ return overrideLevel >= baseLevel ? override : base;
113
+ }
95
114
  // ── Factory name derivation ────────────────────────────────────────
96
115
  export function deriveServerName(description) {
97
116
  const slug = description
@@ -1,2 +1,2 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- export declare function registerFactoryTool(server: McpServer, serverEntry: string): void;
2
+ export declare function registerFactoryTool(server: McpServer): void;
@@ -23,14 +23,14 @@ function buildEnvFromParams(params) {
23
23
  }
24
24
  return env;
25
25
  }
26
- export function registerFactoryTool(server, serverEntry) {
26
+ export function registerFactoryTool(server) {
27
27
  server.registerTool("create_claude_code_mcp", {
28
28
  description: [
29
- "Create a new specialized Claude Code agent as an MCP server.",
30
- "WHEN TO USE: user says 'create agent', 'new agent', 'set up agent',",
31
- "'configure agent', 'add a reviewer', 'add a test writer',",
32
- "'make me a code reviewer', 'I need a specialized agent', etc.",
33
- "DO NOT USE when user just wants to run a task — use the main tool for that.",
29
+ "Generate a .mcp.json config entry for a new Claude Octopus MCP server instance.",
30
+ "WHEN TO USE: user says 'octopus agent', 'octopus mcp',",
31
+ "'new octopus', 'add octopus', 'create octopus',",
32
+ "'octopus instance', 'octopus config', 'octopus server',",
33
+ "or any phrase combining 'octopus' with agent/mcp/new/add/create/config/server/setup.",
34
34
  "This is a wizard: only a description is required.",
35
35
  "Returns a ready-to-use .mcp.json config and lists all customization options.",
36
36
  "Call again with more parameters to refine.",
@@ -73,11 +73,17 @@ export function registerFactoryTool(server, serverEntry) {
73
73
  Object.assign(env, optionEnv);
74
74
  const configured = Object.keys(optionEnv);
75
75
  const notConfigured = OPTION_CATALOG.filter((o) => !configured.includes(o.envVar));
76
+ const SENSITIVE_KEYS = new Set(["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"]);
77
+ // Redact secrets in rendered output, keep real values in config
78
+ const displayEnv = {};
79
+ for (const [k, v] of Object.entries(env)) {
80
+ displayEnv[k] = SENSITIVE_KEYS.has(k) ? "<REDACTED>" : v;
81
+ }
76
82
  const mcpEntry = {
77
83
  [name]: {
78
- command: "node",
79
- args: [serverEntry],
80
- env,
84
+ command: "npx",
85
+ args: ["claude-octopus"],
86
+ env: displayEnv,
81
87
  },
82
88
  };
83
89
  const sections = [];
@@ -89,7 +95,8 @@ export function registerFactoryTool(server, serverEntry) {
89
95
  for (const key of configured) {
90
96
  const opt = OPTION_CATALOG.find((o) => o.envVar === key);
91
97
  if (opt) {
92
- sections.push(`| ${opt.label} | \`${env[key]}\` |`);
98
+ const val = SENSITIVE_KEYS.has(key) ? "<REDACTED>" : env[key];
99
+ sections.push(`| ${opt.label} | \`${val}\` |`);
93
100
  }
94
101
  }
95
102
  if (configured.length === 0) {
@@ -1,17 +1,53 @@
1
1
  import { z } from "zod/v4";
2
2
  import { resolve } from "node:path";
3
3
  import { query, } from "@anthropic-ai/claude-agent-sdk";
4
- import { isDescendantPath, mergeAllowedTools, mergeDisallowedTools, buildResultPayload, formatErrorMessage, } from "../lib.js";
4
+ import { mergeTools, mergeDisallowedTools, narrowPermissionMode, buildResultPayload, formatErrorMessage, } from "../lib.js";
5
5
  async function runQuery(prompt, overrides, baseOptions) {
6
6
  const options = { ...baseOptions };
7
+ // Handle cwd override — accept any path, preserve agent's base access
7
8
  if (overrides.cwd) {
8
9
  const baseCwd = baseOptions.cwd || process.cwd();
9
- if (isDescendantPath(overrides.cwd, baseCwd)) {
10
- options.cwd = resolve(baseCwd, overrides.cwd);
10
+ const resolvedCwd = resolve(baseCwd, overrides.cwd);
11
+ if (resolvedCwd !== baseCwd) {
12
+ options.cwd = resolvedCwd;
13
+ // Agent's base dir becomes an additional dir so it keeps its knowledge
14
+ const dirs = new Set(options.additionalDirectories || []);
15
+ dirs.add(baseCwd);
16
+ options.additionalDirectories = [...dirs];
11
17
  }
12
18
  }
19
+ // Per-invocation additionalDirs — unions with server-level + auto-added dirs
20
+ if (overrides.additionalDirs?.length) {
21
+ const dirs = new Set(options.additionalDirectories || []);
22
+ for (const dir of overrides.additionalDirs) {
23
+ dirs.add(dir);
24
+ }
25
+ options.additionalDirectories = [...dirs];
26
+ }
27
+ // Per-invocation plugins — unions with server-level plugins
28
+ if (overrides.plugins?.length) {
29
+ const base = baseOptions.plugins || [];
30
+ const overridePaths = new Set(base.map((p) => p.path));
31
+ const merged = [...base];
32
+ for (const path of overrides.plugins) {
33
+ if (!overridePaths.has(path)) {
34
+ merged.push({ type: "local", path });
35
+ overridePaths.add(path);
36
+ }
37
+ }
38
+ options.plugins = merged;
39
+ }
13
40
  if (overrides.model)
14
41
  options.model = overrides.model;
42
+ if (overrides.effort)
43
+ options.effort = overrides.effort;
44
+ // Permission mode can only tighten, never loosen
45
+ if (overrides.permissionMode) {
46
+ const base = baseOptions.permissionMode || "default";
47
+ const narrowed = narrowPermissionMode(base, overrides.permissionMode);
48
+ options.permissionMode = narrowed;
49
+ options.allowDangerouslySkipPermissions = narrowed === "bypassPermissions";
50
+ }
15
51
  if (overrides.maxTurns !== undefined && overrides.maxTurns > 0) {
16
52
  options.maxTurns = overrides.maxTurns;
17
53
  }
@@ -19,8 +55,9 @@ async function runQuery(prompt, overrides, baseOptions) {
19
55
  options.maxBudgetUsd = overrides.maxBudgetUsd;
20
56
  if (overrides.resumeSessionId)
21
57
  options.resume = overrides.resumeSessionId;
22
- if (overrides.allowedTools?.length) {
23
- options.allowedTools = mergeAllowedTools(baseOptions.allowedTools, overrides.allowedTools);
58
+ if (overrides.tools?.length) {
59
+ const baseTools = Array.isArray(baseOptions.tools) ? baseOptions.tools : undefined;
60
+ options.tools = mergeTools(baseTools, overrides.tools);
24
61
  }
25
62
  if (overrides.disallowedTools?.length) {
26
63
  options.disallowedTools = mergeDisallowedTools(baseOptions.disallowedTools, overrides.disallowedTools);
@@ -87,16 +124,20 @@ export function registerQueryTools(server, baseOptions, toolName, toolDescriptio
87
124
  prompt: z.string().describe("Task or question for Claude Code"),
88
125
  cwd: z.string().optional().describe("Working directory (overrides CLAUDE_CWD)"),
89
126
  model: z.string().optional().describe('Model override (e.g. "sonnet", "opus", "haiku")'),
90
- allowedTools: z.array(z.string()).optional().describe("Tool whitelist override"),
91
- disallowedTools: z.array(z.string()).optional().describe("Tool blacklist override"),
127
+ tools: z.array(z.string()).optional().describe("Restrict available tools to this list (intersects with server-level restriction)"),
128
+ disallowedTools: z.array(z.string()).optional().describe("Additional tools to block (unions with server-level blacklist)"),
129
+ additionalDirs: z.array(z.string()).optional().describe("Extra directories the agent can access for this invocation"),
130
+ plugins: z.array(z.string()).optional().describe("Additional plugin paths to load for this invocation (unions with server-level plugins)"),
131
+ effort: z.enum(["low", "medium", "high", "max"]).optional().describe("Thinking effort override"),
132
+ permissionMode: z.enum(["default", "acceptEdits", "plan"]).optional().describe("Permission mode override (can only tighten, never loosen)"),
92
133
  maxTurns: z.number().int().positive().optional().describe("Max conversation turns"),
93
- maxBudgetUsd: z.number().optional().describe("Max spend in USD"),
134
+ maxBudgetUsd: z.number().positive().optional().describe("Max spend in USD"),
94
135
  systemPrompt: z.string().optional().describe("Additional system prompt (appended to server default)"),
95
136
  }),
96
- }, async ({ prompt, cwd, model, allowedTools, disallowedTools, maxTurns, maxBudgetUsd, systemPrompt }) => {
137
+ }, async ({ prompt, cwd, model, tools, disallowedTools, additionalDirs, plugins, effort, permissionMode, maxTurns, maxBudgetUsd, systemPrompt }) => {
97
138
  try {
98
139
  const result = await runQuery(prompt, {
99
- cwd, model, allowedTools, disallowedTools, maxTurns, maxBudgetUsd, systemPrompt,
140
+ cwd, model, tools, disallowedTools, additionalDirs, plugins, effort, permissionMode, maxTurns, maxBudgetUsd, systemPrompt,
100
141
  }, baseOptions);
101
142
  return formatResult(result);
102
143
  }
@@ -117,7 +158,7 @@ export function registerQueryTools(server, baseOptions, toolName, toolDescriptio
117
158
  cwd: z.string().optional().describe("Working directory override"),
118
159
  model: z.string().optional().describe("Model override"),
119
160
  maxTurns: z.number().int().positive().optional().describe("Max conversation turns"),
120
- maxBudgetUsd: z.number().optional().describe("Max spend in USD"),
161
+ maxBudgetUsd: z.number().positive().optional().describe("Max spend in USD"),
121
162
  }),
122
163
  }, async ({ session_id, prompt, cwd, model, maxTurns, maxBudgetUsd }) => {
123
164
  try {
package/dist/types.d.ts CHANGED
@@ -2,8 +2,12 @@ import type { Options } from "@anthropic-ai/claude-agent-sdk";
2
2
  export interface InvocationOverrides {
3
3
  cwd?: string;
4
4
  model?: string;
5
- allowedTools?: string[];
5
+ tools?: string[];
6
6
  disallowedTools?: string[];
7
+ additionalDirs?: string[];
8
+ plugins?: string[];
9
+ effort?: string;
10
+ permissionMode?: string;
7
11
  maxTurns?: number;
8
12
  maxBudgetUsd?: number;
9
13
  systemPrompt?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-octopus",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "One brain, many arms — spawn multiple specialized Claude Code agents as MCP servers",
5
5
  "type": "module",
6
6
  "bin": {
package/src/config.ts CHANGED
@@ -29,8 +29,8 @@ export function buildBaseOptions(): Options {
29
29
  const model = envStr("CLAUDE_MODEL");
30
30
  if (model) opts.model = model;
31
31
 
32
- const allowed = envList("CLAUDE_ALLOWED_TOOLS");
33
- if (allowed) opts.allowedTools = allowed;
32
+ const tools = envList("CLAUDE_ALLOWED_TOOLS");
33
+ if (tools) opts.tools = tools;
34
34
  const disallowed = envList("CLAUDE_DISALLOWED_TOOLS");
35
35
  if (disallowed) opts.disallowedTools = disallowed;
36
36
 
@@ -78,14 +78,15 @@ export function buildBaseOptions(): Options {
78
78
 
79
79
  const settings = envStr("CLAUDE_SETTINGS");
80
80
  if (settings) {
81
- if (settings.startsWith("{")) {
81
+ const trimmed = settings.trim();
82
+ if (trimmed.startsWith("{")) {
82
83
  try {
83
- opts.settings = JSON.parse(settings);
84
- } catch {
85
- opts.settings = settings;
84
+ opts.settings = JSON.parse(trimmed);
85
+ } catch (e) {
86
+ console.error(`claude-octopus: invalid CLAUDE_SETTINGS JSON: ${e instanceof Error ? e.message : e}`);
86
87
  }
87
88
  } else {
88
- opts.settings = settings;
89
+ opts.settings = trimmed;
89
90
  }
90
91
  }
91
92
 
package/src/constants.ts CHANGED
@@ -25,8 +25,8 @@ export const OPTION_CATALOG: OptionCatalogEntry[] = [
25
25
  {
26
26
  key: "allowedTools",
27
27
  envVar: "CLAUDE_ALLOWED_TOOLS",
28
- label: "Allowed tools",
29
- hint: "Only these tools are available (comma-separated)",
28
+ label: "Available tools",
29
+ hint: "Restrict agent to only these tools (comma-separated)",
30
30
  example: '"Read,Grep,Glob" for read-only; "Bash,Read,Write,Edit,Grep,Glob" for full access',
31
31
  },
32
32
  {
package/src/index.ts CHANGED
@@ -12,16 +12,14 @@
12
12
 
13
13
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
- import { fileURLToPath } from "node:url";
16
- import { dirname, resolve } from "node:path";
15
+ import { createRequire } from "node:module";
17
16
  import { envStr, envBool, sanitizeToolName } from "./lib.js";
18
17
  import { buildBaseOptions } from "./config.js";
19
18
  import { registerQueryTools } from "./tools/query.js";
20
19
  import { registerFactoryTool } from "./tools/factory.js";
21
20
 
22
- const __filename = fileURLToPath(import.meta.url);
23
- const __dirname = dirname(__filename);
24
- const SERVER_ENTRY = resolve(__dirname, "index.js");
21
+ const require = createRequire(import.meta.url);
22
+ const { version: PKG_VERSION } = require("../package.json");
25
23
 
26
24
  // ── Configuration ──────────────────────────────────────────────────
27
25
 
@@ -43,14 +41,14 @@ const TOOL_DESCRIPTION = envStr("CLAUDE_DESCRIPTION") || DEFAULT_DESCRIPTION;
43
41
 
44
42
  // ── Server ─────────────────────────────────────────────────────────
45
43
 
46
- const server = new McpServer({ name: SERVER_NAME, version: "1.0.0" });
44
+ const server = new McpServer({ name: SERVER_NAME, version: PKG_VERSION });
47
45
 
48
46
  if (!FACTORY_ONLY) {
49
47
  registerQueryTools(server, BASE_OPTIONS, TOOL_NAME, TOOL_DESCRIPTION);
50
48
  }
51
49
 
52
50
  if (FACTORY_ONLY) {
53
- registerFactoryTool(server, SERVER_ENTRY);
51
+ registerFactoryTool(server);
54
52
  }
55
53
 
56
54
  // ── Start ──────────────────────────────────────────────────────────
package/src/lib.test.ts CHANGED
@@ -8,9 +8,10 @@ import {
8
8
  sanitizeToolName,
9
9
  MAX_TOOL_NAME_LEN,
10
10
  isDescendantPath,
11
- mergeAllowedTools,
11
+ mergeTools,
12
12
  mergeDisallowedTools,
13
13
  validatePermissionMode,
14
+ narrowPermissionMode,
14
15
  VALID_PERM_MODES,
15
16
  deriveServerName,
16
17
  deriveToolName,
@@ -202,24 +203,24 @@ describe("isDescendantPath", () => {
202
203
  });
203
204
  });
204
205
 
205
- // ── mergeAllowedTools ──────────────────────────────────────────────
206
+ // ── mergeTools ───────────────────────────────────────────────────
206
207
 
207
- describe("mergeAllowedTools", () => {
208
+ describe("mergeTools", () => {
208
209
  it("intersects when server has a list", () => {
209
210
  expect(
210
- mergeAllowedTools(["Read", "Grep", "Glob"], ["Read", "Write", "Glob"])
211
+ mergeTools(["Read", "Grep", "Glob"], ["Read", "Write", "Glob"])
211
212
  ).toEqual(["Read", "Glob"]);
212
213
  });
213
214
 
214
215
  it("passes through when server has no list", () => {
215
216
  expect(
216
- mergeAllowedTools(undefined, ["Read", "Write"])
217
+ mergeTools(undefined, ["Read", "Write"])
217
218
  ).toEqual(["Read", "Write"]);
218
219
  });
219
220
 
220
221
  it("returns empty when no overlap", () => {
221
222
  expect(
222
- mergeAllowedTools(["Read"], ["Write"])
223
+ mergeTools(["Read"], ["Write"])
223
224
  ).toEqual([]);
224
225
  });
225
226
  });
@@ -257,6 +258,52 @@ describe("validatePermissionMode", () => {
257
258
  expect(validatePermissionMode("allowEdits")).toBe("default");
258
259
  expect(validatePermissionMode("garbage")).toBe("default");
259
260
  expect(validatePermissionMode("")).toBe("default");
261
+ expect(validatePermissionMode("auto")).toBe("default");
262
+ });
263
+ });
264
+
265
+ // ── narrowPermissionMode ──────────────────────────────────────────
266
+
267
+ describe("narrowPermissionMode", () => {
268
+ it("allows tightening from bypassPermissions to stricter modes", () => {
269
+ expect(narrowPermissionMode("bypassPermissions", "acceptEdits")).toBe("acceptEdits");
270
+ expect(narrowPermissionMode("bypassPermissions", "default")).toBe("default");
271
+ expect(narrowPermissionMode("bypassPermissions", "plan")).toBe("plan");
272
+ expect(narrowPermissionMode("bypassPermissions", "dontAsk")).toBe("dontAsk");
273
+ });
274
+
275
+ it("allows tightening from acceptEdits to stricter modes", () => {
276
+ expect(narrowPermissionMode("acceptEdits", "default")).toBe("default");
277
+ expect(narrowPermissionMode("acceptEdits", "plan")).toBe("plan");
278
+ expect(narrowPermissionMode("acceptEdits", "dontAsk")).toBe("dontAsk");
279
+ });
280
+
281
+ it("dontAsk is the strictest — rejects all loosening", () => {
282
+ expect(narrowPermissionMode("dontAsk", "bypassPermissions")).toBe("dontAsk");
283
+ expect(narrowPermissionMode("dontAsk", "acceptEdits")).toBe("dontAsk");
284
+ expect(narrowPermissionMode("dontAsk", "default")).toBe("dontAsk");
285
+ expect(narrowPermissionMode("dontAsk", "plan")).toBe("dontAsk");
286
+ expect(narrowPermissionMode("dontAsk", "dontAsk")).toBe("dontAsk");
287
+ });
288
+
289
+ it("rejects loosening — returns base unchanged", () => {
290
+ expect(narrowPermissionMode("default", "bypassPermissions")).toBe("default");
291
+ expect(narrowPermissionMode("default", "acceptEdits")).toBe("default");
292
+ expect(narrowPermissionMode("plan", "bypassPermissions")).toBe("plan");
293
+ expect(narrowPermissionMode("plan", "acceptEdits")).toBe("plan");
294
+ expect(narrowPermissionMode("plan", "default")).toBe("plan");
295
+ });
296
+
297
+ it("same mode returns same mode", () => {
298
+ expect(narrowPermissionMode("default", "default")).toBe("default");
299
+ expect(narrowPermissionMode("plan", "plan")).toBe("plan");
300
+ expect(narrowPermissionMode("bypassPermissions", "bypassPermissions")).toBe("bypassPermissions");
301
+ });
302
+
303
+ it("invalid override returns base unchanged", () => {
304
+ expect(narrowPermissionMode("default", "garbage")).toBe("default");
305
+ expect(narrowPermissionMode("bypassPermissions", "")).toBe("bypassPermissions");
306
+ expect(narrowPermissionMode("default", "auto")).toBe("default");
260
307
  });
261
308
  });
262
309
 
package/src/lib.ts CHANGED
@@ -94,7 +94,7 @@ export function isDescendantPath(
94
94
 
95
95
  // ── Tool restriction merging ───────────────────────────────────────
96
96
 
97
- export function mergeAllowedTools(
97
+ export function mergeTools(
98
98
  serverList: string[] | undefined,
99
99
  callList: string[]
100
100
  ): string[] {
@@ -121,13 +121,33 @@ export const VALID_PERM_MODES = new Set([
121
121
  "bypassPermissions",
122
122
  "plan",
123
123
  "dontAsk",
124
- "auto",
125
124
  ]);
126
125
 
127
126
  export function validatePermissionMode(mode: string): string {
128
127
  return VALID_PERM_MODES.has(mode) ? mode : "default";
129
128
  }
130
129
 
130
+ // Strictness order: most permissive → most restrictive
131
+ const PERM_STRICTNESS: Record<string, number> = {
132
+ bypassPermissions: 0,
133
+ acceptEdits: 1,
134
+ default: 2,
135
+ plan: 3,
136
+ dontAsk: 4,
137
+ };
138
+
139
+ /**
140
+ * Narrow permission mode: returns the stricter of base and override.
141
+ * Callers can tighten permissions but never loosen them.
142
+ * Returns base unchanged if override is invalid or less strict.
143
+ */
144
+ export function narrowPermissionMode(base: string, override: string): string {
145
+ if (!VALID_PERM_MODES.has(override)) return base;
146
+ const baseLevel = PERM_STRICTNESS[base] ?? 2;
147
+ const overrideLevel = PERM_STRICTNESS[override] ?? 2;
148
+ return overrideLevel >= baseLevel ? override : base;
149
+ }
150
+
131
151
  // ── Factory name derivation ────────────────────────────────────────
132
152
 
133
153
  export function deriveServerName(description: string): string {
@@ -33,15 +33,14 @@ function buildEnvFromParams(
33
33
 
34
34
  export function registerFactoryTool(
35
35
  server: McpServer,
36
- serverEntry: string
37
36
  ) {
38
37
  server.registerTool("create_claude_code_mcp", {
39
38
  description: [
40
- "Create a new specialized Claude Code agent as an MCP server.",
41
- "WHEN TO USE: user says 'create agent', 'new agent', 'set up agent',",
42
- "'configure agent', 'add a reviewer', 'add a test writer',",
43
- "'make me a code reviewer', 'I need a specialized agent', etc.",
44
- "DO NOT USE when user just wants to run a task — use the main tool for that.",
39
+ "Generate a .mcp.json config entry for a new Claude Octopus MCP server instance.",
40
+ "WHEN TO USE: user says 'octopus agent', 'octopus mcp',",
41
+ "'new octopus', 'add octopus', 'create octopus',",
42
+ "'octopus instance', 'octopus config', 'octopus server',",
43
+ "or any phrase combining 'octopus' with agent/mcp/new/add/create/config/server/setup.",
45
44
  "This is a wizard: only a description is required.",
46
45
  "Returns a ready-to-use .mcp.json config and lists all customization options.",
47
46
  "Call again with more parameters to refine.",
@@ -93,11 +92,19 @@ export function registerFactoryTool(
93
92
  (o) => !configured.includes(o.envVar)
94
93
  );
95
94
 
95
+ const SENSITIVE_KEYS = new Set(["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"]);
96
+
97
+ // Redact secrets in rendered output, keep real values in config
98
+ const displayEnv: Record<string, string> = {};
99
+ for (const [k, v] of Object.entries(env)) {
100
+ displayEnv[k] = SENSITIVE_KEYS.has(k) ? "<REDACTED>" : v;
101
+ }
102
+
96
103
  const mcpEntry = {
97
104
  [name]: {
98
- command: "node",
99
- args: [serverEntry],
100
- env,
105
+ command: "npx",
106
+ args: ["claude-octopus"],
107
+ env: displayEnv,
101
108
  },
102
109
  };
103
110
 
@@ -120,7 +127,8 @@ export function registerFactoryTool(
120
127
  for (const key of configured) {
121
128
  const opt = OPTION_CATALOG.find((o) => o.envVar === key);
122
129
  if (opt) {
123
- sections.push(`| ${opt.label} | \`${env[key]}\` |`);
130
+ const val = SENSITIVE_KEYS.has(key) ? "<REDACTED>" : env[key];
131
+ sections.push(`| ${opt.label} | \`${val}\` |`);
124
132
  }
125
133
  }
126
134
  if (configured.length === 0) {
@@ -7,9 +7,9 @@ import {
7
7
  } from "@anthropic-ai/claude-agent-sdk";
8
8
  import type { Options, InvocationOverrides } from "../types.js";
9
9
  import {
10
- isDescendantPath,
11
- mergeAllowedTools,
10
+ mergeTools,
12
11
  mergeDisallowedTools,
12
+ narrowPermissionMode,
13
13
  buildResultPayload,
14
14
  formatErrorMessage,
15
15
  } from "../lib.js";
@@ -21,13 +21,53 @@ async function runQuery(
21
21
  ): Promise<SDKResultMessage> {
22
22
  const options: Options = { ...baseOptions };
23
23
 
24
+ // Handle cwd override — accept any path, preserve agent's base access
24
25
  if (overrides.cwd) {
25
26
  const baseCwd = baseOptions.cwd || process.cwd();
26
- if (isDescendantPath(overrides.cwd, baseCwd)) {
27
- options.cwd = resolve(baseCwd, overrides.cwd);
27
+ const resolvedCwd = resolve(baseCwd, overrides.cwd);
28
+ if (resolvedCwd !== baseCwd) {
29
+ options.cwd = resolvedCwd;
30
+ // Agent's base dir becomes an additional dir so it keeps its knowledge
31
+ const dirs = new Set(options.additionalDirectories || []);
32
+ dirs.add(baseCwd);
33
+ options.additionalDirectories = [...dirs];
28
34
  }
29
35
  }
36
+
37
+ // Per-invocation additionalDirs — unions with server-level + auto-added dirs
38
+ if (overrides.additionalDirs?.length) {
39
+ const dirs = new Set(options.additionalDirectories || []);
40
+ for (const dir of overrides.additionalDirs) {
41
+ dirs.add(dir);
42
+ }
43
+ options.additionalDirectories = [...dirs];
44
+ }
45
+
46
+ // Per-invocation plugins — unions with server-level plugins
47
+ if (overrides.plugins?.length) {
48
+ const base = baseOptions.plugins || [];
49
+ const overridePaths = new Set(base.map((p) => p.path));
50
+ const merged = [...base];
51
+ for (const path of overrides.plugins) {
52
+ if (!overridePaths.has(path)) {
53
+ merged.push({ type: "local" as const, path });
54
+ overridePaths.add(path);
55
+ }
56
+ }
57
+ options.plugins = merged;
58
+ }
59
+
30
60
  if (overrides.model) options.model = overrides.model;
61
+ if (overrides.effort) options.effort = overrides.effort as Options["effort"];
62
+
63
+ // Permission mode can only tighten, never loosen
64
+ if (overrides.permissionMode) {
65
+ const base = (baseOptions.permissionMode as string) || "default";
66
+ const narrowed = narrowPermissionMode(base, overrides.permissionMode);
67
+ options.permissionMode = narrowed as Options["permissionMode"];
68
+ options.allowDangerouslySkipPermissions = narrowed === "bypassPermissions";
69
+ }
70
+
31
71
  if (overrides.maxTurns !== undefined && overrides.maxTurns > 0) {
32
72
  options.maxTurns = overrides.maxTurns;
33
73
  }
@@ -35,11 +75,9 @@ async function runQuery(
35
75
  options.maxBudgetUsd = overrides.maxBudgetUsd;
36
76
  if (overrides.resumeSessionId) options.resume = overrides.resumeSessionId;
37
77
 
38
- if (overrides.allowedTools?.length) {
39
- options.allowedTools = mergeAllowedTools(
40
- baseOptions.allowedTools,
41
- overrides.allowedTools
42
- );
78
+ if (overrides.tools?.length) {
79
+ const baseTools = Array.isArray(baseOptions.tools) ? baseOptions.tools : undefined;
80
+ options.tools = mergeTools(baseTools, overrides.tools);
43
81
  }
44
82
  if (overrides.disallowedTools?.length) {
45
83
  options.disallowedTools = mergeDisallowedTools(
@@ -125,16 +163,20 @@ export function registerQueryTools(
125
163
  prompt: z.string().describe("Task or question for Claude Code"),
126
164
  cwd: z.string().optional().describe("Working directory (overrides CLAUDE_CWD)"),
127
165
  model: z.string().optional().describe('Model override (e.g. "sonnet", "opus", "haiku")'),
128
- allowedTools: z.array(z.string()).optional().describe("Tool whitelist override"),
129
- disallowedTools: z.array(z.string()).optional().describe("Tool blacklist override"),
166
+ tools: z.array(z.string()).optional().describe("Restrict available tools to this list (intersects with server-level restriction)"),
167
+ disallowedTools: z.array(z.string()).optional().describe("Additional tools to block (unions with server-level blacklist)"),
168
+ additionalDirs: z.array(z.string()).optional().describe("Extra directories the agent can access for this invocation"),
169
+ plugins: z.array(z.string()).optional().describe("Additional plugin paths to load for this invocation (unions with server-level plugins)"),
170
+ effort: z.enum(["low", "medium", "high", "max"]).optional().describe("Thinking effort override"),
171
+ permissionMode: z.enum(["default", "acceptEdits", "plan"]).optional().describe("Permission mode override (can only tighten, never loosen)"),
130
172
  maxTurns: z.number().int().positive().optional().describe("Max conversation turns"),
131
- maxBudgetUsd: z.number().optional().describe("Max spend in USD"),
173
+ maxBudgetUsd: z.number().positive().optional().describe("Max spend in USD"),
132
174
  systemPrompt: z.string().optional().describe("Additional system prompt (appended to server default)"),
133
175
  }),
134
- }, async ({ prompt, cwd, model, allowedTools, disallowedTools, maxTurns, maxBudgetUsd, systemPrompt }) => {
176
+ }, async ({ prompt, cwd, model, tools, disallowedTools, additionalDirs, plugins, effort, permissionMode, maxTurns, maxBudgetUsd, systemPrompt }) => {
135
177
  try {
136
178
  const result = await runQuery(prompt, {
137
- cwd, model, allowedTools, disallowedTools, maxTurns, maxBudgetUsd, systemPrompt,
179
+ cwd, model, tools, disallowedTools, additionalDirs, plugins, effort, permissionMode, maxTurns, maxBudgetUsd, systemPrompt,
138
180
  }, baseOptions);
139
181
  return formatResult(result);
140
182
  } catch (error) {
@@ -155,7 +197,7 @@ export function registerQueryTools(
155
197
  cwd: z.string().optional().describe("Working directory override"),
156
198
  model: z.string().optional().describe("Model override"),
157
199
  maxTurns: z.number().int().positive().optional().describe("Max conversation turns"),
158
- maxBudgetUsd: z.number().optional().describe("Max spend in USD"),
200
+ maxBudgetUsd: z.number().positive().optional().describe("Max spend in USD"),
159
201
  }),
160
202
  }, async ({ session_id, prompt, cwd, model, maxTurns, maxBudgetUsd }) => {
161
203
  try {
package/src/types.ts CHANGED
@@ -3,8 +3,12 @@ import type { Options } from "@anthropic-ai/claude-agent-sdk";
3
3
  export interface InvocationOverrides {
4
4
  cwd?: string;
5
5
  model?: string;
6
- allowedTools?: string[];
6
+ tools?: string[];
7
7
  disallowedTools?: string[];
8
+ additionalDirs?: string[];
9
+ plugins?: string[];
10
+ effort?: string;
11
+ permissionMode?: string;
8
12
  maxTurns?: number;
9
13
  maxBudgetUsd?: number;
10
14
  systemPrompt?: string;