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.
Files changed (268) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +28 -0
  3. package/agent-names.json +52 -0
  4. package/bin/janus.mjs +27 -0
  5. package/chrome-extension/_metadata/generated_indexed_rulesets/_ruleset1 +0 -0
  6. package/chrome-extension/content-script.js +62 -0
  7. package/chrome-extension/manifest.json +25 -0
  8. package/chrome-extension/rules.json +16 -0
  9. package/dist/acp/index.js +93 -0
  10. package/dist/acp/loop.js +75 -0
  11. package/dist/acp/manager.js +126 -0
  12. package/dist/acp/rate-limit.js +8 -0
  13. package/dist/acp/runner.js +18 -0
  14. package/dist/acp/tools.js +27 -0
  15. package/dist/acp-loop.js +75 -0
  16. package/dist/acp-manager.js +109 -0
  17. package/dist/acp-runner.js +18 -0
  18. package/dist/acp.js +91 -0
  19. package/dist/agent/commands.js +29 -0
  20. package/dist/agent/communication-manager.js +71 -0
  21. package/dist/agent/state.js +58 -0
  22. package/dist/agent-commands.js +29 -0
  23. package/dist/agent-communication-manager.js +71 -0
  24. package/dist/agent-state.js +58 -0
  25. package/dist/app-themes.js +13 -0
  26. package/dist/browser/command.js +104 -0
  27. package/dist/browser/index.js +124 -0
  28. package/dist/browser/tab-helpers.js +51 -0
  29. package/dist/browser/tab.js +99 -0
  30. package/dist/browser-command.js +104 -0
  31. package/dist/browser-tab-helpers.js +51 -0
  32. package/dist/browser-tab.js +99 -0
  33. package/dist/browser.js +124 -0
  34. package/dist/buffer.js +42 -0
  35. package/dist/bus.js +52 -0
  36. package/dist/capture/manager.js +48 -0
  37. package/dist/capture/router.js +18 -0
  38. package/dist/capture-manager.js +48 -0
  39. package/dist/capture-router.js +20 -0
  40. package/dist/chrome-extension-loader.js +62 -0
  41. package/dist/cli-args.js +80 -0
  42. package/dist/command-manager.js +126 -0
  43. package/dist/command-router.js +14 -0
  44. package/dist/commands/acp-reset.js +13 -0
  45. package/dist/commands/acp.js +5 -0
  46. package/dist/commands/agent.js +5 -0
  47. package/dist/commands/broadcast.js +22 -0
  48. package/dist/commands/browser.js +5 -0
  49. package/dist/commands/clear.js +5 -0
  50. package/dist/commands/close.js +42 -0
  51. package/dist/commands/connection.js +5 -0
  52. package/dist/commands/db.js +5 -0
  53. package/dist/commands/edit.js +18 -0
  54. package/dist/commands/files.js +13 -0
  55. package/dist/commands/hist.js +7 -0
  56. package/dist/commands/index.js +62 -0
  57. package/dist/commands/monitor.js +61 -0
  58. package/dist/commands/msg.js +17 -0
  59. package/dist/commands/next.js +7 -0
  60. package/dist/commands/notifications.js +16 -0
  61. package/dist/commands/notify.js +20 -0
  62. package/dist/commands/open.js +41 -0
  63. package/dist/commands/profile.js +5 -0
  64. package/dist/commands/queue.js +37 -0
  65. package/dist/commands/quit.js +6 -0
  66. package/dist/commands/rename.js +12 -0
  67. package/dist/commands/resolve-target.js +12 -0
  68. package/dist/commands/schedule.js +74 -0
  69. package/dist/commands/search.js +26 -0
  70. package/dist/commands/send.js +51 -0
  71. package/dist/commands/state.js +9 -0
  72. package/dist/commands/syntax.js +36 -0
  73. package/dist/commands/tasks.js +7 -0
  74. package/dist/commands/theme.js +41 -0
  75. package/dist/commands/types.js +1 -0
  76. package/dist/commands.js +70 -0
  77. package/dist/completion/browser.js +24 -0
  78. package/dist/completion/fs.js +26 -0
  79. package/dist/completion/handlers.js +81 -0
  80. package/dist/completion/helpers.js +43 -0
  81. package/dist/completion/index.js +43 -0
  82. package/dist/completion-browser.js +24 -0
  83. package/dist/completion-fs.js +26 -0
  84. package/dist/completion-handlers.js +69 -0
  85. package/dist/completion-helpers.js +43 -0
  86. package/dist/completion.js +42 -0
  87. package/dist/config.js +66 -0
  88. package/dist/connection/close.js +41 -0
  89. package/dist/connection/manager.js +93 -0
  90. package/dist/connection/parsing.js +28 -0
  91. package/dist/connection-close.js +41 -0
  92. package/dist/connection-manager.js +93 -0
  93. package/dist/connection-parsing.js +28 -0
  94. package/dist/connections.js +75 -0
  95. package/dist/controller-events.js +34 -0
  96. package/dist/controller-file-tree.js +36 -0
  97. package/dist/controller.js +201 -0
  98. package/dist/database/index.js +83 -0
  99. package/dist/database/manager.js +67 -0
  100. package/dist/database/parsing.js +54 -0
  101. package/dist/database/query.js +41 -0
  102. package/dist/database-manager.js +67 -0
  103. package/dist/database-parsing.js +54 -0
  104. package/dist/database-query.js +41 -0
  105. package/dist/database.js +83 -0
  106. package/dist/datetime.js +15 -0
  107. package/dist/editor/save.js +27 -0
  108. package/dist/editor/sync.js +10 -0
  109. package/dist/editor/watch-manager.js +84 -0
  110. package/dist/editor-save.js +24 -0
  111. package/dist/editor-watch-manager.js +84 -0
  112. package/dist/file-tree-args.js +36 -0
  113. package/dist/file-tree-git-refresh.js +33 -0
  114. package/dist/file-tree-manager.js +252 -0
  115. package/dist/file-tree-moves.js +26 -0
  116. package/dist/file-tree-open.js +56 -0
  117. package/dist/file-tree-rebuild.js +22 -0
  118. package/dist/file-tree-watch.js +27 -0
  119. package/dist/file-tree.js +82 -0
  120. package/dist/git-status.js +64 -0
  121. package/dist/github-token.js +19 -0
  122. package/dist/global-history.js +48 -0
  123. package/dist/harness/auto-approve.js +79 -0
  124. package/dist/harness/busy-status.js +91 -0
  125. package/dist/harness/capture-file.js +28 -0
  126. package/dist/harness/index.js +87 -0
  127. package/dist/harness/manager.js +182 -0
  128. package/dist/harness/models.js +8 -0
  129. package/dist/harness/recorder.js +89 -0
  130. package/dist/harness/recording-file.js +25 -0
  131. package/dist/harness/screen.js +79 -0
  132. package/dist/harness-capture-file.js +28 -0
  133. package/dist/harness-manager.js +112 -0
  134. package/dist/harness-models.js +8 -0
  135. package/dist/harness-screen.js +72 -0
  136. package/dist/harness.js +54 -0
  137. package/dist/index.js +163 -0
  138. package/dist/instance-lock.js +34 -0
  139. package/dist/interactive.js +33 -0
  140. package/dist/main.js +203 -0
  141. package/dist/managers.js +1 -0
  142. package/dist/message-handler.js +134 -0
  143. package/dist/messaging.js +47 -0
  144. package/dist/monitor/acp.js +24 -0
  145. package/dist/monitor/ask.js +31 -0
  146. package/dist/monitor/context.js +27 -0
  147. package/dist/monitor/editor-feed.js +49 -0
  148. package/dist/monitor/feed-diff.js +22 -0
  149. package/dist/monitor/feeds.js +19 -0
  150. package/dist/monitor/framing.js +29 -0
  151. package/dist/monitor/harness-feed.js +21 -0
  152. package/dist/monitor/info.js +29 -0
  153. package/dist/monitor/manager.js +221 -0
  154. package/dist/monitor/page-feed.js +21 -0
  155. package/dist/monitor/parsing.js +82 -0
  156. package/dist/monitor/reply.js +11 -0
  157. package/dist/monitor/session.js +36 -0
  158. package/dist/monitor/stop.js +33 -0
  159. package/dist/monitor/suggestion.js +12 -0
  160. package/dist/monitor/targets.js +69 -0
  161. package/dist/monitor/window.js +109 -0
  162. package/dist/monitor-acp.js +22 -0
  163. package/dist/monitor-info.js +18 -0
  164. package/dist/monitor-manager.js +233 -0
  165. package/dist/monitor-parsing.js +68 -0
  166. package/dist/monitor-session.js +19 -0
  167. package/dist/monitor-targets.js +37 -0
  168. package/dist/monitor-window.js +81 -0
  169. package/dist/notifications-tab.js +32 -0
  170. package/dist/notifications.js +87 -0
  171. package/dist/open-file-manager.js +100 -0
  172. package/dist/openers/editor.js +41 -0
  173. package/dist/openers/external-viewer.js +11 -0
  174. package/dist/openers/image.js +27 -0
  175. package/dist/openers/index.js +16 -0
  176. package/dist/openers/markdown.js +20 -0
  177. package/dist/openers/os-open.js +24 -0
  178. package/dist/openers/page.js +51 -0
  179. package/dist/openers/size.js +13 -0
  180. package/dist/openers/types.js +1 -0
  181. package/dist/page/sync.js +10 -0
  182. package/dist/paths.js +45 -0
  183. package/dist/persona-parsing.js +49 -0
  184. package/dist/personas.js +44 -0
  185. package/dist/profile/agent-opener.js +154 -0
  186. package/dist/profile/files.js +17 -0
  187. package/dist/profile/manager.js +72 -0
  188. package/dist/profile/monitors.js +23 -0
  189. package/dist/profile/notifications.js +11 -0
  190. package/dist/profile-agent-opener.js +136 -0
  191. package/dist/profile-manager.js +72 -0
  192. package/dist/profiles.js +143 -0
  193. package/dist/protocol.js +1 -0
  194. package/dist/pseudoterminal-manager.js +106 -0
  195. package/dist/pty.js +41 -0
  196. package/dist/recognizers/acp.js +38 -0
  197. package/dist/recognizers/analyze.js +49 -0
  198. package/dist/recognizers/bash.js +51 -0
  199. package/dist/recognizers/db.js +26 -0
  200. package/dist/recognizers/index.js +1 -0
  201. package/dist/recognizers/lexicon.js +11 -0
  202. package/dist/recognizers/types.js +5 -0
  203. package/dist/resolve.js +39 -0
  204. package/dist/route-choice.js +11 -0
  205. package/dist/sandbox/index.js +217 -0
  206. package/dist/sandbox/paths.js +178 -0
  207. package/dist/sandbox/profile.js +161 -0
  208. package/dist/sandbox-paths.js +159 -0
  209. package/dist/sandbox-profile.js +161 -0
  210. package/dist/sandbox.js +217 -0
  211. package/dist/schedule/display.js +19 -0
  212. package/dist/schedule/helpers.js +76 -0
  213. package/dist/schedule/index.js +76 -0
  214. package/dist/schedule/manager.js +101 -0
  215. package/dist/schedule/parsing.js +63 -0
  216. package/dist/schedule/time.js +34 -0
  217. package/dist/schedule-display.js +19 -0
  218. package/dist/schedule-helpers.js +76 -0
  219. package/dist/schedule-manager.js +98 -0
  220. package/dist/schedule-parsing.js +63 -0
  221. package/dist/schedule-time.js +34 -0
  222. package/dist/schedule.js +76 -0
  223. package/dist/search-matches.js +40 -0
  224. package/dist/security.js +44 -0
  225. package/dist/shell-manager.js +139 -0
  226. package/dist/shell.js +59 -0
  227. package/dist/ssh-manager.js +42 -0
  228. package/dist/ssh.js +39 -0
  229. package/dist/startup-errors.js +21 -0
  230. package/dist/state-event.js +14 -0
  231. package/dist/state-format.js +38 -0
  232. package/dist/syntax-themes.js +14 -0
  233. package/dist/tab/agent-state.js +19 -0
  234. package/dist/tab/cleanup.js +33 -0
  235. package/dist/tab/colors.js +43 -0
  236. package/dist/tab/creators.js +95 -0
  237. package/dist/tab/dock.js +35 -0
  238. package/dist/tab/focus-history.js +41 -0
  239. package/dist/tab/formatting-handlers.js +77 -0
  240. package/dist/tab/formatting.js +21 -0
  241. package/dist/tab/history.js +14 -0
  242. package/dist/tab/index.js +65 -0
  243. package/dist/tab/manager.js +329 -0
  244. package/dist/tab/openers.js +35 -0
  245. package/dist/tab/queue.js +33 -0
  246. package/dist/tab/rehydrate.js +15 -0
  247. package/dist/tab/reorder.js +15 -0
  248. package/dist/tab/transcript.js +15 -0
  249. package/dist/tab/utils.js +55 -0
  250. package/dist/tab/view.js +42 -0
  251. package/dist/tab-cleanup.js +27 -0
  252. package/dist/tab-colors.js +43 -0
  253. package/dist/tab-creators.js +85 -0
  254. package/dist/tab-formatting-handlers.js +77 -0
  255. package/dist/tab-formatting.js +21 -0
  256. package/dist/tab-manager.js +418 -0
  257. package/dist/tab-utils.js +55 -0
  258. package/dist/tab.js +57 -0
  259. package/dist/tasks.js +32 -0
  260. package/dist/transcript/logger.js +36 -0
  261. package/dist/transcript/store.js +66 -0
  262. package/dist/types.js +6 -0
  263. package/dist/user-agent.js +65 -0
  264. package/dist/word-wrapping.js +42 -0
  265. package/dist/workspace-manager.js +40 -0
  266. package/dist/workspace.js +120 -0
  267. package/help.md +97 -0
  268. 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
+ }