pikiclaw 0.2.63 → 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 CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  > npx pikiclaw@latest
10
10
 
11
+ <img src="docs/promo-install.gif" alt="Quick install" width="700">
12
+
11
13
  <p align="center">
12
14
  <a href="https://www.npmjs.com/package/pikiclaw"><img src="https://img.shields.io/npm/v/pikiclaw" alt="npm"></a>
13
15
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
@@ -45,7 +47,20 @@ pikiclaw 的目标很直接:
45
47
  你的电脑
46
48
  ```
47
49
 
48
- 它适合的不是“演示一次 AI”,而是你离开电脑以后,Agent 还能继续在本机把事做完。
50
+ 它适合的不是”演示一次 AI”,而是你离开电脑以后,Agent 还能继续在本机把事做完。
51
+
52
+ ### 在 Telegram 里长这样
53
+
54
+ <table>
55
+ <tr>
56
+ <td align=”center”><b>命令与 Agent 切换</b><br><img src=”docs/promo-tg-commands.png” alt=”Commands” width=”320”></td>
57
+ <td align=”center”><b>代码审查</b><br><img src=”docs/promo-tg-task.png” alt=”Code review” width=”320”></td>
58
+ </tr>
59
+ <tr>
60
+ <td align=”center”><b>多轮编码 + 文件回传</b><br><img src=”docs/promo-tg-complex.png” alt=”Complex task” width=”320”></td>
61
+ <td align=”center”><b>状态监控 + 会话管理</b><br><img src=”docs/promo-tg-sessions.png” alt=”Sessions” width=”320”></td>
62
+ </tr>
63
+ </table>
49
64
 
50
65
  ---
51
66
 
@@ -76,6 +91,19 @@ npx pikiclaw@latest
76
91
  - 工作目录切换
77
92
  - 会话和运行状态查看
78
93
 
94
+ <details>
95
+ <summary>Dashboard 截图</summary>
96
+
97
+ **配置管理** — IM 接入、AI Agent、系统权限
98
+
99
+ <img src="docs/promo-dashboard-config.png" alt="Config" width="700">
100
+
101
+ **会话管理** — 按 Agent 分组的会话泳道
102
+
103
+ <img src="docs/promo-dashboard-sessions.png" alt="Sessions" width="700">
104
+
105
+ </details>
106
+
79
107
  如果你更喜欢终端向导:
80
108
 
81
109
  ```bash
@@ -130,7 +158,7 @@ npx pikiclaw@latest --doctor
130
158
 
131
159
  可选 GUI 能力:
132
160
 
133
- - 浏览器自动化:通过 `@playwright/mcp` 补充接入,默认支持 Chrome extension mode,也可切到 headless / isolated 模式
161
+ - 浏览器自动化:通过 `@playwright/mcp` 管理一个专用的持久化 Chrome profile;第一次使用时在这个自动化浏览器里登录需要的网站,后续任务会复用同一个 profile
134
162
  - macOS 桌面自动化:通过 Appium Mac2 提供 `desktop_open_app`、`desktop_snapshot`、`desktop_click`、`desktop_type`、`desktop_screenshot` 等工具
135
163
 
136
164
  ---
@@ -152,22 +180,25 @@ npx pikiclaw@latest --doctor
152
180
 
153
181
  普通文本消息会直接转给当前 Agent。
154
182
 
183
+ <details>
184
+ <summary>Telegram 命令效果预览</summary>
185
+
186
+ <img src="docs/promo-tg-commands.png" alt="Commands in Telegram" width="360">
187
+
188
+ </details>
189
+
155
190
  ---
156
191
 
157
192
  ## Config And Setup Notes
158
193
 
159
194
  - 持久化配置在 `~/.pikiclaw/setting.json`
160
- - Dashboard 是主配置入口,环境变量仍然可用
161
- - 浏览器 GUI 相关常用变量:
162
- - `PIKICLAW_BROWSER_GUI`
163
- - `PIKICLAW_BROWSER_USE_EXTENSION`
164
- - `PIKICLAW_BROWSER_HEADLESS`
165
- - `PIKICLAW_BROWSER_ISOLATED`
166
- - `PLAYWRIGHT_MCP_EXTENSION_TOKEN`
195
+ - Dashboard 是主配置入口,其他运行时配置仍然可用
167
196
  - 桌面 GUI 相关常用变量:
168
197
  - `PIKICLAW_DESKTOP_GUI`
169
198
  - `PIKICLAW_DESKTOP_APPIUM_URL`
170
199
 
200
+ 浏览器自动化由 dashboard 和本地运行时共同管理,会自动创建并复用专用的 Chrome profile 目录。你只需要在这个专用浏览器里登录需要自动化的网站账号一次。
201
+
171
202
  如果要启用 macOS 桌面自动化,需要先准备 Appium Mac2:
172
203
 
173
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
- ? (mcpSendFile ? appendExtraPrompt(systemPrompt, buildMcpDeliveryPrompt()) : systemPrompt)
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,