janissary 0.5.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.
- package/LICENSE +75 -0
- package/README.md +28 -0
- package/agent-names.json +52 -0
- package/bin/janus.mjs +27 -0
- package/chrome-extension/_metadata/generated_indexed_rulesets/_ruleset1 +0 -0
- package/chrome-extension/content-script.js +62 -0
- package/chrome-extension/manifest.json +25 -0
- package/chrome-extension/rules.json +16 -0
- package/dist/acp/index.js +93 -0
- package/dist/acp/loop.js +75 -0
- package/dist/acp/manager.js +126 -0
- package/dist/acp/rate-limit.js +8 -0
- package/dist/acp/runner.js +18 -0
- package/dist/acp/tools.js +27 -0
- package/dist/acp-loop.js +75 -0
- package/dist/acp-manager.js +109 -0
- package/dist/acp-runner.js +18 -0
- package/dist/acp.js +91 -0
- package/dist/agent/commands.js +29 -0
- package/dist/agent/communication-manager.js +71 -0
- package/dist/agent/state.js +58 -0
- package/dist/agent-commands.js +29 -0
- package/dist/agent-communication-manager.js +71 -0
- package/dist/agent-state.js +58 -0
- package/dist/app-themes.js +13 -0
- package/dist/browser/command.js +104 -0
- package/dist/browser/index.js +124 -0
- package/dist/browser/tab-helpers.js +51 -0
- package/dist/browser/tab.js +99 -0
- package/dist/browser-command.js +104 -0
- package/dist/browser-tab-helpers.js +51 -0
- package/dist/browser-tab.js +99 -0
- package/dist/browser.js +124 -0
- package/dist/buffer.js +42 -0
- package/dist/bus.js +52 -0
- package/dist/capture/manager.js +48 -0
- package/dist/capture/router.js +18 -0
- package/dist/capture-manager.js +48 -0
- package/dist/capture-router.js +20 -0
- package/dist/chrome-extension-loader.js +62 -0
- package/dist/cli-args.js +80 -0
- package/dist/command-manager.js +126 -0
- package/dist/command-router.js +14 -0
- package/dist/commands/acp-reset.js +13 -0
- package/dist/commands/acp.js +5 -0
- package/dist/commands/agent.js +5 -0
- package/dist/commands/broadcast.js +22 -0
- package/dist/commands/browser.js +5 -0
- package/dist/commands/clear.js +5 -0
- package/dist/commands/close.js +42 -0
- package/dist/commands/connection.js +5 -0
- package/dist/commands/db.js +5 -0
- package/dist/commands/edit.js +18 -0
- package/dist/commands/files.js +13 -0
- package/dist/commands/hist.js +7 -0
- package/dist/commands/index.js +62 -0
- package/dist/commands/monitor.js +61 -0
- package/dist/commands/msg.js +17 -0
- package/dist/commands/next.js +7 -0
- package/dist/commands/notifications.js +16 -0
- package/dist/commands/notify.js +20 -0
- package/dist/commands/open.js +41 -0
- package/dist/commands/profile.js +5 -0
- package/dist/commands/queue.js +37 -0
- package/dist/commands/quit.js +6 -0
- package/dist/commands/rename.js +12 -0
- package/dist/commands/resolve-target.js +12 -0
- package/dist/commands/schedule.js +74 -0
- package/dist/commands/search.js +26 -0
- package/dist/commands/send.js +51 -0
- package/dist/commands/state.js +9 -0
- package/dist/commands/syntax.js +36 -0
- package/dist/commands/tasks.js +7 -0
- package/dist/commands/theme.js +41 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands.js +70 -0
- package/dist/completion/browser.js +24 -0
- package/dist/completion/fs.js +26 -0
- package/dist/completion/handlers.js +81 -0
- package/dist/completion/helpers.js +43 -0
- package/dist/completion/index.js +43 -0
- package/dist/completion-browser.js +24 -0
- package/dist/completion-fs.js +26 -0
- package/dist/completion-handlers.js +69 -0
- package/dist/completion-helpers.js +43 -0
- package/dist/completion.js +42 -0
- package/dist/config.js +66 -0
- package/dist/connection/close.js +41 -0
- package/dist/connection/manager.js +93 -0
- package/dist/connection/parsing.js +28 -0
- package/dist/connection-close.js +41 -0
- package/dist/connection-manager.js +93 -0
- package/dist/connection-parsing.js +28 -0
- package/dist/connections.js +75 -0
- package/dist/controller-events.js +34 -0
- package/dist/controller-file-tree.js +36 -0
- package/dist/controller.js +201 -0
- package/dist/database/index.js +83 -0
- package/dist/database/manager.js +67 -0
- package/dist/database/parsing.js +54 -0
- package/dist/database/query.js +41 -0
- package/dist/database-manager.js +67 -0
- package/dist/database-parsing.js +54 -0
- package/dist/database-query.js +41 -0
- package/dist/database.js +83 -0
- package/dist/datetime.js +15 -0
- package/dist/editor/save.js +27 -0
- package/dist/editor/sync.js +10 -0
- package/dist/editor/watch-manager.js +84 -0
- package/dist/editor-save.js +24 -0
- package/dist/editor-watch-manager.js +84 -0
- package/dist/file-tree-args.js +36 -0
- package/dist/file-tree-git-refresh.js +33 -0
- package/dist/file-tree-manager.js +252 -0
- package/dist/file-tree-moves.js +26 -0
- package/dist/file-tree-open.js +56 -0
- package/dist/file-tree-rebuild.js +22 -0
- package/dist/file-tree-watch.js +27 -0
- package/dist/file-tree.js +82 -0
- package/dist/git-status.js +64 -0
- package/dist/github-token.js +19 -0
- package/dist/global-history.js +48 -0
- package/dist/harness/auto-approve.js +79 -0
- package/dist/harness/busy-status.js +91 -0
- package/dist/harness/capture-file.js +28 -0
- package/dist/harness/index.js +87 -0
- package/dist/harness/manager.js +182 -0
- package/dist/harness/models.js +8 -0
- package/dist/harness/recorder.js +89 -0
- package/dist/harness/recording-file.js +25 -0
- package/dist/harness/screen.js +79 -0
- package/dist/harness-capture-file.js +28 -0
- package/dist/harness-manager.js +112 -0
- package/dist/harness-models.js +8 -0
- package/dist/harness-screen.js +72 -0
- package/dist/harness.js +54 -0
- package/dist/index.js +163 -0
- package/dist/instance-lock.js +34 -0
- package/dist/interactive.js +33 -0
- package/dist/main.js +203 -0
- package/dist/managers.js +1 -0
- package/dist/message-handler.js +134 -0
- package/dist/messaging.js +47 -0
- package/dist/monitor/acp.js +24 -0
- package/dist/monitor/ask.js +31 -0
- package/dist/monitor/context.js +27 -0
- package/dist/monitor/editor-feed.js +49 -0
- package/dist/monitor/feed-diff.js +22 -0
- package/dist/monitor/feeds.js +19 -0
- package/dist/monitor/framing.js +29 -0
- package/dist/monitor/harness-feed.js +21 -0
- package/dist/monitor/info.js +29 -0
- package/dist/monitor/manager.js +221 -0
- package/dist/monitor/page-feed.js +21 -0
- package/dist/monitor/parsing.js +82 -0
- package/dist/monitor/reply.js +11 -0
- package/dist/monitor/session.js +36 -0
- package/dist/monitor/stop.js +33 -0
- package/dist/monitor/suggestion.js +12 -0
- package/dist/monitor/targets.js +69 -0
- package/dist/monitor/window.js +109 -0
- package/dist/monitor-acp.js +22 -0
- package/dist/monitor-info.js +18 -0
- package/dist/monitor-manager.js +233 -0
- package/dist/monitor-parsing.js +68 -0
- package/dist/monitor-session.js +19 -0
- package/dist/monitor-targets.js +37 -0
- package/dist/monitor-window.js +81 -0
- package/dist/notifications-tab.js +32 -0
- package/dist/notifications.js +87 -0
- package/dist/open-file-manager.js +100 -0
- package/dist/openers/editor.js +41 -0
- package/dist/openers/external-viewer.js +11 -0
- package/dist/openers/image.js +27 -0
- package/dist/openers/index.js +16 -0
- package/dist/openers/markdown.js +20 -0
- package/dist/openers/os-open.js +24 -0
- package/dist/openers/page.js +51 -0
- package/dist/openers/size.js +13 -0
- package/dist/openers/types.js +1 -0
- package/dist/page/sync.js +10 -0
- package/dist/paths.js +45 -0
- package/dist/persona-parsing.js +49 -0
- package/dist/personas.js +44 -0
- package/dist/profile/agent-opener.js +154 -0
- package/dist/profile/files.js +17 -0
- package/dist/profile/manager.js +72 -0
- package/dist/profile/monitors.js +23 -0
- package/dist/profile/notifications.js +11 -0
- package/dist/profile-agent-opener.js +136 -0
- package/dist/profile-manager.js +72 -0
- package/dist/profiles.js +143 -0
- package/dist/protocol.js +1 -0
- package/dist/pseudoterminal-manager.js +106 -0
- package/dist/pty.js +41 -0
- package/dist/recognizers/acp.js +38 -0
- package/dist/recognizers/analyze.js +49 -0
- package/dist/recognizers/bash.js +51 -0
- package/dist/recognizers/db.js +26 -0
- package/dist/recognizers/index.js +1 -0
- package/dist/recognizers/lexicon.js +11 -0
- package/dist/recognizers/types.js +5 -0
- package/dist/resolve.js +39 -0
- package/dist/route-choice.js +11 -0
- package/dist/sandbox/index.js +217 -0
- package/dist/sandbox/paths.js +178 -0
- package/dist/sandbox/profile.js +161 -0
- package/dist/sandbox-paths.js +159 -0
- package/dist/sandbox-profile.js +161 -0
- package/dist/sandbox.js +217 -0
- package/dist/schedule/display.js +19 -0
- package/dist/schedule/helpers.js +76 -0
- package/dist/schedule/index.js +76 -0
- package/dist/schedule/manager.js +101 -0
- package/dist/schedule/parsing.js +63 -0
- package/dist/schedule/time.js +34 -0
- package/dist/schedule-display.js +19 -0
- package/dist/schedule-helpers.js +76 -0
- package/dist/schedule-manager.js +98 -0
- package/dist/schedule-parsing.js +63 -0
- package/dist/schedule-time.js +34 -0
- package/dist/schedule.js +76 -0
- package/dist/search-matches.js +40 -0
- package/dist/security.js +44 -0
- package/dist/shell-manager.js +139 -0
- package/dist/shell.js +59 -0
- package/dist/ssh-manager.js +42 -0
- package/dist/ssh.js +39 -0
- package/dist/startup-errors.js +21 -0
- package/dist/state-event.js +14 -0
- package/dist/state-format.js +38 -0
- package/dist/syntax-themes.js +14 -0
- package/dist/tab/agent-state.js +19 -0
- package/dist/tab/cleanup.js +33 -0
- package/dist/tab/colors.js +43 -0
- package/dist/tab/creators.js +95 -0
- package/dist/tab/dock.js +35 -0
- package/dist/tab/focus-history.js +41 -0
- package/dist/tab/formatting-handlers.js +77 -0
- package/dist/tab/formatting.js +21 -0
- package/dist/tab/history.js +14 -0
- package/dist/tab/index.js +65 -0
- package/dist/tab/manager.js +329 -0
- package/dist/tab/openers.js +35 -0
- package/dist/tab/queue.js +33 -0
- package/dist/tab/rehydrate.js +15 -0
- package/dist/tab/reorder.js +15 -0
- package/dist/tab/transcript.js +15 -0
- package/dist/tab/utils.js +55 -0
- package/dist/tab/view.js +42 -0
- package/dist/tab-cleanup.js +27 -0
- package/dist/tab-colors.js +43 -0
- package/dist/tab-creators.js +85 -0
- package/dist/tab-formatting-handlers.js +77 -0
- package/dist/tab-formatting.js +21 -0
- package/dist/tab-manager.js +418 -0
- package/dist/tab-utils.js +55 -0
- package/dist/tab.js +57 -0
- package/dist/tasks.js +32 -0
- package/dist/transcript/logger.js +36 -0
- package/dist/transcript/store.js +66 -0
- package/dist/types.js +6 -0
- package/dist/user-agent.js +65 -0
- package/dist/word-wrapping.js +42 -0
- package/dist/workspace-manager.js +40 -0
- package/dist/workspace.js +120 -0
- package/help.md +97 -0
- package/package.json +138 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Parser for the `browser` command plus the two helpers the ACP tool loop needs
|
|
2
|
+
// (`extractBrowserCommand` / `BROWSER_PRIMER`), mirroring `parseConnectionCommand`
|
|
3
|
+
// (src/connections.ts) and the `db` equivalents in src/db.ts. Pure — no I/O. The host
|
|
4
|
+
// (src/cli.tsx) performs the actual Playwright actions against the tab's browser.
|
|
5
|
+
const USAGE = 'Usage: browser <open [name] [--headed]|list|use <id>|goto <url>|eval <js>|shot|content|close [id]|window close <id>>';
|
|
6
|
+
// First-position subcommand keywords, exported for tab completion.
|
|
7
|
+
export const BROWSER_SUBCOMMANDS = [
|
|
8
|
+
'open',
|
|
9
|
+
'list',
|
|
10
|
+
'use',
|
|
11
|
+
'goto',
|
|
12
|
+
'eval',
|
|
13
|
+
'shot',
|
|
14
|
+
'content',
|
|
15
|
+
'close',
|
|
16
|
+
'window',
|
|
17
|
+
];
|
|
18
|
+
/** Parse a `browser ...` command. Pure — performs no I/O. */
|
|
19
|
+
export function parseBrowserCommand(input) {
|
|
20
|
+
const rest = input.trim().replace(/^browser\b\s*/i, '').trim();
|
|
21
|
+
if (!rest)
|
|
22
|
+
return { error: USAGE };
|
|
23
|
+
const [actionRaw, ...tail] = rest.split(/\s+/);
|
|
24
|
+
const action = actionRaw.toLowerCase();
|
|
25
|
+
// The argument text after the action verb, with original spacing preserved.
|
|
26
|
+
const argument = rest.slice(actionRaw.length).trim();
|
|
27
|
+
switch (action) {
|
|
28
|
+
case 'open': {
|
|
29
|
+
const tokens = tail;
|
|
30
|
+
const isHeaded = tokens.some((t) => t === '--headed' || t === '-H');
|
|
31
|
+
const name = tokens.find((t) => !t.startsWith('-'));
|
|
32
|
+
return { action: 'open', name, headed: isHeaded };
|
|
33
|
+
}
|
|
34
|
+
case 'list': {
|
|
35
|
+
return { action: 'list' };
|
|
36
|
+
}
|
|
37
|
+
case 'use': {
|
|
38
|
+
if (!tail[0])
|
|
39
|
+
return { error: 'Usage: browser use <id>' };
|
|
40
|
+
return { action: 'use', id: tail[0] };
|
|
41
|
+
}
|
|
42
|
+
case 'goto': {
|
|
43
|
+
if (!argument)
|
|
44
|
+
return { error: 'Usage: browser goto <url>' };
|
|
45
|
+
return { action: 'goto', url: argument };
|
|
46
|
+
}
|
|
47
|
+
case 'eval': {
|
|
48
|
+
if (!argument)
|
|
49
|
+
return { error: 'Usage: browser eval <js>' };
|
|
50
|
+
return { action: 'eval', js: argument };
|
|
51
|
+
}
|
|
52
|
+
case 'shot': {
|
|
53
|
+
return { action: 'shot' };
|
|
54
|
+
}
|
|
55
|
+
case 'content': {
|
|
56
|
+
return { action: 'content' };
|
|
57
|
+
}
|
|
58
|
+
case 'close': {
|
|
59
|
+
// `browser close <id>` is an alias for `browser window close <id>`; bare
|
|
60
|
+
// `browser close` closes the current window.
|
|
61
|
+
if (tail[0])
|
|
62
|
+
return { action: 'closeWindow', id: tail[0] };
|
|
63
|
+
return { action: 'close' };
|
|
64
|
+
}
|
|
65
|
+
case 'window': {
|
|
66
|
+
if (tail[0]?.toLowerCase() !== 'close' || !tail[1]) {
|
|
67
|
+
return { error: 'Usage: browser window close <id>' };
|
|
68
|
+
}
|
|
69
|
+
return { action: 'closeWindow', id: tail[1] };
|
|
70
|
+
}
|
|
71
|
+
default: {
|
|
72
|
+
return { error: USAGE };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Pull a proposed `browser ...` command out of an agent reply, if present. Scans
|
|
78
|
+
* bottom-up (the primer asks for the command on the last line) and tolerates a
|
|
79
|
+
* surrounding code fence or a leading `$ `/`> ` prompt marker — same shape as
|
|
80
|
+
* `extractDbCommand`.
|
|
81
|
+
*/
|
|
82
|
+
export function extractBrowserCommand(text) {
|
|
83
|
+
const lines = text.split('\n');
|
|
84
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
85
|
+
const line = lines[index].replace(/^[\s`$>]+/, '').replace(/`+\s*$/, '').trim();
|
|
86
|
+
if (/^browser\s+(open|list|use|goto|eval|shot|content|close|window)\b/i.test(line))
|
|
87
|
+
return line;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// Primer injected into an ACP agent so it can drive a real browser through the autonomous
|
|
92
|
+
// tool loop. It exposes a deliberately simplified surface — window/headless/mode management
|
|
93
|
+
// is handled by the host, which auto-launches the tab's browser (headless) and auto-opens a
|
|
94
|
+
// window on first use.
|
|
95
|
+
export const BROWSER_PRIMER = [
|
|
96
|
+
'This host CLI can also drive a real web browser via `browser` commands. Syntax:',
|
|
97
|
+
' browser goto <url> # navigate to a URL (a browser/window opens automatically)',
|
|
98
|
+
' browser content # return the current page\'s rendered text',
|
|
99
|
+
' browser eval <js> # run JavaScript in the page and return the result',
|
|
100
|
+
'To read a web page, end your reply with exactly one `browser` command on its own final',
|
|
101
|
+
'line (no code fence, nothing after it). The host runs it and returns the output to you,',
|
|
102
|
+
'so you can issue further commands. When the task is done, reply with the final answer and',
|
|
103
|
+
'NO trailing command.',
|
|
104
|
+
].join('\n');
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdtempSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { chromium } from 'playwright';
|
|
6
|
+
import { randomBrowserProfile, acceptLanguage, DEFAULT_CHROME_MAJOR } from '../user-agent.js';
|
|
7
|
+
// Pull the major version out of a Playwright version string like "149.0.7827.55" so the
|
|
8
|
+
// generated UA claims the same Chrome version the engine actually reports via client hints.
|
|
9
|
+
function chromeMajor(version) {
|
|
10
|
+
const major = Number(version.split('.', 1)[0]);
|
|
11
|
+
return Number.isFinite(major) ? major : DEFAULT_CHROME_MAJOR;
|
|
12
|
+
}
|
|
13
|
+
// A headless/headed browser is owned per janissary tab: each tab launches its own
|
|
14
|
+
// Playwright process (so modes can differ between tabs) and holds one or more isolated
|
|
15
|
+
// windows. A "window" is a BrowserContext (its own cookies/storage) plus a single Page —
|
|
16
|
+
// the viewport. The Browser handle stays private to the TabBrowser; callers only ever see
|
|
17
|
+
// the BrowserWindow surface and the TabBrowser lifecycle methods.
|
|
18
|
+
// Cap on `content()` output so a large page does not blow up the prompt fed back to an
|
|
19
|
+
// ACP agent. Truncation is flagged in the returned text.
|
|
20
|
+
const CONTENT_LIMIT = 10_000;
|
|
21
|
+
function makeWindow(id, page) {
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
goto: async (url) => {
|
|
25
|
+
await page.goto(url, { waitUntil: 'load' });
|
|
26
|
+
const title = await page.title();
|
|
27
|
+
return `${title || '(untitled)'} — ${page.url()}`;
|
|
28
|
+
},
|
|
29
|
+
eval: async (js) => {
|
|
30
|
+
const result = await page.evaluate(js);
|
|
31
|
+
return result === undefined ? 'undefined' : JSON.stringify(result, undefined, 2);
|
|
32
|
+
},
|
|
33
|
+
shot: async () => {
|
|
34
|
+
const temporaryDirectory = mkdtempSync(path.join(tmpdir(), 'janissary-'));
|
|
35
|
+
const screenshotPath = path.join(temporaryDirectory, `${id}-${Date.now()}.png`);
|
|
36
|
+
await page.screenshot({ path: screenshotPath });
|
|
37
|
+
if (process.platform === 'darwin') {
|
|
38
|
+
// Open the screenshot detached so it never blocks the main process.
|
|
39
|
+
const child = spawn('open', ['-a', 'Preview', screenshotPath], { stdio: 'ignore', detached: true });
|
|
40
|
+
child.on('error', () => { });
|
|
41
|
+
child.unref();
|
|
42
|
+
}
|
|
43
|
+
return screenshotPath;
|
|
44
|
+
},
|
|
45
|
+
content: async () => {
|
|
46
|
+
const title = await page.title();
|
|
47
|
+
const body = await page.evaluate(() => document.body?.textContent ?? ''); // eslint-disable-line unicorn/no-optional-chaining-on-undeclared-variable
|
|
48
|
+
const text = `${title}\n\n${body}`.trim();
|
|
49
|
+
return text.length > CONTENT_LIMIT
|
|
50
|
+
? `${text.slice(0, CONTENT_LIMIT)}\n… (truncated, ${text.length - CONTENT_LIMIT} more chars)`
|
|
51
|
+
: text;
|
|
52
|
+
},
|
|
53
|
+
url: () => page.url(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Launch a tab's browser. The mode is fixed for the lifetime of the returned TabBrowser's
|
|
58
|
+
* Playwright process; to switch modes, close all windows (which ends the process) and
|
|
59
|
+
* launch again.
|
|
60
|
+
*/
|
|
61
|
+
export async function launchTabBrowser(isHeadless) {
|
|
62
|
+
// `channel: 'chromium'` selects Chromium's new headless mode (a real browser run
|
|
63
|
+
// headless) rather than the legacy headless shell, which is far more detectable.
|
|
64
|
+
// `--disable-blink-features=AutomationControlled` drops the automation flag that sets
|
|
65
|
+
// navigator.webdriver and the "Chrome is being controlled by automated test software"
|
|
66
|
+
// banner.
|
|
67
|
+
const browser = await chromium.launch({
|
|
68
|
+
channel: 'chromium',
|
|
69
|
+
headless: isHeadless,
|
|
70
|
+
args: ['--disable-blink-features=AutomationControlled'],
|
|
71
|
+
});
|
|
72
|
+
const major = chromeMajor(browser.version());
|
|
73
|
+
const windows = new Map();
|
|
74
|
+
return {
|
|
75
|
+
mode: isHeadless ? 'headless' : 'headed',
|
|
76
|
+
openWindow: async (id) => {
|
|
77
|
+
// A fresh, internally consistent fingerprint per window so isolated windows don't
|
|
78
|
+
// share an identical signature: UA (version pinned to the real engine), matching
|
|
79
|
+
// client-hint platform header, Accept-Language, timezone, and a common viewport.
|
|
80
|
+
const profile = randomBrowserProfile(major);
|
|
81
|
+
const context = await browser.newContext({
|
|
82
|
+
userAgent: profile.userAgent,
|
|
83
|
+
locale: profile.locale,
|
|
84
|
+
timezoneId: profile.timezoneId,
|
|
85
|
+
viewport: profile.viewport,
|
|
86
|
+
extraHTTPHeaders: {
|
|
87
|
+
'Accept-Language': acceptLanguage(profile.locale),
|
|
88
|
+
'Sec-CH-UA-Platform': `"${profile.platform}"`,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
// Ensure navigator.webdriver is never true in any frame of this context.
|
|
92
|
+
await context.addInitScript(() => {
|
|
93
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => false });
|
|
94
|
+
});
|
|
95
|
+
const page = await context.newPage();
|
|
96
|
+
const window = makeWindow(id, page);
|
|
97
|
+
windows.set(id, { window, context });
|
|
98
|
+
return window;
|
|
99
|
+
},
|
|
100
|
+
closeWindow: async (id) => {
|
|
101
|
+
const entry = windows.get(id);
|
|
102
|
+
if (!entry)
|
|
103
|
+
return;
|
|
104
|
+
windows.delete(id);
|
|
105
|
+
try {
|
|
106
|
+
await entry.context.close();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
/* already gone */
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
window: (id) => windows.get(id)?.window,
|
|
113
|
+
windowIds: () => [...windows.keys()],
|
|
114
|
+
close: async () => {
|
|
115
|
+
windows.clear();
|
|
116
|
+
try {
|
|
117
|
+
await browser.close();
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
/* already gone */
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { launchTabBrowser } from './index.js';
|
|
2
|
+
export async function ensureCurrentWindow(browsers, label) {
|
|
3
|
+
let entry = browsers.get(label);
|
|
4
|
+
if (!entry) {
|
|
5
|
+
entry = { browser: await launchTabBrowser(true), counter: 0 };
|
|
6
|
+
browsers.set(label, entry);
|
|
7
|
+
}
|
|
8
|
+
if (!entry.current || !entry.browser.window(entry.current)) {
|
|
9
|
+
const id = `w${++entry.counter}`;
|
|
10
|
+
await entry.browser.openWindow(id);
|
|
11
|
+
entry.current = id;
|
|
12
|
+
}
|
|
13
|
+
return entry.browser.window(entry.current);
|
|
14
|
+
}
|
|
15
|
+
export async function runGoto(browsers, label, url) {
|
|
16
|
+
const page = await ensureCurrentWindow(browsers, label);
|
|
17
|
+
return await page.goto(url);
|
|
18
|
+
}
|
|
19
|
+
export async function runEval(browsers, label, js) {
|
|
20
|
+
const page = await ensureCurrentWindow(browsers, label);
|
|
21
|
+
return await page.eval(js);
|
|
22
|
+
}
|
|
23
|
+
export async function runContent(browsers, label) {
|
|
24
|
+
const page = await ensureCurrentWindow(browsers, label);
|
|
25
|
+
return await page.content();
|
|
26
|
+
}
|
|
27
|
+
export async function runShot(browsers, label) {
|
|
28
|
+
const page = await ensureCurrentWindow(browsers, label);
|
|
29
|
+
const path = await page.shot();
|
|
30
|
+
const opened = process.platform === 'darwin' ? ' (opening in Preview)' : '';
|
|
31
|
+
return `Screenshot saved: ${path}${opened}`;
|
|
32
|
+
}
|
|
33
|
+
export async function closeBrowserWindow(browsers, label, id) {
|
|
34
|
+
const entry = browsers.get(label);
|
|
35
|
+
if (!entry || !entry.browser.window(id))
|
|
36
|
+
return `No open connection browser:${id}.`;
|
|
37
|
+
await entry.browser.closeWindow(id);
|
|
38
|
+
if (entry.current === id)
|
|
39
|
+
entry.current = entry.browser.windowIds()[0];
|
|
40
|
+
if (entry.browser.windowIds().length === 0) {
|
|
41
|
+
await entry.browser.close();
|
|
42
|
+
browsers.delete(label);
|
|
43
|
+
}
|
|
44
|
+
return `Closed connection browser:${id}.`;
|
|
45
|
+
}
|
|
46
|
+
export function formatList(entry) {
|
|
47
|
+
const ids = entry?.browser.windowIds() ?? [];
|
|
48
|
+
if (ids.length === 0)
|
|
49
|
+
return 'No browser windows.';
|
|
50
|
+
return ids.map((id) => `${id === entry.current ? '* ' : ' '}browser:${id}`).join('\n');
|
|
51
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { launchTabBrowser } from './index.js';
|
|
2
|
+
import { parseBrowserCommand } from './command.js';
|
|
3
|
+
import { closeBrowserWindow, formatList, runContent, runEval, runGoto, runShot, } from './tab-helpers.js';
|
|
4
|
+
export class BrowserManager {
|
|
5
|
+
managers;
|
|
6
|
+
browsers = new Map();
|
|
7
|
+
constructor(managers) {
|
|
8
|
+
this.managers = managers;
|
|
9
|
+
}
|
|
10
|
+
has(label) {
|
|
11
|
+
return this.browsers.has(label);
|
|
12
|
+
}
|
|
13
|
+
// Live window state for a tab (for the connections panel / `connection list`).
|
|
14
|
+
info(label) {
|
|
15
|
+
const entry = this.browsers.get(label);
|
|
16
|
+
return entry ? { ids: entry.browser.windowIds(), mode: entry.browser.mode, current: entry.current } : null;
|
|
17
|
+
}
|
|
18
|
+
closeTab(label) {
|
|
19
|
+
const entry = this.browsers.get(label);
|
|
20
|
+
if (entry) {
|
|
21
|
+
void entry.browser.close();
|
|
22
|
+
this.browsers.delete(label);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
closeAll() {
|
|
26
|
+
for (const [, entry] of this.browsers)
|
|
27
|
+
void entry.browser.close();
|
|
28
|
+
this.browsers.clear();
|
|
29
|
+
}
|
|
30
|
+
// Run a `browser ...` command against a tab's browser, returning text to show/return. Shared by
|
|
31
|
+
// the interactive `browser` command, `connection close browser:*`, and the ACP tool loop.
|
|
32
|
+
async run(label, command) {
|
|
33
|
+
const parsed = parseBrowserCommand(command);
|
|
34
|
+
if ('error' in parsed)
|
|
35
|
+
return parsed.error;
|
|
36
|
+
try {
|
|
37
|
+
switch (parsed.action) {
|
|
38
|
+
case 'open': {
|
|
39
|
+
let entry = this.browsers.get(label);
|
|
40
|
+
const notice = entry && parsed.headed && entry.browser.mode === 'headless'
|
|
41
|
+
? ' (this tab is already running headless; close all windows to relaunch headed)' : '';
|
|
42
|
+
if (!entry) {
|
|
43
|
+
entry = { browser: await launchTabBrowser(!parsed.headed), counter: 0 };
|
|
44
|
+
this.browsers.set(label, entry);
|
|
45
|
+
}
|
|
46
|
+
const id = `w${++entry.counter}`;
|
|
47
|
+
await entry.browser.openWindow(id);
|
|
48
|
+
entry.current = id;
|
|
49
|
+
return `Opened browser window ${id} (${entry.browser.mode}).${notice}`;
|
|
50
|
+
}
|
|
51
|
+
case 'list': {
|
|
52
|
+
return formatList(this.browsers.get(label));
|
|
53
|
+
}
|
|
54
|
+
case 'use': {
|
|
55
|
+
const entry = this.browsers.get(label);
|
|
56
|
+
if (!entry || !entry.browser.window(parsed.id))
|
|
57
|
+
return `No browser window ${parsed.id}.`;
|
|
58
|
+
entry.current = parsed.id;
|
|
59
|
+
return `Using browser window ${parsed.id}.`;
|
|
60
|
+
}
|
|
61
|
+
case 'close': {
|
|
62
|
+
const entry = this.browsers.get(label);
|
|
63
|
+
if (!entry?.current)
|
|
64
|
+
return 'No browser window to close.';
|
|
65
|
+
return await closeBrowserWindow(this.browsers, label, entry.current);
|
|
66
|
+
}
|
|
67
|
+
case 'closeWindow': {
|
|
68
|
+
return await closeBrowserWindow(this.browsers, label, parsed.id);
|
|
69
|
+
}
|
|
70
|
+
case 'goto': {
|
|
71
|
+
return await runGoto(this.browsers, label, parsed.url);
|
|
72
|
+
}
|
|
73
|
+
case 'eval': {
|
|
74
|
+
return await runEval(this.browsers, label, parsed.js);
|
|
75
|
+
}
|
|
76
|
+
case 'content': {
|
|
77
|
+
return await runContent(this.browsers, label);
|
|
78
|
+
}
|
|
79
|
+
case 'shot': {
|
|
80
|
+
return await runShot(this.browsers, label);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return `Browser error: ${error instanceof Error ? error.message : String(error)}`;
|
|
86
|
+
}
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
runInteractive(command, label, onDone) {
|
|
90
|
+
this.managers.tab.startRunning(label, command);
|
|
91
|
+
void this.run(label, command)
|
|
92
|
+
.then((out) => { this.managers.tab.finishRunning(label, out); onDone?.(out); })
|
|
93
|
+
.catch((error) => {
|
|
94
|
+
const message = `Browser error: ${error instanceof Error ? error.message : String(error)}`;
|
|
95
|
+
this.managers.tab.finishRunning(label, message);
|
|
96
|
+
onDone?.(message);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Parser for the `browser` command plus the two helpers the ACP tool loop needs
|
|
2
|
+
// (`extractBrowserCommand` / `BROWSER_PRIMER`), mirroring `parseConnectionCommand`
|
|
3
|
+
// (src/connections.ts) and the `db` equivalents in src/db.ts. Pure — no I/O. The host
|
|
4
|
+
// (src/cli.tsx) performs the actual Playwright actions against the tab's browser.
|
|
5
|
+
const USAGE = 'Usage: browser <open [name] [--headed]|list|use <id>|goto <url>|eval <js>|shot|content|close [id]|window close <id>>';
|
|
6
|
+
// First-position subcommand keywords, exported for tab completion.
|
|
7
|
+
export const BROWSER_SUBCOMMANDS = [
|
|
8
|
+
'open',
|
|
9
|
+
'list',
|
|
10
|
+
'use',
|
|
11
|
+
'goto',
|
|
12
|
+
'eval',
|
|
13
|
+
'shot',
|
|
14
|
+
'content',
|
|
15
|
+
'close',
|
|
16
|
+
'window',
|
|
17
|
+
];
|
|
18
|
+
/** Parse a `browser ...` command. Pure — performs no I/O. */
|
|
19
|
+
export function parseBrowserCommand(input) {
|
|
20
|
+
const rest = input.trim().replace(/^browser\b\s*/i, '').trim();
|
|
21
|
+
if (!rest)
|
|
22
|
+
return { error: USAGE };
|
|
23
|
+
const [actionRaw, ...tail] = rest.split(/\s+/);
|
|
24
|
+
const action = actionRaw.toLowerCase();
|
|
25
|
+
// The argument text after the action verb, with original spacing preserved.
|
|
26
|
+
const argument = rest.slice(actionRaw.length).trim();
|
|
27
|
+
switch (action) {
|
|
28
|
+
case 'open': {
|
|
29
|
+
const tokens = tail;
|
|
30
|
+
const isHeaded = tokens.some((t) => t === '--headed' || t === '-H');
|
|
31
|
+
const name = tokens.find((t) => !t.startsWith('-'));
|
|
32
|
+
return { action: 'open', name, headed: isHeaded };
|
|
33
|
+
}
|
|
34
|
+
case 'list': {
|
|
35
|
+
return { action: 'list' };
|
|
36
|
+
}
|
|
37
|
+
case 'use': {
|
|
38
|
+
if (!tail[0])
|
|
39
|
+
return { error: 'Usage: browser use <id>' };
|
|
40
|
+
return { action: 'use', id: tail[0] };
|
|
41
|
+
}
|
|
42
|
+
case 'goto': {
|
|
43
|
+
if (!argument)
|
|
44
|
+
return { error: 'Usage: browser goto <url>' };
|
|
45
|
+
return { action: 'goto', url: argument };
|
|
46
|
+
}
|
|
47
|
+
case 'eval': {
|
|
48
|
+
if (!argument)
|
|
49
|
+
return { error: 'Usage: browser eval <js>' };
|
|
50
|
+
return { action: 'eval', js: argument };
|
|
51
|
+
}
|
|
52
|
+
case 'shot': {
|
|
53
|
+
return { action: 'shot' };
|
|
54
|
+
}
|
|
55
|
+
case 'content': {
|
|
56
|
+
return { action: 'content' };
|
|
57
|
+
}
|
|
58
|
+
case 'close': {
|
|
59
|
+
// `browser close <id>` is an alias for `browser window close <id>`; bare
|
|
60
|
+
// `browser close` closes the current window.
|
|
61
|
+
if (tail[0])
|
|
62
|
+
return { action: 'closeWindow', id: tail[0] };
|
|
63
|
+
return { action: 'close' };
|
|
64
|
+
}
|
|
65
|
+
case 'window': {
|
|
66
|
+
if (tail[0]?.toLowerCase() !== 'close' || !tail[1]) {
|
|
67
|
+
return { error: 'Usage: browser window close <id>' };
|
|
68
|
+
}
|
|
69
|
+
return { action: 'closeWindow', id: tail[1] };
|
|
70
|
+
}
|
|
71
|
+
default: {
|
|
72
|
+
return { error: USAGE };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Pull a proposed `browser ...` command out of an agent reply, if present. Scans
|
|
78
|
+
* bottom-up (the primer asks for the command on the last line) and tolerates a
|
|
79
|
+
* surrounding code fence or a leading `$ `/`> ` prompt marker — same shape as
|
|
80
|
+
* `extractDbCommand`.
|
|
81
|
+
*/
|
|
82
|
+
export function extractBrowserCommand(text) {
|
|
83
|
+
const lines = text.split('\n');
|
|
84
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
85
|
+
const line = lines[index].replace(/^[\s`$>]+/, '').replace(/`+\s*$/, '').trim();
|
|
86
|
+
if (/^browser\s+(open|list|use|goto|eval|shot|content|close|window)\b/i.test(line))
|
|
87
|
+
return line;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// Primer injected into an ACP agent so it can drive a real browser through the autonomous
|
|
92
|
+
// tool loop. It exposes a deliberately simplified surface — window/headless/mode management
|
|
93
|
+
// is handled by the host, which auto-launches the tab's browser (headless) and auto-opens a
|
|
94
|
+
// window on first use.
|
|
95
|
+
export const BROWSER_PRIMER = [
|
|
96
|
+
'This host CLI can also drive a real web browser via `browser` commands. Syntax:',
|
|
97
|
+
' browser goto <url> # navigate to a URL (a browser/window opens automatically)',
|
|
98
|
+
' browser content # return the current page\'s rendered text',
|
|
99
|
+
' browser eval <js> # run JavaScript in the page and return the result',
|
|
100
|
+
'To read a web page, end your reply with exactly one `browser` command on its own final',
|
|
101
|
+
'line (no code fence, nothing after it). The host runs it and returns the output to you,',
|
|
102
|
+
'so you can issue further commands. When the task is done, reply with the final answer and',
|
|
103
|
+
'NO trailing command.',
|
|
104
|
+
].join('\n');
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { launchTabBrowser } from './browser.js';
|
|
2
|
+
export async function ensureCurrentWindow(browsers, label) {
|
|
3
|
+
let entry = browsers.get(label);
|
|
4
|
+
if (!entry) {
|
|
5
|
+
entry = { browser: await launchTabBrowser(true), counter: 0 };
|
|
6
|
+
browsers.set(label, entry);
|
|
7
|
+
}
|
|
8
|
+
if (!entry.current || !entry.browser.window(entry.current)) {
|
|
9
|
+
const id = `w${++entry.counter}`;
|
|
10
|
+
await entry.browser.openWindow(id);
|
|
11
|
+
entry.current = id;
|
|
12
|
+
}
|
|
13
|
+
return entry.browser.window(entry.current);
|
|
14
|
+
}
|
|
15
|
+
export async function runGoto(browsers, label, url) {
|
|
16
|
+
const page = await ensureCurrentWindow(browsers, label);
|
|
17
|
+
return await page.goto(url);
|
|
18
|
+
}
|
|
19
|
+
export async function runEval(browsers, label, js) {
|
|
20
|
+
const page = await ensureCurrentWindow(browsers, label);
|
|
21
|
+
return await page.eval(js);
|
|
22
|
+
}
|
|
23
|
+
export async function runContent(browsers, label) {
|
|
24
|
+
const page = await ensureCurrentWindow(browsers, label);
|
|
25
|
+
return await page.content();
|
|
26
|
+
}
|
|
27
|
+
export async function runShot(browsers, label) {
|
|
28
|
+
const page = await ensureCurrentWindow(browsers, label);
|
|
29
|
+
const path = await page.shot();
|
|
30
|
+
const opened = process.platform === 'darwin' ? ' (opening in Preview)' : '';
|
|
31
|
+
return `Screenshot saved: ${path}${opened}`;
|
|
32
|
+
}
|
|
33
|
+
export async function closeBrowserWindow(browsers, label, id) {
|
|
34
|
+
const entry = browsers.get(label);
|
|
35
|
+
if (!entry || !entry.browser.window(id))
|
|
36
|
+
return `No open connection browser:${id}.`;
|
|
37
|
+
await entry.browser.closeWindow(id);
|
|
38
|
+
if (entry.current === id)
|
|
39
|
+
entry.current = entry.browser.windowIds()[0];
|
|
40
|
+
if (entry.browser.windowIds().length === 0) {
|
|
41
|
+
await entry.browser.close();
|
|
42
|
+
browsers.delete(label);
|
|
43
|
+
}
|
|
44
|
+
return `Closed connection browser:${id}.`;
|
|
45
|
+
}
|
|
46
|
+
export function formatList(entry) {
|
|
47
|
+
const ids = entry?.browser.windowIds() ?? [];
|
|
48
|
+
if (ids.length === 0)
|
|
49
|
+
return 'No browser windows.';
|
|
50
|
+
return ids.map((id) => `${id === entry.current ? '* ' : ' '}browser:${id}`).join('\n');
|
|
51
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { launchTabBrowser } from './browser.js';
|
|
2
|
+
import { parseBrowserCommand } from './browser-command.js';
|
|
3
|
+
import { closeBrowserWindow, formatList, runContent, runEval, runGoto, runShot, } from './browser-tab-helpers.js';
|
|
4
|
+
export class BrowserManager {
|
|
5
|
+
managers;
|
|
6
|
+
browsers = new Map();
|
|
7
|
+
constructor(managers) {
|
|
8
|
+
this.managers = managers;
|
|
9
|
+
}
|
|
10
|
+
has(label) {
|
|
11
|
+
return this.browsers.has(label);
|
|
12
|
+
}
|
|
13
|
+
// Live window state for a tab (for the connections panel / `connection list`).
|
|
14
|
+
info(label) {
|
|
15
|
+
const entry = this.browsers.get(label);
|
|
16
|
+
return entry ? { ids: entry.browser.windowIds(), mode: entry.browser.mode, current: entry.current } : null;
|
|
17
|
+
}
|
|
18
|
+
closeTab(label) {
|
|
19
|
+
const entry = this.browsers.get(label);
|
|
20
|
+
if (entry) {
|
|
21
|
+
void entry.browser.close();
|
|
22
|
+
this.browsers.delete(label);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
closeAll() {
|
|
26
|
+
for (const [, entry] of this.browsers)
|
|
27
|
+
void entry.browser.close();
|
|
28
|
+
this.browsers.clear();
|
|
29
|
+
}
|
|
30
|
+
// Run a `browser ...` command against a tab's browser, returning text to show/return. Shared by
|
|
31
|
+
// the interactive `browser` command, `connection close browser:*`, and the ACP tool loop.
|
|
32
|
+
async run(label, command) {
|
|
33
|
+
const parsed = parseBrowserCommand(command);
|
|
34
|
+
if ('error' in parsed)
|
|
35
|
+
return parsed.error;
|
|
36
|
+
try {
|
|
37
|
+
switch (parsed.action) {
|
|
38
|
+
case 'open': {
|
|
39
|
+
let entry = this.browsers.get(label);
|
|
40
|
+
const notice = entry && parsed.headed && entry.browser.mode === 'headless'
|
|
41
|
+
? ' (this tab is already running headless; close all windows to relaunch headed)' : '';
|
|
42
|
+
if (!entry) {
|
|
43
|
+
entry = { browser: await launchTabBrowser(!parsed.headed), counter: 0 };
|
|
44
|
+
this.browsers.set(label, entry);
|
|
45
|
+
}
|
|
46
|
+
const id = `w${++entry.counter}`;
|
|
47
|
+
await entry.browser.openWindow(id);
|
|
48
|
+
entry.current = id;
|
|
49
|
+
return `Opened browser window ${id} (${entry.browser.mode}).${notice}`;
|
|
50
|
+
}
|
|
51
|
+
case 'list': {
|
|
52
|
+
return formatList(this.browsers.get(label));
|
|
53
|
+
}
|
|
54
|
+
case 'use': {
|
|
55
|
+
const entry = this.browsers.get(label);
|
|
56
|
+
if (!entry || !entry.browser.window(parsed.id))
|
|
57
|
+
return `No browser window ${parsed.id}.`;
|
|
58
|
+
entry.current = parsed.id;
|
|
59
|
+
return `Using browser window ${parsed.id}.`;
|
|
60
|
+
}
|
|
61
|
+
case 'close': {
|
|
62
|
+
const entry = this.browsers.get(label);
|
|
63
|
+
if (!entry?.current)
|
|
64
|
+
return 'No browser window to close.';
|
|
65
|
+
return await closeBrowserWindow(this.browsers, label, entry.current);
|
|
66
|
+
}
|
|
67
|
+
case 'closeWindow': {
|
|
68
|
+
return await closeBrowserWindow(this.browsers, label, parsed.id);
|
|
69
|
+
}
|
|
70
|
+
case 'goto': {
|
|
71
|
+
return await runGoto(this.browsers, label, parsed.url);
|
|
72
|
+
}
|
|
73
|
+
case 'eval': {
|
|
74
|
+
return await runEval(this.browsers, label, parsed.js);
|
|
75
|
+
}
|
|
76
|
+
case 'content': {
|
|
77
|
+
return await runContent(this.browsers, label);
|
|
78
|
+
}
|
|
79
|
+
case 'shot': {
|
|
80
|
+
return await runShot(this.browsers, label);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return `Browser error: ${error instanceof Error ? error.message : String(error)}`;
|
|
86
|
+
}
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
runInteractive(command, label, onDone) {
|
|
90
|
+
this.managers.tab.startRunning(label, command);
|
|
91
|
+
void this.run(label, command)
|
|
92
|
+
.then((out) => { this.managers.tab.finishRunning(label, out); onDone?.(out); })
|
|
93
|
+
.catch((error) => {
|
|
94
|
+
const message = `Browser error: ${error instanceof Error ? error.message : String(error)}`;
|
|
95
|
+
this.managers.tab.finishRunning(label, message);
|
|
96
|
+
onDone?.(message);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|