@link-assistant/hive-mind 1.53.1 → 1.54.1
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/CHANGELOG.md +27 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +7 -10
- package/src/solve.config.lib.mjs +5 -0
- package/src/tool-comments.lib.mjs +6 -5
- package/src/useless-tools.lib.mjs +187 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.54.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5f70953: fix(solve): post tool-generated PR comments again after v1.53.1 regression
|
|
8
|
+
|
|
9
|
+
`postTrackedComment()` in `src/tool-comments.lib.mjs` (added in #1626) was
|
|
10
|
+
passing the comment body to `gh api --input -` via `$({ input: payload })`,
|
|
11
|
+
but command-stream's option is `stdin`, not `input`. The misnamed key was
|
|
12
|
+
silently ignored, so `gh` read from the parent's stdin, sent an empty POST
|
|
13
|
+
body, and GitHub's edge returned `HTTP 400 "Whoa there!"`. Every tool-posted
|
|
14
|
+
comment — `AI Work Session Started`, log-upload link, `Ready to merge`,
|
|
15
|
+
`Auto-merged`, billing-limit notice, usage-limit notice — failed from this
|
|
16
|
+
one call path starting with v1.53.1.
|
|
17
|
+
|
|
18
|
+
Fix: use the documented `stdin` option so the JSON payload actually reaches
|
|
19
|
+
the child's stdin. The regression test pins the option name so a future
|
|
20
|
+
rename can't silently recur.
|
|
21
|
+
|
|
22
|
+
Fixes #1631.
|
|
23
|
+
|
|
24
|
+
## 1.54.0
|
|
25
|
+
|
|
26
|
+
### Minor Changes
|
|
27
|
+
|
|
28
|
+
- ee156ba: Disable Claude Code built-in tools and MCP servers that have no value in autonomous headless runs. A new `--useless-tools-disabled` flag (default: `true`, use `--no-useless-tools-disabled` to opt out) adds `AskUserQuestion`, `CronCreate/Delete/List`, `EnterPlanMode/ExitPlanMode`, `EnterWorktree/ExitWorktree`, `Monitor`, `NotebookEdit`, `PushNotification`, `RemoteTrigger`, `ScheduleWakeup` and the three `claude.ai` OAuth MCP connectors (Gmail, Google Drive, Google Calendar) to `--disallowedTools` / `--strict-mcp-config` on each `solve` run. The Docker images (`Dockerfile`, `coolify/Dockerfile`) also bake the same `disallowedTools` list into the baseline `~/.claude/settings.json` so interactive `claude` sessions inside the image don't surface them either (issue #1627).
|
|
29
|
+
|
|
3
30
|
## 1.53.1
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { SESSION_FORCE_KILLED_MARKER, postTrackedComment } from './tool-comments
|
|
|
20
20
|
import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; // see issue #1141
|
|
21
21
|
import { CLAUDE_MODELS as availableModels } from './models/index.mjs'; // Issue #1221
|
|
22
22
|
import { buildMcpConfigWithoutPlaywright } from './playwright-mcp.lib.mjs';
|
|
23
|
+
import { resolveClaudeSessionToolFlags } from './useless-tools.lib.mjs';
|
|
23
24
|
export { availableModels }; // Re-export for backward compatibility
|
|
24
25
|
const showResumeCommand = async (sessionId, tempDir, claudePath, model, log) => {
|
|
25
26
|
if (!sessionId || !tempDir) return;
|
|
@@ -774,14 +775,9 @@ export const executeClaudeCommand = async params => {
|
|
|
774
775
|
await log(`🔄 Resuming from session: ${argv.resume}`);
|
|
775
776
|
claudeArgs = `--resume ${argv.resume} ${claudeArgs}`;
|
|
776
777
|
}
|
|
777
|
-
|
|
778
|
-
if (
|
|
779
|
-
|
|
780
|
-
if (mcpConfigPath) {
|
|
781
|
-
claudeArgs += ` --strict-mcp-config --mcp-config "${mcpConfigPath}"`;
|
|
782
|
-
await log('🎭 Playwright MCP physically disabled for this session via --strict-mcp-config', { verbose: true });
|
|
783
|
-
}
|
|
784
|
-
}
|
|
778
|
+
const { mcpConfigPath, disallowedToolsList } = await resolveClaudeSessionToolFlags({ argv, log, fallbackBuildMcpConfigWithoutPlaywright: buildMcpConfigWithoutPlaywright });
|
|
779
|
+
if (mcpConfigPath) claudeArgs += ` --strict-mcp-config --mcp-config "${mcpConfigPath}"`;
|
|
780
|
+
if (disallowedToolsList.length) claudeArgs += ` --disallowedTools ${disallowedToolsList.join(' ')}`;
|
|
785
781
|
claudeArgs += ` -p "${escapedPrompt}" --append-system-prompt "${escapedSystemPrompt}"`;
|
|
786
782
|
const fullCommand = `(cd "${tempDir}" && ${claudePath} ${claudeArgs} | jq -c .)`;
|
|
787
783
|
await log(`\n${formatAligned('📝', 'Raw command:', '')}`);
|
|
@@ -806,11 +802,12 @@ export const executeClaudeCommand = async params => {
|
|
|
806
802
|
}
|
|
807
803
|
const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
|
|
808
804
|
const mcpDisableArgs = mcpConfigPath ? ['--strict-mcp-config', '--mcp-config', mcpConfigPath] : [];
|
|
805
|
+
const disallowedToolsArgs = disallowedToolsList.length ? ['--disallowedTools', ...disallowedToolsList] : [];
|
|
809
806
|
if (argv.resume) {
|
|
810
807
|
const simpleEscapedPrompt = prompt.replace(/"/g, '\\"');
|
|
811
|
-
execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
|
|
808
|
+
execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} ${disallowedToolsArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
|
|
812
809
|
} else {
|
|
813
|
-
execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
810
|
+
execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} ${disallowedToolsArgs} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
814
811
|
}
|
|
815
812
|
await log(`${formatAligned('📋', 'Command details:', '')}`);
|
|
816
813
|
await log(formatAligned('📂', 'Working directory:', tempDir, 2));
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -394,6 +394,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
394
394
|
description: 'Automatically remove .playwright-mcp/ folder before checking for uncommitted changes. This prevents browser automation artifacts from triggering auto-restart. Use --no-playwright-mcp-auto-cleanup to keep the folder for debugging.',
|
|
395
395
|
default: true,
|
|
396
396
|
},
|
|
397
|
+
'useless-tools-disabled': {
|
|
398
|
+
type: 'boolean',
|
|
399
|
+
description: 'Disable Claude Code built-in tools and MCP servers that have no value (and may be harmful) in autonomous headless runs: AskUserQuestion, CronCreate/Delete/List, EnterPlanMode/ExitPlanMode, EnterWorktree/ExitWorktree, Monitor, NotebookEdit, PushNotification, RemoteTrigger, ScheduleWakeup, and the claude.ai Gmail/Drive/Calendar OAuth connectors. Default: true. Use --no-useless-tools-disabled to keep them enabled. Supported for --tool claude (issue #1627).',
|
|
400
|
+
default: true,
|
|
401
|
+
},
|
|
397
402
|
'auto-gh-configuration-repair': {
|
|
398
403
|
type: 'boolean',
|
|
399
404
|
description: 'Automatically repair git configuration using gh-setup-git-identity --repair when git identity is not configured. Requires gh-setup-git-identity to be installed.',
|
|
@@ -199,13 +199,14 @@ export const postTrackedComment = async ({ $, owner, repo, targetNumber, body })
|
|
|
199
199
|
const apiPath = `repos/${owner}/${repo}/issues/${targetNumber}/comments`;
|
|
200
200
|
const payload = JSON.stringify({ body });
|
|
201
201
|
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
202
|
+
// command-stream's options key is `stdin`, not `input` — unknown keys are
|
|
203
|
+
// silently ignored, which previously left stdin inherited from the parent
|
|
204
|
+
// and caused `gh api --input -` to POST an empty body. GitHub's edge
|
|
205
|
+
// replied with HTTP 400 "Whoa there!" *before* the API layer ran. See
|
|
206
|
+
// issue #1631.
|
|
206
207
|
let result;
|
|
207
208
|
try {
|
|
208
|
-
result = await $({
|
|
209
|
+
result = await $({ stdin: payload })`gh api ${apiPath} -X POST --input -`;
|
|
209
210
|
} catch (err) {
|
|
210
211
|
return { ok: false, commentId: null, stderr: err && err.message ? err.message : String(err) };
|
|
211
212
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Useless Claude Code tools and MCP servers for autonomous headless workflows.
|
|
3
|
+
//
|
|
4
|
+
// Hive-mind runs `claude` inside Docker with `--print --dangerously-skip-permissions`
|
|
5
|
+
// and no human operator. Several built-in Claude Code tools and three `claude.ai`
|
|
6
|
+
// OAuth MCP connectors are active by default but either:
|
|
7
|
+
// - wait for a human reaction that will never come (`AskUserQuestion`,
|
|
8
|
+
// `EnterPlanMode`);
|
|
9
|
+
// - have side effects that outlive the session (`CronCreate`,
|
|
10
|
+
// `EnterWorktree`);
|
|
11
|
+
// - can never complete authentication without an interactive browser
|
|
12
|
+
// (`claude.ai Gmail`, `claude.ai Google Drive`, `claude.ai Google Calendar`).
|
|
13
|
+
//
|
|
14
|
+
// This module centralises the block-list and provides helpers that both the
|
|
15
|
+
// Docker image baseline and the `solve` runtime use to disable them.
|
|
16
|
+
//
|
|
17
|
+
// Related issue: https://github.com/link-assistant/hive-mind/issues/1627
|
|
18
|
+
|
|
19
|
+
if (typeof globalThis.use === 'undefined') {
|
|
20
|
+
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
21
|
+
}
|
|
22
|
+
const fs = (await use('fs')).promises;
|
|
23
|
+
const os = await use('os');
|
|
24
|
+
const path = (await use('path')).default;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Built-in Claude Code tools that have no value (and may be harmful) in
|
|
28
|
+
* autonomous headless hive-mind runs. Every entry is a tool name as it
|
|
29
|
+
* appears in the stream-json `tools` array emitted by `claude --verbose`.
|
|
30
|
+
*/
|
|
31
|
+
export const USELESS_CLAUDE_BUILTIN_TOOLS = Object.freeze(['AskUserQuestion', 'CronCreate', 'CronDelete', 'CronList', 'EnterPlanMode', 'EnterWorktree', 'ExitPlanMode', 'ExitWorktree', 'Monitor', 'NotebookEdit', 'PushNotification', 'RemoteTrigger', 'ScheduleWakeup']);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Name prefixes of MCP servers that are always unusable in headless Docker
|
|
35
|
+
* runs because they require interactive OAuth that cannot complete without
|
|
36
|
+
* a browser. Match is case-insensitive on the full MCP server name.
|
|
37
|
+
*/
|
|
38
|
+
export const USELESS_MCP_SERVER_NAME_PREFIXES = Object.freeze(['claude.ai gmail', 'claude.ai google drive', 'claude.ai google calendar']);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* MCP tool-name prefixes derived from {@link USELESS_MCP_SERVER_NAME_PREFIXES}.
|
|
42
|
+
* Claude Code exposes MCP tools as `mcp__<server-name-slug>__<tool-name>`,
|
|
43
|
+
* replacing non-alphanumerics in the server name with `_`. Passing these
|
|
44
|
+
* entries to `--disallowedTools` is a belt-and-braces measure that complements
|
|
45
|
+
* filtering the MCP server itself.
|
|
46
|
+
*/
|
|
47
|
+
export const USELESS_MCP_TOOL_NAME_PREFIXES = Object.freeze(['mcp__claude_ai_Gmail', 'mcp__claude_ai_Google_Drive', 'mcp__claude_ai_Google_Calendar']);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Tool identifiers accepted by `claude --disallowedTools ...`. This is a
|
|
51
|
+
* flat list combining the built-in tools with the wildcard forms of the
|
|
52
|
+
* useless MCP tool-name prefixes.
|
|
53
|
+
*/
|
|
54
|
+
export const buildDisallowedToolsList = () => [...USELESS_CLAUDE_BUILTIN_TOOLS, ...USELESS_MCP_TOOL_NAME_PREFIXES.map(prefix => `${prefix}__*`)];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns true if `name` matches one of the useless MCP server prefixes.
|
|
58
|
+
*/
|
|
59
|
+
export const isUselessMcpServerName = name => {
|
|
60
|
+
if (!name || typeof name !== 'string') return false;
|
|
61
|
+
const lower = name.toLowerCase();
|
|
62
|
+
return USELESS_MCP_SERVER_NAME_PREFIXES.some(prefix => lower.startsWith(prefix));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns the set of MCP server entries from an object (typically
|
|
67
|
+
* `~/.claude.json` `mcpServers` block) with useless entries removed.
|
|
68
|
+
*/
|
|
69
|
+
export const filterMcpServersObject = (mcpServers = {}) => {
|
|
70
|
+
const filtered = {};
|
|
71
|
+
for (const [name, config] of Object.entries(mcpServers || {})) {
|
|
72
|
+
if (isUselessMcpServerName(name)) continue;
|
|
73
|
+
filtered[name] = config;
|
|
74
|
+
}
|
|
75
|
+
return filtered;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build a temporary MCP config JSON file that filters out both the three
|
|
80
|
+
* `claude.ai` OAuth connectors and (optionally) Playwright.
|
|
81
|
+
*
|
|
82
|
+
* Designed to be used with `--strict-mcp-config --mcp-config <file>` so the
|
|
83
|
+
* excluded servers are not even advertised to the model for this run.
|
|
84
|
+
*
|
|
85
|
+
* @param {Object} [options]
|
|
86
|
+
* @param {boolean} [options.excludePlaywright] - Also exclude Playwright.
|
|
87
|
+
* @param {Function} [options.log] - Async logger with (msg, opts) signature.
|
|
88
|
+
* @returns {Promise<string|null>} absolute path to the temp config, or null
|
|
89
|
+
* if the home `.claude.json` file cannot be read (fatal errors are
|
|
90
|
+
* caught — callers should treat `null` as "skip --strict-mcp-config").
|
|
91
|
+
*/
|
|
92
|
+
export const buildFilteredMcpConfig = async ({ excludePlaywright = false, log } = {}) => {
|
|
93
|
+
try {
|
|
94
|
+
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
95
|
+
const claudeJson = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
|
|
96
|
+
const mcpServers = claudeJson.mcpServers || {};
|
|
97
|
+
const filtered = {};
|
|
98
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
99
|
+
if (isUselessMcpServerName(name)) continue;
|
|
100
|
+
if (excludePlaywright && name.toLowerCase().includes('playwright')) continue;
|
|
101
|
+
filtered[name] = config;
|
|
102
|
+
}
|
|
103
|
+
const suffix = excludePlaywright ? 'no-playwright-no-useless' : 'no-useless';
|
|
104
|
+
const tempConfigPath = path.join(os.tmpdir(), `claude-mcp-${suffix}-${Date.now()}-${process.pid}.json`);
|
|
105
|
+
await fs.writeFile(tempConfigPath, JSON.stringify({ mcpServers: filtered }, null, 2));
|
|
106
|
+
if (log) {
|
|
107
|
+
const excluded = [...USELESS_MCP_SERVER_NAME_PREFIXES.map(p => `'${p}*'`), ...(excludePlaywright ? ["'playwright*'"] : [])].join(', ');
|
|
108
|
+
await log(`🧰 Created filtered MCP config (excluding ${excluded}): ${tempConfigPath}`, { verbose: true });
|
|
109
|
+
}
|
|
110
|
+
return tempConfigPath;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (log) await log(`⚠️ Could not build filtered useless-MCP config: ${err.message}`, { verbose: true });
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the per-session Claude CLI args for the useless-tools flag and the
|
|
119
|
+
* playwright-mcp flag in a single step. Returns an object with:
|
|
120
|
+
* - mcpConfigPath (string|null): temp file path for `--strict-mcp-config --mcp-config`
|
|
121
|
+
* - disallowedToolsList (string[]): values for `--disallowedTools`
|
|
122
|
+
*
|
|
123
|
+
* Callers are expected to append the returned values to the Claude command.
|
|
124
|
+
* Extracted from claude.lib.mjs to keep that file under the 1500-line cap.
|
|
125
|
+
*/
|
|
126
|
+
export const resolveClaudeSessionToolFlags = async ({ argv, log, fallbackBuildMcpConfigWithoutPlaywright } = {}) => {
|
|
127
|
+
const uselessToolsDisabled = argv?.uselessToolsDisabled !== false;
|
|
128
|
+
const excludePlaywright = argv?.playwrightMcp === false;
|
|
129
|
+
let mcpConfigPath = null;
|
|
130
|
+
if (uselessToolsDisabled || excludePlaywright) {
|
|
131
|
+
mcpConfigPath = await buildFilteredMcpConfig({ excludePlaywright, log });
|
|
132
|
+
if (!mcpConfigPath && excludePlaywright && fallbackBuildMcpConfigWithoutPlaywright) {
|
|
133
|
+
mcpConfigPath = await fallbackBuildMcpConfigWithoutPlaywright(log);
|
|
134
|
+
}
|
|
135
|
+
if (mcpConfigPath && log) {
|
|
136
|
+
if (excludePlaywright) await log('🎭 Playwright MCP physically disabled for this session via --strict-mcp-config', { verbose: true });
|
|
137
|
+
if (uselessToolsDisabled) await log('🧰 Useless MCP servers (claude.ai Gmail/Drive/Calendar) disabled for this session via --strict-mcp-config (issue #1627)', { verbose: true });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const disallowedToolsList = uselessToolsDisabled ? buildDisallowedToolsList() : [];
|
|
141
|
+
if (uselessToolsDisabled && log) await log(`🧰 Disallowed ${disallowedToolsList.length} useless Claude Code tool(s) for this session (issue #1627)`, { verbose: true });
|
|
142
|
+
return { mcpConfigPath, disallowedToolsList };
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Persist `disallowedTools` in `~/.claude/settings.json` so even interactive
|
|
147
|
+
* `claude` sessions launched outside of `solve` don't surface the useless
|
|
148
|
+
* tools. Existing entries in the settings file are preserved (shallow
|
|
149
|
+
* merge) and any existing `disallowedTools` list has the useless tools
|
|
150
|
+
* added to it without duplicates. Returns the set of tools that were
|
|
151
|
+
* newly added.
|
|
152
|
+
*/
|
|
153
|
+
export const ensureDisallowedToolsInSettings = async ({ settingsPath, log } = {}) => {
|
|
154
|
+
const resolvedPath = settingsPath || path.join(os.homedir(), '.claude', 'settings.json');
|
|
155
|
+
const toBlock = buildDisallowedToolsList();
|
|
156
|
+
let settings = {};
|
|
157
|
+
try {
|
|
158
|
+
const content = await fs.readFile(resolvedPath, 'utf-8');
|
|
159
|
+
settings = JSON.parse(content);
|
|
160
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) settings = {};
|
|
161
|
+
} catch (err) {
|
|
162
|
+
if (err.code !== 'ENOENT' && log) {
|
|
163
|
+
await log(`⚠️ Could not read ${resolvedPath}: ${err.message}`, { verbose: true });
|
|
164
|
+
}
|
|
165
|
+
settings = {};
|
|
166
|
+
}
|
|
167
|
+
const existing = Array.isArray(settings.disallowedTools) ? settings.disallowedTools : [];
|
|
168
|
+
const merged = [...existing];
|
|
169
|
+
const added = [];
|
|
170
|
+
for (const tool of toBlock) {
|
|
171
|
+
if (!merged.includes(tool)) {
|
|
172
|
+
merged.push(tool);
|
|
173
|
+
added.push(tool);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
settings.disallowedTools = merged;
|
|
177
|
+
try {
|
|
178
|
+
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
179
|
+
await fs.writeFile(resolvedPath, JSON.stringify(settings, null, 2));
|
|
180
|
+
if (log && added.length) {
|
|
181
|
+
await log(`🧰 Added ${added.length} useless tool(s) to ${resolvedPath} disallowedTools`, { verbose: true });
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (log) await log(`⚠️ Could not write ${resolvedPath}: ${err.message}`, { verbose: true });
|
|
185
|
+
}
|
|
186
|
+
return { added, total: merged.length, path: resolvedPath };
|
|
187
|
+
};
|