pikiloop 0.4.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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +353 -0
  3. package/README.v2.md +287 -0
  4. package/README.zh-CN.md +352 -0
  5. package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
  6. package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
  7. package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
  8. package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
  9. package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
  10. package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
  11. package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
  12. package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
  13. package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
  14. package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
  15. package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
  16. package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
  17. package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
  18. package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
  19. package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
  20. package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
  21. package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
  22. package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
  23. package/dashboard/dist/assets/index-reSbuley.css +1 -0
  24. package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
  25. package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
  26. package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
  27. package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
  28. package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
  29. package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
  30. package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
  31. package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
  32. package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
  33. package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
  34. package/dashboard/dist/favicon.svg +28 -0
  35. package/dashboard/dist/index.html +17 -0
  36. package/dist/agent/acp-client.js +261 -0
  37. package/dist/agent/auto-update.js +432 -0
  38. package/dist/agent/await-resume.js +50 -0
  39. package/dist/agent/cli/auth.js +325 -0
  40. package/dist/agent/cli/catalog.js +40 -0
  41. package/dist/agent/cli/detector.js +136 -0
  42. package/dist/agent/cli/index.js +7 -0
  43. package/dist/agent/cli/registry.js +33 -0
  44. package/dist/agent/driver.js +39 -0
  45. package/dist/agent/drivers/claude-tui.js +2297 -0
  46. package/dist/agent/drivers/claude.js +2689 -0
  47. package/dist/agent/drivers/codex.js +2210 -0
  48. package/dist/agent/drivers/gemini.js +1059 -0
  49. package/dist/agent/drivers/hermes.js +795 -0
  50. package/dist/agent/goal.js +274 -0
  51. package/dist/agent/handover.js +130 -0
  52. package/dist/agent/images.js +355 -0
  53. package/dist/agent/index.js +50 -0
  54. package/dist/agent/mcp/bridge.js +791 -0
  55. package/dist/agent/mcp/extensions.js +637 -0
  56. package/dist/agent/mcp/oauth.js +353 -0
  57. package/dist/agent/mcp/registry.js +119 -0
  58. package/dist/agent/mcp/session-server.js +229 -0
  59. package/dist/agent/mcp/tools/ask-user.js +113 -0
  60. package/dist/agent/mcp/tools/await-resume.js +77 -0
  61. package/dist/agent/mcp/tools/goal.js +144 -0
  62. package/dist/agent/mcp/tools/types.js +12 -0
  63. package/dist/agent/mcp/tools/workspace.js +212 -0
  64. package/dist/agent/npm.js +31 -0
  65. package/dist/agent/session.js +1206 -0
  66. package/dist/agent/skill-installer.js +160 -0
  67. package/dist/agent/skills.js +257 -0
  68. package/dist/agent/stream.js +743 -0
  69. package/dist/agent/types.js +13 -0
  70. package/dist/agent/utils.js +687 -0
  71. package/dist/bot/bot.js +2499 -0
  72. package/dist/bot/command-ui.js +633 -0
  73. package/dist/bot/commands.js +513 -0
  74. package/dist/bot/headless-bot.js +36 -0
  75. package/dist/bot/host.js +192 -0
  76. package/dist/bot/human-loop.js +168 -0
  77. package/dist/bot/menu.js +48 -0
  78. package/dist/bot/orchestration.js +79 -0
  79. package/dist/bot/render-shared.js +309 -0
  80. package/dist/bot/session-hub.js +361 -0
  81. package/dist/bot/session-status.js +55 -0
  82. package/dist/bot/streaming.js +309 -0
  83. package/dist/browser-profile.js +579 -0
  84. package/dist/browser-supervisor.js +249 -0
  85. package/dist/catalog/cli-tools.js +421 -0
  86. package/dist/catalog/index.js +21 -0
  87. package/dist/catalog/local-models.js +94 -0
  88. package/dist/catalog/mcp-servers.js +315 -0
  89. package/dist/catalog/skill-repos.js +173 -0
  90. package/dist/channels/base.js +55 -0
  91. package/dist/channels/dingtalk/bot.js +549 -0
  92. package/dist/channels/dingtalk/channel.js +268 -0
  93. package/dist/channels/discord/bot.js +552 -0
  94. package/dist/channels/discord/channel.js +245 -0
  95. package/dist/channels/feishu/bot.js +1275 -0
  96. package/dist/channels/feishu/channel.js +911 -0
  97. package/dist/channels/feishu/markdown.js +91 -0
  98. package/dist/channels/feishu/render.js +619 -0
  99. package/dist/channels/health.js +109 -0
  100. package/dist/channels/slack/bot.js +554 -0
  101. package/dist/channels/slack/channel.js +283 -0
  102. package/dist/channels/states.js +6 -0
  103. package/dist/channels/telegram/bot.js +1310 -0
  104. package/dist/channels/telegram/channel.js +820 -0
  105. package/dist/channels/telegram/directory.js +111 -0
  106. package/dist/channels/telegram/live-preview.js +220 -0
  107. package/dist/channels/telegram/render.js +384 -0
  108. package/dist/channels/wecom/bot.js +558 -0
  109. package/dist/channels/wecom/channel.js +479 -0
  110. package/dist/channels/weixin/api.js +520 -0
  111. package/dist/channels/weixin/bot.js +1000 -0
  112. package/dist/channels/weixin/channel.js +222 -0
  113. package/dist/cli/autostart.js +262 -0
  114. package/dist/cli/channel-supervisor.js +313 -0
  115. package/dist/cli/channels.js +54 -0
  116. package/dist/cli/main.js +726 -0
  117. package/dist/cli/onboarding.js +227 -0
  118. package/dist/cli/run.js +308 -0
  119. package/dist/cli/setup-wizard.js +235 -0
  120. package/dist/core/config/runtime-config.js +201 -0
  121. package/dist/core/config/user-config.js +510 -0
  122. package/dist/core/config/validation.js +521 -0
  123. package/dist/core/constants.js +400 -0
  124. package/dist/core/git.js +145 -0
  125. package/dist/core/legacy-compat.js +60 -0
  126. package/dist/core/logging.js +101 -0
  127. package/dist/core/platform.js +59 -0
  128. package/dist/core/process-control.js +315 -0
  129. package/dist/core/secrets/index.js +42 -0
  130. package/dist/core/secrets/inline-seal.js +60 -0
  131. package/dist/core/secrets/ref.js +33 -0
  132. package/dist/core/secrets/resolver.js +65 -0
  133. package/dist/core/secrets/store.js +63 -0
  134. package/dist/core/utils.js +233 -0
  135. package/dist/core/version.js +15 -0
  136. package/dist/dashboard/platform.js +219 -0
  137. package/dist/dashboard/routes/agents.js +450 -0
  138. package/dist/dashboard/routes/cli.js +174 -0
  139. package/dist/dashboard/routes/config.js +523 -0
  140. package/dist/dashboard/routes/extensions.js +745 -0
  141. package/dist/dashboard/routes/local-models.js +290 -0
  142. package/dist/dashboard/routes/models.js +324 -0
  143. package/dist/dashboard/routes/sessions.js +838 -0
  144. package/dist/dashboard/runtime.js +410 -0
  145. package/dist/dashboard/server.js +237 -0
  146. package/dist/dashboard/session-control.js +347 -0
  147. package/dist/model/catalog.js +104 -0
  148. package/dist/model/index.js +20 -0
  149. package/dist/model/injector.js +272 -0
  150. package/dist/model/provider-models.js +112 -0
  151. package/dist/model/store.js +212 -0
  152. package/dist/model/types.js +13 -0
  153. package/dist/model/validation.js +203 -0
  154. package/package.json +82 -0
@@ -0,0 +1,579 @@
1
+ /**
2
+ * Managed browser profile directory for Playwright integration.
3
+ */
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { createRequire } from 'node:module';
8
+ import { spawn, spawnSync } from 'node:child_process';
9
+ import { MANAGED_BROWSER_PROFILE_SUBPATH, PIKILOOP_BROWSER_CDP_URL_ENV, PLAYWRIGHT_MCP_PACKAGE_NAME, PLAYWRIGHT_MCP_PACKAGE_SPEC, PLAYWRIGHT_MCP_BROWSER_ARGS, } from './core/constants.js';
10
+ const MANAGED_BROWSER_SETUP_STATE_FILENAME = 'managed-browser-setup.json';
11
+ const MANAGED_BROWSER_SHUTDOWN_TIMEOUT_MS = 5_000;
12
+ const MANAGED_BROWSER_SHUTDOWN_POLL_MS = 100;
13
+ const require = createRequire(import.meta.url);
14
+ /**
15
+ * Read Chrome's `DevToolsActivePort` file from the managed profile dir.
16
+ * Chrome writes this file when launched with `--remote-debugging-port=<n>`
17
+ * (including `=0`, which lets the OS assign a free port). Format is two lines:
18
+ * <port>
19
+ * /devtools/browser/<id>
20
+ * We use it to discover the actual port rather than hard-coding one.
21
+ */
22
+ function readDevToolsActivePort(profileDir) {
23
+ try {
24
+ const raw = fs.readFileSync(path.join(profileDir, 'DevToolsActivePort'), 'utf8');
25
+ const firstLine = raw.split('\n')[0]?.trim();
26
+ if (!firstLine)
27
+ return null;
28
+ const port = parseInt(firstLine, 10);
29
+ if (!Number.isFinite(port) || port <= 0 || port > 65535)
30
+ return null;
31
+ return port;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ function normalizeBrowserCdpEndpoint(endpoint) {
38
+ const value = String(endpoint || '').trim();
39
+ if (!value)
40
+ return '';
41
+ return value.replace(/\/+$/, '');
42
+ }
43
+ /**
44
+ * The external CDP endpoint configured via {@link PIKILOOP_BROWSER_CDP_URL_ENV},
45
+ * normalized (trailing slashes stripped, blank → null). Single source of truth
46
+ * for the remote-browser override: when this returns a value, every browser
47
+ * codepath attaches to it and pikiloop never launches, probes, or kills a local
48
+ * Chrome. Read from `env` (defaults to `process.env`) so callers can inject it
49
+ * in tests.
50
+ */
51
+ export function getConfiguredRemoteCdpUrl(env = process.env) {
52
+ const raw = String(env[PIKILOOP_BROWSER_CDP_URL_ENV] || '').trim();
53
+ if (!raw)
54
+ return null;
55
+ return normalizeBrowserCdpEndpoint(raw);
56
+ }
57
+ async function resolveBrowserCdpEndpoint(endpoint) {
58
+ const normalizedEndpoint = normalizeBrowserCdpEndpoint(endpoint);
59
+ if (!normalizedEndpoint)
60
+ return null;
61
+ const controller = new AbortController();
62
+ const timeout = setTimeout(() => controller.abort(), 1_500);
63
+ try {
64
+ const response = await fetch(`${normalizedEndpoint}/json/version`, { signal: controller.signal });
65
+ if (!response.ok)
66
+ return null;
67
+ const payload = await response.json().catch(() => null);
68
+ return typeof payload?.webSocketDebuggerUrl === 'string' ? normalizedEndpoint : null;
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ finally {
74
+ clearTimeout(timeout);
75
+ }
76
+ }
77
+ function resolveOnPath(command) {
78
+ const checker = process.platform === 'win32' ? 'where' : 'which';
79
+ try {
80
+ const result = spawnSync(checker, [command], { encoding: 'utf8' });
81
+ if (result.status !== 0)
82
+ return null;
83
+ const lines = String(result.stdout || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean);
84
+ return lines[0] || null;
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
90
+ function resolveCommonChromePaths() {
91
+ if (process.platform === 'darwin') {
92
+ return [
93
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
94
+ path.join(os.homedir(), 'Applications', 'Google Chrome.app', 'Contents', 'MacOS', 'Google Chrome'),
95
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
96
+ path.join(os.homedir(), 'Applications', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'),
97
+ ];
98
+ }
99
+ if (process.platform === 'win32') {
100
+ const programFiles = process.env.ProgramFiles || '';
101
+ const programFilesX86 = process.env['ProgramFiles(x86)'] || '';
102
+ const localAppData = process.env.LOCALAPPDATA || '';
103
+ return [
104
+ programFiles ? path.join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe') : '',
105
+ programFilesX86 ? path.join(programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe') : '',
106
+ localAppData ? path.join(localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe') : '',
107
+ ].filter(Boolean);
108
+ }
109
+ return [];
110
+ }
111
+ function resolveMacBrowserAppName(chromeExecutable) {
112
+ if (process.platform !== 'darwin')
113
+ return null;
114
+ if (chromeExecutable.includes('/Chromium.app/'))
115
+ return 'Chromium';
116
+ if (chromeExecutable.includes('/Google Chrome.app/'))
117
+ return 'Google Chrome';
118
+ return null;
119
+ }
120
+ function normalizeManagedBrowserWindow(chromeExecutable) {
121
+ if (process.platform !== 'darwin')
122
+ return;
123
+ const appName = resolveMacBrowserAppName(chromeExecutable);
124
+ if (!appName)
125
+ return;
126
+ const script = [
127
+ 'set screenBounds to {0, 0, 1440, 900}',
128
+ 'try',
129
+ ' tell application "Finder" to set screenBounds to bounds of window of desktop',
130
+ 'end try',
131
+ `tell application "${appName}"`,
132
+ ' activate',
133
+ ' delay 0.4',
134
+ ' try',
135
+ ' set zoomed of front window to true',
136
+ ' end try',
137
+ ' try',
138
+ ' set bounds of front window to screenBounds',
139
+ ' end try',
140
+ 'end tell',
141
+ ].join('\n');
142
+ setTimeout(() => {
143
+ try {
144
+ const proc = spawn('osascript', ['-e', script], {
145
+ detached: true,
146
+ stdio: 'ignore',
147
+ windowsHide: true,
148
+ });
149
+ proc.unref();
150
+ }
151
+ catch { }
152
+ }, 700);
153
+ }
154
+ export function getManagedBrowserProfileDir() {
155
+ return path.join(os.homedir(), MANAGED_BROWSER_PROFILE_SUBPATH);
156
+ }
157
+ export function ensureManagedBrowserProfileDir() {
158
+ const profileDir = getManagedBrowserProfileDir();
159
+ fs.mkdirSync(profileDir, { recursive: true });
160
+ return profileDir;
161
+ }
162
+ function getPlaywrightMcpConfigPath(outputDir) {
163
+ return path.join(outputDir, 'playwright-mcp-config.json');
164
+ }
165
+ /**
166
+ * Write the playwright/mcp config JSON used by every spawn. Currently it just
167
+ * strips Playwright's default `--disable-blink-features=AutomationControlled`
168
+ * arg so Chrome doesn't show its "unsupported command-line flag" infobar.
169
+ * Playwright adds that flag to hide `navigator.webdriver`; removing it makes
170
+ * the managed browser look like a normal session, which is what we want for
171
+ * pikiloop's logged-in automation profile.
172
+ *
173
+ * Idempotent: rewrites only when the content drifts.
174
+ */
175
+ export function ensurePlaywrightMcpConfigFile(outputDir = path.dirname(getManagedBrowserProfileDir())) {
176
+ const configPath = getPlaywrightMcpConfigPath(outputDir);
177
+ const desired = JSON.stringify({
178
+ browser: {
179
+ launchOptions: {
180
+ ignoreDefaultArgs: ['--disable-blink-features=AutomationControlled'],
181
+ },
182
+ },
183
+ }, null, 2);
184
+ try {
185
+ if (fs.readFileSync(configPath, 'utf8') === desired)
186
+ return configPath;
187
+ }
188
+ catch {
189
+ // File missing or unreadable — fall through to write.
190
+ }
191
+ fs.mkdirSync(outputDir, { recursive: true });
192
+ fs.writeFileSync(configPath, desired, 'utf8');
193
+ return configPath;
194
+ }
195
+ export function getManagedBrowserMcpArgs(profileDir = getManagedBrowserProfileDir(), options = {}) {
196
+ const outputDir = path.dirname(profileDir);
197
+ const configPath = getPlaywrightMcpConfigPath(outputDir);
198
+ if (options.cdpEndpoint) {
199
+ return ['--config', configPath, '--cdp-endpoint', options.cdpEndpoint, '--output-dir', outputDir];
200
+ }
201
+ return [
202
+ '--config', configPath,
203
+ ...PLAYWRIGHT_MCP_BROWSER_ARGS,
204
+ ...(options.headless ? ['--headless'] : []),
205
+ '--user-data-dir',
206
+ profileDir,
207
+ '--output-dir',
208
+ outputDir,
209
+ ];
210
+ }
211
+ export function resolveManagedBrowserMcpCliPath() {
212
+ try {
213
+ const packageJsonPath = require.resolve(`${PLAYWRIGHT_MCP_PACKAGE_NAME}/package.json`);
214
+ const cliPath = path.join(path.dirname(packageJsonPath), 'cli.js');
215
+ return fs.existsSync(cliPath) ? cliPath : null;
216
+ }
217
+ catch {
218
+ return null;
219
+ }
220
+ }
221
+ export function resolveManagedBrowserMcpCommand(profileDir = getManagedBrowserProfileDir(), options = {}) {
222
+ const cliPath = resolveManagedBrowserMcpCliPath();
223
+ const runtimeArgs = getManagedBrowserMcpArgs(profileDir, options);
224
+ if (cliPath) {
225
+ return {
226
+ command: process.execPath,
227
+ args: [cliPath, ...runtimeArgs],
228
+ source: 'local',
229
+ };
230
+ }
231
+ return {
232
+ command: 'npx',
233
+ args: ['-y', PLAYWRIGHT_MCP_PACKAGE_SPEC, ...runtimeArgs],
234
+ source: 'npx',
235
+ };
236
+ }
237
+ export function getManagedBrowserLaunchArgs(profileDir = getManagedBrowserProfileDir()) {
238
+ const windowArgs = process.platform === 'darwin'
239
+ ? ['--start-maximized', '--start-fullscreen', '--window-position=0,0']
240
+ : ['--start-maximized'];
241
+ return [
242
+ `--user-data-dir=${profileDir}`,
243
+ // `=0` makes Chrome pick a free port and write it to <profileDir>/DevToolsActivePort,
244
+ // which `readDevToolsActivePort` reads back. Hard-coding a port causes collisions
245
+ // when another Chrome / debugger is already on it.
246
+ '--remote-debugging-port=0',
247
+ '--no-first-run',
248
+ '--no-default-browser-check',
249
+ '--new-window',
250
+ ...windowArgs,
251
+ 'about:blank',
252
+ ];
253
+ }
254
+ export function findChromeExecutable() {
255
+ for (const candidate of resolveCommonChromePaths()) {
256
+ if (fs.existsSync(candidate))
257
+ return candidate;
258
+ }
259
+ const commands = process.platform === 'win32'
260
+ ? ['chrome', 'google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser']
261
+ : ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'chrome'];
262
+ for (const command of commands) {
263
+ const resolved = resolveOnPath(command);
264
+ if (resolved)
265
+ return resolved;
266
+ }
267
+ return null;
268
+ }
269
+ function getManagedBrowserSetupStatePath(profileDir = getManagedBrowserProfileDir()) {
270
+ return path.join(path.dirname(profileDir), MANAGED_BROWSER_SETUP_STATE_FILENAME);
271
+ }
272
+ function readManagedBrowserSetupState(profileDir = getManagedBrowserProfileDir()) {
273
+ try {
274
+ const raw = fs.readFileSync(getManagedBrowserSetupStatePath(profileDir), 'utf8');
275
+ const parsed = JSON.parse(raw);
276
+ if (!parsed || typeof parsed.pid !== 'number' || parsed.pid <= 0)
277
+ return null;
278
+ if (typeof parsed.profileDir !== 'string' || !parsed.profileDir.trim())
279
+ return null;
280
+ if (typeof parsed.chromeExecutable !== 'string' || !parsed.chromeExecutable.trim())
281
+ return null;
282
+ return {
283
+ pid: parsed.pid,
284
+ profileDir: parsed.profileDir,
285
+ chromeExecutable: parsed.chromeExecutable,
286
+ launchedAt: typeof parsed.launchedAt === 'string' ? parsed.launchedAt : new Date().toISOString(),
287
+ };
288
+ }
289
+ catch {
290
+ return null;
291
+ }
292
+ }
293
+ function writeManagedBrowserSetupState(state) {
294
+ const statePath = getManagedBrowserSetupStatePath(state.profileDir);
295
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
296
+ fs.writeFileSync(statePath, JSON.stringify(state), 'utf8');
297
+ }
298
+ function clearManagedBrowserSetupState(profileDir = getManagedBrowserProfileDir()) {
299
+ try {
300
+ fs.rmSync(getManagedBrowserSetupStatePath(profileDir), { force: true });
301
+ }
302
+ catch { }
303
+ }
304
+ function isPidAlive(pid) {
305
+ try {
306
+ process.kill(pid, 0);
307
+ return true;
308
+ }
309
+ catch {
310
+ return false;
311
+ }
312
+ }
313
+ function readProcessCommand(pid) {
314
+ try {
315
+ if (process.platform === 'win32') {
316
+ const result = spawnSync('powershell', ['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`], { encoding: 'utf8' });
317
+ if (result.status !== 0)
318
+ return '';
319
+ return String(result.stdout || '').trim();
320
+ }
321
+ const result = spawnSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf8' });
322
+ if (result.status !== 0)
323
+ return '';
324
+ return String(result.stdout || '').trim();
325
+ }
326
+ catch {
327
+ return '';
328
+ }
329
+ }
330
+ function commandUsesManagedProfile(command, profileDir) {
331
+ const normalizedCommand = command.trim();
332
+ return normalizedCommand.includes(`--user-data-dir=${profileDir}`)
333
+ || normalizedCommand.includes(`--user-data-dir ${profileDir}`);
334
+ }
335
+ function isManagedBrowserRootProcess(command, profileDir) {
336
+ if (!commandUsesManagedProfile(command, profileDir))
337
+ return false;
338
+ return !command.includes(' --type=');
339
+ }
340
+ function findManagedBrowserRootPids(profileDir = getManagedBrowserProfileDir()) {
341
+ if (process.platform === 'win32') {
342
+ const tracked = readManagedBrowserSetupState(profileDir);
343
+ if (!tracked || !isPidAlive(tracked.pid))
344
+ return [];
345
+ return commandUsesManagedProfile(readProcessCommand(tracked.pid), profileDir) ? [tracked.pid] : [];
346
+ }
347
+ try {
348
+ const result = spawnSync('ps', ['-ax', '-o', 'pid=,command='], { encoding: 'utf8' });
349
+ if (result.status !== 0)
350
+ return [];
351
+ const lines = String(result.stdout || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean);
352
+ const pids = new Set();
353
+ for (const line of lines) {
354
+ const match = line.match(/^(\d+)\s+(.*)$/);
355
+ if (!match)
356
+ continue;
357
+ const pid = Number(match[1]);
358
+ const command = match[2] || '';
359
+ if (!Number.isFinite(pid) || pid <= 0)
360
+ continue;
361
+ if (!isManagedBrowserRootProcess(command, profileDir))
362
+ continue;
363
+ pids.add(pid);
364
+ }
365
+ return [...pids];
366
+ }
367
+ catch {
368
+ return [];
369
+ }
370
+ }
371
+ function resolveManagedBrowserRunningState(profileDir = getManagedBrowserProfileDir()) {
372
+ const tracked = readManagedBrowserSetupState(profileDir);
373
+ if (tracked) {
374
+ const command = readProcessCommand(tracked.pid);
375
+ if (isPidAlive(tracked.pid) && commandUsesManagedProfile(command, profileDir)) {
376
+ return { running: true, pid: tracked.pid };
377
+ }
378
+ clearManagedBrowserSetupState(profileDir);
379
+ }
380
+ const rootPids = findManagedBrowserRootPids(profileDir);
381
+ if (!rootPids.length)
382
+ return { running: false, pid: null };
383
+ return { running: true, pid: rootPids[0] ?? null };
384
+ }
385
+ function sleep(ms) {
386
+ return new Promise(resolve => setTimeout(resolve, ms));
387
+ }
388
+ async function waitForPidExit(pid, timeoutMs = MANAGED_BROWSER_SHUTDOWN_TIMEOUT_MS) {
389
+ const deadline = Date.now() + timeoutMs;
390
+ while (Date.now() < deadline) {
391
+ if (!isPidAlive(pid))
392
+ return true;
393
+ await sleep(MANAGED_BROWSER_SHUTDOWN_POLL_MS);
394
+ }
395
+ return !isPidAlive(pid);
396
+ }
397
+ async function terminatePid(pid) {
398
+ if (!isPidAlive(pid))
399
+ return true;
400
+ try {
401
+ process.kill(pid, 'SIGTERM');
402
+ }
403
+ catch {
404
+ return !isPidAlive(pid);
405
+ }
406
+ if (await waitForPidExit(pid))
407
+ return true;
408
+ try {
409
+ process.kill(pid, 'SIGKILL');
410
+ }
411
+ catch { }
412
+ return waitForPidExit(pid, 1_000);
413
+ }
414
+ export async function resolveManagedBrowserCdpEndpoint(profileDir = getManagedBrowserProfileDir()) {
415
+ const port = readDevToolsActivePort(profileDir);
416
+ if (!port)
417
+ return null;
418
+ return resolveBrowserCdpEndpoint(`http://127.0.0.1:${port}`);
419
+ }
420
+ async function waitForManagedBrowserCdpEndpoint(profileDir, timeoutMs = 6_000) {
421
+ const deadline = Date.now() + timeoutMs;
422
+ while (Date.now() < deadline) {
423
+ const endpoint = await resolveManagedBrowserCdpEndpoint(profileDir);
424
+ if (endpoint)
425
+ return endpoint;
426
+ await sleep(200);
427
+ }
428
+ return resolveManagedBrowserCdpEndpoint(profileDir);
429
+ }
430
+ /**
431
+ * Hard-reset the managed browser: SIGKILL every root pid (skipping the SIGTERM
432
+ * grace window) and wipe Chrome's session-restore files so the relaunch comes
433
+ * up with a fresh `about:blank` instead of replaying the tabs that triggered
434
+ * the failure. Used as the reactive recovery path when an agent stream sees an
435
+ * MCP browser error (Connection closed / Frame has been detached) indicating
436
+ * Chrome's CDP layer is wedged; a normal `closeManagedBrowserProcesses`
437
+ * round-trip won't help because it would restore the same poisoned tabs.
438
+ *
439
+ * Trade-off: any unrelated user-opened tabs are dropped. Acceptable here
440
+ * because this is invoked only after observing a real failure — the user has
441
+ * already lost time, and recovery beats perpetuating the wedge.
442
+ */
443
+ export async function forceCloseManagedBrowser(profileDir = getManagedBrowserProfileDir()) {
444
+ const tracked = readManagedBrowserSetupState(profileDir);
445
+ const candidates = new Set(findManagedBrowserRootPids(profileDir));
446
+ if (tracked?.pid)
447
+ candidates.add(tracked.pid);
448
+ const killedPids = [];
449
+ for (const pid of candidates) {
450
+ try {
451
+ process.kill(pid, 'SIGKILL');
452
+ }
453
+ catch { }
454
+ if (await waitForPidExit(pid, 2_000))
455
+ killedPids.push(pid);
456
+ }
457
+ clearManagedBrowserSetupState(profileDir);
458
+ const defaultDir = path.join(profileDir, 'Default');
459
+ const sessionsDir = path.join(defaultDir, 'Sessions');
460
+ try {
461
+ fs.rmSync(sessionsDir, { recursive: true, force: true });
462
+ }
463
+ catch { }
464
+ for (const name of ['Current Session', 'Current Tabs', 'Last Session', 'Last Tabs']) {
465
+ try {
466
+ fs.rmSync(path.join(defaultDir, name), { force: true });
467
+ }
468
+ catch { }
469
+ }
470
+ return killedPids;
471
+ }
472
+ async function closeManagedBrowserProcesses(profileDir) {
473
+ const tracked = readManagedBrowserSetupState(profileDir);
474
+ const candidates = new Set(findManagedBrowserRootPids(profileDir));
475
+ if (tracked?.pid)
476
+ candidates.add(tracked.pid);
477
+ const closedPids = [];
478
+ for (const pid of candidates) {
479
+ if (await terminatePid(pid))
480
+ closedPids.push(pid);
481
+ }
482
+ const remaining = findManagedBrowserRootPids(profileDir);
483
+ if (remaining.length) {
484
+ throw new Error(`Managed browser profile is still in use by pid ${remaining.join(', ')}. Close the setup browser before retrying.`);
485
+ }
486
+ clearManagedBrowserSetupState(profileDir);
487
+ return closedPids;
488
+ }
489
+ export async function prepareManagedBrowserForAutomation(profileDir = getManagedBrowserProfileDir(), options = {}) {
490
+ // CDP probe is the authoritative signal: if the managed Chrome answers on
491
+ // its debug port, attach to it regardless of what the setup-state file or
492
+ // the process table say. The ps-based running-state check is heuristic and
493
+ // can be wrong after a daemon restart, which would otherwise kill the
494
+ // user's live (and signed-in) browser.
495
+ const reachableEndpoint = await resolveManagedBrowserCdpEndpoint(profileDir);
496
+ if (reachableEndpoint) {
497
+ return {
498
+ profileDir,
499
+ closedPids: [],
500
+ cdpEndpoint: reachableEndpoint,
501
+ connectionMode: 'attach',
502
+ };
503
+ }
504
+ const closedPids = await closeManagedBrowserProcesses(profileDir);
505
+ if (!options.headless) {
506
+ launchManagedBrowserSetup();
507
+ const cdpEndpoint = await waitForManagedBrowserCdpEndpoint(profileDir);
508
+ if (cdpEndpoint) {
509
+ return {
510
+ profileDir,
511
+ closedPids,
512
+ cdpEndpoint,
513
+ connectionMode: 'launch',
514
+ };
515
+ }
516
+ }
517
+ return {
518
+ profileDir,
519
+ closedPids,
520
+ cdpEndpoint: null,
521
+ connectionMode: 'launch',
522
+ };
523
+ }
524
+ export function getManagedBrowserStatus() {
525
+ const profileDir = getManagedBrowserProfileDir();
526
+ const profileCreated = fs.existsSync(profileDir);
527
+ const chromeExecutable = findChromeExecutable();
528
+ const chromeInstalled = !!chromeExecutable;
529
+ const runningState = resolveManagedBrowserRunningState(profileDir);
530
+ return {
531
+ status: chromeInstalled
532
+ ? profileCreated
533
+ ? 'ready'
534
+ : 'needs_setup'
535
+ : 'chrome_missing',
536
+ profileDir,
537
+ profileCreated,
538
+ chromeInstalled,
539
+ running: runningState.running,
540
+ pid: runningState.pid,
541
+ detail: chromeInstalled
542
+ ? runningState.running
543
+ ? 'Managed browser is open for sign-in. pikiloop will close it automatically before browser automation starts.'
544
+ : profileCreated
545
+ ? 'Managed browser profile is ready. Launch it to confirm login state. If it is still open later, pikiloop will close it automatically before browser automation starts.'
546
+ : 'Chrome is installed. Launch the managed browser once and sign in to the sites you need. If it is still open later, pikiloop will close it automatically before browser automation starts.'
547
+ : 'Chrome is not available on this machine. Install Google Chrome or Chromium to use browser automation.',
548
+ chromeExecutable,
549
+ launchCommand: chromeExecutable ? [chromeExecutable, ...getManagedBrowserLaunchArgs(profileDir)] : [],
550
+ };
551
+ }
552
+ export function launchManagedBrowserSetup() {
553
+ const profileDir = ensureManagedBrowserProfileDir();
554
+ const chromeExecutable = findChromeExecutable();
555
+ if (!chromeExecutable) {
556
+ throw new Error('Chrome is not available on this machine');
557
+ }
558
+ const existing = resolveManagedBrowserRunningState(profileDir);
559
+ if (existing.running) {
560
+ normalizeManagedBrowserWindow(chromeExecutable);
561
+ return { ...getManagedBrowserStatus(), pid: existing.pid };
562
+ }
563
+ const child = spawn(chromeExecutable, getManagedBrowserLaunchArgs(profileDir), {
564
+ detached: true,
565
+ stdio: 'ignore',
566
+ windowsHide: true,
567
+ });
568
+ child.unref();
569
+ normalizeManagedBrowserWindow(chromeExecutable);
570
+ if (child.pid) {
571
+ writeManagedBrowserSetupState({
572
+ pid: child.pid,
573
+ profileDir,
574
+ chromeExecutable,
575
+ launchedAt: new Date().toISOString(),
576
+ });
577
+ }
578
+ return { ...getManagedBrowserStatus(), pid: child.pid ?? null };
579
+ }