@link-assistant/hive-mind 1.78.3 → 1.78.5
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 +12 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +2 -12
- package/src/codex.lib.mjs +2 -11
- package/src/interactive-mcp-status.lib.mjs +44 -0
- package/src/interactive-mode.lib.mjs +5 -5
- package/src/playwright-mcp.lib.mjs +92 -0
- package/src/solve.mjs +12 -24
- package/src/use-m-bootstrap.lib.mjs +2 -7
- package/src/version-info.lib.mjs +3 -2
- package/src/npm-global-prefix.lib.mjs +0 -160
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.78.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b3d6588: Remove Hive Mind's npm global prefix workaround now that use-m handles non-writable npm global roots upstream.
|
|
8
|
+
|
|
9
|
+
## 1.78.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 798c352: Fix Playwright MCP availability detection so pending or unavailable server status no longer enables browser automation hints, surface pending status in interactive session comments, and harden Docker verification so Playwright MCP/CLI availability is checked instead of only grepping for a registration.
|
|
14
|
+
|
|
3
15
|
## 1.78.3
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import { buildSolveResumeCommand } from './solve.resume-command.lib.mjs'; // Iss
|
|
|
22
22
|
import { SESSION_FORCE_KILLED_MARKER, postTrackedComment } from './tool-comments.lib.mjs'; // Issue #1625
|
|
23
23
|
import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; // see issue #1141
|
|
24
24
|
import { CLAUDE_MODELS as availableModels } from './models/index.mjs'; // Issue #1221
|
|
25
|
-
import { buildMcpConfigWithoutPlaywright } from './playwright-mcp.lib.mjs';
|
|
25
|
+
import { buildMcpConfigWithoutPlaywright, ensureClaudePlaywrightMcpServer } from './playwright-mcp.lib.mjs';
|
|
26
26
|
import { resolveClaudeSessionToolFlags } from './useless-tools.lib.mjs';
|
|
27
27
|
import { ensureClaudeQuietConfig } from './claude-quiet-config.lib.mjs';
|
|
28
28
|
import { fetchModelInfo } from './model-info.lib.mjs';
|
|
@@ -268,17 +268,7 @@ export const resolveThinkingSettings = async (argv, log) => {
|
|
|
268
268
|
return { thinkingBudget, thinkLevel, translation, isNewVersion, maxBudget };
|
|
269
269
|
};
|
|
270
270
|
/** Check if Playwright MCP is available and connected to Claude @returns {Promise<boolean>} */
|
|
271
|
-
export const checkPlaywrightMcpAvailability =
|
|
272
|
-
try {
|
|
273
|
-
const result = await $`timeout 5 claude mcp list 2>&1`.catch(() => null);
|
|
274
|
-
if (!result || result.code !== 0) return false;
|
|
275
|
-
const output = result.stdout?.toString() || '';
|
|
276
|
-
if (output.toLowerCase().includes('playwright')) return true;
|
|
277
|
-
return false;
|
|
278
|
-
} catch {
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
};
|
|
271
|
+
export const checkPlaywrightMcpAvailability = ensureClaudePlaywrightMcpServer;
|
|
282
272
|
/** Execute Claude with all prompts and settings - main entry point */
|
|
283
273
|
export const executeClaude = async params => {
|
|
284
274
|
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, setLogFile, getLogFile, formatAligned, getResourceSnapshot, claudePath, $ } = params;
|
package/src/codex.lib.mjs
CHANGED
|
@@ -24,7 +24,7 @@ import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
|
24
24
|
import { mapModelToId, resolveCodexReasoningEffort } from './codex.options.lib.mjs';
|
|
25
25
|
import { createInteractiveHandler } from './interactive-mode.lib.mjs';
|
|
26
26
|
import { initProgressMonitoring } from './solve.progress-monitoring.lib.mjs';
|
|
27
|
-
import { getCodexPlaywrightMcpDisableConfigArgs } from './playwright-mcp.lib.mjs';
|
|
27
|
+
import { ensureCodexPlaywrightMcpServer, getCodexPlaywrightMcpDisableConfigArgs } from './playwright-mcp.lib.mjs';
|
|
28
28
|
import { fetchModelInfo } from './model-info.lib.mjs';
|
|
29
29
|
import { defaultModels } from './models/index.mjs';
|
|
30
30
|
import { classifyRetryableError, getRetryDelayMs, maybeSwitchToFallbackModel, waitWithCountdown } from './tool-retry.lib.mjs';
|
|
@@ -573,16 +573,7 @@ export const handleCodexRuntimeSwitch = async () => {
|
|
|
573
573
|
};
|
|
574
574
|
|
|
575
575
|
/** Check if Playwright MCP is available and connected to Codex @returns {Promise<boolean>} */
|
|
576
|
-
export const checkPlaywrightMcpAvailability =
|
|
577
|
-
try {
|
|
578
|
-
const result = await $`timeout 5 codex mcp list 2>&1`.catch(() => null);
|
|
579
|
-
if (!result || result.code !== 0) return false;
|
|
580
|
-
const output = `${result.stdout?.toString() || ''}${result.stderr?.toString() || ''}`;
|
|
581
|
-
return output.toLowerCase().includes('playwright');
|
|
582
|
-
} catch {
|
|
583
|
-
return false;
|
|
584
|
-
}
|
|
585
|
-
};
|
|
576
|
+
export const checkPlaywrightMcpAvailability = ensureCodexPlaywrightMcpServer;
|
|
586
577
|
|
|
587
578
|
// Main function to execute Codex with prompts and settings
|
|
588
579
|
export const executeCodex = async params => {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const PLAYWRIGHT_TOOL_PREFIX = 'mcp__playwright__';
|
|
2
|
+
|
|
3
|
+
export const isUnavailableMcpStatus = status => {
|
|
4
|
+
const normalized = String(status || '').toLowerCase();
|
|
5
|
+
return /\b(pending|disabled|failed|error|disconnected|not[-_\s]+connected|unavailable|timed[-_\s]+out)\b|(?:^|[^a-z0-9_-])timeout(?:$|[^a-z0-9_-])/.test(normalized);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const hasPlaywrightMcpTools = tools => (Array.isArray(tools) ? tools : []).some(tool => String(tool || '').startsWith(PLAYWRIGHT_TOOL_PREFIX));
|
|
9
|
+
|
|
10
|
+
export const formatInteractiveMcpServerStatus = server => {
|
|
11
|
+
const name = server?.name || 'unknown';
|
|
12
|
+
const status = String(server?.status || 'unknown').trim() || 'unknown';
|
|
13
|
+
const normalizedStatus = status.toLowerCase();
|
|
14
|
+
let displayStatus = status;
|
|
15
|
+
|
|
16
|
+
if (normalizedStatus === 'pending') {
|
|
17
|
+
displayStatus = 'pending - not connected; MCP tools unavailable';
|
|
18
|
+
} else if (isUnavailableMcpStatus(status)) {
|
|
19
|
+
displayStatus = `${status} - MCP tools unavailable`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return `\`${name}\` (${displayStatus})`;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const getInteractiveMcpDiagnostics = (mcpServers = [], tools = []) => {
|
|
26
|
+
const servers = Array.isArray(mcpServers) ? mcpServers : [];
|
|
27
|
+
const diagnostics = [];
|
|
28
|
+
|
|
29
|
+
for (const server of servers) {
|
|
30
|
+
const name = String(server?.name || '').toLowerCase();
|
|
31
|
+
if (!name.includes('playwright')) continue;
|
|
32
|
+
if (!isUnavailableMcpStatus(server?.status)) continue;
|
|
33
|
+
if (hasPlaywrightMcpTools(tools)) continue;
|
|
34
|
+
|
|
35
|
+
diagnostics.push(`⚠️ Playwright MCP server is ${server?.status || 'unknown'}, but no \`${PLAYWRIGHT_TOOL_PREFIX}*\` browser tools were exposed. Browser automation hints are disabled until the MCP client reports the server as connected.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return diagnostics;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const formatInteractiveMcpServersList = (mcpServers = []) => {
|
|
42
|
+
const servers = Array.isArray(mcpServers) ? mcpServers : [];
|
|
43
|
+
return servers.length > 0 ? servers.map(formatInteractiveMcpServerStatus).join(', ') : '_None_';
|
|
44
|
+
};
|
|
@@ -39,6 +39,7 @@ import { createCodexEventHandlers } from './interactive-codex-events.lib.mjs';
|
|
|
39
39
|
import { createSystemLifecycleHandlers } from './interactive-system-events.lib.mjs';
|
|
40
40
|
// Issue #1843: turn base64 image tool-results into inline PR-comment images.
|
|
41
41
|
import { createImageRenderer, extractImagePayload, isImageNode } from './interactive-image-render.lib.mjs';
|
|
42
|
+
import { formatInteractiveMcpServersList, getInteractiveMcpDiagnostics } from './interactive-mcp-status.lib.mjs';
|
|
42
43
|
// Issue #1625: track interactive-mode comment IDs so they're excluded from
|
|
43
44
|
// the "did the AI post anything?" check in checkForAiCreatedComments().
|
|
44
45
|
// Use the session-started marker as the single source of truth for the
|
|
@@ -411,7 +412,9 @@ export const createInteractiveHandler = options => {
|
|
|
411
412
|
|
|
412
413
|
// Format MCP servers
|
|
413
414
|
const mcpServers = data.mcp_servers || [];
|
|
414
|
-
const mcpServersList = mcpServers
|
|
415
|
+
const mcpServersList = formatInteractiveMcpServersList(mcpServers);
|
|
416
|
+
const mcpDiagnostics = getInteractiveMcpDiagnostics(mcpServers, tools);
|
|
417
|
+
const mcpDiagnosticsBlock = mcpDiagnostics.length > 0 ? `\n${mcpDiagnostics.map(message => `> ${message}`).join('\n')}\n` : '';
|
|
415
418
|
|
|
416
419
|
// Format slash commands
|
|
417
420
|
const slashCommands = data.slash_commands || [];
|
|
@@ -434,6 +437,7 @@ export const createInteractiveHandler = options => {
|
|
|
434
437
|
| **MCP Servers** | ${mcpServersList} |
|
|
435
438
|
| **Slash Commands** | ${slashCommandsList} |
|
|
436
439
|
| **Agents** | ${agentsList} |
|
|
440
|
+
${mcpDiagnosticsBlock}
|
|
437
441
|
|
|
438
442
|
---
|
|
439
443
|
|
|
@@ -1374,10 +1378,6 @@ export const validateInteractiveModeConfig = async (argv, log) => {
|
|
|
1374
1378
|
return false;
|
|
1375
1379
|
}
|
|
1376
1380
|
|
|
1377
|
-
// Check PR requirement
|
|
1378
|
-
// Note: This should be called after PR is created/determined
|
|
1379
|
-
// The actual PR number check happens during execution
|
|
1380
|
-
|
|
1381
1381
|
await log('🔌 Interactive mode: ENABLED (experimental)', { level: 'info' });
|
|
1382
1382
|
await log(` ${argv.tool || 'claude'} output will be posted as PR comments in real-time.`, { level: 'info' });
|
|
1383
1383
|
|
|
@@ -15,6 +15,21 @@ export const getCommandResultOutput = result => `${result?.stdout?.toString() ||
|
|
|
15
15
|
|
|
16
16
|
export const isCommandResultSuccess = result => getCommandResultCode(result) === 0;
|
|
17
17
|
|
|
18
|
+
export const PLAYWRIGHT_MCP_UNAVAILABLE_PATTERN = /\b(pending|disabled|failed|error|disconnected|not[-_\s]+connected|unavailable|timed[-_\s]+out)\b|(?:^|[^A-Za-z0-9_-])timeout(?:$|[^A-Za-z0-9_-])|\benabled\s*[:=]?\s*false\b/i;
|
|
19
|
+
export const PLAYWRIGHT_MCP_CONNECTED_PATTERN = /\b(connected|enabled)\b|[✓✔]/i;
|
|
20
|
+
|
|
21
|
+
export const getPlaywrightMcpListRows = output =>
|
|
22
|
+
String(output || '')
|
|
23
|
+
.split(/\r?\n/)
|
|
24
|
+
.map(line => line.trim())
|
|
25
|
+
.filter(line => line.toLowerCase().includes('playwright'));
|
|
26
|
+
|
|
27
|
+
export const hasConnectedPlaywrightMcpServer = output => {
|
|
28
|
+
const rows = getPlaywrightMcpListRows(output);
|
|
29
|
+
if (rows.length === 0) return false;
|
|
30
|
+
return rows.some(row => PLAYWRIGHT_MCP_CONNECTED_PATTERN.test(row) && !PLAYWRIGHT_MCP_UNAVAILABLE_PATTERN.test(row));
|
|
31
|
+
};
|
|
32
|
+
|
|
18
33
|
export const checkPlaywrightMcpPackageAvailability = async () => {
|
|
19
34
|
try {
|
|
20
35
|
const result = await $`timeout 5 npx --no-install @playwright/mcp --help 2>&1`.catch(() => null);
|
|
@@ -26,6 +41,83 @@ export const checkPlaywrightMcpPackageAvailability = async () => {
|
|
|
26
41
|
}
|
|
27
42
|
};
|
|
28
43
|
|
|
44
|
+
export const ensureConnectedPlaywrightMcpServer = async ({ list, add, hasPackage = checkPlaywrightMcpPackageAvailability }) => {
|
|
45
|
+
try {
|
|
46
|
+
const result = await list().catch(() => null);
|
|
47
|
+
if (!isCommandResultSuccess(result)) return false;
|
|
48
|
+
const output = getCommandResultOutput(result);
|
|
49
|
+
if (hasConnectedPlaywrightMcpServer(output)) return true;
|
|
50
|
+
if (getPlaywrightMcpListRows(output).length > 0) return false;
|
|
51
|
+
if (!(await hasPackage())) return false;
|
|
52
|
+
|
|
53
|
+
await add().catch(() => null);
|
|
54
|
+
const retryResult = await list().catch(() => null);
|
|
55
|
+
return isCommandResultSuccess(retryResult) && hasConnectedPlaywrightMcpServer(getCommandResultOutput(retryResult));
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const ensureClaudePlaywrightMcpServer = async () =>
|
|
62
|
+
ensureConnectedPlaywrightMcpServer({
|
|
63
|
+
list: () => $`timeout 5 claude mcp list 2>&1`,
|
|
64
|
+
add: () => $`claude mcp add playwright -s user -- npx -y @playwright/mcp@latest --isolated --headless --no-sandbox --timeout-action=600000 --viewport-size 1920x1080`,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const ensureCodexPlaywrightMcpServer = async () =>
|
|
68
|
+
ensureConnectedPlaywrightMcpServer({
|
|
69
|
+
list: () => $`timeout 5 codex mcp list 2>&1`,
|
|
70
|
+
add: () => $`codex mcp add playwright -- npx -y @playwright/mcp@latest --isolated --headless --no-sandbox --timeout-action=600000 --viewport-size 1920x1080`,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const SOLVE_PLAYWRIGHT_MCP_CHECKS = {
|
|
74
|
+
claude: ensureClaudePlaywrightMcpServer,
|
|
75
|
+
codex: ensureCodexPlaywrightMcpServer,
|
|
76
|
+
opencode: checkPlaywrightMcpPackageAvailability,
|
|
77
|
+
agent: checkPlaywrightMcpPackageAvailability,
|
|
78
|
+
gemini: checkPlaywrightMcpPackageAvailability,
|
|
79
|
+
qwen: checkPlaywrightMcpPackageAvailability,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const SOLVE_PLAYWRIGHT_MCP_LABELS = {
|
|
83
|
+
claude: 'Claude Code',
|
|
84
|
+
codex: 'Codex',
|
|
85
|
+
opencode: 'OpenCode',
|
|
86
|
+
agent: 'Agent',
|
|
87
|
+
gemini: 'Gemini',
|
|
88
|
+
qwen: 'Qwen Code',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const getSolvePlaywrightMcpCheckTool = (argv = {}) => argv.tool || 'claude';
|
|
92
|
+
|
|
93
|
+
export const ensureSolvePlaywrightMcpReady = async ({ argv = {}, log = async () => {}, checks = SOLVE_PLAYWRIGHT_MCP_CHECKS } = {}) => {
|
|
94
|
+
if (argv.playwrightMcp === false) {
|
|
95
|
+
await log('🎭 Playwright MCP preflight skipped because --no-playwright-mcp is set', { verbose: true });
|
|
96
|
+
return { ok: true, checkedTools: [], skipped: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const tool = getSolvePlaywrightMcpCheckTool(argv);
|
|
100
|
+
const label = SOLVE_PLAYWRIGHT_MCP_LABELS[tool] || tool;
|
|
101
|
+
const checkFn = checks[tool] || SOLVE_PLAYWRIGHT_MCP_CHECKS[tool] || checkPlaywrightMcpPackageAvailability;
|
|
102
|
+
await log(`🎭 Checking Playwright MCP preflight for ${label}...`, { verbose: true });
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
if (await checkFn()) {
|
|
106
|
+
await log(`🎭 Playwright MCP ready for ${label}`, { verbose: true });
|
|
107
|
+
return { ok: true, checkedTools: [tool], skipped: false };
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
await log(`⚠️ Playwright MCP preflight check threw for ${label}: ${error.message}`, { verbose: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await log('', { level: 'error' });
|
|
114
|
+
await log(`❌ Playwright MCP preflight failed for ${label}`, { level: 'error' });
|
|
115
|
+
await log(' Playwright support is enabled by default, so solve stops before starting an AI working session.', { level: 'error' });
|
|
116
|
+
await log(' Fix the MCP registration or run with --no-playwright-mcp only when browser automation is intentionally disabled.', { level: 'error' });
|
|
117
|
+
await log('');
|
|
118
|
+
return { ok: false, checkedTools: [tool], skipped: false };
|
|
119
|
+
};
|
|
120
|
+
|
|
29
121
|
export const parseCodexMcpServerNames = output =>
|
|
30
122
|
output
|
|
31
123
|
.split(/\r?\n/)
|
package/src/solve.mjs
CHANGED
|
@@ -33,7 +33,7 @@ const { setupTempDirectory, cleanupTempDirectory } = repository;
|
|
|
33
33
|
const results = await import('./solve.results.lib.mjs');
|
|
34
34
|
const { cleanupClaudeFile, showSessionSummary, verifyResults, buildClaudeResumeCommand, buildClaudeAutonomousResumeCommand, buildSolveResumeCommand, maybeAttachWorkingSessionSummary, verifyPullRequestIssueLinkAfterAutoRestart } = results;
|
|
35
35
|
const claudeLib = await import('./claude.lib.mjs');
|
|
36
|
-
const { executeClaude
|
|
36
|
+
const { executeClaude } = claudeLib;
|
|
37
37
|
const githubLinking = await import('./github-linking.lib.mjs');
|
|
38
38
|
const { extractLinkedIssueNumber } = githubLinking;
|
|
39
39
|
const usageLimitLib = await import('./usage-limit.lib.mjs');
|
|
@@ -244,9 +244,20 @@ if (argv.planModel) {
|
|
|
244
244
|
|
|
245
245
|
// Perform all system checks (skip tool connection check in dry-run or when --skip-tool-connection-check; model validation always runs)
|
|
246
246
|
const skipToolConnectionCheck = argv.dryRun || argv.skipToolConnectionCheck || argv.toolConnectionCheck === false;
|
|
247
|
+
const { cascadePlaywrightMcpDisable, ensureSolvePlaywrightMcpReady } = await import('./playwright-mcp.lib.mjs');
|
|
248
|
+
await cascadePlaywrightMcpDisable(argv, log);
|
|
247
249
|
if (!(await performSystemChecks(argv.minDiskSpace || 2048, skipToolConnectionCheck, argv.model, argv))) {
|
|
248
250
|
await safeExit(1, 'System checks failed');
|
|
249
251
|
}
|
|
252
|
+
// Playwright MCP preflight is local/free and stays independent from paid tool connection checks.
|
|
253
|
+
if (!argv.dryRun && argv.playwrightMcp !== false) {
|
|
254
|
+
const playwrightMcpPreflight = await ensureSolvePlaywrightMcpReady({ argv, log });
|
|
255
|
+
if (!playwrightMcpPreflight.ok) {
|
|
256
|
+
await safeExit(1, 'Playwright MCP preflight failed');
|
|
257
|
+
}
|
|
258
|
+
} else if (argv.dryRun && argv.playwrightMcp !== false) {
|
|
259
|
+
await log('⏩ Skipping Playwright MCP preflight (dry-run mode)', { verbose: true });
|
|
260
|
+
}
|
|
250
261
|
// URL validation debug logging
|
|
251
262
|
if (argv.verbose) {
|
|
252
263
|
await log('📋 URL validation:', { verbose: true });
|
|
@@ -692,24 +703,6 @@ try {
|
|
|
692
703
|
$,
|
|
693
704
|
});
|
|
694
705
|
|
|
695
|
-
const { cascadePlaywrightMcpDisable } = await import('./playwright-mcp.lib.mjs');
|
|
696
|
-
await cascadePlaywrightMcpDisable(argv, log);
|
|
697
|
-
|
|
698
|
-
async function resolvePlaywrightMcp(checkFn) {
|
|
699
|
-
if (argv.playwrightMcp === false) return;
|
|
700
|
-
if (argv.promptPlaywrightMcp) {
|
|
701
|
-
const available = await checkFn();
|
|
702
|
-
if (available) {
|
|
703
|
-
await log('🎭 Playwright MCP detected - enabling browser automation hints', { verbose: true });
|
|
704
|
-
} else {
|
|
705
|
-
await log('ℹ️ Playwright MCP not detected - browser automation hints will be disabled', { verbose: true });
|
|
706
|
-
argv.promptPlaywrightMcp = false;
|
|
707
|
-
}
|
|
708
|
-
} else {
|
|
709
|
-
await log('ℹ️ Playwright MCP explicitly disabled via --no-prompt-playwright-mcp', { verbose: true });
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
706
|
// Execute tool command with all prompts and settings
|
|
714
707
|
let toolResult;
|
|
715
708
|
|
|
@@ -734,7 +727,6 @@ try {
|
|
|
734
727
|
}
|
|
735
728
|
|
|
736
729
|
await log(`\n[agent-commander] Using agent-commander for ${argv.tool || 'claude'} execution`);
|
|
737
|
-
await agentCommanderLib.resolvePlaywrightMcpForAgentCommander({ argv, log, tool: argv.tool || 'claude' });
|
|
738
730
|
|
|
739
731
|
toolResult = await agentCommanderLib.executeWithAgentCommander({
|
|
740
732
|
issueUrl,
|
|
@@ -768,7 +760,6 @@ try {
|
|
|
768
760
|
qwen: { lib: './qwen.lib.mjs', execFn: 'executeQwen', envVar: 'QWEN_PATH', defaultBin: 'qwen', pathKey: 'qwenPath' },
|
|
769
761
|
}[argv.tool];
|
|
770
762
|
const toolLib = await import(toolDispatch.lib);
|
|
771
|
-
await resolvePlaywrightMcp(toolLib.checkPlaywrightMcpAvailability);
|
|
772
763
|
|
|
773
764
|
toolResult = await toolLib[toolDispatch.execFn]({
|
|
774
765
|
issueUrl,
|
|
@@ -796,9 +787,6 @@ try {
|
|
|
796
787
|
});
|
|
797
788
|
} else {
|
|
798
789
|
// Default to Claude
|
|
799
|
-
if (argv.tool === 'claude' || !argv.tool) {
|
|
800
|
-
await resolvePlaywrightMcp(checkPlaywrightMcpAvailability);
|
|
801
|
-
}
|
|
802
790
|
const claudeResult = await executeClaude({
|
|
803
791
|
issueUrl,
|
|
804
792
|
issueNumber,
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { ensureWritableNpmGlobalPrefix } from './npm-global-prefix.lib.mjs';
|
|
4
|
-
|
|
5
3
|
const defaultFetchUseMCode = async () => (await fetch('https://unpkg.com/use-m/use.js')).text();
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
|
-
* Load the use-m bootstrap
|
|
9
|
-
* use-m's Node resolver.
|
|
6
|
+
* Load the shared use-m bootstrap.
|
|
10
7
|
*
|
|
11
8
|
* @param {object} [options]
|
|
12
|
-
* @param {(message: string) => void} [options.log]
|
|
13
9
|
* @param {() => Promise<string>} [options.fetchUseMCode]
|
|
14
10
|
* @returns {Promise<Function>} The global use-m `use` function.
|
|
15
11
|
*/
|
|
16
12
|
export const ensureUseM = async (options = {}) => {
|
|
17
|
-
const {
|
|
18
|
-
await ensureWritableNpmGlobalPrefix({ log });
|
|
13
|
+
const { fetchUseMCode = defaultFetchUseMCode } = options;
|
|
19
14
|
if (typeof globalThis.use === 'undefined') {
|
|
20
15
|
globalThis.use = (await eval(await fetchUseMCode())).use;
|
|
21
16
|
}
|
package/src/version-info.lib.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { getVersion } from './version.lib.mjs';
|
|
11
11
|
import { t } from './i18n.lib.mjs';
|
|
12
|
+
import { hasConnectedPlaywrightMcpServer } from './playwright-mcp.lib.mjs';
|
|
12
13
|
import { exec } from 'child_process';
|
|
13
14
|
import { promisify } from 'util';
|
|
14
15
|
|
|
@@ -1221,8 +1222,8 @@ export function formatVersionMessage(versions, options = {}) {
|
|
|
1221
1222
|
// Playwright MCP: show version with Claude Code and Codex connection status inline
|
|
1222
1223
|
if (versions.playwrightMcp) {
|
|
1223
1224
|
const mcpVersion = parseVersion('playwrightMcp', versions.playwrightMcp);
|
|
1224
|
-
const claudeStatus = versions.playwrightMcpClaudeStatus ? vt('connected', {}, vOptions) : vt('not_connected', {}, vOptions);
|
|
1225
|
-
const codexStatus = versions.playwrightMcpCodexStatus ? vt('connected', {}, vOptions) : vt('not_connected', {}, vOptions);
|
|
1225
|
+
const claudeStatus = hasConnectedPlaywrightMcpServer(versions.playwrightMcpClaudeStatus) ? vt('connected', {}, vOptions) : vt('not_connected', {}, vOptions);
|
|
1226
|
+
const codexStatus = hasConnectedPlaywrightMcpServer(versions.playwrightMcpCodexStatus) ? vt('connected', {}, vOptions) : vt('not_connected', {}, vOptions);
|
|
1226
1227
|
browserAutoLines.push(`• Playwright MCP: \`${mcpVersion} | Claude Code: ${claudeStatus} | Codex: ${codexStatus}\``);
|
|
1227
1228
|
}
|
|
1228
1229
|
addVersionLine(browserAutoLines, 'Puppeteer Browsers', versions.puppeteerBrowsers, 'puppeteerBrowsers');
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Ensure npm's global install directory is writable before `use-m` runs.
|
|
5
|
-
*
|
|
6
|
-
* Issue #1897: `use-m` loads runtime dependencies (command-stream, getenv,
|
|
7
|
-
* yargs, …) by shelling out to `npm install -g <alias>@npm:<pkg>@latest`.
|
|
8
|
-
* npm installs into the global prefix reported by `npm root -g`. When the
|
|
9
|
-
* CLI is launched with a system-wide Node.js whose global `node_modules`
|
|
10
|
-
* directory is owned by root (e.g. `/opt/node-v24.16.0-linux-x64/lib/node_modules`),
|
|
11
|
-
* the install fails with `EACCES: permission denied` and the whole process
|
|
12
|
-
* crashes at the very first `use()` call with an unhandled error:
|
|
13
|
-
*
|
|
14
|
-
* Error: Failed to install command-stream@latest globally.
|
|
15
|
-
* ... npm error code EACCES
|
|
16
|
-
* ... npm error syscall rename
|
|
17
|
-
* ... npm error path /opt/node-.../lib/node_modules/command-stream-v-latest
|
|
18
|
-
*
|
|
19
|
-
* This commonly happens when the package was installed with one runtime
|
|
20
|
-
* (e.g. `bun add -g`, which writes to a user-owned `~/.bun/...`) but launched
|
|
21
|
-
* under a system Node whose global prefix needs root.
|
|
22
|
-
*
|
|
23
|
-
* The fix mirrors npm's own documented recommendation for EACCES errors:
|
|
24
|
-
* point the global prefix at a user-writable directory. We detect the
|
|
25
|
-
* non-writable prefix up front and redirect `npm_config_prefix` (which both
|
|
26
|
-
* `npm install -g` and `npm root -g` honour) to `~/.npm-global`, so use-m's
|
|
27
|
-
* install succeeds without sudo. When the prefix is already writable we do
|
|
28
|
-
* nothing — the common case stays a no-op with no extra `npm` spawn.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import { access, mkdir } from 'node:fs/promises';
|
|
32
|
-
import { constants as fsConstants } from 'node:fs';
|
|
33
|
-
import { dirname, join } from 'node:path';
|
|
34
|
-
import { homedir } from 'node:os';
|
|
35
|
-
import { exec } from 'node:child_process';
|
|
36
|
-
import { promisify } from 'node:util';
|
|
37
|
-
|
|
38
|
-
const execAsync = promisify(exec);
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Derive the likely global `node_modules` path from the Node binary location
|
|
42
|
-
* without spawning npm. For a standard POSIX layout the node binary lives at
|
|
43
|
-
* `<prefix>/bin/node` and global packages at `<prefix>/lib/node_modules`.
|
|
44
|
-
*
|
|
45
|
-
* @param {string} execPath - Absolute path to the running node binary.
|
|
46
|
-
* @returns {string} Candidate global `node_modules` directory.
|
|
47
|
-
*/
|
|
48
|
-
export const deriveGlobalNodeModules = execPath => join(dirname(dirname(execPath)), 'lib', 'node_modules');
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Check whether a directory is writable, walking up to the nearest existing
|
|
52
|
-
* ancestor when the leaf does not yet exist (npm would create it on install).
|
|
53
|
-
*
|
|
54
|
-
* @param {string} startDir - Directory to test.
|
|
55
|
-
* @param {(path: string, mode: number) => Promise<void>} accessFn - fs.access.
|
|
56
|
-
* @returns {Promise<boolean>}
|
|
57
|
-
*/
|
|
58
|
-
export const isPathWritable = async (startDir, accessFn = access) => {
|
|
59
|
-
let current = startDir;
|
|
60
|
-
for (;;) {
|
|
61
|
-
try {
|
|
62
|
-
await accessFn(current, fsConstants.W_OK);
|
|
63
|
-
return true;
|
|
64
|
-
} catch (error) {
|
|
65
|
-
if (error && error.code === 'ENOENT') {
|
|
66
|
-
const parent = dirname(current);
|
|
67
|
-
if (parent === current) return false; // reached filesystem root
|
|
68
|
-
current = parent;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
// EACCES / EPERM / anything else → treat as not writable.
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const queryNpmRoot = async runner => {
|
|
78
|
-
try {
|
|
79
|
-
const { stdout } = await runner('npm root -g');
|
|
80
|
-
const value = String(stdout).trim();
|
|
81
|
-
return value || null;
|
|
82
|
-
} catch {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Detect a non-writable npm global prefix and, if found, redirect global
|
|
89
|
-
* installs to a user-writable directory by setting `npm_config_prefix`.
|
|
90
|
-
*
|
|
91
|
-
* Idempotent and dependency-injectable for tests. Returns an object describing
|
|
92
|
-
* what happened: `{ changed: boolean, reason: string, prefix?, previousRoot? }`.
|
|
93
|
-
*
|
|
94
|
-
* @param {object} [options]
|
|
95
|
-
* @param {Record<string, string>} [options.env=process.env] - Environment to mutate.
|
|
96
|
-
* @param {string} [options.execPath=process.execPath] - Running node binary path.
|
|
97
|
-
* @param {string} [options.platform=process.platform] - OS platform.
|
|
98
|
-
* @param {string} [options.home] - User home directory.
|
|
99
|
-
* @param {Function} [options.accessFn=fs.access]
|
|
100
|
-
* @param {Function} [options.mkdirFn=fs.mkdir]
|
|
101
|
-
* @param {Function} [options.runner=execAsync] - Runs a shell command, returns {stdout}.
|
|
102
|
-
* @param {(message: string) => void} [options.log] - Informational logger.
|
|
103
|
-
* @returns {Promise<{changed: boolean, reason: string, prefix?: string, previousRoot?: string, error?: Error}>}
|
|
104
|
-
*/
|
|
105
|
-
export const ensureWritableNpmGlobalPrefix = async (options = {}) => {
|
|
106
|
-
const { env = process.env, execPath = process.execPath, platform = process.platform, home = homedir(), accessFn = access, mkdirFn = mkdir, runner = execAsync, log = () => {}, isBunRuntime = typeof Bun !== 'undefined', isDenoRuntime = typeof Deno !== 'undefined' } = options;
|
|
107
|
-
|
|
108
|
-
// Windows' global layout differs (`<prefix>/node_modules`, AppData), and the
|
|
109
|
-
// EACCES scenario this guards against is POSIX-specific. Skip to avoid false
|
|
110
|
-
// positives that would needlessly relocate the prefix.
|
|
111
|
-
if (platform === 'win32') return { changed: false, reason: 'win32' };
|
|
112
|
-
|
|
113
|
-
// This workaround only protects use-m's Node/npm resolver. Bun and Deno use
|
|
114
|
-
// different install paths and should not have npm configuration changed.
|
|
115
|
-
if (isBunRuntime) return { changed: false, reason: 'bun-runtime' };
|
|
116
|
-
if (isDenoRuntime) return { changed: false, reason: 'deno-runtime' };
|
|
117
|
-
|
|
118
|
-
// Respect an explicitly configured prefix — the user (or a parent process)
|
|
119
|
-
// already chose where global installs go.
|
|
120
|
-
if (env.npm_config_prefix || env.NPM_CONFIG_PREFIX) return { changed: false, reason: 'preset' };
|
|
121
|
-
|
|
122
|
-
// Fast path: derive the likely global node_modules from the node binary and
|
|
123
|
-
// check writability without spawning npm. Most installs land here.
|
|
124
|
-
const derived = deriveGlobalNodeModules(execPath);
|
|
125
|
-
if (await isPathWritable(derived, accessFn)) {
|
|
126
|
-
return { changed: false, reason: 'writable' };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// The cheap heuristic says non-writable. Confirm against the authoritative
|
|
130
|
-
// path npm/use-m actually use before changing anything (handles custom prefixes).
|
|
131
|
-
const authoritative = (await queryNpmRoot(runner)) || derived;
|
|
132
|
-
if (await isPathWritable(authoritative, accessFn)) {
|
|
133
|
-
return { changed: false, reason: 'writable' };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Global prefix is genuinely not writable by the current user (issue #1897).
|
|
137
|
-
if (!home) {
|
|
138
|
-
return { changed: false, reason: 'no-home' };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const prefix = join(home, '.npm-global');
|
|
142
|
-
try {
|
|
143
|
-
// npm installs into `<prefix>/lib/node_modules`; create it so the very
|
|
144
|
-
// first `npm install -g` does not have to.
|
|
145
|
-
await mkdirFn(join(prefix, 'lib', 'node_modules'), { recursive: true });
|
|
146
|
-
} catch (error) {
|
|
147
|
-
return { changed: false, reason: 'mkdir-failed', error };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
env.npm_config_prefix = prefix;
|
|
151
|
-
// Make globally-installed binaries from the new prefix resolvable too.
|
|
152
|
-
const binDir = join(prefix, 'bin');
|
|
153
|
-
const pathParts = String(env.PATH || '').split(':');
|
|
154
|
-
if (!pathParts.includes(binDir)) {
|
|
155
|
-
env.PATH = env.PATH ? `${binDir}:${env.PATH}` : binDir;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
log(`ℹ️ npm global directory (${authoritative}) is not writable; redirecting global installs to ${prefix} (issue #1897).`);
|
|
159
|
-
return { changed: true, reason: 'redirected', prefix, previousRoot: authoritative };
|
|
160
|
-
};
|