@53able/pi-agent-browser 0.2.0 → 0.3.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/README.ja-JP.md CHANGED
@@ -167,6 +167,8 @@ https://example.com をブラウザ経由で読み、outputs/browser/example.md
167
167
 
168
168
  どちらのツールも対話的なセッション(TUI、またはUI対応のRPC)を必要とします。無人・headless実行では、黙って進めるのではなく拒否します。
169
169
 
170
+ `agent_browser_open`は、各セッションが`headed: true`で起動されたかどうかも記録します。`agent_browser_handoff`が、headedで開かれていないセッションに対して呼ばれた場合、人間への警告に加えて、可視ウィンドウを開き直すための`agent_browser_open`の引数(セッション名、`headed: true`、`restore: true`、最後に開いていたURL)をそのまま提示します — どのコマンドを打ち直せばよいか推測する必要はありません。ログインや2FA、CAPTCHA、本人確認に差し掛かる可能性があるタスクでは、最初から`headed: true`で開くことを推奨します。一部の確認チャレンジは使い切り・期限付きのため、途中で開き直すとログインからやり直しになることがあります。
171
+
170
172
  ## Troubleshooting
171
173
 
172
174
  ```bash
package/README.md CHANGED
@@ -167,6 +167,8 @@ Not every step should run unattended. Two tools hand control back to a human at
167
167
 
168
168
  Both tools require an interactive session (TUI or RPC with UI support); in headless/unattended runs they refuse rather than proceed silently.
169
169
 
170
+ `agent_browser_open` also remembers whether each named session was launched with `headed: true`. If `agent_browser_handoff` is called for a session that was not opened headed, it automatically warns the human and includes the exact `agent_browser_open` arguments (session name, `headed: true`, `restore: true`, and the last known URL) needed to reopen a visible window — no more guessing which command to rerun. Prefer `headed: true` up front for any task that might hit a login, 2FA, CAPTCHA, or identity-verification step, since some challenges are single-use and reopening mid-flow may require restarting the login.
171
+
170
172
  ## Troubleshooting
171
173
 
172
174
  Run:
package/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI, AgentToolUpdateCallback, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
- import { access, mkdir, writeFile } from "node:fs/promises";
3
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { constants, existsSync } from "node:fs";
5
5
  import { join, resolve, basename } from "node:path";
6
6
  import { spawn } from "node:child_process";
@@ -35,6 +35,51 @@ function scanForSensitiveContent(text: string): string[] {
35
35
  return hits;
36
36
  }
37
37
 
38
+ // Tracks whether each agent-browser session was last opened headed, so
39
+ // agent_browser_handoff / agent_browser_commit can tell a human definitively
40
+ // whether a visible browser window exists for them to act in, instead of guessing.
41
+ const SESSION_STATE_DIR = ".pi-agent-browser";
42
+ const SESSION_STATE_FILE = "sessions.json";
43
+ const DEFAULT_SESSION_KEY = "__default__";
44
+
45
+ type SessionState = { headed: boolean; lastUrl?: string; updatedAt: string };
46
+
47
+ function sessionKey(params: JsonObject): string {
48
+ return typeof params.session === "string" && params.session.trim() ? params.session.trim() : DEFAULT_SESSION_KEY;
49
+ }
50
+
51
+ async function readSessionStates(cwd: string): Promise<Record<string, SessionState>> {
52
+ try {
53
+ const raw = await readFile(join(cwd, SESSION_STATE_DIR, SESSION_STATE_FILE), "utf8");
54
+ return JSON.parse(raw) as Record<string, SessionState>;
55
+ } catch {
56
+ return {};
57
+ }
58
+ }
59
+
60
+ async function writeSessionState(cwd: string, session: string, patch: Partial<Omit<SessionState, "updatedAt">>): Promise<void> {
61
+ const dir = join(cwd, SESSION_STATE_DIR);
62
+ await mkdir(dir, { recursive: true });
63
+ const states = await readSessionStates(cwd);
64
+ const previous = states[session];
65
+ states[session] = {
66
+ headed: patch.headed ?? previous?.headed ?? false,
67
+ lastUrl: patch.lastUrl ?? previous?.lastUrl,
68
+ updatedAt: new Date().toISOString(),
69
+ };
70
+ await writeFile(join(dir, SESSION_STATE_FILE), JSON.stringify(states, null, 2), "utf8");
71
+ }
72
+
73
+ function reopenHeadedGuidance(session: string, lastUrl: string | undefined): string[] {
74
+ const sessionArg = session === DEFAULT_SESSION_KEY ? {} : { session };
75
+ const reopenArgs = JSON.stringify({ ...sessionArg, headed: true, restore: true, ...(lastUrl ? { url: lastUrl } : {}) });
76
+ return [
77
+ `⚠ このセッション(${session === DEFAULT_SESSION_KEY ? "既定セッション" : session})はheadless(ウィンドウ非表示)で開かれています。`,
78
+ `人間が直接操作できるようにするには、agent_browser_open を次の引数で呼び直してください: ${reopenArgs}`,
79
+ "同じセッション名なのでcookie等は復元されますが、2FA/CAPTCHAなど使い切り・期限付きのチャレンジは、再度最初からやり直しになる場合があります。",
80
+ ];
81
+ }
82
+
38
83
  type JsonObject = Record<string, unknown>;
39
84
 
40
85
  type RunResult = {
@@ -346,6 +391,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
346
391
  promptGuidelines: [
347
392
  "Use agent_browser_snapshot after opening or navigating before clicking by @ref.",
348
393
  "Pass allowedDomains for untrusted or task-scoped browsing when possible.",
394
+ "Prefer headed: true whenever the task might hit a login, 2FA, CAPTCHA, or identity-verification step — a human can only intervene through agent_browser_handoff if a visible window already exists.",
349
395
  ],
350
396
  parameters: OpenSchema,
351
397
  async execute(_id, rawParams, signal, onUpdate, ctx) {
@@ -365,6 +411,12 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
365
411
  if (typeof params.url === "string" && params.url.trim()) args.push(params.url.trim());
366
412
  pushExtraArgs(args, params);
367
413
  const result = await runAgentBrowser(args, { signal, timeoutMs: params.timeoutMs as number | undefined, cwd: ctx.cwd, onUpdate });
414
+ if (result.ok) {
415
+ await writeSessionState(ctx.cwd, sessionKey(params), {
416
+ headed: params.headed === true,
417
+ lastUrl: typeof params.url === "string" && params.url.trim() ? params.url.trim() : undefined,
418
+ });
419
+ }
368
420
  return buildTextResult(result);
369
421
  },
370
422
  });
@@ -582,6 +634,9 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
582
634
  const category = String(params.category);
583
635
  const reason = String(params.reason);
584
636
  const artifacts: string[] = [];
637
+ const session = sessionKey(params);
638
+ const sessionState = (await readSessionStates(ctx.cwd))[session];
639
+ const headlessNotes = sessionState?.headed === true ? [] : reopenHeadedGuidance(session, sessionState?.lastUrl);
585
640
 
586
641
  if (params.screenshot !== false) {
587
642
  const path = resolveOutputPath(ctx, undefined, "handoff.png");
@@ -601,6 +656,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
601
656
  `Human handoff required (${category}) but no interactive UI is available in this run mode.`,
602
657
  `Reason: ${reason}`,
603
658
  artifacts.length ? `Evidence: ${artifacts.join(", ")}` : undefined,
659
+ ...headlessNotes,
604
660
  "Stop this task now and ask the operator to rerun it interactively so the handoff can be completed.",
605
661
  ].filter(Boolean).join("\n"),
606
662
  }],
@@ -609,8 +665,9 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
609
665
  };
610
666
  }
611
667
 
668
+ if (headlessNotes.length) ctx.ui.notify(headlessNotes.join(" "), "warning");
612
669
  ctx.ui.notify(`Human handoff needed (${category}): ${reason}`, "warning");
613
- ctx.ui.setStatus("agent-browser-handoff", `⏸ Waiting for human: ${reason}`);
670
+ ctx.ui.setStatus("agent-browser-handoff", headlessNotes.length ? `⏸ ブラウザ非表示 — ${reason}` : `⏸ Waiting for human: ${reason}`);
614
671
  let choice: string | undefined;
615
672
  try {
616
673
  choice = await ctx.ui.select(
@@ -626,6 +683,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
626
683
  content: [{
627
684
  type: "text",
628
685
  text: [
686
+ ...headlessNotes,
629
687
  resumed
630
688
  ? "Human confirmed the handoff step is complete. Take a fresh agent_browser_snapshot before continuing."
631
689
  : "Human aborted the task during handoff. Stop and report back; do not retry automatically.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@53able/pi-agent-browser",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Typed Pi tools for browser automation, rendered-page context extraction, screenshots, and saved session state via agent-browser.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,6 +36,10 @@ Two tools exist so the agent stops and hands control back to a human instead of
36
36
 
37
37
  `agent_browser_read` and `agent_browser_snapshot` also run a lightweight heuristic scan for 2FA/CAPTCHA/identity-verification keywords and prepend a warning to their output when detected — treat that warning as a signal to call `agent_browser_handoff` next, not as a blocker by itself.
38
38
 
39
+ ### If the human can't find the browser window
40
+
41
+ `agent_browser_open` records whether each named session was launched with `headed: true`. When `agent_browser_handoff` runs for a session that was not opened headed, it automatically prepends a warning naming the session and the exact `agent_browser_open` arguments (including the last known URL) needed to reopen it visibly, instead of leaving the human to guess. Prefer `headed: true` from the start for any task that might hit a login, 2FA, CAPTCHA, or identity-verification step — a human can only intervene through `agent_browser_handoff` if a visible window already exists. Note that some verification challenges are single-use or session-bound, so reopening may require restarting the login flow rather than resuming mid-challenge.
42
+
39
43
  ## Common examples
40
44
 
41
45
  Read a public page: