pikiclaw 0.2.64 → 0.2.65
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 +4 -12
- package/dist/bot.js +23 -1
- package/dist/browser-profile.js +458 -0
- package/dist/cli.js +4 -0
- package/dist/constants.js +15 -4
- package/dist/dashboard-routes-config.js +47 -52
- package/dist/dashboard-ui.js +9 -9
- package/dist/dashboard.js +33 -7
- package/dist/mcp-bridge.js +64 -51
- package/dist/mcp-playwright-proxy.js +197 -0
- package/dist/tools/desktop.js +1 -1
- package/dist/user-config.js +13 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -98,10 +98,6 @@ npx pikiclaw@latest
|
|
|
98
98
|
|
|
99
99
|
<img src="docs/promo-dashboard-config.png" alt="Config" width="700">
|
|
100
100
|
|
|
101
|
-
**插件中心** — 浏览器操控、桌面自动化
|
|
102
|
-
|
|
103
|
-
<img src="docs/promo-dashboard-extensions.png" alt="Extensions" width="700">
|
|
104
|
-
|
|
105
101
|
**会话管理** — 按 Agent 分组的会话泳道
|
|
106
102
|
|
|
107
103
|
<img src="docs/promo-dashboard-sessions.png" alt="Sessions" width="700">
|
|
@@ -162,7 +158,7 @@ npx pikiclaw@latest --doctor
|
|
|
162
158
|
|
|
163
159
|
可选 GUI 能力:
|
|
164
160
|
|
|
165
|
-
- 浏览器自动化:通过 `@playwright/mcp`
|
|
161
|
+
- 浏览器自动化:通过 `@playwright/mcp` 管理一个专用的持久化 Chrome profile;第一次使用时在这个自动化浏览器里登录需要的网站,后续任务会复用同一个 profile
|
|
166
162
|
- macOS 桌面自动化:通过 Appium Mac2 提供 `desktop_open_app`、`desktop_snapshot`、`desktop_click`、`desktop_type`、`desktop_screenshot` 等工具
|
|
167
163
|
|
|
168
164
|
---
|
|
@@ -196,17 +192,13 @@ npx pikiclaw@latest --doctor
|
|
|
196
192
|
## Config And Setup Notes
|
|
197
193
|
|
|
198
194
|
- 持久化配置在 `~/.pikiclaw/setting.json`
|
|
199
|
-
- Dashboard
|
|
200
|
-
- 浏览器 GUI 相关常用变量:
|
|
201
|
-
- `PIKICLAW_BROWSER_GUI`
|
|
202
|
-
- `PIKICLAW_BROWSER_USE_EXTENSION`
|
|
203
|
-
- `PIKICLAW_BROWSER_HEADLESS`
|
|
204
|
-
- `PIKICLAW_BROWSER_ISOLATED`
|
|
205
|
-
- `PLAYWRIGHT_MCP_EXTENSION_TOKEN`
|
|
195
|
+
- Dashboard 是主配置入口,其他运行时配置仍然可用
|
|
206
196
|
- 桌面 GUI 相关常用变量:
|
|
207
197
|
- `PIKICLAW_DESKTOP_GUI`
|
|
208
198
|
- `PIKICLAW_DESKTOP_APPIUM_URL`
|
|
209
199
|
|
|
200
|
+
浏览器自动化由 dashboard 和本地运行时共同管理,会自动创建并复用专用的 Chrome profile 目录。你只需要在这个专用浏览器里登录需要自动化的网站账号一次。
|
|
201
|
+
|
|
210
202
|
如果要启用 macOS 桌面自动化,需要先准备 Appium Mac2:
|
|
211
203
|
|
|
212
204
|
```bash
|
package/dist/bot.js
CHANGED
|
@@ -10,6 +10,7 @@ import { execSync, spawn } from 'node:child_process';
|
|
|
10
10
|
import { getActiveUserConfig, onUserConfigChange, resolveUserWorkdir, setUserWorkdir } from './user-config.js';
|
|
11
11
|
import { doStream, getSessions, getSessionTail, getUsage, initializeProjectSkills, listAgents, listModels, listSkills, stageSessionFiles, isPendingSessionId, normalizeClaudeModelId, } from './code-agent.js';
|
|
12
12
|
import { getDriver, hasDriver, allDriverIds } from './agent-driver.js';
|
|
13
|
+
import { resolveGuiIntegrationConfig } from './mcp-bridge.js';
|
|
13
14
|
import { terminateProcessTree } from './process-control.js';
|
|
14
15
|
import { VERSION } from './version.js';
|
|
15
16
|
import { buildHumanLoopResponse, createEmptyHumanLoopAnswer, currentHumanLoopQuestion, isHumanLoopAwaitingText, setHumanLoopOption, setHumanLoopText, skipHumanLoopQuestion, } from './human-loop.js';
|
|
@@ -221,6 +222,23 @@ function buildMcpDeliveryPrompt() {
|
|
|
221
222
|
'This is an IM conversation, so pay attention to the IM tools.',
|
|
222
223
|
].join('\n');
|
|
223
224
|
}
|
|
225
|
+
function buildBrowserAutomationPrompt(browserEnabled) {
|
|
226
|
+
if (!browserEnabled) {
|
|
227
|
+
return [
|
|
228
|
+
'[Browser Automation]',
|
|
229
|
+
'Managed browser automation is disabled by default for this session.',
|
|
230
|
+
process.platform === 'darwin'
|
|
231
|
+
? 'On macOS, operate your main browser directly with native commands such as open, osascript, and screencapture when needed.'
|
|
232
|
+
: 'Use native OS or browser commands directly when browser automation is not enabled.',
|
|
233
|
+
].join('\n');
|
|
234
|
+
}
|
|
235
|
+
return [
|
|
236
|
+
'[Browser Automation]',
|
|
237
|
+
'A Playwright MCP browser server is already configured to use the local Chrome channel with a persistent profile.',
|
|
238
|
+
'Do not call browser_install unless a browser tool explicitly reports that Chrome or the browser is missing.',
|
|
239
|
+
'If you need a new tab, use browser_tabs with action="new".',
|
|
240
|
+
].join('\n');
|
|
241
|
+
}
|
|
224
242
|
function configModelValue(config, agent) {
|
|
225
243
|
switch (agent) {
|
|
226
244
|
case 'claude': return normalizeClaudeModelId(config.claudeModel || process.env.CLAUDE_MODEL || 'claude-opus-4-6');
|
|
@@ -1072,11 +1090,15 @@ export class Bot {
|
|
|
1072
1090
|
const resolvedModel = cs.modelId || this.modelForAgent(cs.agent);
|
|
1073
1091
|
const agentConfig = this.agentConfigs[cs.agent] || {};
|
|
1074
1092
|
const extraArgs = agentConfig.extraArgs || [];
|
|
1093
|
+
const browserEnabled = resolveGuiIntegrationConfig(getActiveUserConfig()).browserEnabled;
|
|
1075
1094
|
this.log(`[runStream] agent=${cs.agent} session=${cs.sessionId || '(new)'} workdir=${this.workdir} timeout=${this.runTimeout}s attachments=${attachments.length}`);
|
|
1076
1095
|
this.log(`[runStream] ${cs.agent} config: model=${resolvedModel} extraArgs=[${extraArgs.join(' ')}]`);
|
|
1077
1096
|
const isFirstTurnOfSession = !cs.sessionId || isPendingSessionId(cs.sessionId);
|
|
1097
|
+
const mcpSystemPrompt = mcpSendFile
|
|
1098
|
+
? appendExtraPrompt(buildMcpDeliveryPrompt(), buildBrowserAutomationPrompt(browserEnabled))
|
|
1099
|
+
: '';
|
|
1078
1100
|
const effectiveSystemPrompt = isFirstTurnOfSession
|
|
1079
|
-
?
|
|
1101
|
+
? appendExtraPrompt(systemPrompt, mcpSystemPrompt)
|
|
1080
1102
|
: undefined;
|
|
1081
1103
|
const opts = {
|
|
1082
1104
|
agent: cs.agent, prompt, workdir: this.workdir, timeout: this.runTimeout,
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
6
|
+
import { MANAGED_BROWSER_PROFILE_SUBPATH, PLAYWRIGHT_MCP_PACKAGE_NAME, PLAYWRIGHT_MCP_PACKAGE_SPEC, PLAYWRIGHT_MCP_BROWSER_ARGS, } from './constants.js';
|
|
7
|
+
const MANAGED_BROWSER_SETUP_STATE_FILENAME = 'managed-browser-setup.json';
|
|
8
|
+
const MANAGED_BROWSER_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
9
|
+
const MANAGED_BROWSER_SHUTDOWN_POLL_MS = 100;
|
|
10
|
+
const MANAGED_BROWSER_DEBUG_PORT = 39222;
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
function normalizeBrowserCdpEndpoint(endpoint) {
|
|
13
|
+
const value = String(endpoint || '').trim();
|
|
14
|
+
if (!value)
|
|
15
|
+
return '';
|
|
16
|
+
return value.replace(/\/+$/, '');
|
|
17
|
+
}
|
|
18
|
+
async function resolveBrowserCdpEndpoint(endpoint) {
|
|
19
|
+
const normalizedEndpoint = normalizeBrowserCdpEndpoint(endpoint);
|
|
20
|
+
if (!normalizedEndpoint)
|
|
21
|
+
return null;
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timeout = setTimeout(() => controller.abort(), 1_500);
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(`${normalizedEndpoint}/json/version`, { signal: controller.signal });
|
|
26
|
+
if (!response.ok)
|
|
27
|
+
return null;
|
|
28
|
+
const payload = await response.json().catch(() => null);
|
|
29
|
+
return typeof payload?.webSocketDebuggerUrl === 'string' ? normalizedEndpoint : null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
clearTimeout(timeout);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function resolveOnPath(command) {
|
|
39
|
+
const checker = process.platform === 'win32' ? 'where' : 'which';
|
|
40
|
+
try {
|
|
41
|
+
const result = spawnSync(checker, [command], { encoding: 'utf8' });
|
|
42
|
+
if (result.status !== 0)
|
|
43
|
+
return null;
|
|
44
|
+
const lines = String(result.stdout || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
45
|
+
return lines[0] || null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function resolveCommonChromePaths() {
|
|
52
|
+
if (process.platform === 'darwin') {
|
|
53
|
+
return [
|
|
54
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
55
|
+
path.join(os.homedir(), 'Applications', 'Google Chrome.app', 'Contents', 'MacOS', 'Google Chrome'),
|
|
56
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
57
|
+
path.join(os.homedir(), 'Applications', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'),
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
if (process.platform === 'win32') {
|
|
61
|
+
const programFiles = process.env.ProgramFiles || '';
|
|
62
|
+
const programFilesX86 = process.env['ProgramFiles(x86)'] || '';
|
|
63
|
+
const localAppData = process.env.LOCALAPPDATA || '';
|
|
64
|
+
return [
|
|
65
|
+
programFiles ? path.join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe') : '',
|
|
66
|
+
programFilesX86 ? path.join(programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe') : '',
|
|
67
|
+
localAppData ? path.join(localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe') : '',
|
|
68
|
+
].filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
function resolveMacBrowserAppName(chromeExecutable) {
|
|
73
|
+
if (process.platform !== 'darwin')
|
|
74
|
+
return null;
|
|
75
|
+
if (chromeExecutable.includes('/Chromium.app/'))
|
|
76
|
+
return 'Chromium';
|
|
77
|
+
if (chromeExecutable.includes('/Google Chrome.app/'))
|
|
78
|
+
return 'Google Chrome';
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function normalizeManagedBrowserWindow(chromeExecutable) {
|
|
82
|
+
if (process.platform !== 'darwin')
|
|
83
|
+
return;
|
|
84
|
+
const appName = resolveMacBrowserAppName(chromeExecutable);
|
|
85
|
+
if (!appName)
|
|
86
|
+
return;
|
|
87
|
+
const script = [
|
|
88
|
+
'set screenBounds to {0, 0, 1440, 900}',
|
|
89
|
+
'try',
|
|
90
|
+
' tell application "Finder" to set screenBounds to bounds of window of desktop',
|
|
91
|
+
'end try',
|
|
92
|
+
`tell application "${appName}"`,
|
|
93
|
+
' activate',
|
|
94
|
+
' delay 0.4',
|
|
95
|
+
' try',
|
|
96
|
+
' set zoomed of front window to true',
|
|
97
|
+
' end try',
|
|
98
|
+
' try',
|
|
99
|
+
' set bounds of front window to screenBounds',
|
|
100
|
+
' end try',
|
|
101
|
+
'end tell',
|
|
102
|
+
].join('\n');
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
try {
|
|
105
|
+
const proc = spawn('osascript', ['-e', script], {
|
|
106
|
+
detached: true,
|
|
107
|
+
stdio: 'ignore',
|
|
108
|
+
windowsHide: true,
|
|
109
|
+
});
|
|
110
|
+
proc.unref();
|
|
111
|
+
}
|
|
112
|
+
catch { }
|
|
113
|
+
}, 700);
|
|
114
|
+
}
|
|
115
|
+
export function getManagedBrowserProfileDir() {
|
|
116
|
+
return path.join(os.homedir(), MANAGED_BROWSER_PROFILE_SUBPATH);
|
|
117
|
+
}
|
|
118
|
+
export function ensureManagedBrowserProfileDir() {
|
|
119
|
+
const profileDir = getManagedBrowserProfileDir();
|
|
120
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
121
|
+
return profileDir;
|
|
122
|
+
}
|
|
123
|
+
export function getManagedBrowserMcpArgs(profileDir = getManagedBrowserProfileDir(), options = {}) {
|
|
124
|
+
if (options.cdpEndpoint) {
|
|
125
|
+
return ['--cdp-endpoint', options.cdpEndpoint];
|
|
126
|
+
}
|
|
127
|
+
return [
|
|
128
|
+
...PLAYWRIGHT_MCP_BROWSER_ARGS,
|
|
129
|
+
...(options.headless ? ['--headless'] : []),
|
|
130
|
+
'--user-data-dir',
|
|
131
|
+
profileDir,
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
export function resolveManagedBrowserMcpCliPath() {
|
|
135
|
+
try {
|
|
136
|
+
const packageJsonPath = require.resolve(`${PLAYWRIGHT_MCP_PACKAGE_NAME}/package.json`);
|
|
137
|
+
const cliPath = path.join(path.dirname(packageJsonPath), 'cli.js');
|
|
138
|
+
return fs.existsSync(cliPath) ? cliPath : null;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function resolveManagedBrowserMcpCommand(profileDir = getManagedBrowserProfileDir(), options = {}) {
|
|
145
|
+
const cliPath = resolveManagedBrowserMcpCliPath();
|
|
146
|
+
const runtimeArgs = getManagedBrowserMcpArgs(profileDir, options);
|
|
147
|
+
if (cliPath) {
|
|
148
|
+
return {
|
|
149
|
+
command: process.execPath,
|
|
150
|
+
args: [cliPath, ...runtimeArgs],
|
|
151
|
+
source: 'local',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
command: 'npx',
|
|
156
|
+
args: ['-y', PLAYWRIGHT_MCP_PACKAGE_SPEC, ...runtimeArgs],
|
|
157
|
+
source: 'npx',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export function getManagedBrowserLaunchArgs(profileDir = getManagedBrowserProfileDir()) {
|
|
161
|
+
const windowArgs = process.platform === 'darwin'
|
|
162
|
+
? ['--start-maximized', '--start-fullscreen', '--window-position=0,0']
|
|
163
|
+
: ['--start-maximized'];
|
|
164
|
+
return [
|
|
165
|
+
`--user-data-dir=${profileDir}`,
|
|
166
|
+
`--remote-debugging-port=${MANAGED_BROWSER_DEBUG_PORT}`,
|
|
167
|
+
'--no-first-run',
|
|
168
|
+
'--no-default-browser-check',
|
|
169
|
+
'--new-window',
|
|
170
|
+
...windowArgs,
|
|
171
|
+
'about:blank',
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
export function findChromeExecutable() {
|
|
175
|
+
for (const candidate of resolveCommonChromePaths()) {
|
|
176
|
+
if (fs.existsSync(candidate))
|
|
177
|
+
return candidate;
|
|
178
|
+
}
|
|
179
|
+
const commands = process.platform === 'win32'
|
|
180
|
+
? ['chrome', 'google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser']
|
|
181
|
+
: ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'chrome'];
|
|
182
|
+
for (const command of commands) {
|
|
183
|
+
const resolved = resolveOnPath(command);
|
|
184
|
+
if (resolved)
|
|
185
|
+
return resolved;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
function getManagedBrowserSetupStatePath(profileDir = getManagedBrowserProfileDir()) {
|
|
190
|
+
return path.join(path.dirname(profileDir), MANAGED_BROWSER_SETUP_STATE_FILENAME);
|
|
191
|
+
}
|
|
192
|
+
function readManagedBrowserSetupState(profileDir = getManagedBrowserProfileDir()) {
|
|
193
|
+
try {
|
|
194
|
+
const raw = fs.readFileSync(getManagedBrowserSetupStatePath(profileDir), 'utf8');
|
|
195
|
+
const parsed = JSON.parse(raw);
|
|
196
|
+
if (!parsed || typeof parsed.pid !== 'number' || parsed.pid <= 0)
|
|
197
|
+
return null;
|
|
198
|
+
if (typeof parsed.profileDir !== 'string' || !parsed.profileDir.trim())
|
|
199
|
+
return null;
|
|
200
|
+
if (typeof parsed.chromeExecutable !== 'string' || !parsed.chromeExecutable.trim())
|
|
201
|
+
return null;
|
|
202
|
+
return {
|
|
203
|
+
pid: parsed.pid,
|
|
204
|
+
profileDir: parsed.profileDir,
|
|
205
|
+
chromeExecutable: parsed.chromeExecutable,
|
|
206
|
+
debugPort: typeof parsed.debugPort === 'number' && parsed.debugPort > 0 ? parsed.debugPort : MANAGED_BROWSER_DEBUG_PORT,
|
|
207
|
+
launchedAt: typeof parsed.launchedAt === 'string' ? parsed.launchedAt : new Date().toISOString(),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function writeManagedBrowserSetupState(state) {
|
|
215
|
+
const statePath = getManagedBrowserSetupStatePath(state.profileDir);
|
|
216
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
217
|
+
fs.writeFileSync(statePath, JSON.stringify(state), 'utf8');
|
|
218
|
+
}
|
|
219
|
+
function clearManagedBrowserSetupState(profileDir = getManagedBrowserProfileDir()) {
|
|
220
|
+
try {
|
|
221
|
+
fs.rmSync(getManagedBrowserSetupStatePath(profileDir), { force: true });
|
|
222
|
+
}
|
|
223
|
+
catch { }
|
|
224
|
+
}
|
|
225
|
+
function isPidAlive(pid) {
|
|
226
|
+
try {
|
|
227
|
+
process.kill(pid, 0);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function readProcessCommand(pid) {
|
|
235
|
+
try {
|
|
236
|
+
if (process.platform === 'win32') {
|
|
237
|
+
const result = spawnSync('powershell', ['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`], { encoding: 'utf8' });
|
|
238
|
+
if (result.status !== 0)
|
|
239
|
+
return '';
|
|
240
|
+
return String(result.stdout || '').trim();
|
|
241
|
+
}
|
|
242
|
+
const result = spawnSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf8' });
|
|
243
|
+
if (result.status !== 0)
|
|
244
|
+
return '';
|
|
245
|
+
return String(result.stdout || '').trim();
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return '';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function commandUsesManagedProfile(command, profileDir) {
|
|
252
|
+
const normalizedCommand = command.trim();
|
|
253
|
+
return normalizedCommand.includes(`--user-data-dir=${profileDir}`)
|
|
254
|
+
|| normalizedCommand.includes(`--user-data-dir ${profileDir}`);
|
|
255
|
+
}
|
|
256
|
+
function isManagedBrowserRootProcess(command, profileDir) {
|
|
257
|
+
if (!commandUsesManagedProfile(command, profileDir))
|
|
258
|
+
return false;
|
|
259
|
+
return !command.includes(' --type=');
|
|
260
|
+
}
|
|
261
|
+
function findManagedBrowserRootPids(profileDir = getManagedBrowserProfileDir()) {
|
|
262
|
+
if (process.platform === 'win32') {
|
|
263
|
+
const tracked = readManagedBrowserSetupState(profileDir);
|
|
264
|
+
if (!tracked || !isPidAlive(tracked.pid))
|
|
265
|
+
return [];
|
|
266
|
+
return commandUsesManagedProfile(readProcessCommand(tracked.pid), profileDir) ? [tracked.pid] : [];
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const result = spawnSync('ps', ['-ax', '-o', 'pid=,command='], { encoding: 'utf8' });
|
|
270
|
+
if (result.status !== 0)
|
|
271
|
+
return [];
|
|
272
|
+
const lines = String(result.stdout || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
273
|
+
const pids = new Set();
|
|
274
|
+
for (const line of lines) {
|
|
275
|
+
const match = line.match(/^(\d+)\s+(.*)$/);
|
|
276
|
+
if (!match)
|
|
277
|
+
continue;
|
|
278
|
+
const pid = Number(match[1]);
|
|
279
|
+
const command = match[2] || '';
|
|
280
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
281
|
+
continue;
|
|
282
|
+
if (!isManagedBrowserRootProcess(command, profileDir))
|
|
283
|
+
continue;
|
|
284
|
+
pids.add(pid);
|
|
285
|
+
}
|
|
286
|
+
return [...pids];
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function resolveManagedBrowserRunningState(profileDir = getManagedBrowserProfileDir()) {
|
|
293
|
+
const tracked = readManagedBrowserSetupState(profileDir);
|
|
294
|
+
if (tracked) {
|
|
295
|
+
const command = readProcessCommand(tracked.pid);
|
|
296
|
+
if (isPidAlive(tracked.pid) && commandUsesManagedProfile(command, profileDir)) {
|
|
297
|
+
return { running: true, pid: tracked.pid };
|
|
298
|
+
}
|
|
299
|
+
clearManagedBrowserSetupState(profileDir);
|
|
300
|
+
}
|
|
301
|
+
const rootPids = findManagedBrowserRootPids(profileDir);
|
|
302
|
+
if (!rootPids.length)
|
|
303
|
+
return { running: false, pid: null };
|
|
304
|
+
return { running: true, pid: rootPids[0] ?? null };
|
|
305
|
+
}
|
|
306
|
+
function sleep(ms) {
|
|
307
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
308
|
+
}
|
|
309
|
+
async function waitForPidExit(pid, timeoutMs = MANAGED_BROWSER_SHUTDOWN_TIMEOUT_MS) {
|
|
310
|
+
const deadline = Date.now() + timeoutMs;
|
|
311
|
+
while (Date.now() < deadline) {
|
|
312
|
+
if (!isPidAlive(pid))
|
|
313
|
+
return true;
|
|
314
|
+
await sleep(MANAGED_BROWSER_SHUTDOWN_POLL_MS);
|
|
315
|
+
}
|
|
316
|
+
return !isPidAlive(pid);
|
|
317
|
+
}
|
|
318
|
+
async function terminatePid(pid) {
|
|
319
|
+
if (!isPidAlive(pid))
|
|
320
|
+
return true;
|
|
321
|
+
try {
|
|
322
|
+
process.kill(pid, 'SIGTERM');
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return !isPidAlive(pid);
|
|
326
|
+
}
|
|
327
|
+
if (await waitForPidExit(pid))
|
|
328
|
+
return true;
|
|
329
|
+
try {
|
|
330
|
+
process.kill(pid, 'SIGKILL');
|
|
331
|
+
}
|
|
332
|
+
catch { }
|
|
333
|
+
return waitForPidExit(pid, 1_000);
|
|
334
|
+
}
|
|
335
|
+
function getManagedBrowserDebugEndpoint(port) {
|
|
336
|
+
return `http://127.0.0.1:${port}`;
|
|
337
|
+
}
|
|
338
|
+
async function resolveManagedBrowserCdpEndpoint(port) {
|
|
339
|
+
return resolveBrowserCdpEndpoint(getManagedBrowserDebugEndpoint(port));
|
|
340
|
+
}
|
|
341
|
+
async function waitForManagedBrowserCdpEndpoint(port, timeoutMs = 6_000) {
|
|
342
|
+
const deadline = Date.now() + timeoutMs;
|
|
343
|
+
while (Date.now() < deadline) {
|
|
344
|
+
const endpoint = await resolveManagedBrowserCdpEndpoint(port);
|
|
345
|
+
if (endpoint)
|
|
346
|
+
return endpoint;
|
|
347
|
+
await sleep(200);
|
|
348
|
+
}
|
|
349
|
+
return resolveManagedBrowserCdpEndpoint(port);
|
|
350
|
+
}
|
|
351
|
+
async function closeManagedBrowserProcesses(profileDir) {
|
|
352
|
+
const tracked = readManagedBrowserSetupState(profileDir);
|
|
353
|
+
const candidates = new Set(findManagedBrowserRootPids(profileDir));
|
|
354
|
+
if (tracked?.pid)
|
|
355
|
+
candidates.add(tracked.pid);
|
|
356
|
+
const closedPids = [];
|
|
357
|
+
for (const pid of candidates) {
|
|
358
|
+
if (await terminatePid(pid))
|
|
359
|
+
closedPids.push(pid);
|
|
360
|
+
}
|
|
361
|
+
const remaining = findManagedBrowserRootPids(profileDir);
|
|
362
|
+
if (remaining.length) {
|
|
363
|
+
throw new Error(`Managed browser profile is still in use by pid ${remaining.join(', ')}. Close the setup browser before retrying.`);
|
|
364
|
+
}
|
|
365
|
+
clearManagedBrowserSetupState(profileDir);
|
|
366
|
+
return closedPids;
|
|
367
|
+
}
|
|
368
|
+
export async function prepareManagedBrowserForAutomation(profileDir = getManagedBrowserProfileDir(), options = {}) {
|
|
369
|
+
const tracked = readManagedBrowserSetupState(profileDir);
|
|
370
|
+
const runningState = resolveManagedBrowserRunningState(profileDir);
|
|
371
|
+
if (runningState.running) {
|
|
372
|
+
const cdpEndpoint = await resolveManagedBrowserCdpEndpoint(tracked?.debugPort || MANAGED_BROWSER_DEBUG_PORT);
|
|
373
|
+
if (cdpEndpoint) {
|
|
374
|
+
return {
|
|
375
|
+
profileDir,
|
|
376
|
+
closedPids: [],
|
|
377
|
+
cdpEndpoint,
|
|
378
|
+
connectionMode: 'attach',
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const closedPids = await closeManagedBrowserProcesses(profileDir);
|
|
383
|
+
if (!options.headless) {
|
|
384
|
+
launchManagedBrowserSetup();
|
|
385
|
+
const cdpEndpoint = await waitForManagedBrowserCdpEndpoint(MANAGED_BROWSER_DEBUG_PORT);
|
|
386
|
+
if (cdpEndpoint) {
|
|
387
|
+
return {
|
|
388
|
+
profileDir,
|
|
389
|
+
closedPids,
|
|
390
|
+
cdpEndpoint,
|
|
391
|
+
connectionMode: 'attach',
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
profileDir,
|
|
397
|
+
closedPids,
|
|
398
|
+
cdpEndpoint: null,
|
|
399
|
+
connectionMode: 'launch',
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
export function getManagedBrowserStatus() {
|
|
403
|
+
const profileDir = getManagedBrowserProfileDir();
|
|
404
|
+
const profileCreated = fs.existsSync(profileDir);
|
|
405
|
+
const chromeExecutable = findChromeExecutable();
|
|
406
|
+
const chromeInstalled = !!chromeExecutable;
|
|
407
|
+
const runningState = resolveManagedBrowserRunningState(profileDir);
|
|
408
|
+
return {
|
|
409
|
+
status: chromeInstalled
|
|
410
|
+
? profileCreated
|
|
411
|
+
? 'ready'
|
|
412
|
+
: 'needs_setup'
|
|
413
|
+
: 'chrome_missing',
|
|
414
|
+
profileDir,
|
|
415
|
+
profileCreated,
|
|
416
|
+
chromeInstalled,
|
|
417
|
+
running: runningState.running,
|
|
418
|
+
pid: runningState.pid,
|
|
419
|
+
detail: chromeInstalled
|
|
420
|
+
? runningState.running
|
|
421
|
+
? 'Managed browser is open for sign-in. pikiclaw will close it automatically before browser automation starts.'
|
|
422
|
+
: profileCreated
|
|
423
|
+
? 'Managed browser profile is ready. Launch it to confirm login state. If it is still open later, pikiclaw will close it automatically before browser automation starts.'
|
|
424
|
+
: 'Chrome is installed. Launch the managed browser once and sign in to the sites you need. If it is still open later, pikiclaw will close it automatically before browser automation starts.'
|
|
425
|
+
: 'Chrome is not available on this machine. Install Google Chrome or Chromium to use browser automation.',
|
|
426
|
+
chromeExecutable,
|
|
427
|
+
launchCommand: chromeExecutable ? [chromeExecutable, ...getManagedBrowserLaunchArgs(profileDir)] : [],
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
export function launchManagedBrowserSetup() {
|
|
431
|
+
const profileDir = ensureManagedBrowserProfileDir();
|
|
432
|
+
const chromeExecutable = findChromeExecutable();
|
|
433
|
+
if (!chromeExecutable) {
|
|
434
|
+
throw new Error('Chrome is not available on this machine');
|
|
435
|
+
}
|
|
436
|
+
const existing = resolveManagedBrowserRunningState(profileDir);
|
|
437
|
+
if (existing.running) {
|
|
438
|
+
normalizeManagedBrowserWindow(chromeExecutable);
|
|
439
|
+
return { ...getManagedBrowserStatus(), pid: existing.pid };
|
|
440
|
+
}
|
|
441
|
+
const child = spawn(chromeExecutable, getManagedBrowserLaunchArgs(profileDir), {
|
|
442
|
+
detached: true,
|
|
443
|
+
stdio: 'ignore',
|
|
444
|
+
windowsHide: true,
|
|
445
|
+
});
|
|
446
|
+
child.unref();
|
|
447
|
+
normalizeManagedBrowserWindow(chromeExecutable);
|
|
448
|
+
if (child.pid) {
|
|
449
|
+
writeManagedBrowserSetupState({
|
|
450
|
+
pid: child.pid,
|
|
451
|
+
profileDir,
|
|
452
|
+
chromeExecutable,
|
|
453
|
+
debugPort: MANAGED_BROWSER_DEBUG_PORT,
|
|
454
|
+
launchedAt: new Date().toISOString(),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return { ...getManagedBrowserStatus(), pid: child.pid ?? null };
|
|
458
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -198,6 +198,10 @@ async function handleMcpServeMode() {
|
|
|
198
198
|
await import('./mcp-session-server.js');
|
|
199
199
|
return true;
|
|
200
200
|
}
|
|
201
|
+
if (process.argv.includes('--playwright-mcp-proxy')) {
|
|
202
|
+
await import('./mcp-playwright-proxy.js');
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
201
205
|
return false;
|
|
202
206
|
}
|
|
203
207
|
/** Print help text and exit. */
|
package/dist/constants.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Grouped by domain / module so each subsystem can import only the
|
|
5
5
|
* bucket it needs.
|
|
6
6
|
*/
|
|
7
|
+
import path from 'node:path';
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// MCP bridge
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
@@ -45,11 +46,21 @@ export const DASHBOARD_TIMEOUTS = {
|
|
|
45
46
|
appiumReachable: 3_000,
|
|
46
47
|
/** Delay between Appium server startup polls. */
|
|
47
48
|
appiumStartPoll: 1_000,
|
|
48
|
-
/** Timeout for the Playwright MCP extension validation spawn. */
|
|
49
|
-
extensionValidationSpawn: 12_000,
|
|
50
|
-
/** Grace period to let Playwright MCP server prove it stays alive. */
|
|
51
|
-
extensionValidationAlive: 5_000,
|
|
52
49
|
};
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Browser automation
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
/**
|
|
54
|
+
* Stable relative path for the managed Chrome profile under the home directory.
|
|
55
|
+
* Keep this outside config-specific directories so `npm run dev` and the main
|
|
56
|
+
* runtime share the same browser login state.
|
|
57
|
+
*/
|
|
58
|
+
export const MANAGED_BROWSER_PROFILE_SUBPATH = path.join('.pikiclaw', 'browser', 'chrome-profile');
|
|
59
|
+
/** Base Playwright MCP args for the managed browser integration. */
|
|
60
|
+
export const PLAYWRIGHT_MCP_PACKAGE_NAME = '@playwright/mcp';
|
|
61
|
+
export const PLAYWRIGHT_MCP_PACKAGE_VERSION = '0.0.68';
|
|
62
|
+
export const PLAYWRIGHT_MCP_PACKAGE_SPEC = `${PLAYWRIGHT_MCP_PACKAGE_NAME}@${PLAYWRIGHT_MCP_PACKAGE_VERSION}`;
|
|
63
|
+
export const PLAYWRIGHT_MCP_BROWSER_ARGS = ['--browser', 'chrome'];
|
|
53
64
|
/** Dashboard session pagination limits. */
|
|
54
65
|
export const DASHBOARD_PAGINATION = {
|
|
55
66
|
defaultPageSize: 6,
|