ptbk 0.119.0-21 → 0.119.0-26
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 +16 -9
- package/dist/browser/browser.d.ts +50 -36
- package/dist/browser/browser.js +162 -118
- package/dist/browser/browser.js.map +1 -1
- package/dist/browser/fleet.d.ts +59 -0
- package/dist/browser/fleet.js +148 -0
- package/dist/browser/fleet.js.map +1 -0
- package/dist/browser/recording.js +2 -2
- package/dist/browser/recording.js.map +1 -1
- package/dist/chats/conversation.d.ts +1 -1
- package/dist/chats/conversation.js +8 -6
- package/dist/chats/conversation.js.map +1 -1
- package/dist/chats/local.js +5 -5
- package/dist/chats/local.js.map +1 -1
- package/dist/cli/commands/add.js +1 -1
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/boilerplates.js +1 -1
- package/dist/cli/commands/boilerplates.js.map +1 -1
- package/dist/cli/commands/find.js +1 -1
- package/dist/cli/commands/find.js.map +1 -1
- package/dist/cli/commands/fix.js +9 -4
- package/dist/cli/commands/fix.js.map +1 -1
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.js +279 -118
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrate.js +1 -1
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands/ping.js +20 -12
- package/dist/cli/commands/ping.js.map +1 -1
- package/dist/cli/commands/plan.js +20 -12
- package/dist/cli/commands/plan.js.map +1 -1
- package/dist/cli/commands/recover.js +1 -1
- package/dist/cli/commands/recover.js.map +1 -1
- package/dist/cli/commands/restart.js +6 -5
- package/dist/cli/commands/restart.js.map +1 -1
- package/dist/cli/commands/run.js +26 -15
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/start.js +11 -7
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/tool.d.ts +19 -0
- package/dist/cli/commands/tool.js +320 -0
- package/dist/cli/commands/tool.js.map +1 -0
- package/dist/cli/commands/verify.js +1 -1
- package/dist/cli/commands/verify.js.map +1 -1
- package/dist/cli/common.d.ts +7 -1
- package/dist/cli/common.js +37 -20
- package/dist/cli/common.js.map +1 -1
- package/dist/cli/compatibility.d.ts +8 -0
- package/dist/cli/compatibility.js +32 -0
- package/dist/cli/compatibility.js.map +1 -0
- package/dist/cli/parser.js +7 -3
- package/dist/cli/parser.js.map +1 -1
- package/dist/cli/registry.js +182 -7
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli/service-view.js +1 -1
- package/dist/cli/service-view.js.map +1 -1
- package/dist/cli/sources.js +2 -0
- package/dist/cli/sources.js.map +1 -1
- package/dist/cli/spec.js +8 -6
- package/dist/cli/spec.js.map +1 -1
- package/dist/common/naming.d.ts +53 -0
- package/dist/common/naming.js +90 -0
- package/dist/common/naming.js.map +1 -0
- package/dist/engine/agenda.d.ts +70 -9
- package/dist/engine/agenda.js +294 -61
- package/dist/engine/agenda.js.map +1 -1
- package/dist/engine/executor.d.ts +5 -0
- package/dist/engine/executor.js +24 -11
- package/dist/engine/executor.js.map +1 -1
- package/dist/engine/harnesses.d.ts +82 -18
- package/dist/engine/harnesses.js +191 -39
- package/dist/engine/harnesses.js.map +1 -1
- package/dist/engine/preview.js +2 -2
- package/dist/engine/preview.js.map +1 -1
- package/dist/engine/routing.d.ts +10 -0
- package/dist/engine/routing.js +67 -28
- package/dist/engine/routing.js.map +1 -1
- package/dist/engine/snapshot.d.ts +107 -7
- package/dist/engine/state.d.ts +4 -2
- package/dist/engine/state.js.map +1 -1
- package/dist/engine/sync.d.ts +96 -0
- package/dist/engine/sync.js +199 -0
- package/dist/engine/sync.js.map +1 -0
- package/dist/engine/team.d.ts +1 -0
- package/dist/engine/team.js +12 -8
- package/dist/engine/team.js.map +1 -1
- package/dist/expert/_main.md +96 -67
- package/dist/expert/agents/_index.md +10 -7
- package/dist/expert/agents/core-agents.md +4 -2
- package/dist/expert/agents/fixer.md +15 -12
- package/dist/expert/cli/acceptance.md +96 -78
- package/dist/expert/cli/api.md +26 -14
- package/dist/expert/cli/autonomous-operation.md +17 -19
- package/dist/expert/cli/browser.md +24 -24
- package/dist/expert/cli/checks.md +18 -10
- package/dist/expert/cli/cli.md +48 -41
- package/dist/expert/cli/configuration-migrations.md +43 -0
- package/dist/expert/cli/configuration.md +24 -14
- package/dist/expert/cli/controls.md +23 -11
- package/dist/expert/cli/dashboard-details.md +28 -10
- package/dist/expert/cli/dashboard-layout.md +15 -15
- package/dist/expert/cli/dashboard.md +8 -10
- package/dist/expert/cli/environment.md +33 -0
- package/dist/expert/cli/execution.md +10 -8
- package/dist/expert/cli/git-persistence.md +27 -21
- package/dist/expert/cli/git-preflight.md +8 -4
- package/dist/expert/cli/git-synchronization.md +32 -6
- package/dist/expert/cli/harness-plugins.md +12 -12
- package/dist/expert/cli/harnesses.md +22 -16
- package/dist/expert/cli/initialization-ui.md +14 -14
- package/dist/expert/cli/initialization.md +31 -21
- package/dist/expert/cli/installation.md +6 -0
- package/dist/expert/cli/isolation.md +5 -3
- package/dist/expert/cli/releasing.md +17 -3
- package/dist/expert/cli/retries.md +10 -8
- package/dist/expert/cli/run.md +7 -5
- package/dist/expert/cli/start.md +21 -15
- package/dist/expert/cli/status-view.md +15 -17
- package/dist/expert/cli/synchronization-setup.md +24 -0
- package/dist/expert/cli/terminal.md +14 -16
- package/dist/expert/cli/tool-management.md +60 -0
- package/dist/expert/cli/tool-runtime.md +35 -0
- package/dist/expert/cli/tools-migration.md +50 -0
- package/dist/expert/cli/tools.md +73 -0
- package/dist/expert/cli/trace-configuration.md +9 -11
- package/dist/expert/cli/updates.md +7 -7
- package/dist/expert/cli/windows-installer.md +67 -0
- package/dist/expert/cli/workspace.md +2 -0
- package/dist/expert/coding-standards/_index.md +11 -0
- package/dist/expert/coding-standards/naming.md +30 -0
- package/dist/expert/coding-standards/shared-logic.md +27 -0
- package/dist/expert/concepts/automatic-repair.md +33 -0
- package/dist/expert/concepts/engine-and-project.md +7 -5
- package/dist/expert/dictionary.md +18 -18
- package/dist/harness/plugins/claude-code/index.js +4 -2
- package/dist/harness/plugins/claude-code/index.js.map +1 -1
- package/dist/harness/plugins/cline/index.js +1 -0
- package/dist/harness/plugins/cline/index.js.map +1 -1
- package/dist/harness/plugins/gemini/index.js +2 -0
- package/dist/harness/plugins/gemini/index.js.map +1 -1
- package/dist/harness/plugins/github-copilot/index.js +1 -0
- package/dist/harness/plugins/github-copilot/index.js.map +1 -1
- package/dist/harness/plugins/openai-codex/index.js +13 -9
- package/dist/harness/plugins/openai-codex/index.js.map +1 -1
- package/dist/harness/plugins/opencode/index.js +1 -0
- package/dist/harness/plugins/opencode/index.js.map +1 -1
- package/dist/harness/plugins/qwen-code/index.js +1 -0
- package/dist/harness/plugins/qwen-code/index.js.map +1 -1
- package/dist/harness/types.d.ts +16 -5
- package/dist/project/config.d.ts +135 -48
- package/dist/project/config.js +331 -119
- package/dist/project/config.js.map +1 -1
- package/dist/project/configuration.d.ts +43 -0
- package/dist/project/configuration.js +165 -0
- package/dist/project/configuration.js.map +1 -0
- package/dist/project/defaults.d.ts +4 -2
- package/dist/project/defaults.js +10 -5
- package/dist/project/defaults.js.map +1 -1
- package/dist/project/git.d.ts +28 -0
- package/dist/project/git.js +81 -0
- package/dist/project/git.js.map +1 -1
- package/dist/project/migrations.d.ts +96 -0
- package/dist/project/migrations.js +301 -0
- package/dist/project/migrations.js.map +1 -0
- package/dist/project/publication.d.ts +57 -0
- package/dist/project/publication.js +117 -0
- package/dist/project/publication.js.map +1 -0
- package/dist/project/readiness.d.ts +14 -5
- package/dist/project/readiness.js +23 -22
- package/dist/project/readiness.js.map +1 -1
- package/dist/project/setup.js +2 -0
- package/dist/project/setup.js.map +1 -1
- package/dist/service/actions.d.ts +12 -4
- package/dist/service/actions.js +32 -2
- package/dist/service/actions.js.map +1 -1
- package/dist/service/control.d.ts +1 -1
- package/dist/service/control.js +1 -1
- package/dist/service/host.d.ts +5 -2
- package/dist/service/host.js +26 -16
- package/dist/service/host.js.map +1 -1
- package/dist/service/http.js +7 -2
- package/dist/service/http.js.map +1 -1
- package/dist/service/launcher.d.ts +2 -1
- package/dist/service/launcher.js +1 -0
- package/dist/service/launcher.js.map +1 -1
- package/dist/service/records.d.ts +3 -0
- package/dist/service/records.js +1 -1
- package/dist/service/records.js.map +1 -1
- package/dist/service/spa/app.css +39 -0
- package/dist/service/spa/app.js +242 -39
- package/dist/service/worker.js +1 -0
- package/dist/service/worker.js.map +1 -1
- package/dist/tools/environment.d.ts +122 -0
- package/dist/tools/environment.js +199 -0
- package/dist/tools/environment.js.map +1 -0
- package/dist/tools/model.d.ts +180 -0
- package/dist/tools/model.js +111 -0
- package/dist/tools/model.js.map +1 -0
- package/dist/tools/normalize.d.ts +51 -0
- package/dist/tools/normalize.js +321 -0
- package/dist/tools/normalize.js.map +1 -0
- package/dist/tools/operations.d.ts +198 -0
- package/dist/tools/operations.js +378 -0
- package/dist/tools/operations.js.map +1 -0
- package/dist/tools/readiness.d.ts +72 -0
- package/dist/tools/readiness.js +160 -0
- package/dist/tools/readiness.js.map +1 -0
- package/dist/ui/dashboard.d.ts +3 -0
- package/dist/ui/dashboard.js +173 -2
- package/dist/ui/dashboard.js.map +1 -1
- package/dist/ui/details.d.ts +35 -1
- package/dist/ui/details.js +103 -2
- package/dist/ui/details.js.map +1 -1
- package/dist/ui/secret.d.ts +8 -0
- package/dist/ui/secret.js +41 -0
- package/dist/ui/secret.js.map +1 -0
- package/dist/ui/status-view.d.ts +1 -0
- package/dist/ui/status-view.js +33 -8
- package/dist/ui/status-view.js.map +1 -1
- package/dist/ui/tool-forms.d.ts +24 -0
- package/dist/ui/tool-forms.js +133 -0
- package/dist/ui/tool-forms.js.map +1 -0
- package/package.json +1 -1
package/dist/browser/browser.js
CHANGED
|
@@ -6,10 +6,11 @@ import { PtbkError, errorMessage } from '../common/errors.js';
|
|
|
6
6
|
import { ensureDirectory, pathExists } from '../common/fs.js';
|
|
7
7
|
import { shortHash, shortId } from '../common/ids.js';
|
|
8
8
|
import { isProcessAlive, terminateProcessTree, which } from '../common/process.js';
|
|
9
|
-
import {
|
|
9
|
+
import { MIGRATED_BROWSER_NAME } from '../project/migrations.js';
|
|
10
|
+
import { describeProfileSource } from '../tools/model.js';
|
|
10
11
|
import { createRedactor } from '../common/redact.js';
|
|
11
12
|
import { promptbookAppData } from '../common/app-data.js';
|
|
12
|
-
import { BrowserRecorder
|
|
13
|
+
import { BrowserRecorder } from './recording.js';
|
|
13
14
|
/**
|
|
14
15
|
* Maximal page text returned to an agent.
|
|
15
16
|
*/
|
|
@@ -27,45 +28,39 @@ const LAUNCH_ATTEMPTS = 3;
|
|
|
27
28
|
*/
|
|
28
29
|
const LAUNCH_RETRY_MS = 3000;
|
|
29
30
|
/**
|
|
30
|
-
* Candidate
|
|
31
|
+
* Candidate executables of each browser adapter per platform (in preference order).
|
|
31
32
|
*/
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
function browserCandidates(variant) {
|
|
34
|
+
const chrome = { mac: ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'], win: [join('Google', 'Chrome', 'Application', 'chrome.exe')], names: ['google-chrome', 'google-chrome-stable', 'chrome'] };
|
|
35
|
+
const chromium = { mac: ['/Applications/Chromium.app/Contents/MacOS/Chromium'], win: [join('Chromium', 'Application', 'chrome.exe')], names: ['chromium', 'chromium-browser'] };
|
|
36
|
+
const edge = { mac: ['/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'], win: [join('Microsoft', 'Edge', 'Application', 'msedge.exe')], names: ['microsoft-edge', 'microsoft-edge-stable', 'msedge'] };
|
|
37
|
+
const brave = { mac: ['/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'], win: [], names: [] };
|
|
38
|
+
// CHROME prefers Google Chrome and accepts another installed Chrome-compatible browser when it is absent.
|
|
39
|
+
const groups = variant === 'CHROMIUM' ? [chromium] : variant === 'EDGE' ? [edge] : [chrome, chromium, edge, brave];
|
|
40
|
+
const roots = [process.env['PROGRAMFILES'], process.env['PROGRAMFILES(X86)'], process.env.LOCALAPPDATA].filter(Boolean);
|
|
41
|
+
const paths = groups.flatMap((group) => (process.platform === 'darwin' ? group.mac : process.platform === 'win32' ? roots.flatMap((root) => group.win.map((relative) => join(root, relative))) : []));
|
|
42
|
+
return { paths, names: groups.flatMap((group) => group.names) };
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
43
|
-
* Locates a
|
|
45
|
+
* Locates the executable of a browser tool (its explicit executable, `CHROME_PATH`, standard locations, PATH).
|
|
44
46
|
*
|
|
45
47
|
* @private project browser
|
|
46
48
|
*/
|
|
47
|
-
export async function findBrowserExecutable(
|
|
48
|
-
const configured =
|
|
49
|
+
export async function findBrowserExecutable(tool, env = process.env) {
|
|
50
|
+
const configured = tool.executable ?? env.CHROME_PATH;
|
|
49
51
|
if (configured)
|
|
50
52
|
return (await pathExists(configured)) ? configured : await which(configured, env);
|
|
51
|
-
|
|
53
|
+
const candidates = browserCandidates(tool.variant);
|
|
54
|
+
for (const candidate of candidates.paths)
|
|
52
55
|
if (await pathExists(candidate))
|
|
53
56
|
return candidate;
|
|
54
|
-
for (const name of
|
|
57
|
+
for (const name of candidates.names) {
|
|
55
58
|
const found = await which(name, env);
|
|
56
59
|
if (found)
|
|
57
60
|
return found;
|
|
58
61
|
}
|
|
59
62
|
return undefined;
|
|
60
63
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Classifies a `browser.profile` value: `global`, `isolated`, `user`, or a custom profile path.
|
|
63
|
-
*
|
|
64
|
-
* @private project browser
|
|
65
|
-
*/
|
|
66
|
-
export function browserProfileSource(value) {
|
|
67
|
-
return value === 'global' || value === 'isolated' || value === 'user' ? value : 'custom';
|
|
68
|
-
}
|
|
69
64
|
export { promptbookAppData };
|
|
70
65
|
/**
|
|
71
66
|
* The dedicated Promptbook-managed browser profile shared by this user's projects (not the user's own Chrome profile).
|
|
@@ -88,40 +83,30 @@ export function userChromeProfile(env = process.env) {
|
|
|
88
83
|
return join(homedir(), '.config', 'google-chrome');
|
|
89
84
|
}
|
|
90
85
|
/**
|
|
91
|
-
* Resolves the profile directory of
|
|
92
|
-
* `.ptbk
|
|
86
|
+
* Resolves the profile directory of a browser tool at runtime: the managed global profile (per OS user), an isolated
|
|
87
|
+
* project profile in `.ptbk/` (separate per instance), the user's Chrome profile, or a custom path (relative paths
|
|
88
|
+
* resolve against the project).
|
|
93
89
|
*
|
|
94
90
|
* @private project browser
|
|
95
91
|
*/
|
|
96
|
-
export function browserProfileDirectory(project,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return project.runtime.browserProfile;
|
|
103
|
-
case '
|
|
104
|
-
return userChromeProfile(env);
|
|
105
|
-
case '
|
|
106
|
-
return resolve(project.root, profile);
|
|
92
|
+
export function browserProfileDirectory(project, tool, env = process.env) {
|
|
93
|
+
switch (tool.profile.source) {
|
|
94
|
+
case 'MANAGED_GLOBAL':
|
|
95
|
+
return tool.profile.path ? resolve(promptbookAppData(env), tool.profile.path) : globalBrowserProfile(env);
|
|
96
|
+
case 'PROJECT':
|
|
97
|
+
// The first (migrated) browser keeps the historical location so existing sessions survive.
|
|
98
|
+
return tool.profile.path ? resolve(project.root, tool.profile.path) : tool.name === MIGRATED_BROWSER_NAME ? project.runtime.browserProfile : join(project.runtime.root, 'browser-profiles', tool.name);
|
|
99
|
+
case 'USER_EXISTING':
|
|
100
|
+
return tool.profile.path ? resolve(tool.profile.path) : userChromeProfile(env);
|
|
101
|
+
case 'CUSTOM':
|
|
102
|
+
return resolve(project.root, tool.profile.path ?? '.');
|
|
107
103
|
}
|
|
108
104
|
}
|
|
109
105
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* @private project browser
|
|
106
|
+
* Whether a profile source is shared beyond this project (claimed per profile across projects, released after tasks).
|
|
113
107
|
*/
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
case 'global':
|
|
117
|
-
return 'dedicated Promptbook profile shared by your projects';
|
|
118
|
-
case 'isolated':
|
|
119
|
-
return 'isolated project profile';
|
|
120
|
-
case 'user':
|
|
121
|
-
return 'your Chrome profile';
|
|
122
|
-
case 'custom':
|
|
123
|
-
return `custom profile ${value}`;
|
|
124
|
-
}
|
|
108
|
+
function isSharedSource(source) {
|
|
109
|
+
return source !== 'PROJECT';
|
|
125
110
|
}
|
|
126
111
|
/**
|
|
127
112
|
* Cross-project ownership of a shared profile: one project's automation at a time (`<app data>/locks/`).
|
|
@@ -258,13 +243,16 @@ class CdpConnection {
|
|
|
258
243
|
}
|
|
259
244
|
}
|
|
260
245
|
/**
|
|
261
|
-
*
|
|
246
|
+
* One named browser tool of the project: one profile and one automation owner at a time, shared by the project's
|
|
247
|
+
* agents. A launched process keeps the configuration it was acquired with (retained use) until the task that uses
|
|
248
|
+
* it releases it; later selections see the new saved configuration.
|
|
262
249
|
*
|
|
263
250
|
* @private project browser
|
|
264
251
|
*/
|
|
265
252
|
export class ProjectBrowser {
|
|
266
253
|
project;
|
|
267
|
-
|
|
254
|
+
name;
|
|
255
|
+
target;
|
|
268
256
|
env;
|
|
269
257
|
onChange;
|
|
270
258
|
child;
|
|
@@ -273,6 +261,8 @@ export class ProjectBrowser {
|
|
|
273
261
|
connection;
|
|
274
262
|
sessionId;
|
|
275
263
|
note;
|
|
264
|
+
/** Configuration the running process was launched with (retained while a task uses it). */
|
|
265
|
+
launchedWith;
|
|
276
266
|
/** A launch in progress (concurrent automatic/manual starts share it: never two owners). */
|
|
277
267
|
launching;
|
|
278
268
|
/** The current task used the browser (a restart waits for the task boundary). */
|
|
@@ -283,15 +273,34 @@ export class ProjectBrowser {
|
|
|
283
273
|
health = { state: 'not-started', ownership: { state: 'unknown' }, recovery: { attempts: 0, state: 'idle' } };
|
|
284
274
|
/** Task-linked recording of browser activity (`traces/<task-file-basename>/video/`). */
|
|
285
275
|
recorder;
|
|
286
|
-
constructor(project,
|
|
276
|
+
constructor(project,
|
|
277
|
+
/** Name of the browser tool this instance serves. */
|
|
278
|
+
name, target, env,
|
|
287
279
|
/** Publishes health changes to every view. */
|
|
288
|
-
onChange = () => undefined) {
|
|
280
|
+
onChange = () => undefined, recordingEnabled = () => true) {
|
|
289
281
|
this.project = project;
|
|
290
|
-
this.
|
|
282
|
+
this.name = name;
|
|
283
|
+
this.target = target;
|
|
291
284
|
this.env = env;
|
|
292
285
|
this.onChange = onChange;
|
|
293
|
-
this.recorder = new BrowserRecorder(project,
|
|
294
|
-
|
|
286
|
+
this.recorder = new BrowserRecorder(project, recordingEnabled, env);
|
|
287
|
+
}
|
|
288
|
+
/** Configuration of the next use: the retained one while a task holds the process, otherwise the saved one. */
|
|
289
|
+
config() {
|
|
290
|
+
if (this.child && this.usedByTask && this.launchedWith)
|
|
291
|
+
return this.launchedWith;
|
|
292
|
+
return this.target()?.tool;
|
|
293
|
+
}
|
|
294
|
+
/** Whether a task currently holds this browser. */
|
|
295
|
+
get inUse() {
|
|
296
|
+
return this.usedByTask && Boolean(this.child);
|
|
297
|
+
}
|
|
298
|
+
/** Whether the running process still uses a configuration that was changed, disabled or removed meanwhile. */
|
|
299
|
+
get retained() {
|
|
300
|
+
if (!this.child || !this.launchedWith)
|
|
301
|
+
return false;
|
|
302
|
+
const saved = this.target()?.tool;
|
|
303
|
+
return !saved || !saved.isEnabled || JSON.stringify(saved) !== JSON.stringify(this.launchedWith);
|
|
295
304
|
}
|
|
296
305
|
/** Updates the observed health and notifies the views. */
|
|
297
306
|
setHealth(changes) {
|
|
@@ -309,19 +318,30 @@ export class ProjectBrowser {
|
|
|
309
318
|
* ownership, last failure and recovery. A profile path is diagnostic data, never permission to read its contents.
|
|
310
319
|
*/
|
|
311
320
|
inspect() {
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
const
|
|
317
|
-
const
|
|
321
|
+
const target = this.target();
|
|
322
|
+
const saved = target?.tool;
|
|
323
|
+
const effective = this.config() ?? saved;
|
|
324
|
+
const profilePath = effective ? browserProfileDirectory(this.project, effective, this.env) : '';
|
|
325
|
+
const savedPath = saved ? browserProfileDirectory(this.project, saved, this.env) : undefined;
|
|
326
|
+
const retained = this.retained;
|
|
318
327
|
return {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
328
|
+
name: this.name,
|
|
329
|
+
variant: effective?.variant ?? 'CHROME',
|
|
330
|
+
isEnabled: saved?.isEnabled ?? false,
|
|
331
|
+
source: effective?.profile.source ?? 'MANAGED_GLOBAL',
|
|
332
|
+
sourceLabel: effective ? describeProfileSource(effective.profile.source, effective.profile.path) : 'not configured',
|
|
333
|
+
profilePath,
|
|
334
|
+
savedProfile: target?.overridden ? 'overridden for this process (command-line flags); the saved tool stays in ptbk.json' : undefined,
|
|
335
|
+
pending: retained
|
|
336
|
+
? saved
|
|
337
|
+
? `The current task keeps using the configuration it acquired; the saved change${savedPath && savedPath !== profilePath ? ` (profile ${savedPath})` : ''} applies after it releases the browser.`
|
|
338
|
+
: 'This tool was disabled or removed; the current task finishes with it, no new selection uses it.'
|
|
339
|
+
: this.pendingRestart
|
|
340
|
+
? 'A browser restart waits for the current task to release the browser.'
|
|
341
|
+
: undefined,
|
|
342
|
+
retained: retained || undefined,
|
|
343
|
+
inUse: this.inUse || undefined,
|
|
344
|
+
mode: { requested: effective?.isHeadless === false ? 'headful' : 'headless', actual: this.child ? this.health.actualMode : undefined, note: this.note },
|
|
325
345
|
executable: this.health.executable,
|
|
326
346
|
state: this.health.state,
|
|
327
347
|
pid: this.child?.pid,
|
|
@@ -342,14 +362,16 @@ export class ProjectBrowser {
|
|
|
342
362
|
* connection when the browser runs.
|
|
343
363
|
*/
|
|
344
364
|
async recheck() {
|
|
345
|
-
const
|
|
365
|
+
const tool = this.config();
|
|
366
|
+
if (!tool)
|
|
367
|
+
return this.acted('recheck', 'failed', `The browser tool "${this.name}" is no longer configured.`);
|
|
346
368
|
const findings = [];
|
|
347
|
-
const executable = await findBrowserExecutable(
|
|
348
|
-
findings.push(executable ? `browser ${executable}` :
|
|
349
|
-
const profile = browserProfileDirectory(this.project,
|
|
350
|
-
const source =
|
|
369
|
+
const executable = await findBrowserExecutable(tool, this.env);
|
|
370
|
+
findings.push(executable ? `browser ${executable}` : `no ${tool.variant === 'CHROME' ? 'Chrome-compatible' : tool.variant} browser installed`);
|
|
371
|
+
const profile = browserProfileDirectory(this.project, tool, this.env);
|
|
372
|
+
const source = tool.profile.source;
|
|
351
373
|
const exists = await pathExists(profile);
|
|
352
|
-
findings.push(exists ? `profile ${profile}` : source === '
|
|
374
|
+
findings.push(exists ? `profile ${profile}` : source === 'USER_EXISTING' || source === 'CUSTOM' ? `profile ${profile} does not exist` : `profile ${profile} is created on first use`);
|
|
353
375
|
const holder = this.child ? undefined : await profileOwner(profile);
|
|
354
376
|
const ownership = this.child ? { state: 'owned' } : holder ? { state: 'busy', holder: `browser PID ${holder}` } : { state: 'free' };
|
|
355
377
|
if (holder)
|
|
@@ -370,13 +392,13 @@ export class ProjectBrowser {
|
|
|
370
392
|
else if (state === 'running')
|
|
371
393
|
state = 'stopped';
|
|
372
394
|
this.setHealth({ state, executable: executable ?? undefined, ownership });
|
|
373
|
-
const healthy = Boolean(executable) && (exists || source === '
|
|
395
|
+
const healthy = Boolean(executable) && (exists || source === 'MANAGED_GLOBAL' || source === 'PROJECT') && !holder && state !== 'failed';
|
|
374
396
|
return this.acted('recheck', healthy ? 'succeeded' : 'failed', `${healthy ? 'Browser diagnostics OK' : 'Browser problem found'}: ${findings.join('; ')}.`);
|
|
375
397
|
}
|
|
376
398
|
/** Retries browser startup now (sharing any launch already in progress); a shared profile is released again when idle. */
|
|
377
399
|
async retry() {
|
|
378
400
|
if (this.connection && this.sessionId)
|
|
379
|
-
return this.acted('retry', 'succeeded',
|
|
401
|
+
return this.acted('retry', 'succeeded', `The browser "${this.name}" is already running.`);
|
|
380
402
|
this.setHealth({ recovery: { ...this.health.recovery, attempts: 0, state: 'retrying' } });
|
|
381
403
|
try {
|
|
382
404
|
await this.ensure();
|
|
@@ -386,7 +408,7 @@ export class ProjectBrowser {
|
|
|
386
408
|
}
|
|
387
409
|
if (!this.usedByTask)
|
|
388
410
|
this.releaseShared();
|
|
389
|
-
return this.acted('retry', 'succeeded', this.child ?
|
|
411
|
+
return this.acted('retry', 'succeeded', this.child ? `The browser "${this.name}" started.` : `The browser "${this.name}" started successfully and was released again (shared profile, no task needs it now).`);
|
|
390
412
|
}
|
|
391
413
|
/**
|
|
392
414
|
* Restarts only this project's own browser process, preserving the selected profile and its sessions. While a task
|
|
@@ -404,22 +426,28 @@ export class ProjectBrowser {
|
|
|
404
426
|
this.pendingRestart = false;
|
|
405
427
|
const wasRunning = Boolean(this.child);
|
|
406
428
|
this.close();
|
|
407
|
-
const shared =
|
|
429
|
+
const shared = isSharedSource(this.config()?.profile.source ?? 'MANAGED_GLOBAL');
|
|
408
430
|
if (shared)
|
|
409
|
-
return this.acted('restart', 'succeeded', wasRunning ?
|
|
431
|
+
return this.acted('restart', 'succeeded', wasRunning ? `Closed the browser "${this.name}"; the next use starts it again with the same profile.` : 'The browser was not running; the next use starts it with the same profile.');
|
|
410
432
|
try {
|
|
411
433
|
await this.ensure();
|
|
412
|
-
return this.acted('restart', 'succeeded',
|
|
434
|
+
return this.acted('restart', 'succeeded', `Restarted the browser "${this.name}" with the same profile.`);
|
|
413
435
|
}
|
|
414
436
|
catch (error) {
|
|
415
437
|
return this.acted('restart', 'failed', `The browser did not start again: ${errorMessage(error)}`);
|
|
416
438
|
}
|
|
417
439
|
}
|
|
418
|
-
/**
|
|
440
|
+
/**
|
|
441
|
+
* After a task: performs a deferred restart, ends a retained use of a changed/disabled/removed configuration and
|
|
442
|
+
* releases a shared profile.
|
|
443
|
+
*/
|
|
419
444
|
async taskFinished() {
|
|
445
|
+
const retained = this.retained;
|
|
420
446
|
this.usedByTask = false;
|
|
421
447
|
if (this.pendingRestart)
|
|
422
448
|
await this.restartNow().catch(() => undefined);
|
|
449
|
+
if (retained)
|
|
450
|
+
this.close();
|
|
423
451
|
this.releaseShared();
|
|
424
452
|
}
|
|
425
453
|
/** Handles a lost connection: stops only this project's browser process; the next use relaunches it. */
|
|
@@ -432,13 +460,9 @@ export class ProjectBrowser {
|
|
|
432
460
|
terminateProcessTree(this.child);
|
|
433
461
|
this.child = undefined;
|
|
434
462
|
void this.releaseProfile();
|
|
435
|
-
this.recordFailure(new PtbkError(
|
|
463
|
+
this.recordFailure(new PtbkError(`The connection to the browser "${this.name}" was lost.`, { kind: 'technical', reason }));
|
|
436
464
|
this.setHealth({ state: 'failed', recovery: { ...this.health.recovery, state: 'retrying' } });
|
|
437
465
|
}
|
|
438
|
-
/** Associates subsequent browser use with a task attempt for recording. */
|
|
439
|
-
async useFor(trace, attempt) {
|
|
440
|
-
await this.recorder.attach(trace, attempt);
|
|
441
|
-
}
|
|
442
466
|
/** Unsubscribes the page-transition listener of the current connection. */
|
|
443
467
|
navigationListener;
|
|
444
468
|
/** Ensures the browser runs and recording of the attached task has begun. */
|
|
@@ -501,21 +525,26 @@ export class ProjectBrowser {
|
|
|
501
525
|
}
|
|
502
526
|
/** One launch attempt of the configured browser and profile. */
|
|
503
527
|
async launchOnce() {
|
|
504
|
-
const
|
|
505
|
-
|
|
528
|
+
const tool = this.target()?.tool;
|
|
529
|
+
if (!tool)
|
|
530
|
+
throw new PtbkError(`The browser tool "${this.name}" is not configured.`, { kind: 'configuration', next: ['Add a browser with `ptbk tool add BROWSER …`.'] });
|
|
531
|
+
if (!tool.isEnabled)
|
|
532
|
+
throw new PtbkError(`The browser tool "${this.name}" is disabled.`, { kind: 'configuration', next: [`Enable it: ptbk tool enable ${this.name}`] });
|
|
533
|
+
const executable = await findBrowserExecutable(tool, this.env);
|
|
506
534
|
if (!executable)
|
|
507
|
-
throw new PtbkError(
|
|
508
|
-
const source =
|
|
509
|
-
const profile = browserProfileDirectory(this.project,
|
|
535
|
+
throw new PtbkError(`No ${tool.variant === 'CHROME' ? 'Chrome-compatible' : tool.variant} browser is installed for "${this.name}".`, { kind: 'setup', next: [`Install Google Chrome or Chromium, or set its executable: ptbk tool modify ${this.name} --executable <path>`] });
|
|
536
|
+
const source = tool.profile.source;
|
|
537
|
+
const profile = browserProfileDirectory(this.project, tool, this.env);
|
|
510
538
|
this.health = { ...this.health, executable, profilePath: profile };
|
|
511
|
-
if (source === '
|
|
539
|
+
if (source === 'USER_EXISTING' || source === 'CUSTOM') {
|
|
512
540
|
// An existing profile is reused as it is; it is never created, copied or migrated implicitly.
|
|
513
541
|
if (!(await pathExists(profile)))
|
|
514
|
-
throw new PtbkError(`The selected browser profile ${profile} does not exist.`, { kind: 'configuration', next: [
|
|
542
|
+
throw new PtbkError(`The selected browser profile ${profile} does not exist.`, { kind: 'configuration', next: [`Choose another profile: ptbk tool modify ${this.name} --profile-source MANAGED_GLOBAL|PROJECT|USER_EXISTING|CUSTOM [--profile-path <path>]`] });
|
|
515
543
|
}
|
|
516
544
|
else
|
|
517
545
|
await ensureDirectory(profile);
|
|
518
|
-
|
|
546
|
+
this.launchedWith = tool;
|
|
547
|
+
if (isSharedSource(source)) {
|
|
519
548
|
let claim = await claimProfile(profile, this.project, this.env);
|
|
520
549
|
for (let attempt = 0; 'owner' in claim && attempt < 10; attempt++) {
|
|
521
550
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -542,10 +571,10 @@ export class ProjectBrowser {
|
|
|
542
571
|
this.setHealth({ ownership: { state: 'busy', holder: `browser PID ${owner}` } });
|
|
543
572
|
throw new PtbkError(`The browser profile ${profile} is in use by another browser (PID ${owner}).`, {
|
|
544
573
|
kind: 'conflict',
|
|
545
|
-
next: [
|
|
574
|
+
next: [`Close that browser to hand the profile over, or choose another profile (ptbk tool modify ${this.name} --profile-source …).`],
|
|
546
575
|
});
|
|
547
576
|
}
|
|
548
|
-
let headless =
|
|
577
|
+
let headless = tool.isHeadless;
|
|
549
578
|
if (!headless && !hasDisplay(this.env)) {
|
|
550
579
|
headless = true;
|
|
551
580
|
this.note = 'No display is available on this host, so the browser runs headless (no window was shown).';
|
|
@@ -711,17 +740,18 @@ export class ProjectBrowser {
|
|
|
711
740
|
releaseShared() {
|
|
712
741
|
if (!this.child && !this.releaseClaim)
|
|
713
742
|
return;
|
|
714
|
-
if (
|
|
743
|
+
if (!isSharedSource(this.config()?.profile.source ?? 'MANAGED_GLOBAL'))
|
|
715
744
|
return;
|
|
716
745
|
this.close();
|
|
717
746
|
}
|
|
718
747
|
}
|
|
719
748
|
/**
|
|
720
|
-
* Browser tools exposed to agents through the ptbk MCP bridge.
|
|
749
|
+
* Browser tools exposed to agents through the ptbk MCP bridge. Every call resolves the live inventory: a browser
|
|
750
|
+
* added during the task is selectable at the next call; `browser` names a specific tool (account/profile).
|
|
721
751
|
*
|
|
722
752
|
* @private project browser
|
|
723
753
|
*/
|
|
724
|
-
export function browserTools(
|
|
754
|
+
export function browserTools(fleet, chats, agent, onUserChat, describe) {
|
|
725
755
|
const run = async (action) => {
|
|
726
756
|
try {
|
|
727
757
|
return { text: await action() };
|
|
@@ -730,50 +760,64 @@ export function browserTools(browser, chats, agent, onUserChat) {
|
|
|
730
760
|
return { text: errorMessage(error), isError: true };
|
|
731
761
|
}
|
|
732
762
|
};
|
|
763
|
+
const name = (input) => (typeof input.browser === 'string' && input.browser.trim() ? input.browser.trim() : undefined);
|
|
764
|
+
const browserProperty = { browser: { type: 'string', description: 'Name of the browser tool to use (see browser_list); defaults to the browser this task already uses, else the first enabled one.' } };
|
|
733
765
|
return [
|
|
766
|
+
{
|
|
767
|
+
name: 'browser_list',
|
|
768
|
+
description: "List the project's enabled browser tools (names, descriptions, profile kinds). Choose the one whose account/profile fits the task; never act from a different account merely because it is logged in.",
|
|
769
|
+
inputSchema: { type: 'object', properties: {} },
|
|
770
|
+
handler: () => run(async () => {
|
|
771
|
+
const browsers = describe();
|
|
772
|
+
if (!browsers.length)
|
|
773
|
+
return 'No browser tool is enabled for this project.';
|
|
774
|
+
return browsers.map((browser) => `- ${browser.name}${browser.description ? `: ${browser.description}` : ''} (${browser.profile}, ${browser.isHeadless ? 'headless' : 'visible window'})`).join('\n');
|
|
775
|
+
}),
|
|
776
|
+
},
|
|
734
777
|
{
|
|
735
778
|
name: 'browser_open',
|
|
736
|
-
description:
|
|
737
|
-
inputSchema: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] },
|
|
738
|
-
handler: (input) => run(() =>
|
|
779
|
+
description: 'Open a URL in a project browser (persistent logged-in sessions of its profile) and return the page text and interactive elements.',
|
|
780
|
+
inputSchema: { type: 'object', properties: { url: { type: 'string' }, ...browserProperty }, required: ['url'] },
|
|
781
|
+
handler: (input) => run(async () => (await fleet.acquire(name(input))).open(String(input.url ?? ''))),
|
|
739
782
|
},
|
|
740
783
|
{
|
|
741
784
|
name: 'browser_read',
|
|
742
785
|
description: 'Return the text and numbered interactive elements of the current page.',
|
|
743
|
-
inputSchema: { type: 'object', properties: {} },
|
|
744
|
-
handler: () => run(() =>
|
|
786
|
+
inputSchema: { type: 'object', properties: { ...browserProperty } },
|
|
787
|
+
handler: (input) => run(async () => (await fleet.acquire(name(input))).read()),
|
|
745
788
|
},
|
|
746
789
|
{
|
|
747
790
|
name: 'browser_click',
|
|
748
791
|
description: 'Click an interactive element by its number from browser_read.',
|
|
749
|
-
inputSchema: { type: 'object', properties: { ref: { type: 'number' } }, required: ['ref'] },
|
|
750
|
-
handler: (input) => run(() =>
|
|
792
|
+
inputSchema: { type: 'object', properties: { ref: { type: 'number' }, ...browserProperty }, required: ['ref'] },
|
|
793
|
+
handler: (input) => run(async () => (await fleet.acquire(name(input))).click(Number(input.ref))),
|
|
751
794
|
},
|
|
752
795
|
{
|
|
753
796
|
name: 'browser_type',
|
|
754
797
|
description: 'Type text into an input by its number from browser_read; optionally submit the form.',
|
|
755
|
-
inputSchema: { type: 'object', properties: { ref: { type: 'number' }, text: { type: 'string' }, submit: { type: 'boolean' } }, required: ['ref', 'text'] },
|
|
756
|
-
handler: (input) => run(() =>
|
|
798
|
+
inputSchema: { type: 'object', properties: { ref: { type: 'number' }, text: { type: 'string' }, submit: { type: 'boolean' }, ...browserProperty }, required: ['ref', 'text'] },
|
|
799
|
+
handler: (input) => run(async () => (await fleet.acquire(name(input))).type(Number(input.ref), String(input.text ?? ''), input.submit === true)),
|
|
757
800
|
},
|
|
758
801
|
{
|
|
759
802
|
name: 'browser_screenshot',
|
|
760
803
|
description: 'Save a screenshot of the current page and return its file path.',
|
|
761
|
-
inputSchema: { type: 'object', properties: {} },
|
|
762
|
-
handler: () => run(() =>
|
|
804
|
+
inputSchema: { type: 'object', properties: { ...browserProperty } },
|
|
805
|
+
handler: (input) => run(async () => (await fleet.acquire(name(input))).screenshot()),
|
|
763
806
|
},
|
|
764
807
|
{
|
|
765
808
|
name: 'browser_request_user_action',
|
|
766
|
-
description: 'Only when a website genuinely requires a human-only step (sign-in, two-factor authentication, consent) that the existing session cannot satisfy: ask the human to act in the
|
|
767
|
-
inputSchema: { type: 'object', properties: { reason: { type: 'string' } }, required: ['reason'] },
|
|
809
|
+
description: 'Only when a website genuinely requires a human-only step (sign-in, two-factor authentication, consent) that the existing session cannot satisfy: ask the human to act in the project browser. Then end your turn with status waiting-for-user.',
|
|
810
|
+
inputSchema: { type: 'object', properties: { reason: { type: 'string' }, ...browserProperty }, required: ['reason'] },
|
|
768
811
|
handler: (input) => run(async () => {
|
|
769
812
|
if (!chats)
|
|
770
813
|
throw new PtbkError('The user is not available in this run; report blocked with the reason instead.', { kind: 'unsupported' });
|
|
814
|
+
const browser = await fleet.acquire(name(input));
|
|
771
815
|
const note = await browser.bringToFront();
|
|
772
816
|
const chat = await chats.askUser({
|
|
773
817
|
agent,
|
|
774
|
-
reason: String(input.reason ??
|
|
775
|
-
attempted:
|
|
776
|
-
question: `Please complete the action in the
|
|
818
|
+
reason: String(input.reason ?? `A website needs your action in the browser "${browser.name}".`),
|
|
819
|
+
attempted: `I used the existing session of the project browser "${browser.name}"; the website still requires this human-only step.`,
|
|
820
|
+
question: `Please complete the action in the browser "${browser.name}"${note ? ` (${note})` : ''}, then confirm here.`,
|
|
777
821
|
choices: ['Done', 'I cannot do it'],
|
|
778
822
|
});
|
|
779
823
|
onUserChat(chat);
|