@link-assistant/hive-mind 1.52.0 → 1.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env node
2
+ // Playwright MCP session-level disable/restore utilities.
3
+ if (typeof globalThis.use === 'undefined') {
4
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
5
+ }
6
+ const { $ } = await use('command-stream');
7
+ const fs = (await use('fs')).promises;
8
+ const os = await use('os');
9
+ const path = (await use('path')).default;
10
+
11
+ export const getCommandResultCode = result => result?.code ?? result?.exitCode ?? null;
12
+
13
+ export const getCommandResultOutput = result => `${result?.stdout?.toString() || ''}${result?.stderr?.toString() || ''}`;
14
+
15
+ export const isCommandResultSuccess = result => getCommandResultCode(result) === 0;
16
+
17
+ export const checkPlaywrightMcpPackageAvailability = async () => {
18
+ try {
19
+ const result = await $`timeout 5 npx --no-install @playwright/mcp --help 2>&1`.catch(() => null);
20
+ if (isCommandResultSuccess(result)) return true;
21
+ const npmResult = await $`timeout 5 npm ls -g @playwright/mcp 2>&1`.catch(() => null);
22
+ return getCommandResultOutput(npmResult).includes('@playwright/mcp');
23
+ } catch {
24
+ return false;
25
+ }
26
+ };
27
+
28
+ export const parseCodexMcpServerNames = output =>
29
+ output
30
+ .split(/\r?\n/)
31
+ .map(line => line.trim())
32
+ .filter(line => line && !line.startsWith('Name '))
33
+ .map(line => line.split(/\s+/)[0])
34
+ .filter(name => /^[A-Za-z0-9_-]+$/.test(name));
35
+
36
+ export const getCodexPlaywrightMcpDisableConfigArgs = async log => {
37
+ try {
38
+ const result = await $`timeout 5 codex mcp list 2>&1`.catch(() => null);
39
+ if (!isCommandResultSuccess(result)) return [];
40
+ const names = parseCodexMcpServerNames(getCommandResultOutput(result)).filter(name => name.toLowerCase().includes('playwright'));
41
+ if (names.length === 0) {
42
+ if (log) await log('🎭 No Codex Playwright MCP server registration found to disable for this session', { verbose: true });
43
+ return [];
44
+ }
45
+ if (log) {
46
+ await log(`🎭 Playwright MCP disabled for this Codex session via config override: ${names.join(', ')}`, {
47
+ verbose: true,
48
+ });
49
+ }
50
+ return names.flatMap(name => ['-c', `mcp_servers.${name}.enabled=false`]);
51
+ } catch (err) {
52
+ if (log) await log(`⚠️ Could not build Codex Playwright MCP disable override: ${err.message}`, { verbose: true });
53
+ return [];
54
+ }
55
+ };
56
+
57
+ const isPlainObject = value => value && typeof value === 'object' && !Array.isArray(value);
58
+
59
+ const mergeDeep = (base, override) => {
60
+ const result = { ...(isPlainObject(base) ? base : {}) };
61
+ for (const [key, value] of Object.entries(isPlainObject(override) ? override : {})) {
62
+ result[key] = isPlainObject(result[key]) && isPlainObject(value) ? mergeDeep(result[key], value) : value;
63
+ }
64
+ return result;
65
+ };
66
+
67
+ const stripJsonComments = input => {
68
+ let output = '';
69
+ let inString = false;
70
+ let quote = '';
71
+ let escaped = false;
72
+ for (let index = 0; index < input.length; index++) {
73
+ const char = input[index];
74
+ const next = input[index + 1];
75
+ if (inString) {
76
+ output += char;
77
+ if (escaped) {
78
+ escaped = false;
79
+ } else if (char === '\\') {
80
+ escaped = true;
81
+ } else if (char === quote) {
82
+ inString = false;
83
+ }
84
+ continue;
85
+ }
86
+ if (char === '"' || char === "'") {
87
+ inString = true;
88
+ quote = char;
89
+ output += char;
90
+ continue;
91
+ }
92
+ if (char === '/' && next === '/') {
93
+ while (index < input.length && input[index] !== '\n') index++;
94
+ output += '\n';
95
+ continue;
96
+ }
97
+ if (char === '/' && next === '*') {
98
+ index += 2;
99
+ while (index < input.length && !(input[index] === '*' && input[index + 1] === '/')) index++;
100
+ index++;
101
+ continue;
102
+ }
103
+ output += char;
104
+ }
105
+ return output;
106
+ };
107
+
108
+ const stripTrailingCommas = input => {
109
+ let output = '';
110
+ let inString = false;
111
+ let quote = '';
112
+ let escaped = false;
113
+ for (let index = 0; index < input.length; index++) {
114
+ const char = input[index];
115
+ if (inString) {
116
+ output += char;
117
+ if (escaped) {
118
+ escaped = false;
119
+ } else if (char === '\\') {
120
+ escaped = true;
121
+ } else if (char === quote) {
122
+ inString = false;
123
+ }
124
+ continue;
125
+ }
126
+ if (char === '"' || char === "'") {
127
+ inString = true;
128
+ quote = char;
129
+ output += char;
130
+ continue;
131
+ }
132
+ if (char === ',') {
133
+ let lookahead = index + 1;
134
+ while (/\s/.test(input[lookahead] || '')) lookahead++;
135
+ if (input[lookahead] === '}' || input[lookahead] === ']') continue;
136
+ }
137
+ output += char;
138
+ }
139
+ return output;
140
+ };
141
+
142
+ const parseConfigContent = content => {
143
+ if (!content || typeof content !== 'string') return {};
144
+ try {
145
+ return JSON.parse(content);
146
+ } catch {
147
+ try {
148
+ return JSON.parse(stripTrailingCommas(stripJsonComments(content)));
149
+ } catch {
150
+ return {};
151
+ }
152
+ }
153
+ };
154
+
155
+ const readConfigFile = async filePath => {
156
+ const content = await fs.readFile(filePath, 'utf-8').catch(() => null);
157
+ return parseConfigContent(content);
158
+ };
159
+
160
+ const pathExists = async filePath =>
161
+ fs
162
+ .stat(filePath)
163
+ .then(() => true)
164
+ .catch(() => false);
165
+
166
+ const findUpConfigPaths = async (startDir, filenames) => {
167
+ const results = [];
168
+ let dir = startDir || process.cwd();
169
+ while (dir) {
170
+ for (const file of filenames) results.push(path.join(dir, file));
171
+ if (await pathExists(path.join(dir, '.git'))) break;
172
+ const parent = path.dirname(dir);
173
+ if (parent === dir) break;
174
+ dir = parent;
175
+ }
176
+ return results;
177
+ };
178
+
179
+ const configFilesInDir = dir => (dir ? ['config.json', 'opencode.json', 'opencode.jsonc'].map(file => path.join(dir, file)) : []);
180
+
181
+ const isPlaywrightMcpEntry = (name, config) => {
182
+ const haystack = `${name || ''} ${JSON.stringify(config || {})}`.toLowerCase();
183
+ return haystack.includes('playwright');
184
+ };
185
+
186
+ export const collectPlaywrightMcpServerNames = (...configs) => {
187
+ const names = new Set();
188
+ for (const config of configs.flat()) {
189
+ if (!isPlainObject(config?.mcp)) continue;
190
+ for (const [name, mcpConfig] of Object.entries(config.mcp)) {
191
+ if (isPlaywrightMcpEntry(name, mcpConfig)) names.add(name);
192
+ }
193
+ }
194
+ return [...names];
195
+ };
196
+
197
+ export const buildPlaywrightMcpDisableConfig = (serverNames = []) => {
198
+ const names = [...new Set(['playwright', ...serverNames].filter(Boolean))];
199
+ const mcp = {};
200
+ const tools = {
201
+ '*playwright*': false,
202
+ 'mcp__playwright__*': false,
203
+ };
204
+ for (const name of names) {
205
+ mcp[name] = {
206
+ type: 'local',
207
+ command: ['npx', '-y', '@playwright/mcp@latest'],
208
+ enabled: false,
209
+ };
210
+ tools[`${name}_*`] = false;
211
+ tools[`mcp__${name}__*`] = false;
212
+ }
213
+ return {
214
+ $schema: 'https://opencode.ai/config.json',
215
+ mcp,
216
+ tools,
217
+ };
218
+ };
219
+
220
+ export const mergePlaywrightMcpDisableConfigContent = (existingContent = '', serverNames = []) => {
221
+ const existingConfig = parseConfigContent(existingContent);
222
+ const detectedNames = collectPlaywrightMcpServerNames(existingConfig);
223
+ const disableConfig = buildPlaywrightMcpDisableConfig([...serverNames, ...detectedNames]);
224
+ return JSON.stringify(mergeDeep(existingConfig, disableConfig), null, 2);
225
+ };
226
+
227
+ const collectPlaywrightMcpServerNamesFromFiles = async filePaths => {
228
+ const configs = [];
229
+ for (const filePath of filePaths) configs.push(await readConfigFile(filePath));
230
+ return collectPlaywrightMcpServerNames(configs);
231
+ };
232
+
233
+ const getConfigHome = env => env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
234
+
235
+ const getOpenCodeConfigFilePaths = async ({ env = process.env, cwd = process.cwd() } = {}) => [...configFilesInDir(path.join(getConfigHome(env), 'opencode')), ...(env.OPENCODE_CONFIG ? [env.OPENCODE_CONFIG] : []), ...(await findUpConfigPaths(cwd, ['opencode.jsonc', 'opencode.json'])), ...configFilesInDir(env.OPENCODE_CONFIG_DIR)];
236
+
237
+ const getAgentConfigFilePaths = async ({ env = process.env, cwd = process.cwd() } = {}) => [...configFilesInDir(path.join(getConfigHome(env), 'link-assistant-agent')), ...(env.LINK_ASSISTANT_AGENT_CONFIG ? [env.LINK_ASSISTANT_AGENT_CONFIG] : []), ...(env.OPENCODE_CONFIG ? [env.OPENCODE_CONFIG] : []), ...(await findUpConfigPaths(cwd, ['opencode.jsonc', 'opencode.json'])), ...configFilesInDir(path.join(cwd, '.link-assistant-agent')), ...configFilesInDir(path.join(cwd, '.opencode')), ...configFilesInDir(env.LINK_ASSISTANT_AGENT_CONFIG_DIR), ...configFilesInDir(env.OPENCODE_CONFIG_DIR)];
238
+
239
+ export const getOpenCodePlaywrightMcpDisableEnv = async ({ env = process.env, cwd = process.cwd(), includeConfigFiles = true, log } = {}) => {
240
+ const inlineConfig = env.OPENCODE_CONFIG_CONTENT || '';
241
+ const names = collectPlaywrightMcpServerNames(parseConfigContent(inlineConfig));
242
+ if (includeConfigFiles) {
243
+ names.push(...(await collectPlaywrightMcpServerNamesFromFiles(await getOpenCodeConfigFilePaths({ env, cwd }))));
244
+ }
245
+ const uniqueNames = [...new Set(names)];
246
+ const displayNames = [...new Set(['playwright', ...uniqueNames])];
247
+ if (log) await log(`🎭 OpenCode Playwright MCP disabled through OPENCODE_CONFIG_CONTENT for: ${displayNames.join(', ')}`, { verbose: true });
248
+ return {
249
+ OPENCODE_CONFIG_CONTENT: mergePlaywrightMcpDisableConfigContent(inlineConfig, uniqueNames),
250
+ };
251
+ };
252
+
253
+ export const getAgentPlaywrightMcpDisableEnv = async ({ env = process.env, cwd = process.cwd(), includeConfigFiles = true, log } = {}) => {
254
+ const agentInlineConfig = env.LINK_ASSISTANT_AGENT_CONFIG_CONTENT || env.OPENCODE_CONFIG_CONTENT || '';
255
+ const names = collectPlaywrightMcpServerNames(parseConfigContent(agentInlineConfig), parseConfigContent(env.OPENCODE_CONFIG_CONTENT || ''));
256
+ if (includeConfigFiles) {
257
+ names.push(...(await collectPlaywrightMcpServerNamesFromFiles(await getAgentConfigFilePaths({ env, cwd }))));
258
+ }
259
+ const uniqueNames = [...new Set(names)];
260
+ const displayNames = [...new Set(['playwright', ...uniqueNames])];
261
+ const configContent = mergePlaywrightMcpDisableConfigContent(agentInlineConfig, uniqueNames);
262
+ if (log) await log(`🎭 Agent Playwright MCP disabled through LINK_ASSISTANT_AGENT_CONFIG_CONTENT for: ${displayNames.join(', ')}`, { verbose: true });
263
+ return {
264
+ LINK_ASSISTANT_AGENT_CONFIG_CONTENT: configContent,
265
+ OPENCODE_CONFIG_CONTENT: mergePlaywrightMcpDisableConfigContent(env.OPENCODE_CONFIG_CONTENT || agentInlineConfig, uniqueNames),
266
+ };
267
+ };
268
+
269
+ /** Build a temporary MCP config JSON excluding Playwright, for use with --strict-mcp-config */
270
+ export const buildMcpConfigWithoutPlaywright = async log => {
271
+ try {
272
+ const claudeJsonPath = path.join(os.homedir(), '.claude.json');
273
+ const claudeJson = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
274
+ const mcpServers = claudeJson.mcpServers || {};
275
+ const filtered = {};
276
+ for (const [name, config] of Object.entries(mcpServers)) {
277
+ if (name.toLowerCase().includes('playwright')) continue;
278
+ filtered[name] = config;
279
+ }
280
+ const tempConfigPath = path.join(os.tmpdir(), `claude-mcp-no-playwright-${Date.now()}-${process.pid}.json`);
281
+ await fs.writeFile(tempConfigPath, JSON.stringify({ mcpServers: filtered }, null, 2));
282
+ if (log) await log(`🎭 Created filtered MCP config (without Playwright): ${tempConfigPath}`, { verbose: true });
283
+ return tempConfigPath;
284
+ } catch (err) {
285
+ if (log) await log(`⚠️ Could not build filtered MCP config: ${err.message}`, { verbose: true });
286
+ return null;
287
+ }
288
+ };
289
+
290
+ /** Cascade --no-playwright-mcp to disable related flags */
291
+ export const cascadePlaywrightMcpDisable = async (argv, log) => {
292
+ if (argv.playwrightMcp === false) {
293
+ if (log) await log('🎭 Playwright MCP physically disabled via --no-playwright-mcp', { verbose: true });
294
+ argv.promptPlaywrightMcp = false;
295
+ argv.playwrightMcpAutoCleanup = false;
296
+ if (log) await log('ℹ️ --prompt-playwright-mcp and --playwright-mcp-auto-cleanup also disabled', { verbose: true });
297
+ }
298
+ };
@@ -366,7 +366,7 @@ export const SOLVE_OPTION_DEFINITIONS = {
366
366
  },
367
367
  'prompt-playwright-mcp': {
368
368
  type: 'boolean',
369
- description: 'Enable Playwright MCP browser automation hints in system prompt (enabled by default, only takes effect if Playwright MCP is installed). Use --no-prompt-playwright-mcp to disable. Supported for --tool claude and --tool codex.',
369
+ description: 'Enable Playwright MCP browser automation hints in system prompt (enabled by default, only takes effect if Playwright MCP is installed). Use --no-prompt-playwright-mcp to disable. Supported for --tool claude, --tool codex, --tool opencode, and --tool agent.',
370
370
  default: true,
371
371
  },
372
372
  'prompt-check-sibling-pull-requests': {
@@ -384,6 +384,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
384
384
  description: 'Path to examples folder used in system prompt. Set to empty string to disable examples folder prompt. Default: ./examples',
385
385
  default: './examples',
386
386
  },
387
+ 'playwright-mcp': {
388
+ type: 'boolean',
389
+ description: 'Enable Playwright MCP server connection for this session (enabled by default). Use --no-playwright-mcp to physically disable the Playwright MCP server without affecting the global MCP registration. When disabled, also disables --prompt-playwright-mcp and --playwright-mcp-auto-cleanup. Supported for --tool claude, --tool codex, --tool opencode, and --tool agent.',
390
+ default: true,
391
+ },
387
392
  'playwright-mcp-auto-cleanup': {
388
393
  type: 'boolean',
389
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.',
package/src/solve.mjs CHANGED
@@ -707,12 +707,31 @@ try {
707
707
  $,
708
708
  });
709
709
 
710
+ const { cascadePlaywrightMcpDisable } = await import('./playwright-mcp.lib.mjs');
711
+ await cascadePlaywrightMcpDisable(argv, log);
712
+
713
+ async function resolvePlaywrightMcp(checkFn) {
714
+ if (argv.playwrightMcp === false) return;
715
+ if (argv.promptPlaywrightMcp) {
716
+ const available = await checkFn();
717
+ if (available) {
718
+ await log('🎭 Playwright MCP detected - enabling browser automation hints', { verbose: true });
719
+ } else {
720
+ await log('ℹ️ Playwright MCP not detected - browser automation hints will be disabled', { verbose: true });
721
+ argv.promptPlaywrightMcp = false;
722
+ }
723
+ } else {
724
+ await log('ℹ️ Playwright MCP explicitly disabled via --no-prompt-playwright-mcp', { verbose: true });
725
+ }
726
+ }
727
+
710
728
  // Execute tool command with all prompts and settings
711
729
  let toolResult;
712
730
  if (argv.tool === 'opencode') {
713
731
  const opencodeLib = await import('./opencode.lib.mjs');
714
- const { executeOpenCode } = opencodeLib;
732
+ const { executeOpenCode, checkPlaywrightMcpAvailability: checkOpenCodePlaywrightMcp } = opencodeLib;
715
733
  const opencodePath = process.env.OPENCODE_PATH || 'opencode';
734
+ await resolvePlaywrightMcp(checkOpenCodePlaywrightMcp);
716
735
 
717
736
  toolResult = await executeOpenCode({
718
737
  issueUrl,
@@ -742,18 +761,7 @@ try {
742
761
  const codexLib = await import('./codex.lib.mjs');
743
762
  const { executeCodex, checkPlaywrightMcpAvailability } = codexLib;
744
763
  const codexPath = process.env.CODEX_PATH || 'codex';
745
-
746
- if (argv.promptPlaywrightMcp) {
747
- const playwrightMcpAvailable = await checkPlaywrightMcpAvailability();
748
- if (playwrightMcpAvailable) {
749
- await log('🎭 Playwright MCP detected - enabling browser automation hints', { verbose: true });
750
- } else {
751
- await log('ℹ️ Playwright MCP not detected - browser automation hints will be disabled', { verbose: true });
752
- argv.promptPlaywrightMcp = false;
753
- }
754
- } else {
755
- await log('ℹ️ Playwright MCP explicitly disabled via --no-prompt-playwright-mcp', { verbose: true });
756
- }
764
+ await resolvePlaywrightMcp(checkPlaywrightMcpAvailability);
757
765
 
758
766
  toolResult = await executeCodex({
759
767
  issueUrl,
@@ -781,8 +789,9 @@ try {
781
789
  });
782
790
  } else if (argv.tool === 'agent') {
783
791
  const agentLib = await import('./agent.lib.mjs');
784
- const { executeAgent } = agentLib;
792
+ const { executeAgent, checkPlaywrightMcpAvailability: checkAgentPlaywrightMcp } = agentLib;
785
793
  const agentPath = process.env.AGENT_PATH || 'agent';
794
+ await resolvePlaywrightMcp(checkAgentPlaywrightMcp);
786
795
 
787
796
  toolResult = await executeAgent({
788
797
  issueUrl,
@@ -810,20 +819,8 @@ try {
810
819
  });
811
820
  } else {
812
821
  // Default to Claude
813
- // Check for Playwright MCP availability if using Claude tool
814
822
  if (argv.tool === 'claude' || !argv.tool) {
815
- // If flag is true (default), check if Playwright MCP is actually available
816
- if (argv.promptPlaywrightMcp) {
817
- const playwrightMcpAvailable = await checkPlaywrightMcpAvailability();
818
- if (playwrightMcpAvailable) {
819
- await log('🎭 Playwright MCP detected - enabling browser automation hints', { verbose: true });
820
- } else {
821
- await log('ℹ️ Playwright MCP not detected - browser automation hints will be disabled', { verbose: true });
822
- argv.promptPlaywrightMcp = false;
823
- }
824
- } else {
825
- await log('ℹ️ Playwright MCP explicitly disabled via --no-prompt-playwright-mcp', { verbose: true });
826
- }
823
+ await resolvePlaywrightMcp(checkPlaywrightMcpAvailability);
827
824
  }
828
825
  const claudeResult = await executeClaude({
829
826
  issueUrl,
@@ -178,13 +178,28 @@ export const executeToolIteration = async params => {
178
178
  const memoryCheck = await import('./memory-check.mjs');
179
179
  const { getResourceSnapshot } = memoryCheck;
180
180
 
181
+ const { cascadePlaywrightMcpDisable } = await import('./playwright-mcp.lib.mjs');
182
+ await cascadePlaywrightMcpDisable(argv, log);
183
+
181
184
  let toolResult;
182
185
  if (argv.tool === 'opencode') {
183
186
  // Use OpenCode
184
187
  const opencodeExecLib = await import('./opencode.lib.mjs');
185
- const { executeOpenCode } = opencodeExecLib;
188
+ const { executeOpenCode, checkPlaywrightMcpAvailability } = opencodeExecLib;
186
189
  const opencodePath = argv.opencodePath || 'opencode';
187
190
 
191
+ if (argv.promptPlaywrightMcp) {
192
+ const playwrightMcpAvailable = await checkPlaywrightMcpAvailability();
193
+ if (playwrightMcpAvailable) {
194
+ await log('🎭 Playwright MCP detected - enabling browser automation hints', { verbose: true });
195
+ } else {
196
+ await log('ℹ️ Playwright MCP not detected - browser automation hints will be disabled', { verbose: true });
197
+ argv.promptPlaywrightMcp = false;
198
+ }
199
+ } else {
200
+ await log('ℹ️ Playwright MCP explicitly disabled via --no-prompt-playwright-mcp', { verbose: true });
201
+ }
202
+
188
203
  toolResult = await executeOpenCode({
189
204
  issueUrl,
190
205
  issueNumber,
@@ -249,9 +264,21 @@ export const executeToolIteration = async params => {
249
264
  } else if (argv.tool === 'agent') {
250
265
  // Use Agent
251
266
  const agentExecLib = await import('./agent.lib.mjs');
252
- const { executeAgent } = agentExecLib;
267
+ const { executeAgent, checkPlaywrightMcpAvailability } = agentExecLib;
253
268
  const agentPath = argv.agentPath || 'agent';
254
269
 
270
+ if (argv.promptPlaywrightMcp) {
271
+ const playwrightMcpAvailable = await checkPlaywrightMcpAvailability();
272
+ if (playwrightMcpAvailable) {
273
+ await log('🎭 Playwright MCP detected - enabling browser automation hints', { verbose: true });
274
+ } else {
275
+ await log('ℹ️ Playwright MCP not detected - browser automation hints will be disabled', { verbose: true });
276
+ argv.promptPlaywrightMcp = false;
277
+ }
278
+ } else {
279
+ await log('ℹ️ Playwright MCP explicitly disabled via --no-prompt-playwright-mcp', { verbose: true });
280
+ }
281
+
255
282
  toolResult = await executeAgent({
256
283
  issueUrl,
257
284
  issueNumber,