publishport-opencli 1.0.40 → 1.0.42
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/clis/xiaohongshu/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{AuthRequiredError as n,CommandExecutionError as l}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as m}from"../_shared/site-auth.js";async function i(o){const
|
|
1
|
+
import{AuthRequiredError as n,CommandExecutionError as l}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as m}from"../_shared/site-auth.js";async function i(o){const a=await o.getCookies({url:"https://creator.xiaohongshu.com"}),r=new Set(a.filter((e)=>e.value).map((e)=>e.name));return r.has("web_session")&&(r.has("x-user-id-creator.xiaohongshu.com")||r.has("id_token"))}async function c(o){await o.goto("https://creator.xiaohongshu.com/new/home");const a=await o.evaluate(`
|
|
2
2
|
async () => {
|
|
3
3
|
try {
|
|
4
4
|
const resp = await fetch('/api/galaxy/creator/home/personal_info', { credentials: 'include' });
|
|
@@ -10,4 +10,4 @@ import{AuthRequiredError as n,CommandExecutionError as l}from"@jackwener/opencli
|
|
|
10
10
|
return [false, 0, null, String(error && error.message || error)];
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
`),[
|
|
13
|
+
`),[r,e,s,h]=Array.isArray(a)?a:[];if(!r){const u=s?.msg??h??`HTTP ${e??""}`;throw new n("creator.xiaohongshu.com",`Xiaohongshu creator profile requires login: ${u}`)}const t=s?.data;if(!t)throw new l("Xiaohongshu creator profile returned malformed personal_info payload");return{username:t.name??"",followers:t.fans_count??0}}m({site:"xiaohongshu",domain:"creator.xiaohongshu.com",loginUrl:"https://creator.xiaohongshu.com/",columns:["username","followers"],quickCheck:i,verify:c,poll:async(o)=>{if(!await i(o))throw new n("creator.xiaohongshu.com","Waiting for Xiaohongshu session cookies");return c(o)}});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output budget for `ppcli browser <session> state`.
|
|
3
|
+
*
|
|
4
|
+
* The DOM snapshot has no size bound of its own: it walks viewport + 2000px and
|
|
5
|
+
* prints every surviving node. On a busy page that is 40k–200k characters
|
|
6
|
+
* (GitHub trending ≈ 207k, Reddit ≈ 150k, Juejin ≈ 54k), and an agent pays for
|
|
7
|
+
* all of it on every `state` call even when it only needs the controls in view.
|
|
8
|
+
*
|
|
9
|
+
* Two steps, applied to the printed text only (refs are already annotated in the
|
|
10
|
+
* page, so every `[N]` stays clickable whether or not it is printed):
|
|
11
|
+
*
|
|
12
|
+
* 1. Lossless: drop attribute-less, text-less, ref-less wrapper lines
|
|
13
|
+
* (`<div />`, `<span />`, …) that have at most one child, and pull their
|
|
14
|
+
* subtree up one level. Grouping is untouched — a bare wrapper with two or
|
|
15
|
+
* more children is kept because it is the only thing telling siblings apart.
|
|
16
|
+
* 2. Budget: if the result is still over `maxChars`, keep the header, the tree
|
|
17
|
+
* prefix that fits, and the footer sections (compounds limited to the refs
|
|
18
|
+
* still shown), plus one line saying what was cut and how to reach it.
|
|
19
|
+
*
|
|
20
|
+
* `maxChars <= 0` disables step 2.
|
|
21
|
+
*/
|
|
22
|
+
export declare const DEFAULT_STATE_MAX_CHARS = 12000;
|
|
23
|
+
/** Step 1 — remove single-child anonymous wrappers, re-indenting what they held. */
|
|
24
|
+
export declare function collapseBareWrappers(tree: string[]): string[];
|
|
25
|
+
/** Apply both steps. Unknown formats (no `---` sections) are only cut at a line boundary. */
|
|
26
|
+
export declare function budgetStateText(text: string, maxChars: number, session?: string): string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const DEFAULT_STATE_MAX_CHARS=12000;const B=/^ *<[a-z][a-z0-9-]* \/>$/,m=/\[(\d+)\]</,k=2,M=420;function O(o){let t=0;while(t<o.length&&o[t]===" ")t++;return t}export function collapseBareWrappers(o){const t=o.length,r=o.map(O),l=Array(t).fill(!1);for(let e=0;e<t;e++){if(!B.test(o[e]))continue;let f=0;for(let h=e+1;h<t&&r[h]>r[e];h++)if(r[h]===r[e]+k&&++f>1)break;if(f<=1)l[e]=!0}const i=[],s=[];for(let e=0;e<t;e++){while(s.length&&s[s.length-1]>=r[e])s.pop();if(l[e]){s.push(r[e]);continue}const f=Math.min(r[e],s.length*k);i.push(o[e].slice(f))}return i}function z(o){const t=o.split(`
|
|
2
|
+
`),r=t.indexOf("---");if(r<0)return null;let l=t.indexOf("---",r+1);if(l<0)l=t.length;const i=[];let s=null;for(let e=l;e<t.length;e++)if(t[e]==="---"){s=["---"];i.push(s)}else if(s)s.push(t[e]);return{header:t.slice(0,r+1),tree:t.slice(r+1,l),tail:i}}const p=(o)=>o.reduce((t,r)=>t+r.length+1,0);function A(o,t,r,l){const i=t.length?` (${t.length} with [N] refs)`:"";return`… truncated: ${o} more lines${i} not printed (--max-chars ${r}); those refs still work. To reach them: \`ppcli browser ${l} find --text <keyword>\` (or --role/--name, --css <selector>), \`scroll down\` then \`state\` again, \`extract\` for long text, or \`state --max-chars 0\` for the full tree.`}export function budgetStateText(o,t,r="<session>"){const l=z(o);if(!l){if(t<=0||o.length<=t)return o;const n=o.lastIndexOf(`
|
|
3
|
+
`,t),a=o.slice(0,n>0?n:t),u=o.slice(a.length).split(`
|
|
4
|
+
`).filter(Boolean),b=u.map((c)=>m.exec(c)?.[1]).filter((c)=>!!c).map(Number);return`${a}
|
|
5
|
+
${A(u.length,b,t,r)}`}const i=collapseBareWrappers(l.tree),s=[...l.header,...i,...l.tail.flat()].join(`
|
|
6
|
+
`);if(t<=0||s.length<=t)return s;const e=(n)=>n[1]?.startsWith("compounds (")===!0,f=l.tail.filter((n)=>!e(n)),h=p(l.tail.filter(e).flat()),N=t-p(l.header)-p(f.flat())-M-Math.min(h,Math.floor(t/4)),d=[];let g=0;for(const n of i){if(g+n.length+1>N)break;d.push(n);g+=n.length+1}const w=i.slice(d.length),T=w.map((n)=>m.exec(n)?.[1]).filter((n)=>!!n).map(Number),E=A(w.length,T,t,r),S=new Set(d.map((n)=>m.exec(n)?.[1]).filter(Boolean));let R=t-p(l.header)-g-E.length-1-p(f.flat());const _=l.tail.flatMap((n)=>{if(!e(n))return[n];const a=n.slice(2).filter((c)=>S.has(/^ {2}\[(\d+)\]/.exec(c)?.[1])),u=[];for(const c of a){if(c.length+1>R-80)break;u.push(c);R-=c.length+1}if(!a.length)return[];return[["---",`compounds (${u.length} shown of ${n.length-2}; others: \`find --css <selector>\` returns them inline):`,...u]]});return[...l.header,...d,E,..._.flat()].join(`
|
|
7
|
+
`)}
|
package/dist/src/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import*as A from"node:fs";import*as ge from"node:os";import*as k from"node:path";import{fileURLToPath as po}from"node:url";import{Command as Ne,InvalidArgumentError as Ct,Option as fo}from"commander";import go from"js-yaml";import{findPackageRoot,getBuiltEntryCandidates as ho}from"./package-paths.js";import{fullName as He,getRegistry as Ee,strategyLabel as Et}from"./registry.js";import{serializeCommand as wo,formatArgSummary as yo}from"./serialization.js";import{render as _e}from"./output.js";import{PKG_VERSION as ke}from"./version.js";import{printCompletionScript as bo}from"./completion.js";import{loadExternalClis as qe,executeExternalCli as xo,installExternalCli as So,registerExternalCli as vo,isBinaryInstalled as Ve,formatExternalCliLabel as _t}from"./external.js";import{listOpenCliSkills as Ro,readOpenCliSkill as Co}from"./skills.js";import{registerAllCommands as Eo}from"./commanderAdapter.js";import{classifyAdapter as Ot,formatRootAdapterHelpText as _o,installCommanderNamespaceStructuredHelp as he,installStructuredHelp as Oo,leadingPositionalFromUsage as $o,rootHelpData as No}from"./help.js";import{EXIT_CODES as f,getErrorMessage as U,BrowserConnectError as ko,CliError as Xe,CommandExecutionError as F,toEnvelope as Ao}from"./errors.js";import{TargetError as $t}from"./browser/target-errors.js";import{resolveTargetJs as To,getTextResolvedJs as Po,getValueResolvedJs as Io,getAttributesResolvedJs as Jo,selectResolvedJs as jo,isAutocompleteResolvedJs as Mo}from"./browser/target-resolver.js";import{wrapTopLevelAwait as Do}from"./browser/utils.js";import{buildFindJs as Uo,buildSemanticFindJs as Nt,isFindError as kt}from"./browser/find.js";import{inferShape as At}from"./browser/shape.js";import{assignKeys as Tt}from"./browser/network-key.js";import{DEFAULT_TTL_MS as Pt,findEntry as Go,loadNetworkCache as Fo,saveNetworkCache as Bo}from"./browser/network-cache.js";import{NETWORK_INTERCEPTOR_JS as Ae}from"./browser/network-interceptor.js";import{parseFilter as Lo,shapeMatchesFilter as zo}from"./browser/shape-filter.js";import{buildHtmlTreeJs as Wo}from"./browser/html-tree.js";import{buildExtractHtmlJs as Ho,runExtractFromHtml as qo}from"./browser/extract.js";import{analyzeSite as Vo}from"./browser/analyze.js";import{registerAuthCommands as Xo}from"./commands/auth.js";import{daemonRestart as Ko,daemonStatus as Zo,daemonStop as Yo,daemonWarm as Qo}from"./commands/daemon.js";import{log as Q}from"./logger.js";import{bindTab as er,BrowserCommandError as It,sendCommand as tr}from"./browser/daemon-client.js";import{fetchDaemonStatus as or}from"./browser/daemon-transport.js";import{aliasForContextId as Jt,listManagedProfiles as Ke,loadProfileConfig as rr,renameProfile as nr,resolveProfile as jt,resolveProfileContextId as ir,setDefaultProfile as sr}from"./browser/profile.js";import{createManagedProfile as ar,ensureManagedChrome as Ze,isManagedChromeRunning as cr,removeManagedProfile as lr,stopManagedChrome as dr,resolveManagedOpts as Mt,updateManagedProfileConfig as ur}from"./browser/managed-chrome.js";import{testProxy as mr,parseProxyUrl as Ye}from"./browser/proxy-test.js";import{formatDaemonVersion as pr,isDaemonStale as fr}from"./browser/daemon-version.js";import{DEFAULT_BROWSER_CONNECT_TIMEOUT as Qe}from"./browser/config.js";const et=po(import.meta.url),gr='Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"',Dt=1000;function Te(c,l){if(c===void 0||c===null||c==="")return null;const a=String(c).trim(),p=/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(a);if(!p)return{error:`--${l} must be a duration like 500ms, 30s, 2m, got "${a}"`};const v=Number.parseFloat(p[1]),h=p[2]??"ms";return Math.round(v*(h==="h"?3600000:h==="m"?60000:h==="s"?1000:1))}function tt(c){return typeof c==="number"&&Number.isFinite(c)&&c>0?c:Date.now()}function we(c){if(typeof c!=="number"||!Number.isFinite(c)||c<=0)return;return new Date(c).toISOString()}function ot(c,l,a=Date.now()){const p=l.sinceMs!=null?a-l.sinceMs:void 0,v=l.untilMs!=null?a-l.untilMs:void 0;return c.filter((h)=>{const T=h.timestamp??a;if(p!==void 0&&T<p)return!1;if(v!==void 0&&T>v)return!1;return!0})}export function selectFreshByTimestamp(c,l){const a=c.filter((v)=>Number(v.timestamp??0)>l),p=a.length>0?Math.max(l,...a.map((v)=>Number(v.timestamp??0)).filter(Number.isFinite)):l;return{fresh:a,lastSeenTs:p}}async function ye(c){if(c.readNetworkCapture){const a=await c.readNetworkCapture();if(Array.isArray(a)&&a.length>0)return a.map((p)=>{const v=p.responsePreview??null;let h=null;if(v)try{h=JSON.parse(v)}catch{h=v}const T=typeof p.responseBodyFullSize==="number"?p.responseBodyFullSize:v?v.length:0,E=p.responseBodyTruncated===!0;return{url:p.url||"",method:p.method||"GET",status:p.responseStatus||0,size:T,ct:p.responseContentType||"",body:h,bodyFullSize:T,bodyTruncated:E,timestamp:tt(p.timestamp),...p.channel?{channel:p.channel}:{},...p.direction?{direction:p.direction}:{},...p.eventName?{eventName:String(p.eventName)}:{},...p.eventId?{eventId:String(p.eventId)}:{}}})}const l=await c.evaluate("(function(){ var out = window.__opencli_net || []; window.__opencli_net = []; return JSON.stringify(out); })()");try{return JSON.parse(l).map((p)=>({...p,timestamp:tt(p.timestamp)}))}catch{if(process.env.OPENCLI_VERBOSE)Q.warn(`[network] Failed to parse interceptor buffer: ${typeof l==="string"?l.slice(0,200):String(l)}`);return[]}}const Ut=4000;function hr(c){if(typeof c!=="string")return c??null;return c.length>Ut?`${c.slice(0,Ut)}…[truncated ${c.length} chars]`:c}function Gt(c){return c.filter((l)=>{if(l.channel==="websocket"||l.channel==="eventsource")return!0;const a=l.ct?.toLowerCase()??"";return(a.includes("json")||a.includes("xml")||a.includes("text/plain")||a.includes("javascript"))&&!/\.(js|css|png|jpg|gif|svg|woff|ico|map)(\?|$)/i.test(l.url)&&!/analytics|tracking|telemetry|beacon|pixel|gtag|fbevents/i.test(l.url)})}const wr={invalid_args:f.USAGE_ERROR,invalid_filter:f.USAGE_ERROR,invalid_max_body:f.USAGE_ERROR};function Z(c,l,a={}){console.log(JSON.stringify({error:{code:c,message:l,...a}},null,2));process.exitCode=wr[c]??f.GENERIC_ERROR}const yr="Site sitemap available. For navigation context, use the opencli-browser-sitemap skill; treat browser state as truth if it disagrees.";function br(c,l=Ee()){let a;try{a=new URL(c).hostname.toLowerCase().replace(/^www\./,"")}catch{return[]}const p=new Map;for(const E of l.values()){if(!E.domain)continue;let N=E.domain.toLowerCase().trim();try{N=new URL(N.includes("://")?N:`https://${N}`).hostname.toLowerCase()}catch{N=N.split("/")[0]??N}N=N.replace(/^www\./,"");if(!N)continue;if(a===N||a.endsWith(`.${N}`))p.set(E.site,Math.max(p.get(E.site)??0,N.length))}const v=[...p.entries()].sort((E,N)=>N[1]-E[1]||E[0].localeCompare(N[0])).map(([E])=>E),h=a.split(".").filter(Boolean),T=h.length>=2?h[h.length-2]:h[0];return[...new Set([...v,...T?[T]:[]])]}function Ft(c,l){return c.find((a)=>l(a))}function xr(c,l){const a=c.replace(/[^a-zA-Z0-9_-]+/g,"-");if(!a)return{};const p=k.join(l.homeDir,".ppcli","sites",a);return{local:Ft([k.join(p,"sitemap"),k.join(p,"sitemap.md")],l.fileExists),global:Ft([k.join(l.packageRoot,"sitemaps",a),k.join(l.packageRoot,"sitemaps",`${a}.md`)],l.fileExists)}}export function resolveSitemapAvailabilityForUrl(c,l={}){const a=l.homeDir??ge.homedir(),p=l.packageRoot??findPackageRoot(et),v=l.registry??Ee(),h=l.fileExists??A.existsSync;for(const T of br(c,v)){const E=xr(T,{homeDir:a,packageRoot:p,fileExists:h});if(!E.local&&!E.global)continue;const N=E.local&&E.global?"local+global":E.local?"local":"global";return{site:T,available:!0,source:N,hint:yr,paths:E}}return null}function Bt(c){const l=c.replace(/[^a-zA-Z0-9_-]+/g,"_");return k.join(zt(),"browser-sitemap-hints",`${l}.json`)}function Lt(c){try{const l=JSON.parse(A.readFileSync(Bt(c),"utf-8"));if(l&&typeof l==="object"&&Array.isArray(l.seenSites))return{seenSites:l.seenSites.filter((a)=>typeof a==="string"),updatedAt:typeof l.updatedAt==="string"?l.updatedAt:new Date(0).toISOString()}}catch{}return{seenSites:[],updatedAt:new Date(0).toISOString()}}function Sr(c,l){const a=Lt(c);if(!a.seenSites.includes(l))a.seenSites.push(l);const p=Bt(c);A.mkdirSync(k.dirname(p),{recursive:!0});A.writeFileSync(p,JSON.stringify({seenSites:a.seenSites,updatedAt:new Date().toISOString()}),"utf-8")}function vr(c,l,a){const p=resolveSitemapAvailabilityForUrl(c);if(!p)return null;if(!a.oncePerSession)return p;if(Lt(l).seenSites.includes(p.site))return null;Sr(l,p.site);return p}export function checkSiteMemory(c){const l=k.join(ge.homedir(),".ppcli","sites",c),a=k.join(l,"endpoints.json"),p=k.join(l,"notes.md");let v=0,h=A.existsSync(a);if(h)try{const E=JSON.parse(A.readFileSync(a,"utf-8"));if(E&&typeof E==="object"&&!Array.isArray(E))v=Object.keys(E).length;else if(Array.isArray(E))v=E.length}catch{h=!1}const T=A.existsSync(p);return{ok:h&&v>0&&T,siteDir:l,endpoints:{present:h,count:v,path:a},notes:{present:T,path:p}}}export function printSiteMemoryReport(c,l){if(c.ok){console.log(` ✓ Memory: endpoints.json (${c.endpoints.count}), notes.md present at ${c.siteDir}`);return}const a=l?"✗":"⚠",p=[];if(!c.endpoints.present)p.push("endpoints.json");else if(c.endpoints.count===0)p.push("endpoints.json (empty)");if(!c.notes.present)p.push("notes.md");console.log(` ${a} Memory: missing ${p.join(", ")} under ${c.siteDir}`);console.log(" Write the endpoint you just verified + a 1-line session note so the next agent starts from minute 0, not minute 95.");if(!l)console.log(" (Re-run with --strict-memory to fail instead of warn.)")}export function normalizeVerifyRows(c){if(Array.isArray(c))return c.map((l)=>l&&typeof l==="object"?l:{value:l});if(c&&typeof c==="object"){const l=c;for(const a of["rows","items","data","results"])if(Array.isArray(l[a]))return l[a].map((p)=>p&&typeof p==="object"?p:{value:p});return[l]}return[]}export function renderVerifyPreview(c,l={}){const a=l.maxRows??10,p=l.maxCols??6,v=l.cellMax??40;if(c.length===0)return" (no rows)";const h=Array.from(new Set(c.flatMap((I)=>Object.keys(I)))),T=h.slice(0,p),E=c.slice(0,a),N=(I)=>{if(I===null||I===void 0)return"";return(typeof I==="object"?JSON.stringify(I):String(I)).replace(/\s+/g," ").slice(0,v)},te=T.map((I)=>Math.max(I.length,...E.map((K)=>N(K[I]).length))),Y=(I)=>I.map((K,x)=>K.padEnd(te[x])).join(" "),X=[];X.push(` ${Y(T)}`);X.push(` ${te.map((I)=>"-".repeat(I)).join(" ")}`);for(const I of E)X.push(` ${Y(T.map((K)=>N(I[K])))}`);if(c.length>a)X.push(` ... and ${c.length-a} more row(s)`);if(h.length>p)X.push(` (${h.length-p} more column(s) hidden)`);return X.join(`
|
|
2
|
-
`)}function
|
|
1
|
+
import*as A from"node:fs";import*as ge from"node:os";import*as k from"node:path";import{fileURLToPath as fo}from"node:url";import{Command as Ne,InvalidArgumentError as Et,Option as go}from"commander";import ho from"js-yaml";import{findPackageRoot,getBuiltEntryCandidates as wo}from"./package-paths.js";import{fullName as He,getRegistry as Ee,strategyLabel as _t}from"./registry.js";import{serializeCommand as yo,formatArgSummary as bo}from"./serialization.js";import{render as _e}from"./output.js";import{PKG_VERSION as ke}from"./version.js";import{printCompletionScript as xo}from"./completion.js";import{loadExternalClis as qe,executeExternalCli as So,installExternalCli as vo,registerExternalCli as Ro,isBinaryInstalled as Ve,formatExternalCliLabel as Ot}from"./external.js";import{listOpenCliSkills as Co,readOpenCliSkill as Eo}from"./skills.js";import{registerAllCommands as _o}from"./commanderAdapter.js";import{classifyAdapter as $t,formatRootAdapterHelpText as Oo,installCommanderNamespaceStructuredHelp as he,installStructuredHelp as $o,leadingPositionalFromUsage as No,rootHelpData as ko}from"./help.js";import{EXIT_CODES as f,getErrorMessage as U,BrowserConnectError as Ao,CliError as Xe,CommandExecutionError as F,toEnvelope as To}from"./errors.js";import{TargetError as Nt}from"./browser/target-errors.js";import{resolveTargetJs as Po,getTextResolvedJs as Io,getValueResolvedJs as Jo,getAttributesResolvedJs as jo,selectResolvedJs as Mo,isAutocompleteResolvedJs as Do}from"./browser/target-resolver.js";import{wrapTopLevelAwait as Uo}from"./browser/utils.js";import{buildFindJs as Go,buildSemanticFindJs as kt,isFindError as At}from"./browser/find.js";import{inferShape as Tt}from"./browser/shape.js";import{assignKeys as Pt}from"./browser/network-key.js";import{DEFAULT_TTL_MS as It,findEntry as Fo,loadNetworkCache as Bo,saveNetworkCache as Lo}from"./browser/network-cache.js";import{NETWORK_INTERCEPTOR_JS as Ae}from"./browser/network-interceptor.js";import{parseFilter as zo,shapeMatchesFilter as Wo}from"./browser/shape-filter.js";import{buildHtmlTreeJs as Ho}from"./browser/html-tree.js";import{buildExtractHtmlJs as qo,runExtractFromHtml as Vo}from"./browser/extract.js";import{analyzeSite as Xo}from"./browser/analyze.js";import{registerAuthCommands as Ko}from"./commands/auth.js";import{daemonRestart as Zo,daemonStatus as Yo,daemonStop as Qo,daemonWarm as er}from"./commands/daemon.js";import{log as Q}from"./logger.js";import{bindTab as tr,BrowserCommandError as Jt,sendCommand as or}from"./browser/daemon-client.js";import{fetchDaemonStatus as rr}from"./browser/daemon-transport.js";import{aliasForContextId as jt,listManagedProfiles as Ke,loadProfileConfig as nr,renameProfile as ir,resolveProfile as Mt,resolveProfileContextId as sr,setDefaultProfile as ar}from"./browser/profile.js";import{createManagedProfile as cr,ensureManagedChrome as Ze,isManagedChromeRunning as lr,removeManagedProfile as dr,stopManagedChrome as ur,resolveManagedOpts as Dt,updateManagedProfileConfig as mr}from"./browser/managed-chrome.js";import{testProxy as pr,parseProxyUrl as Ye}from"./browser/proxy-test.js";import{formatDaemonVersion as fr,isDaemonStale as gr}from"./browser/daemon-version.js";import{DEFAULT_BROWSER_CONNECT_TIMEOUT as Qe}from"./browser/config.js";import{budgetStateText as hr,DEFAULT_STATE_MAX_CHARS as Ut}from"./browser/state-budget.js";const et=fo(import.meta.url),wr='Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"',Gt=1000;function Te(c,l){if(c===void 0||c===null||c==="")return null;const a=String(c).trim(),p=/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(a);if(!p)return{error:`--${l} must be a duration like 500ms, 30s, 2m, got "${a}"`};const v=Number.parseFloat(p[1]),h=p[2]??"ms";return Math.round(v*(h==="h"?3600000:h==="m"?60000:h==="s"?1000:1))}function tt(c){return typeof c==="number"&&Number.isFinite(c)&&c>0?c:Date.now()}function we(c){if(typeof c!=="number"||!Number.isFinite(c)||c<=0)return;return new Date(c).toISOString()}function ot(c,l,a=Date.now()){const p=l.sinceMs!=null?a-l.sinceMs:void 0,v=l.untilMs!=null?a-l.untilMs:void 0;return c.filter((h)=>{const T=h.timestamp??a;if(p!==void 0&&T<p)return!1;if(v!==void 0&&T>v)return!1;return!0})}export function selectFreshByTimestamp(c,l){const a=c.filter((v)=>Number(v.timestamp??0)>l),p=a.length>0?Math.max(l,...a.map((v)=>Number(v.timestamp??0)).filter(Number.isFinite)):l;return{fresh:a,lastSeenTs:p}}async function ye(c){if(c.readNetworkCapture){const a=await c.readNetworkCapture();if(Array.isArray(a)&&a.length>0)return a.map((p)=>{const v=p.responsePreview??null;let h=null;if(v)try{h=JSON.parse(v)}catch{h=v}const T=typeof p.responseBodyFullSize==="number"?p.responseBodyFullSize:v?v.length:0,E=p.responseBodyTruncated===!0;return{url:p.url||"",method:p.method||"GET",status:p.responseStatus||0,size:T,ct:p.responseContentType||"",body:h,bodyFullSize:T,bodyTruncated:E,timestamp:tt(p.timestamp),...p.channel?{channel:p.channel}:{},...p.direction?{direction:p.direction}:{},...p.eventName?{eventName:String(p.eventName)}:{},...p.eventId?{eventId:String(p.eventId)}:{}}})}const l=await c.evaluate("(function(){ var out = window.__opencli_net || []; window.__opencli_net = []; return JSON.stringify(out); })()");try{return JSON.parse(l).map((p)=>({...p,timestamp:tt(p.timestamp)}))}catch{if(process.env.OPENCLI_VERBOSE)Q.warn(`[network] Failed to parse interceptor buffer: ${typeof l==="string"?l.slice(0,200):String(l)}`);return[]}}const Ft=4000;function yr(c){if(typeof c!=="string")return c??null;return c.length>Ft?`${c.slice(0,Ft)}…[truncated ${c.length} chars]`:c}function Bt(c){return c.filter((l)=>{if(l.channel==="websocket"||l.channel==="eventsource")return!0;const a=l.ct?.toLowerCase()??"";return(a.includes("json")||a.includes("xml")||a.includes("text/plain")||a.includes("javascript"))&&!/\.(js|css|png|jpg|gif|svg|woff|ico|map)(\?|$)/i.test(l.url)&&!/analytics|tracking|telemetry|beacon|pixel|gtag|fbevents/i.test(l.url)})}const br={invalid_args:f.USAGE_ERROR,invalid_filter:f.USAGE_ERROR,invalid_max_body:f.USAGE_ERROR};function Z(c,l,a={}){console.log(JSON.stringify({error:{code:c,message:l,...a}},null,2));process.exitCode=br[c]??f.GENERIC_ERROR}const xr="Site sitemap available. For navigation context, use the opencli-browser-sitemap skill; treat browser state as truth if it disagrees.";function Sr(c,l=Ee()){let a;try{a=new URL(c).hostname.toLowerCase().replace(/^www\./,"")}catch{return[]}const p=new Map;for(const E of l.values()){if(!E.domain)continue;let N=E.domain.toLowerCase().trim();try{N=new URL(N.includes("://")?N:`https://${N}`).hostname.toLowerCase()}catch{N=N.split("/")[0]??N}N=N.replace(/^www\./,"");if(!N)continue;if(a===N||a.endsWith(`.${N}`))p.set(E.site,Math.max(p.get(E.site)??0,N.length))}const v=[...p.entries()].sort((E,N)=>N[1]-E[1]||E[0].localeCompare(N[0])).map(([E])=>E),h=a.split(".").filter(Boolean),T=h.length>=2?h[h.length-2]:h[0];return[...new Set([...v,...T?[T]:[]])]}function Lt(c,l){return c.find((a)=>l(a))}function vr(c,l){const a=c.replace(/[^a-zA-Z0-9_-]+/g,"-");if(!a)return{};const p=k.join(l.homeDir,".ppcli","sites",a);return{local:Lt([k.join(p,"sitemap"),k.join(p,"sitemap.md")],l.fileExists),global:Lt([k.join(l.packageRoot,"sitemaps",a),k.join(l.packageRoot,"sitemaps",`${a}.md`)],l.fileExists)}}export function resolveSitemapAvailabilityForUrl(c,l={}){const a=l.homeDir??ge.homedir(),p=l.packageRoot??findPackageRoot(et),v=l.registry??Ee(),h=l.fileExists??A.existsSync;for(const T of Sr(c,v)){const E=vr(T,{homeDir:a,packageRoot:p,fileExists:h});if(!E.local&&!E.global)continue;const N=E.local&&E.global?"local+global":E.local?"local":"global";return{site:T,available:!0,source:N,hint:xr,paths:E}}return null}function zt(c){const l=c.replace(/[^a-zA-Z0-9_-]+/g,"_");return k.join(Ht(),"browser-sitemap-hints",`${l}.json`)}function Wt(c){try{const l=JSON.parse(A.readFileSync(zt(c),"utf-8"));if(l&&typeof l==="object"&&Array.isArray(l.seenSites))return{seenSites:l.seenSites.filter((a)=>typeof a==="string"),updatedAt:typeof l.updatedAt==="string"?l.updatedAt:new Date(0).toISOString()}}catch{}return{seenSites:[],updatedAt:new Date(0).toISOString()}}function Rr(c,l){const a=Wt(c);if(!a.seenSites.includes(l))a.seenSites.push(l);const p=zt(c);A.mkdirSync(k.dirname(p),{recursive:!0});A.writeFileSync(p,JSON.stringify({seenSites:a.seenSites,updatedAt:new Date().toISOString()}),"utf-8")}function Cr(c,l,a){const p=resolveSitemapAvailabilityForUrl(c);if(!p)return null;if(!a.oncePerSession)return p;if(Wt(l).seenSites.includes(p.site))return null;Rr(l,p.site);return p}export function checkSiteMemory(c){const l=k.join(ge.homedir(),".ppcli","sites",c),a=k.join(l,"endpoints.json"),p=k.join(l,"notes.md");let v=0,h=A.existsSync(a);if(h)try{const E=JSON.parse(A.readFileSync(a,"utf-8"));if(E&&typeof E==="object"&&!Array.isArray(E))v=Object.keys(E).length;else if(Array.isArray(E))v=E.length}catch{h=!1}const T=A.existsSync(p);return{ok:h&&v>0&&T,siteDir:l,endpoints:{present:h,count:v,path:a},notes:{present:T,path:p}}}export function printSiteMemoryReport(c,l){if(c.ok){console.log(` ✓ Memory: endpoints.json (${c.endpoints.count}), notes.md present at ${c.siteDir}`);return}const a=l?"✗":"⚠",p=[];if(!c.endpoints.present)p.push("endpoints.json");else if(c.endpoints.count===0)p.push("endpoints.json (empty)");if(!c.notes.present)p.push("notes.md");console.log(` ${a} Memory: missing ${p.join(", ")} under ${c.siteDir}`);console.log(" Write the endpoint you just verified + a 1-line session note so the next agent starts from minute 0, not minute 95.");if(!l)console.log(" (Re-run with --strict-memory to fail instead of warn.)")}export function normalizeVerifyRows(c){if(Array.isArray(c))return c.map((l)=>l&&typeof l==="object"?l:{value:l});if(c&&typeof c==="object"){const l=c;for(const a of["rows","items","data","results"])if(Array.isArray(l[a]))return l[a].map((p)=>p&&typeof p==="object"?p:{value:p});return[l]}return[]}export function renderVerifyPreview(c,l={}){const a=l.maxRows??10,p=l.maxCols??6,v=l.cellMax??40;if(c.length===0)return" (no rows)";const h=Array.from(new Set(c.flatMap((I)=>Object.keys(I)))),T=h.slice(0,p),E=c.slice(0,a),N=(I)=>{if(I===null||I===void 0)return"";return(typeof I==="object"?JSON.stringify(I):String(I)).replace(/\s+/g," ").slice(0,v)},te=T.map((I)=>Math.max(I.length,...E.map((K)=>N(K[I]).length))),Y=(I)=>I.map((K,x)=>K.padEnd(te[x])).join(" "),X=[];X.push(` ${Y(T)}`);X.push(` ${te.map((I)=>"-".repeat(I)).join(" ")}`);for(const I of E)X.push(` ${Y(T.map((K)=>N(I[K])))}`);if(c.length>a)X.push(` ... and ${c.length-a} more row(s)`);if(h.length>p)X.push(` (${h.length-p} more column(s) hidden)`);return X.join(`
|
|
2
|
+
`)}function Ht(){return process.env.OPENCLI_CACHE_DIR||k.join(ge.homedir(),".ppcli","cache")}function qt(c){const l=c.replace(/[^a-zA-Z0-9_-]+/g,"_");return k.join(Ht(),"browser-state",`${l}.json`)}function Vt(c){try{const l=A.readFileSync(qt(c),"utf-8"),a=JSON.parse(l);return a&&typeof a==="object"?a:null}catch{return null}}function be(c,l){const a=qt(l);if(!c){A.rmSync(a,{force:!0});return}A.mkdirSync(k.dirname(a),{recursive:!0});A.writeFileSync(a,JSON.stringify({defaultPage:c,updatedAt:new Date().toISOString()}),"utf-8")}function Er(c,l){return c.some((a)=>{return typeof a==="object"&&a!==null&&"page"in a&&typeof a.page==="string"&&a.page===l})}async function rt(c,l,a){const p=l.trim();if(!p)return;let v;try{v=await c.tabs()}catch(h){if(a.source==="saved"){be(void 0,a.scope);return}throw Error(`Target tab ${p} could not be validated in the current browser session. The Browser Bridge session may have restarted; re-run "ppcli browser tab list" and choose a current target.`,{cause:h})}if(Array.isArray(v)&&Er(v,p))return p;if(a.source==="saved"){be(void 0,a.scope);return}throw Error(`Target tab ${p} is not part of the current browser session. The Browser Bridge session may have restarted; re-run "ppcli browser tab list" and choose a current target.`)}function Pe(c,l){return l?`${l}:${c}`:c}async function _r(c,l){const a=Vt(l)?.defaultPage?.trim();if(!a)return;return rt(c,a,{scope:l,source:"saved"})}async function Or(c,l,a,p={}){if(p.adapter&&!/^site:[a-z0-9][a-z0-9_-]*$/.test(c))throw new F("--adapter requires the existing adapter session name: site:<site>, e.g. site:douyin");const v=p.adapter?"adapter":"browser",h=Mt(p.profile);if(h.kind==="managed"){if(l)throw Error(`托管环境「${h.name}」是单窗口复用标签模型,不支持 --tab 定位。直接省略 --tab 即可。`);const K=p.windowMode??nt(void 0,"background"),{ensureManagedChrome:x}=await import("./browser/managed-chrome.js"),{CDPBridge:Oe}=await import("./browser/cdp.js"),{generateFingerprintJs:le}=await import("./browser/fingerprint.js"),xe=await x(h.name,h.entry,{windowMode:K}),oe=h.entry.fingerprint,ie=h.entry.proxy,Se=new Oe;return{page:await Se.connect({timeout:Qe,session:c,surface:v,...p.adapter?{siteSession:"persistent"}:{},cdpEndpoint:xe,contextId:h.name,windowMode:K,...oe?.enabled?{initScript:le(oe)}:{},...ie?.username?{proxyAuth:{username:ie.username,password:ie.password??""}}:{}}),close:()=>Se.close()}}const{BrowserBridge:T}=await import("./browser/index.js"),E=new T,N=process.env.OPENCLI_BROWSER_IDLE_TIMEOUT,te=N?parseInt(N,10):void 0,Y=await E.connect({timeout:Qe,session:c,surface:v,...p.adapter?{siteSession:"persistent"}:{},...a&&{contextId:a},...te&&te>0&&{idleTimeout:te},windowMode:p.windowMode??nt(void 0,"background")}),X=Pe(p.adapter?`adapter:${c}`:c,a),I=l?await rt(Y,l,{scope:X,source:"explicit"}):await _r(Y,X);if(I){if(!Y.setActivePage)throw Error("This browser session does not support explicit tab targeting");Y.setActivePage(I)}return{page:Y,close:()=>E.close()}}function nt(c,l){const a=Ie(c,"window");if(a!==void 0&&a!==""){if(a==="foreground"||a==="background")return a;throw Error(`--window must be one of: foreground, background. Received: "${String(a)}"`)}const p=process.env.OPENCLI_WINDOW;if(p!==void 0&&p!==""){if(p==="foreground"||p==="background")return p;throw Error(`OPENCLI_WINDOW must be one of: foreground, background. Received: "${p}"`)}return l}function C(c){return c.option("--tab <targetId>",wr)}function $r(c){if(!c)return;const l=c.optsWithGlobals?c.optsWithGlobals():c.opts();return typeof l.tab==="string"&&l.tab.trim()?l.tab.trim():void 0}function Ie(c,l){let a=c;while(a){const p=a.opts();if(Object.prototype.hasOwnProperty.call(p,l)&&p[l]!==void 0)return p[l];a=a.parent}return}function it(c){const l=Ie(c,"session");if(typeof l==="string"&&l.trim())return l.trim();throw Error("<session> is a required positional argument: ppcli browser <session> <command>")}function Xt(c){return sr(st(c))}function st(c){const l=Ie(c,"profile");return typeof l==="string"&&l.trim()?l.trim():void 0}function at(c){const l=c.session;if(typeof l==="string"&&l.trim())return l.trim();throw Error("Browser page is missing a session")}function Je(c){const l=c.contextId;return Pe(at(c),typeof l==="string"&&l.trim()?l.trim():void 0)}function Nr(c){return typeof c==="string"?c:JSON.stringify(c,null,2)}function kr(c,l){const a=Nr(c),p=a.match(/^interactive:\s*(\d+)\s*$/m);return{ok:!0,chars:a.length,bytes:Buffer.byteLength(a,"utf8"),lines:a?a.split(/\r?\n/).length:0,approx_tokens:Math.ceil(a.length/4),refs:(a.match(/(^|\n)\s*\[\d+\]/g)??[]).length,frame_sections:(a.match(/(^|\n)frame /g)??[]).length,...p?{interactive:Number(p[1])}:{},elapsed_ms:l}}async function Kt(c,l){const a=Date.now();try{const p=await c.snapshot({viewportExpand:2000,source:l});return kr(p,Date.now()-a)}catch(p){return{ok:!1,elapsed_ms:Date.now()-a,error:{...p instanceof Error&&"code"in p?{code:String(p.code)}:{},message:p instanceof Error?p.message:String(p)}}}}function Zt(c,l){if(typeof c==="string"&&c.trim())return c.trim();const a=l instanceof Ne?l.opts().tab:l?.tab;if(typeof a==="string"&&a.trim())return a.trim();return}function Yt(c,l,a){if(c===void 0)return a;const p=parseInt(c,10);if(Number.isNaN(p)||p<=0){console.error(`[cli] Invalid ${l}="${c}", using default ${a}`);return a}return p}function Qt(c,l){if(!/^\d+$/.test(c))throw new Et(`--${l} must be a positive integer (got "${c}")`);const a=parseInt(c,10);if(a<=0)throw new Et(`--${l} must be a positive integer (got "${c}")`);return a}function Ar(c){if(c.verbose)process.env.OPENCLI_VERBOSE="1"}function Tr(c){return[...new Set(c.commands.map((l)=>l.name()))].sort((l,a)=>l.localeCompare(a)).join(", ")}function Pr(c){for(const l of c.commands){if(l.commands.length===0)continue;const a=Tr(l);if(a)l.description(a)}}export function createProgram(c,l){const a=new Ne;a.name("ppcli").description("Make any website your CLI. Zero setup. AI-powered.").version(ke).option("--profile <name>","Chrome profile/context alias for Browser Bridge commands").enablePositionalOptions();a.command("list").description("List all available CLI commands").option("-f, --format <fmt>","Output format: table, json, yaml, md, csv","table").action((e)=>{const t=Ee(),o=[...new Set(t.values())].sort((m,g)=>He(m).localeCompare(He(g))),r=e.format,n=r==="json"||r==="yaml";if(r!=="table"){const m=n?o.map(yo):o.map((g)=>({command:He(g),site:g.site,name:g.name,aliases:g.aliases?.join(", ")??"",description:g.description,access:g.access,strategy:_t(g),browser:!!g.browser,args:bo(g.args)}));_e(m,{fmt:r,columns:["command","site","name","aliases","description","access","strategy","browser","args",...n?["columns","domain"]:[]],title:"ppcli/list",source:"ppcli list"});return}const i=new Map,s=new Map;for(const m of o){const g=$t(m.domain)==="app"?i:s,b=g.get(m.site)??[];b.push(m);g.set(m.site,b)}const d=(m,g)=>{console.log(` ${m}`);for(const b of g){const R=_t(b),P=R==="public"?"[public]":`[${R}]`,G=b.aliases?.length?` (aliases: ${b.aliases.join(", ")})`:"";console.log(` ${b.name} ${P}${G}${b.description?` — ${b.description}`:""}`)}console.log()};console.log();console.log(" ppcli"+" — available commands");console.log();if(i.size>0){console.log(" App adapters");console.log();for(const[m,g]of i)d(m,g)}if(s.size>0){console.log(" Site adapters");console.log();for(const[m,g]of s)d(m,g)}const u=qe();if(u.length>0){console.log(" external CLIs");for(const m of u){const b=Ve(m.binary)?"[installed]":"[auto-install]";console.log(` ${Ot(m)} ${b}${m.description?` — ${m.description}`:""}`)}console.log()}console.log(` ${o.length} built-in commands across ${i.size} apps + ${s.size} sites, ${u.length} external CLIs`);console.log()});a.command("validate").description("Validate CLI definitions").argument("[target]","site or site/name").action(async(e)=>{const{validateClisWithTarget:t,renderValidationReport:o}=await import("./validate.js");console.log(o(t([c,l],e)))});a.command("verify").description("Validate + smoke test").argument("[target]").option("--smoke","Run smoke tests",!1).action(async(e,t)=>{const{verifyClis:o,renderVerifyReport:r}=await import("./verify.js"),n=await o({builtinClis:c,userClis:l,target:e,smoke:t.smoke});console.log(r(n));process.exitCode=n.ok?f.SUCCESS:f.GENERIC_ERROR});const p=a.command("skills").description("Read bundled ppcli skills");p.command("list").description("List bundled ppcli-* skills").option("-f, --format <fmt>","Output format: table, json, yaml, md, csv","table").action((e)=>{const t=Co();_e(t,{fmt:e.format,fmtExplicit:!!e.format,columns:["name","description","version","path"],title:"ppcli/skills/list",source:"ppcli skills list"})});p.command("read").description("Print an ppcli-* skill's SKILL.md or reference file").argument("<skill>","Skill name, or skill/path like opencli-browser/references/foo.md").argument("[path]","Path under the skill directory").option("--json","Output a JSON envelope instead of raw markdown",!1).action((e,t,o)=>{let r;try{r=Eo(e,t??"")}catch(n){console.error(`Error: ${U(n)}`);if(n instanceof Xe&&n.hint)console.error(`Hint: ${n.hint}`);process.exitCode=n instanceof Xe?n.exitCode:f.GENERIC_ERROR;return}if(o.json){console.log(JSON.stringify(r,null,2));return}process.stdout.write(r.content);if(!r.content.endsWith(`
|
|
3
3
|
`))process.stdout.write(`
|
|
4
|
-
`)});const v=
|
|
4
|
+
`)});const v=Ko(a);a.command("convention-audit").description("Scan adapters for agent-native convention violations").argument("[target]","site or site/name").option("--site <site>","Limit audit to one site").option("-f, --format <fmt>","Output format: table, json, yaml","table").option("--strict","Exit non-zero when violations are found",!1).action(async(e,t)=>{const{runConventionAudit:o,renderConventionAuditText:r}=await import("./convention-audit.js"),n=o({projectRoot:findPackageRoot(et),target:e,site:t.site}),i=String(t.format??"table").toLowerCase();if(i==="json"||i==="yaml"||i==="yml")_e(n,{fmt:i});else console.log(r(n));if(t.strict&&!n.ok)process.exitCode=f.GENERIC_ERROR});const h=a.command("browser").addOption(new go("--session <name>","Internal — set automatically from the <session> positional").hideHelp()).option("--window <mode>","Browser window mode: foreground or background").description("Browser control — navigate, click, type, extract, wait (no LLM needed)").usage("<session> <command> [options]").addHelpText("after",`
|
|
5
5
|
<session> is a required positional: pass the name of the browser session every subcommand should operate on. Reuse the same name across calls to keep the tab/state alive; pick a different name to isolate parallel browser work.
|
|
6
6
|
|
|
7
7
|
Examples:
|
|
@@ -11,8 +11,8 @@ Examples:
|
|
|
11
11
|
$ ppcli browser work state
|
|
12
12
|
$ ppcli browser work bind
|
|
13
13
|
$ ppcli browser work unbind
|
|
14
|
-
`),T=h.description();async function E(e,t,o={}){const r=await e.evaluate(
|
|
15
|
-
`);console.log(
|
|
14
|
+
`),T=h.description();async function E(e,t,o={}){const r=await e.evaluate(Po(t,o));if(!r.ok)throw new Nt({code:r.code,message:r.message,hint:r.hint,candidates:r.candidates,matches_n:r.matches_n});return{matches_n:r.matches_n,match_level:r.match_level}}function N(e){if(e===void 0||e===null||e==="")return null;const t=String(e);if(!/^\d+$/.test(t))return{error:`--nth must be a non-negative integer, got "${t}"`};return Number.parseInt(t,10)}function te(e){console.log(JSON.stringify({error:{code:e.code,message:e.message,hint:e.hint,...e.candidates&&{candidates:e.candidates},...e.matches_n!==void 0&&{matches_n:e.matches_n}}},null,2))}function Y(e){return e.toLowerCase().includes("javascript dialog")}function X(e){console.log(JSON.stringify({error:{code:"javascript_dialog_open",message:e,hint:"Handle the modal first: ppcli browser dialog accept (or dismiss). Use --text for prompt dialogs."}},null,2))}function I(e){if(!e.code)return;console.log(JSON.stringify({error:{code:e.code,message:e.message,...e.hint?{hint:e.hint}:{}}},null,2))}function K(e){Q.error(e.message);if(e.hint)Q.error(`Hint: ${e.hint}`)}function x(e){return async(...t)=>{let o=null;try{const r=t.at(-1)instanceof Ne?t.at(-1):void 0,n=$r(r),i=it(r),s=Xt(r),d=nt(r,"background");o=await Or(i,n,s,{windowMode:d,profile:st(r),adapter:Ie(r,"adapter")===!0});await e(o.page,...t)}catch(r){if(r instanceof Ao){Q.error(r.message);if(r.hint)Q.error(`Hint: ${r.hint}`)}else if(r instanceof Jt){if(Y(r.message))X(r.message);else I(r);K(r)}else if(r instanceof Nt){te(r);Q.error(`[${r.code}] ${r.message}`);if(r.hint)Q.error(`Hint: ${r.hint}`)}else{const n=U(r);if(Y(n)){X(n);Q.error(n)}else if(n.includes("attach failed")||n.includes("chrome-extension://"))Q.error("Browser attach failed — another extension may be interfering. Try disabling 1Password.");else Q.error(n)}process.exitCode=f.GENERIC_ERROR}finally{await o?.close()}}}function Oe(e,t){return async(o,r)=>{const n=o instanceof Ne?o:r,i=it(n),s=Xt(n);try{if(t?.managedGuardMessage&&Mt(st(n)).kind==="managed")throw Error(t.managedGuardMessage);const{BrowserBridge:d}=await import("./browser/index.js");await new d().connect({timeout:Qe,session:i,surface:"browser",...s&&{contextId:s}});await e({session:i,contextId:s})}catch(d){if(d instanceof Jt){I(d);K(d)}else Q.error(d instanceof Error?d.message:String(d));process.exitCode=f.GENERIC_ERROR}}}h.command("bind").description("Bind the current Chrome tab/window to the browser session named by <session>").action(Oe(async({session:e,contextId:t})=>{const o=await tr(e,{...t&&{contextId:t}});be(void 0,Pe(e,t));console.log(JSON.stringify({session:e,...o&&typeof o==="object"?o:{data:o}},null,2))},{managedGuardMessage:"托管环境不支持 bind:它是独立自动化窗口,直接用 browser open/eval 等命令即可。"}));h.command("unbind").description("Detach the bound browser session named by <session> without closing the user tab/window").action(Oe(async({session:e,contextId:t})=>{await or("close-window",{session:e,surface:"browser",...t&&{contextId:t}});be(void 0,Pe(e,t));console.log(JSON.stringify({unbound:!0,session:e},null,2))},{managedGuardMessage:"托管环境不支持 unbind:它没有绑定到扩展的会话。"}));const le=h.command("tab").description("Tab management — list, create, and close tabs in the browser session");le.command("list").description("List tabs in the browser session with target IDs").action(x(async(e)=>{const t=await e.tabs();console.log(JSON.stringify(t,null,2))}));le.command("new").argument("[url]","Optional URL to open in the new tab").description("Create a new tab and print its target ID").action(x(async(e,t)=>{if(!e.newTab)throw Error("This browser session does not support creating tabs");const o=await e.newTab(t);console.log(JSON.stringify({page:o,url:t??null},null,2))}));C(le.command("select").argument("[targetId]",'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"').description("Select a tab by target ID and make it the default browser tab")).action(x(async(e,t,o)=>{const r=Zt(t,o);if(!r)throw Error("Target tab required. Pass it as an argument or --tab <targetId>.");await e.selectTab(r);be(r,Je(e));console.log(JSON.stringify({selected:r},null,2))}));C(le.command("close").argument("[targetId]",'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"').description("Close a tab by target ID")).action(x(async(e,t,o)=>{const r=Zt(t,o);if(!e.closeTab)throw Error("This browser session does not support closing tabs");if(!r)throw Error("Target tab required. Pass it as an argument or --tab <targetId>.");const n=await rt(e,r,{scope:Je(e),source:"explicit"});if(!n)throw Error(`Target tab ${r} is not part of the current browser session.`);await e.closeTab(n);const i=Je(e);if(Vt(i)?.defaultPage===n)be(void 0,i);console.log(JSON.stringify({closed:n},null,2))}));C(h.command("open").argument("<url>").description("Open URL in the browser session")).action(x(async(e,t,o)=>{const r=await e.startNetworkCapture?.()??!1;await e.goto(t);await e.wait(2);if(!r)try{await e.evaluate(Ae)}catch{}const n=await e.getCurrentUrl?.()??t,i=Cr(n,Je(e),{oncePerSession:!0});console.log(JSON.stringify({url:n,...e.getActivePage?.()?{page:e.getActivePage?.()}:{},...i?{sitemap:i}:{}},null,2))}));C(h.command("back").description("Go back in browser history")).action(x(async(e,t)=>{await e.evaluate("history.back()");await e.wait(2);console.log("Navigated back")}));C(h.command("scroll").argument("<direction>","up or down").option("--amount <pixels>","Pixels to scroll","500")).description("Scroll page").action(x(async(e,t,o)=>{if(t!=="up"&&t!=="down"){console.error(`Invalid direction "${t}". Use "up" or "down".`);process.exitCode=f.USAGE_ERROR;return}await e.scroll(t,parseInt(o.amount,10));console.log(`Scrolled ${t}`)}));C(h.command("state").description("Page state: URL, title, interactive elements with [N] indices").option("--source <source>","Snapshot backend: dom (default) or ax prototype","dom").option("--compare-sources","Print DOM vs AX snapshot metrics for observation promotion decisions",!1).option("--max-chars <n>","Output budget in characters; past it the tree is cut with a note on how to reach the rest (0 = no limit)",String(Ut))).action(x(async(e,t,o)=>{if(t.compareSources===!0){const[u,m]=await Promise.all([Kt(e,"dom"),Kt(e,"ax")]);console.log(JSON.stringify({url:await e.getCurrentUrl?.()??"",sources:{dom:u,ax:m}},null,2));return}const r=String(t.source??"dom").toLowerCase();if(r!=="dom"&&r!=="ax"){console.log(JSON.stringify({error:{code:"invalid_source",message:`--source must be "dom" or "ax", got "${t.source}"`}},null,2));process.exitCode=f.USAGE_ERROR;return}const n=Number(t.maxChars??Ut);if(!Number.isInteger(n)||n<0){console.log(JSON.stringify({error:{code:"invalid_max_chars",message:`--max-chars must be a non-negative integer, got "${t.maxChars}"`}},null,2));process.exitCode=f.USAGE_ERROR;return}const i=await e.snapshot({viewportExpand:2000,source:r}),s=await e.getCurrentUrl?.()??"",d=typeof i==="string"?i:JSON.stringify(i,null,2);console.log(`URL: ${s}
|
|
15
|
+
`);console.log(hr(d,n,it(o)))}));C(h.command("frames").description("List cross-origin iframe targets in snapshot order")).action(x(async(e)=>{const t=await e.frames?.()??[];console.log(JSON.stringify(t,null,2))}));C(h.command("screenshot").argument("[path]","Save to file (base64 if omitted)")).option("--full-page","Capture the full scrollable page, not just the viewport",!1).option("--annotate","Overlay visible browser state ref labels on the screenshot",!1).option("--width <n>","Override viewport width in CSS pixels for this screenshot only",(e)=>Qt(e,"width")).option("--height <n>","Override viewport height in CSS pixels for this screenshot only (ignored with --full-page)",(e)=>Qt(e,"height")).option("--scroll-to <selector>","Scroll this element into view (centered) before capturing").description("Take screenshot").action(x(async(e,t,o)=>{if(o.scrollTo){const i=JSON.stringify(String(o.scrollTo));if(!await e.evaluate(`(() => { const el = document.querySelector(${i}); if (!el) return false; el.scrollIntoView({ block: 'center', inline: 'center' }); return true; })()`))throw new F(`--scroll-to 没匹配到元素: ${o.scrollTo}`);await e.wait(150)}const r={fullPage:o.fullPage===!0,annotate:o.annotate===!0,width:o.width,height:o.height},n=o.annotate===!0?(e.annotatedScreenshot??e.screenshot).bind(e):e.screenshot.bind(e);if(t){await n({...r,path:t});console.log(`Screenshot saved to: ${t}`)}else console.log(await n({...r,format:"png"}))}));const xe=['iframe[src*="/verifycenter/captcha/"]',".js-mocaptcha-img",".moclickcaptcha-img",".moclickcaptcha-draw",".geetest_item_img",".geetest_widget canvas",".captcha-image img",".captcha img",".captcha canvas",'img[src*="captcha" i]','img[class*="captcha" i]','canvas[class*="captcha" i]'],oe=6;function ie(e){const t=e&&e.trim()?[e.trim()]:xe;return`(() => {
|
|
16
16
|
var sels = ${JSON.stringify(t)};
|
|
17
17
|
var el = null, used = '';
|
|
18
18
|
for (var i = 0; i < sels.length; i++) {
|
|
@@ -71,7 +71,7 @@ Examples:
|
|
|
71
71
|
}
|
|
72
72
|
} catch (e) {}
|
|
73
73
|
return { captcha_present: present, status_text: status, url: (location.href || '').slice(0, 100) };
|
|
74
|
-
})()`}function $e(e,t){const o=Math.max(0,e.x-oe),r=Math.max(0,e.y-oe),n=Math.min(t.w,e.x+e.w+oe)-o,i=Math.min(t.h,e.y+e.h+oe)-r;if(n<=0||i<=0)throw new F("验证码区域不在可见视口内,请重新截图定位");return{x:o,y:r,w:n,h:i}}const je="__pp_captcha_grid";function
|
|
74
|
+
})()`}function $e(e,t){const o=Math.max(0,e.x-oe),r=Math.max(0,e.y-oe),n=Math.min(t.w,e.x+e.w+oe)-o,i=Math.min(t.h,e.y+e.h+oe)-r;if(n<=0||i<=0)throw new F("验证码区域不在可见视口内,请重新截图定位");return{x:o,y:r,w:n,h:i}}const je="__pp_captcha_grid";function eo(e){const t=Math.min(e.w,e.h)>=240?50:Math.min(e.w,e.h)>=100?25:10,o=[],r=(i,s,d,u)=>`<text x="${i}" y="${s}" text-anchor="${u}" font-size="10" font-family="sans-serif" fill="#e00" stroke="#fff" stroke-width="2.5" paint-order="stroke">${d}</text>`;for(let i=t;i<e.w;i+=t)o.push(`<line x1="${i}" y1="0" x2="${i}" y2="${e.h}"/>`,r(i,10,i,"middle"));for(let i=t;i<e.h;i+=t)o.push(`<line x1="0" y1="${i}" x2="${e.w}" y2="${i}"/>`,r(2,i+3,i,"start"));const n=`<svg width="${e.w}" height="${e.h}" style="display:block"><g stroke="rgba(255,0,0,.35)" stroke-width="1">${o.join("")}</g></svg>`;return`(() => {
|
|
75
75
|
document.getElementById(${JSON.stringify(je)})?.remove();
|
|
76
76
|
const div = document.createElement('div');
|
|
77
77
|
div.id = ${JSON.stringify(je)};
|
|
@@ -79,7 +79,7 @@ Examples:
|
|
|
79
79
|
div.innerHTML = ${JSON.stringify(n)};
|
|
80
80
|
document.documentElement.appendChild(div);
|
|
81
81
|
return new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(() => r(true))));
|
|
82
|
-
})()`}async function Me(e,t){const o=await e.evaluate(ie(t));if(!o||!o.found||!o.rect)return null;return{crop:$e(o.rect,o.viewport),scroll:o.scroll??{x:0,y:0},viewport:o.viewport}}function re(e,t,o){const r=String(e??"").split(",").map((n)=>Number(n.trim()));if(r.length!==t||r.some((n)=>!Number.isFinite(n)||n<0))throw new F(`${o} 应为 ${t} 个逗号分隔的非负数(crop 网格刻度,CSS 像素)`);return r}async function De(e,t,o,r){await new Promise((s)=>setTimeout(s,900));let n={};try{n=await e.evaluate(Se(t))}catch(s){n={read_error:U(s)}}const i=o?String(o):void 0;if(i)await e.screenshot({path:i,format:"png"});console.log(JSON.stringify({ok:!0,...r,after:n,...i?{saved:i}:{}},null,2))}C(h.command("captcha").argument("[selector]","验证码图片元素 CSS 选择器(缺省自动探测常见点选验证码)")).option("--adapter","定位已有平台适配器会话;session 必须是 site:<site>,如 site:douyin").option("--save <path>","把全视口截图存到该路径(缺省存到临时 PNG 文件)").option("--scale <n>","验证码区域裁剪图的放大倍数(1..6,缺省 3;放大后认扭曲字更准)","3").option("--grid","在裁剪图上画坐标网格(刻度是裁剪图里的 CSS 像素),给 captcha-slide / captcha-rotate / captcha-drag 读位置用").description("抓取验证码:存整图 + 只含验证码的放大裁剪图 + 返回坐标与提示文字,供 AI 视觉识别后配合 captcha-click / captcha-slide / captcha-rotate / captcha-drag").action(x(async(e,t,o)=>{const r=await e.evaluate(ie(typeof t==="string"?t:void 0));if(!r||!r.found){console.log(JSON.stringify({error:{code:"captcha_not_found",message:'未探测到点选验证码图片。可显式传选择器,如 ppcli browser <s> captcha ".js-mocaptcha-img"',tried:xe}},null,2));process.exitCode=f.USAGE_ERROR;return}const n=o?.save&&String(o.save)||k.join(ge.tmpdir(),`opencli-captcha-${Date.now()}.png`);await e.screenshot({format:"png",path:n});const i=r.rect,s=r.dpr||1,d=$e(i,r.viewport),u=Math.max(1,Math.min(6,Number(o?.scale??3)||3)),m=oe;let g=null;const b=e.cdp;if(typeof b==="function"){const R={x:d.x+(r.scroll?.x??0),y:d.y+(r.scroll?.y??0),width:d.w,height:d.h,scale:u};if(o?.grid)await e.evaluate(Qt(d));const P=await b.call(e,"Page.captureScreenshot",{format:"jpeg",quality:82,clip:R,captureBeyondViewport:!1}).finally(()=>o?.grid?e.evaluate(`document.getElementById(${JSON.stringify(je)})?.remove()`):void 0);if(P&&P.data){g=n.replace(/\.png$/i,"")+".crop.jpg";A.writeFileSync(g,Buffer.from(P.data,"base64"))}}console.log(JSON.stringify({ok:!0,selector:r.selector,iframe:r.iframe===!0,crop_rect_css:g?d:null,prompt:r.prompt||"",crop:g,crop_scale:g?u:null,crop_pad:g?m:null,saved:n,rect_css:i,rect_screenshot_px:{x:Math.round(i.x*s),y:Math.round(i.y*s),w:Math.round(i.w*s),h:Math.round(i.h*s)},dpr:s,natural:r.natural,viewport:r.viewport,...o?.grid?{grid_guide:"网格刻度是 crop 里的 CSS 像素(左上角 0,0),captcha-slide / captcha-rotate / captcha-drag 的坐标都按刻度读,并传同一个 --in 选择器。"}:{},guide:"① read_local_file 打开 crop(只含验证码、已放大)认清每个待点字与顺序(注意会混入不在 prompt 里的干扰字,只点 prompt 要求的)。"+"② 坐标直接读成「在 crop 这张图里的比例」(0..1,左上角为 0,0)——不用管 pad/rect,看到字在 crop 的哪就填哪。"+"③ 执行:ppcli browser <session> captcha-click"+(o.adapter?" --adapter":"")+" --crop --in '"+r.selector.replace(/'/g,"'\\''")+"'"+' "x1,y1 x2,y2 ..."(--crop 表示坐标是 crop 图比例;空格分隔,顺序即点击顺序)。'+"iframe 的提示与确认按钮请从截图读取;点完文字后可能仍需点确认。验证码仍在不代表可以重复点字;先截图检查。"},null,2))}));C(h.command("captcha-click").argument("<points>",'待点击坐标,空格分隔且顺序即点击顺序,如 "0.30,0.46 0.68,0.66"')).option("--adapter","定位已有平台适配器会话;session 必须是 site:<site>,如 site:douyin").option("--save <path>","点击后保存全视口截图,以核对点选标记、确认按钮及验证结果").option("--in <selector>","坐标映射基准元素(缺省自动探测验证码图片)").option("--crop","坐标按「captcha 裁剪图(crop)里的比例」解释(0..1,左上角为0,0)——AI 看哪张就按那张填,内部自动扣掉 pad。推荐").option("--px","坐标按「截图设备像素」解释(自动按 devicePixelRatio 换算成 CSS 像素)").option("--css","坐标按「CSS 视口像素」解释(直接点击,不做映射)").option("--delay <ms>","每次点击之间的间隔毫秒","250").description("按坐标依次点击(点选验证码用):把 AI 从图上读到的位置一次性点上,点完回读验证码是否还在/是否跳转。默认坐标为相对基准元素的比例(0..1),--crop 则为裁剪图比例").action(x(async(e,t,o)=>{const n=String(t??"").trim().split(/\s+/).filter(Boolean).map((y)=>{const S=y.split(",");if(S.length!==2||S.some((D)=>!D.trim()))return{x:NaN,y:NaN};return{x:Number(S[0]),y:Number(S[1])}});if(n.length===0||n.some((y)=>!Number.isFinite(y.x)||!Number.isFinite(y.y))){console.log(JSON.stringify({error:{code:"usage_error",message:'坐标格式应为 "x,y x,y ...",如 "0.30,0.46 0.68,0.66"'}},null,2));process.exitCode=f.USAGE_ERROR;return}if([o?.css,o?.px,o?.crop].filter(Boolean).length>1)throw new F("--css / --px / --crop 不能同时使用");const i=o?.css?"css":o?.px?"px":o?.crop?"crop":"frac";if(n.some((y)=>y.x<0||y.y<0||(i==="frac"||i==="crop")&&(y.x>1||y.y>1)))throw new F("相对坐标必须在 0..1 内,绝对坐标必须非负;尚未点击");const s=Number(o?.delay??250);if(!Number.isFinite(s)||s<0||s>1e4)throw new F("--delay 必须在 0..10000 毫秒内");let d={x:0,y:0,w:0,h:0},u=1,m;if(i==="css"||i==="px"){const y=await e.evaluate("({w: innerWidth, h: innerHeight, dpr: devicePixelRatio})");m=y;u=y.dpr}else{const y=await e.evaluate(ie(typeof o?.in==="string"?o.in:void 0));if(!y||!y.found||!y.rect){console.log(JSON.stringify({error:{code:"captcha_not_found",message:"captcha-click 找不到基准元素,请用 --in <selector> 指定,或用 --css 传绝对坐标"}},null,2));process.exitCode=f.USAGE_ERROR;return}d=y.rect;u=y.dpr||1;m=y.viewport}const g=i==="crop"?$e(d,m):d,b=(y)=>{if(i==="css")return y;if(i==="px")return{x:y.x/u,y:y.y/u};if(i==="crop")return{x:g.x+y.x*g.w,y:g.y+y.y*g.h};return{x:d.x+y.x*d.w,y:d.y+y.y*d.h}},{cdp:R,nativeClick:P}=e,G=n.map(b).map((y)=>({x:Math.round(y.x),y:Math.round(y.y)}));if(G.some((y)=>y.x<0||y.y<0||y.x>=m.w||y.y>=m.h))throw new F("点击坐标超出视口;尚未点击,请重新截图定位");if(typeof R!=="function"&&typeof P!=="function")throw new F("当前浏览器不支持原生坐标点击");const _=(y)=>new Promise((S)=>setTimeout(S,y));async function M(y,S,D,B){if(typeof R==="function"){const ae=12+B%4;for(let q=1;q<=ae;q++){const H=q/ae,ce=H*H*(3-2*H),fe=Math.sin(H*Math.PI)*10*(B%2===0?1:-1),j=D.x+(y-D.x)*ce,V=D.y+(S-D.y)*ce+fe;await R.call(e,"Input.dispatchMouseEvent",{type:"mouseMoved",x:Math.round(j),y:Math.round(V)});await _(8+q%3*6)}await R.call(e,"Input.dispatchMouseEvent",{type:"mouseMoved",x:y,y:S});await _(40+B%3*25);await R.call(e,"Input.dispatchMouseEvent",{type:"mousePressed",x:y,y:S,button:"left",clickCount:1});await _(45+B%4*20);await R.call(e,"Input.dispatchMouseEvent",{type:"mouseReleased",x:y,y:S,button:"left",clickCount:1});return"cdp"}if(typeof P==="function"){await P.call(e,y,S);return"native"}throw new F("当前浏览器不支持原生坐标点击")}const J=[];let W="",ne={x:Math.round(d.x+d.w/2)||400,y:Math.round(d.y-40)||200};for(let y=0;y<G.length;y++){const S=G[y],D=Math.round(S.x),B=Math.round(S.y);W=await M(D,B,ne,y);ne={x:D,y:B};J.push({x:D,y:B});if(y<n.length-1&&s>0)await _(s)}await _(700);let w={};try{w=await e.evaluate(Se(typeof o?.in==="string"?o.in:void 0))}catch(y){w={read_error:U(y)}}const O=o.save?String(o.save):void 0;if(O)await e.screenshot({path:O,format:"png"});console.log(JSON.stringify({ok:!0,mode:i,method:W,clicked_n:J.length,points_css:J,after:w,...O?{saved:O}:{}},null,2))}));const Ue='与 captcha 相同的验证码元素选择器(缺省自动探测;抖音为 iframe[src*="/verifycenter/captcha/"])';C(h.command("captcha-slide")).option("--adapter","定位已有平台适配器会话;session 必须是 site:<site>,如 site:douyin").option("--in <selector>",Ue).requiredOption("--handle <x,y>","滑块把手中心(crop 网格刻度)").requiredOption("--picture <x,y,w,h>","带拼图和缺口的那张图片的区域(crop 网格刻度)").option("--gap <px>","目测拼图大概要往右挪多远(可选,只用来缩小搜索范围)").option("--save <path>","松手后保存全视口截图").description("缺口拼图(「拖动滑块填充拼图」):按住先拖一小段,自己量拖动比例、找缺口,拖到位纠偏后松手").action(x(async(e,t)=>{const o=typeof t?.in==="string"?t.in:void 0,[r,n]=re(t.handle,2,"--handle"),[i,s,d,u]=re(t.picture,4,"--picture");if(d<20||u<20)throw new F("--picture 太小:框的应是整张带缺口的图片");const m=t.gap===void 0?void 0:Number(t.gap);if(m!==void 0&&!Number.isFinite(m))throw new F("--gap 必须是数字");const g=await Me(e,o);if(!g)throw new F("找不到验证码元素:传与 captcha 相同的 --in <selector>");const{solveSlidePuzzle:b}=await import("./browser/captcha-drag.js"),R=await b(e,{x:g.crop.x+r,y:g.crop.y+n,clip:{x:g.crop.x+i+g.scroll.x,y:g.crop.y+s+g.scroll.y,width:d,height:u},hint:m});if(!R)throw new F("没认出拼图和缺口,已拖回原位未提交:重新 captcha --grid 看清 --picture 框的是不是整张图");await De(e,o,t.save,R)}));C(h.command("captcha-rotate")).option("--adapter","定位已有平台适配器会话;session 必须是 site:<site>,如 site:douyin").option("--in <selector>",Ue).requiredOption("--handle <x,y>","滑块把手中心(crop 网格刻度)").requiredOption("--track <px>","把手大概能往右拖多远(工具会自己量轨道,量不出才用它)").requiredOption("--center <x,y>","圆图圆心(crop 网格刻度)").requiredOption("--radius <px>","内圈那块圆图的半径(不含外圈)").option("--save <path>","松手后保存全视口截图").description("旋转拼图(中间圆图和外圈一起转,抖音 / 字节验证中心那种):自己量内外圈差几度,拖到对齐、纠偏后松手").action(x(async(e,t)=>{const o=typeof t?.in==="string"?t.in:void 0,[r,n]=re(t.handle,2,"--handle"),[i,s]=re(t.center,2,"--center"),[d]=re(t.track,1,"--track"),[u]=re(t.radius,1,"--radius");if(u<15)throw new F("--radius 太小:量的是内圈圆图的半径");const m=await Me(e,o);if(!m)throw new F("找不到验证码元素:传与 captcha 相同的 --in <selector>");const g=u*1.5,{solveRotate:b}=await import("./browser/captcha-drag.js"),R=await b(e,{x:m.crop.x+r,y:m.crop.y+n,track:d,clip:{x:m.crop.x+i-g+m.scroll.x,y:m.crop.y+s-g+m.scroll.y,width:g*2,height:g*2},ring:{cx:g,cy:g,r:u},bar:{x:m.crop.x+m.scroll.x,y:m.crop.y+n+m.scroll.y,width:m.crop.w,handleX:r}});await De(e,o,t.save,R)}));C(h.command("captcha-drag")).option("--adapter","定位已有平台适配器会话;session 必须是 site:<site>,如 site:douyin").option("--in <selector>",Ue).requiredOption("--handle <x,y>","滑块把手中心(crop 网格刻度)").requiredOption("--dx <px>","往右拖多远(「拖到最右边」就给轨道剩余长度)").option("--save <path>","松手后保存全视口截图").description("按人手轨迹按住把手拖一段再松开:「请按住滑块拖到最右边」这类;拼图 / 旋转用 captcha-slide / captcha-rotate").action(x(async(e,t)=>{const o=typeof t?.in==="string"?t.in:void 0,[r,n]=re(t.handle,2,"--handle"),[i]=re(t.dx,1,"--dx"),s=await Me(e,o);if(!s)throw new F("找不到验证码元素:传与 captcha 相同的 --in <selector>");const{humanDrag:d}=await import("./browser/captcha-drag.js"),u=await d(e,s.crop.x+r,s.crop.y+n,i);await De(e,o,t.save,u)}));C(h.command("console")).option("--level <level>","Console level: all, error, warning, log, info, debug","all").option("--since <duration>","Only include messages from the last duration (for example: 30s, 2m)").option("--until <duration>","Only include messages older than the duration from now").option("--follow","Continuously print new console messages as JSON lines",!1).description("Read recent browser console messages").action(x(async(e,t)=>{const o=Te(t.since,"since"),r=Te(t.until,"until");if(o&&typeof o==="object"){console.log(JSON.stringify({error:{code:"invalid_since",message:o.error}},null,2));process.exitCode=f.USAGE_ERROR;return}if(r&&typeof r==="object"){console.log(JSON.stringify({error:{code:"invalid_until",message:r.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const n=(d)=>d.map((u)=>{if(u&&typeof u==="object"){const m=u;return{...m,timestamp:tt(m.timestamp)}}return{type:"log",text:String(u),timestamp:Date.now()}}),i=(d)=>ot(d,{sinceMs:o,untilMs:r}).filter((u)=>{if(t.level==="all")return!0;const m=String(u.type??u.level??"").toLowerCase();return t.level==="error"?m==="error"||m==="warning":m===String(t.level).toLowerCase()});if(t.follow){let d=0;while(!0){const u=i(n(await e.consoleMessages("all"))),m=selectFreshByTimestamp(u,d);for(const g of m.fresh)console.log(JSON.stringify({...g,timestamp:we(g.timestamp)}));d=m.lastSeenTs;await new Promise((g)=>setTimeout(g,Dt))}}const s=i(n(await e.consoleMessages(t.level)));console.log(JSON.stringify({session:st(e),captured_at:new Date().toISOString(),count:s.length,messages:s.map((d)=>({...d,timestamp:we(d.timestamp)}))},null,2))}));C(h.command("analyze").argument("<url>")).description("Classify site: anti-bot vendor, real-data API candidates, pattern (A/B/C/D), nearest adapter, next step").action(x(async(e,t)=>{const o=await e.startNetworkCapture?.()??!1;await e.goto(t);await e.wait(2);if(!o)try{await e.evaluate(Ae)}catch{}await ye(e);await e.wait(1);const n=(await ye(e)).map((R)=>({url:R.url,status:R.status,contentType:R.ct,bodyPreview:typeof R.body==="string"?R.body.slice(0,2000):R.body?JSON.stringify(R.body).slice(0,2000):null})),i=`(function(){
|
|
82
|
+
})()`}async function Me(e,t){const o=await e.evaluate(ie(t));if(!o||!o.found||!o.rect)return null;return{crop:$e(o.rect,o.viewport),scroll:o.scroll??{x:0,y:0},viewport:o.viewport}}function re(e,t,o){const r=String(e??"").split(",").map((n)=>Number(n.trim()));if(r.length!==t||r.some((n)=>!Number.isFinite(n)||n<0))throw new F(`${o} 应为 ${t} 个逗号分隔的非负数(crop 网格刻度,CSS 像素)`);return r}async function De(e,t,o,r){await new Promise((s)=>setTimeout(s,900));let n={};try{n=await e.evaluate(Se(t))}catch(s){n={read_error:U(s)}}const i=o?String(o):void 0;if(i)await e.screenshot({path:i,format:"png"});console.log(JSON.stringify({ok:!0,...r,after:n,...i?{saved:i}:{}},null,2))}C(h.command("captcha").argument("[selector]","验证码图片元素 CSS 选择器(缺省自动探测常见点选验证码)")).option("--adapter","定位已有平台适配器会话;session 必须是 site:<site>,如 site:douyin").option("--save <path>","把全视口截图存到该路径(缺省存到临时 PNG 文件)").option("--scale <n>","验证码区域裁剪图的放大倍数(1..6,缺省 3;放大后认扭曲字更准)","3").option("--grid","在裁剪图上画坐标网格(刻度是裁剪图里的 CSS 像素),给 captcha-slide / captcha-rotate / captcha-drag 读位置用").description("抓取验证码:存整图 + 只含验证码的放大裁剪图 + 返回坐标与提示文字,供 AI 视觉识别后配合 captcha-click / captcha-slide / captcha-rotate / captcha-drag").action(x(async(e,t,o)=>{const r=await e.evaluate(ie(typeof t==="string"?t:void 0));if(!r||!r.found){console.log(JSON.stringify({error:{code:"captcha_not_found",message:'未探测到点选验证码图片。可显式传选择器,如 ppcli browser <s> captcha ".js-mocaptcha-img"',tried:xe}},null,2));process.exitCode=f.USAGE_ERROR;return}const n=o?.save&&String(o.save)||k.join(ge.tmpdir(),`opencli-captcha-${Date.now()}.png`);await e.screenshot({format:"png",path:n});const i=r.rect,s=r.dpr||1,d=$e(i,r.viewport),u=Math.max(1,Math.min(6,Number(o?.scale??3)||3)),m=oe;let g=null;const b=e.cdp;if(typeof b==="function"){const R={x:d.x+(r.scroll?.x??0),y:d.y+(r.scroll?.y??0),width:d.w,height:d.h,scale:u};if(o?.grid)await e.evaluate(eo(d));const P=await b.call(e,"Page.captureScreenshot",{format:"jpeg",quality:82,clip:R,captureBeyondViewport:!1}).finally(()=>o?.grid?e.evaluate(`document.getElementById(${JSON.stringify(je)})?.remove()`):void 0);if(P&&P.data){g=n.replace(/\.png$/i,"")+".crop.jpg";A.writeFileSync(g,Buffer.from(P.data,"base64"))}}console.log(JSON.stringify({ok:!0,selector:r.selector,iframe:r.iframe===!0,crop_rect_css:g?d:null,prompt:r.prompt||"",crop:g,crop_scale:g?u:null,crop_pad:g?m:null,saved:n,rect_css:i,rect_screenshot_px:{x:Math.round(i.x*s),y:Math.round(i.y*s),w:Math.round(i.w*s),h:Math.round(i.h*s)},dpr:s,natural:r.natural,viewport:r.viewport,...o?.grid?{grid_guide:"网格刻度是 crop 里的 CSS 像素(左上角 0,0),captcha-slide / captcha-rotate / captcha-drag 的坐标都按刻度读,并传同一个 --in 选择器。"}:{},guide:"① read_local_file 打开 crop(只含验证码、已放大)认清每个待点字与顺序(注意会混入不在 prompt 里的干扰字,只点 prompt 要求的)。"+"② 坐标直接读成「在 crop 这张图里的比例」(0..1,左上角为 0,0)——不用管 pad/rect,看到字在 crop 的哪就填哪。"+"③ 执行:ppcli browser <session> captcha-click"+(o.adapter?" --adapter":"")+" --crop --in '"+r.selector.replace(/'/g,"'\\''")+"'"+' "x1,y1 x2,y2 ..."(--crop 表示坐标是 crop 图比例;空格分隔,顺序即点击顺序)。'+"iframe 的提示与确认按钮请从截图读取;点完文字后可能仍需点确认。验证码仍在不代表可以重复点字;先截图检查。"},null,2))}));C(h.command("captcha-click").argument("<points>",'待点击坐标,空格分隔且顺序即点击顺序,如 "0.30,0.46 0.68,0.66"')).option("--adapter","定位已有平台适配器会话;session 必须是 site:<site>,如 site:douyin").option("--save <path>","点击后保存全视口截图,以核对点选标记、确认按钮及验证结果").option("--in <selector>","坐标映射基准元素(缺省自动探测验证码图片)").option("--crop","坐标按「captcha 裁剪图(crop)里的比例」解释(0..1,左上角为0,0)——AI 看哪张就按那张填,内部自动扣掉 pad。推荐").option("--px","坐标按「截图设备像素」解释(自动按 devicePixelRatio 换算成 CSS 像素)").option("--css","坐标按「CSS 视口像素」解释(直接点击,不做映射)").option("--delay <ms>","每次点击之间的间隔毫秒","250").description("按坐标依次点击(点选验证码用):把 AI 从图上读到的位置一次性点上,点完回读验证码是否还在/是否跳转。默认坐标为相对基准元素的比例(0..1),--crop 则为裁剪图比例").action(x(async(e,t,o)=>{const n=String(t??"").trim().split(/\s+/).filter(Boolean).map((y)=>{const S=y.split(",");if(S.length!==2||S.some((D)=>!D.trim()))return{x:NaN,y:NaN};return{x:Number(S[0]),y:Number(S[1])}});if(n.length===0||n.some((y)=>!Number.isFinite(y.x)||!Number.isFinite(y.y))){console.log(JSON.stringify({error:{code:"usage_error",message:'坐标格式应为 "x,y x,y ...",如 "0.30,0.46 0.68,0.66"'}},null,2));process.exitCode=f.USAGE_ERROR;return}if([o?.css,o?.px,o?.crop].filter(Boolean).length>1)throw new F("--css / --px / --crop 不能同时使用");const i=o?.css?"css":o?.px?"px":o?.crop?"crop":"frac";if(n.some((y)=>y.x<0||y.y<0||(i==="frac"||i==="crop")&&(y.x>1||y.y>1)))throw new F("相对坐标必须在 0..1 内,绝对坐标必须非负;尚未点击");const s=Number(o?.delay??250);if(!Number.isFinite(s)||s<0||s>1e4)throw new F("--delay 必须在 0..10000 毫秒内");let d={x:0,y:0,w:0,h:0},u=1,m;if(i==="css"||i==="px"){const y=await e.evaluate("({w: innerWidth, h: innerHeight, dpr: devicePixelRatio})");m=y;u=y.dpr}else{const y=await e.evaluate(ie(typeof o?.in==="string"?o.in:void 0));if(!y||!y.found||!y.rect){console.log(JSON.stringify({error:{code:"captcha_not_found",message:"captcha-click 找不到基准元素,请用 --in <selector> 指定,或用 --css 传绝对坐标"}},null,2));process.exitCode=f.USAGE_ERROR;return}d=y.rect;u=y.dpr||1;m=y.viewport}const g=i==="crop"?$e(d,m):d,b=(y)=>{if(i==="css")return y;if(i==="px")return{x:y.x/u,y:y.y/u};if(i==="crop")return{x:g.x+y.x*g.w,y:g.y+y.y*g.h};return{x:d.x+y.x*d.w,y:d.y+y.y*d.h}},{cdp:R,nativeClick:P}=e,G=n.map(b).map((y)=>({x:Math.round(y.x),y:Math.round(y.y)}));if(G.some((y)=>y.x<0||y.y<0||y.x>=m.w||y.y>=m.h))throw new F("点击坐标超出视口;尚未点击,请重新截图定位");if(typeof R!=="function"&&typeof P!=="function")throw new F("当前浏览器不支持原生坐标点击");const _=(y)=>new Promise((S)=>setTimeout(S,y));async function M(y,S,D,B){if(typeof R==="function"){const ae=12+B%4;for(let q=1;q<=ae;q++){const H=q/ae,ce=H*H*(3-2*H),fe=Math.sin(H*Math.PI)*10*(B%2===0?1:-1),j=D.x+(y-D.x)*ce,V=D.y+(S-D.y)*ce+fe;await R.call(e,"Input.dispatchMouseEvent",{type:"mouseMoved",x:Math.round(j),y:Math.round(V)});await _(8+q%3*6)}await R.call(e,"Input.dispatchMouseEvent",{type:"mouseMoved",x:y,y:S});await _(40+B%3*25);await R.call(e,"Input.dispatchMouseEvent",{type:"mousePressed",x:y,y:S,button:"left",clickCount:1});await _(45+B%4*20);await R.call(e,"Input.dispatchMouseEvent",{type:"mouseReleased",x:y,y:S,button:"left",clickCount:1});return"cdp"}if(typeof P==="function"){await P.call(e,y,S);return"native"}throw new F("当前浏览器不支持原生坐标点击")}const J=[];let W="",ne={x:Math.round(d.x+d.w/2)||400,y:Math.round(d.y-40)||200};for(let y=0;y<G.length;y++){const S=G[y],D=Math.round(S.x),B=Math.round(S.y);W=await M(D,B,ne,y);ne={x:D,y:B};J.push({x:D,y:B});if(y<n.length-1&&s>0)await _(s)}await _(700);let w={};try{w=await e.evaluate(Se(typeof o?.in==="string"?o.in:void 0))}catch(y){w={read_error:U(y)}}const O=o.save?String(o.save):void 0;if(O)await e.screenshot({path:O,format:"png"});console.log(JSON.stringify({ok:!0,mode:i,method:W,clicked_n:J.length,points_css:J,after:w,...O?{saved:O}:{}},null,2))}));const Ue='与 captcha 相同的验证码元素选择器(缺省自动探测;抖音为 iframe[src*="/verifycenter/captcha/"])';C(h.command("captcha-slide")).option("--adapter","定位已有平台适配器会话;session 必须是 site:<site>,如 site:douyin").option("--in <selector>",Ue).requiredOption("--handle <x,y>","滑块把手中心(crop 网格刻度)").requiredOption("--picture <x,y,w,h>","带拼图和缺口的那张图片的区域(crop 网格刻度)").option("--gap <px>","目测拼图大概要往右挪多远(可选,只用来缩小搜索范围)").option("--save <path>","松手后保存全视口截图").description("缺口拼图(「拖动滑块填充拼图」):按住先拖一小段,自己量拖动比例、找缺口,拖到位纠偏后松手").action(x(async(e,t)=>{const o=typeof t?.in==="string"?t.in:void 0,[r,n]=re(t.handle,2,"--handle"),[i,s,d,u]=re(t.picture,4,"--picture");if(d<20||u<20)throw new F("--picture 太小:框的应是整张带缺口的图片");const m=t.gap===void 0?void 0:Number(t.gap);if(m!==void 0&&!Number.isFinite(m))throw new F("--gap 必须是数字");const g=await Me(e,o);if(!g)throw new F("找不到验证码元素:传与 captcha 相同的 --in <selector>");const{solveSlidePuzzle:b}=await import("./browser/captcha-drag.js"),R=await b(e,{x:g.crop.x+r,y:g.crop.y+n,clip:{x:g.crop.x+i+g.scroll.x,y:g.crop.y+s+g.scroll.y,width:d,height:u},hint:m});if(!R)throw new F("没认出拼图和缺口,已拖回原位未提交:重新 captcha --grid 看清 --picture 框的是不是整张图");await De(e,o,t.save,R)}));C(h.command("captcha-rotate")).option("--adapter","定位已有平台适配器会话;session 必须是 site:<site>,如 site:douyin").option("--in <selector>",Ue).requiredOption("--handle <x,y>","滑块把手中心(crop 网格刻度)").requiredOption("--track <px>","把手大概能往右拖多远(工具会自己量轨道,量不出才用它)").requiredOption("--center <x,y>","圆图圆心(crop 网格刻度)").requiredOption("--radius <px>","内圈那块圆图的半径(不含外圈)").option("--save <path>","松手后保存全视口截图").description("旋转拼图(中间圆图和外圈一起转,抖音 / 字节验证中心那种):自己量内外圈差几度,拖到对齐、纠偏后松手").action(x(async(e,t)=>{const o=typeof t?.in==="string"?t.in:void 0,[r,n]=re(t.handle,2,"--handle"),[i,s]=re(t.center,2,"--center"),[d]=re(t.track,1,"--track"),[u]=re(t.radius,1,"--radius");if(u<15)throw new F("--radius 太小:量的是内圈圆图的半径");const m=await Me(e,o);if(!m)throw new F("找不到验证码元素:传与 captcha 相同的 --in <selector>");const g=u*1.5,{solveRotate:b}=await import("./browser/captcha-drag.js"),R=await b(e,{x:m.crop.x+r,y:m.crop.y+n,track:d,clip:{x:m.crop.x+i-g+m.scroll.x,y:m.crop.y+s-g+m.scroll.y,width:g*2,height:g*2},ring:{cx:g,cy:g,r:u},bar:{x:m.crop.x+m.scroll.x,y:m.crop.y+n+m.scroll.y,width:m.crop.w,handleX:r}});await De(e,o,t.save,R)}));C(h.command("captcha-drag")).option("--adapter","定位已有平台适配器会话;session 必须是 site:<site>,如 site:douyin").option("--in <selector>",Ue).requiredOption("--handle <x,y>","滑块把手中心(crop 网格刻度)").requiredOption("--dx <px>","往右拖多远(「拖到最右边」就给轨道剩余长度)").option("--save <path>","松手后保存全视口截图").description("按人手轨迹按住把手拖一段再松开:「请按住滑块拖到最右边」这类;拼图 / 旋转用 captcha-slide / captcha-rotate").action(x(async(e,t)=>{const o=typeof t?.in==="string"?t.in:void 0,[r,n]=re(t.handle,2,"--handle"),[i]=re(t.dx,1,"--dx"),s=await Me(e,o);if(!s)throw new F("找不到验证码元素:传与 captcha 相同的 --in <selector>");const{humanDrag:d}=await import("./browser/captcha-drag.js"),u=await d(e,s.crop.x+r,s.crop.y+n,i);await De(e,o,t.save,u)}));C(h.command("console")).option("--level <level>","Console level: all, error, warning, log, info, debug","all").option("--since <duration>","Only include messages from the last duration (for example: 30s, 2m)").option("--until <duration>","Only include messages older than the duration from now").option("--follow","Continuously print new console messages as JSON lines",!1).description("Read recent browser console messages").action(x(async(e,t)=>{const o=Te(t.since,"since"),r=Te(t.until,"until");if(o&&typeof o==="object"){console.log(JSON.stringify({error:{code:"invalid_since",message:o.error}},null,2));process.exitCode=f.USAGE_ERROR;return}if(r&&typeof r==="object"){console.log(JSON.stringify({error:{code:"invalid_until",message:r.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const n=(d)=>d.map((u)=>{if(u&&typeof u==="object"){const m=u;return{...m,timestamp:tt(m.timestamp)}}return{type:"log",text:String(u),timestamp:Date.now()}}),i=(d)=>ot(d,{sinceMs:o,untilMs:r}).filter((u)=>{if(t.level==="all")return!0;const m=String(u.type??u.level??"").toLowerCase();return t.level==="error"?m==="error"||m==="warning":m===String(t.level).toLowerCase()});if(t.follow){let d=0;while(!0){const u=i(n(await e.consoleMessages("all"))),m=selectFreshByTimestamp(u,d);for(const g of m.fresh)console.log(JSON.stringify({...g,timestamp:we(g.timestamp)}));d=m.lastSeenTs;await new Promise((g)=>setTimeout(g,Gt))}}const s=i(n(await e.consoleMessages(t.level)));console.log(JSON.stringify({session:at(e),captured_at:new Date().toISOString(),count:s.length,messages:s.map((d)=>({...d,timestamp:we(d.timestamp)}))},null,2))}));C(h.command("analyze").argument("<url>")).description("Classify site: anti-bot vendor, real-data API candidates, pattern (A/B/C/D), nearest adapter, next step").action(x(async(e,t)=>{const o=await e.startNetworkCapture?.()??!1;await e.goto(t);await e.wait(2);if(!o)try{await e.evaluate(Ae)}catch{}await ye(e);await e.wait(1);const n=(await ye(e)).map((R)=>({url:R.url,status:R.status,contentType:R.ct,bodyPreview:typeof R.body==="string"?R.body.slice(0,2000):R.body?JSON.stringify(R.body).slice(0,2000):null})),i=`(function(){
|
|
83
83
|
return {
|
|
84
84
|
cookieNames: (document.cookie || '').split(';').map(function(c){ return c.trim().split('=')[0]; }).filter(Boolean),
|
|
85
85
|
initialState: {
|
|
@@ -91,7 +91,7 @@ Examples:
|
|
|
91
91
|
title: document.title || '',
|
|
92
92
|
finalUrl: location.href,
|
|
93
93
|
};
|
|
94
|
-
})()`,s=await e.evaluate(i),d=(await e.getCookies({url:s.finalUrl||t}).catch(()=>[])).map((R)=>R.name).filter(Boolean),u=[...new Set([...s.cookieNames,...d])],m={requestedUrl:t,finalUrl:s.finalUrl,cookieNames:u,networkEntries:n,initialState:s.initialState,title:s.title},g=Vo(m,Ee()),b=resolveSitemapAvailabilityForUrl(s.finalUrl||t);console.log(JSON.stringify({...g,...b?{sitemap:b}:{}},null,2))}));const z=(e)=>e.option("--role <role>","Semantic role (button, link, textbox, option, etc.)").option("--name <text>","Accessible name contains text (aria-label, label, title, placeholder, or visible text)").option("--label <text>","Associated label contains text").option("--text <text>","Visible text contains text").option("--testid <id>","data-testid / data-test / test-id contains id"),at=(e,t)=>e.option(`--${t}-role <role>`,`${t} semantic role`).option(`--${t}-name <text>`,`${t} accessible name contains text`).option(`--${t}-label <text>`,`${t} associated label contains text`).option(`--${t}-text <text>`,`${t} visible text contains text`).option(`--${t}-testid <id>`,`${t} data-testid / data-test / test-id contains id`),ve=(e)=>{const t={};for(const o of["role","name","label","text","testid"]){const r=e[o];if(typeof r==="string"&&r.trim())t[o]=r.trim()}return Object.keys(t).length>0?t:null},ct=(e,t)=>{const o={},r={role:`${t}Role`,name:`${t}Name`,label:`${t}Label`,text:`${t}Text`,testid:`${t}Testid`};for(const n of["role","name","label","text","testid"]){const i=e[r[n]];if(typeof i==="string"&&i.trim())o[n]=i.trim()}return Object.keys(o).length>0?o:null},lt=async(e,t,o)=>{const r=await e.evaluate(Nt({...t,limit:6}));if(kt(r))return r;if(o==="write"&&r.matches_n!==1)return{error:{code:"semantic_ambiguous",message:`Semantic locator matched ${r.matches_n} elements; write actions require a unique target.`,hint:"Add --name/--label/--text/--testid or use browser find with a narrower locator.",matches_n:r.matches_n,entries:r.entries}};const n=r.entries[0];if(!n)return{error:{code:"semantic_not_found",message:"Semantic locator matched 0 elements",hint:"Try browser state, --source ax, or relax the semantic locator."}};const i=String(n.ref);if(o==="read")return{target:i,...r.matches_n>1?{total_matches:r.matches_n}:{}};return i},eo=async(e,t,o)=>{const r=ve(t);if(!r)return null;return lt(e,r,o)},Ge=async(e,t,o,r)=>{const n=typeof t==="string"&&t.trim()?t.trim():"",i=!!ve(o);if(n&&i)return{error:{code:"usage_error",message:"Pass either <target> or semantic locator flags, not both."}};if(n)return n;const s=await eo(e,o,r);if(s)return s;return{error:{code:"usage_error",message:"Missing target. Pass a numeric ref/CSS selector, or semantic flags like --role button --name Submit."}}},de=(e)=>{console.log(JSON.stringify(e,null,2));process.exitCode=f.USAGE_ERROR},ue=async(e,t,o)=>{const r=await Ge(e,t,o,"write");if(typeof r==="string")return r;if("error"in r)de(r);return null},Fe=async(e,t,o,r,n)=>{const i=!!ve(r);if(i&&o!==void 0){de({error:{code:"usage_error",message:`When using semantic locator flags, pass only <${n}> as the positional argument.`}});return null}const s=i?t:o;if(s===void 0){de({error:{code:"usage_error",message:`Missing ${n}.`,hint:i?`With semantic locator flags, pass the ${n} as the only positional argument.`:`Pass both a target and ${n}.`}});return null}const d=await ue(e,i?void 0:t,r);if(!d)return null;return{target:d,value:String(s)}},dt=async(e,t,o,r,n)=>{const i=typeof t==="string"&&t.trim()?t.trim():"",s=ct(o,r);if(i&&s){de({error:{code:"usage_error",message:`Pass either <${n}> or --${r}-* semantic locator flags, not both.`}});return null}if(i)return i;if(s){const d=await lt(e,s,"write");if(typeof d==="string")return d;if("error"in d)de(d);return null}de({error:{code:"usage_error",message:`Missing ${n}. Pass a numeric ref/CSS selector, or --${r}-role/--${r}-name semantic flags.`}});return null};C(z(h.command("find")).option("--css <selector>","CSS selector; combine with --text to keep only matches whose text / aria-label / title / placeholder / value contains it").option("--visible","Keep only visible elements",!1).option("--limit <n>","Max entries returned","50").option("--text-max <n>","Max chars of trimmed text per entry","120").description("Find DOM elements by CSS or semantic locator — returns JSON {matches_n, visible_n, filtered_n?, entries[]}; visible entries are listed first")).action(x(async(e,t)=>{let o=ve(t),r;if(t.css&&o&&Object.keys(o).length===1&&typeof o.text==="string"){r=o.text;o=null}const n=t.visible===!0;if((!t.css||typeof t.css!=="string")&&!o){console.log(JSON.stringify({error:{code:"usage_error",message:"--css <selector> or a semantic locator flag is required",hint:'Examples: ppcli browser find --css ".btn.primary"; ppcli browser find --css button --visible --text 发布; ppcli browser find --role button --name Save'}},null,2));process.exitCode=f.USAGE_ERROR;return}if(t.css&&o){console.log(JSON.stringify({error:{code:"usage_error",message:"Pass either --css or semantic locator flags (--role/--name/--label/--testid), not both.",hint:"Only --text and --visible combine with --css."}},null,2));process.exitCode=f.USAGE_ERROR;return}const i=N(t.limit);if(i&&typeof i==="object"&&"error"in i){console.log(JSON.stringify({error:{code:"usage_error",message:i.error.replace("--nth","--limit")}},null,2));process.exitCode=f.USAGE_ERROR;return}const s=N(t.textMax);if(s&&typeof s==="object"&&"error"in s){console.log(JSON.stringify({error:{code:"usage_error",message:s.error.replace("--nth","--text-max")}},null,2));process.exitCode=f.USAGE_ERROR;return}const d=await e.evaluate(o?Nt({...o,limit:i??void 0,textMax:s??void 0,visibleOnly:n,visibleFirst:!0}):Uo(t.css,{limit:i??void 0,textMax:s??void 0,visibleOnly:n,visibleFirst:!0,textFilter:r}));if(kt(d)){console.log(JSON.stringify(d,null,2));process.exitCode=f.GENERIC_ERROR;return}console.log(JSON.stringify(d,null,2))}));const me=h.command("get").description("Get page properties");C(me.command("title").description("Page title")).action(x(async(e)=>{console.log(await e.evaluate("document.title"))}));C(me.command("url").description("Current page URL")).action(x(async(e)=>{console.log(await e.getCurrentUrl?.()??await e.evaluate("location.href"))}));const Be=async(e,t,o,r,n)=>{const i=await Ge(e,t,o,"read");if(typeof i!=="string"&&"error"in i){console.log(JSON.stringify(i,null,2));process.exitCode=f.USAGE_ERROR;return}const s=typeof i==="string"?i:i.target,d=typeof i==="string"?void 0:i.total_matches,u=N(o.nth);if(u&&typeof u==="object"&&"error"in u){console.log(JSON.stringify({error:{code:"usage_error",message:u.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const{matches_n:m,match_level:g}=await E(e,s,{firstOnMulti:u===null,...typeof u==="number"?{nth:u}:{}}),b=await e.evaluate(r);let R;if(n==="attributes")try{R=b==null?{}:JSON.parse(String(b))}catch{R=b}else R=b??null;console.log(JSON.stringify({value:R,matches_n:m,match_level:g,...d&&d>1?{total_matches:d}:{}},null,2))};C(z(me.command("text")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","Pick the nth match (0-based) when <target> is a multi-match CSS selector").description("Element text content — JSON envelope {value, matches_n}")).action(x(async(e,t,o)=>Be(e,t,o??{},Po(),"text")));C(z(me.command("value")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","Pick the nth match (0-based) when <target> is a multi-match CSS selector").description("Input/textarea value — JSON envelope {value, matches_n}")).action(x(async(e,t,o)=>Be(e,t,o??{},Io(),"value")));C(me.command("html").option("--selector <css>","CSS selector scope (first match)").option("--as <format>",'Output format: "html" (default) or "json" for structured tree',"html").option("--max <n>","Max characters of raw HTML to return (0 = unlimited)","0").option("--depth <n>","(--as json) Max tree depth below root (0 = root only, 0 disables = unlimited via empty)","").option("--children-max <n>","(--as json) Max element children kept per node (empty = unlimited)","").option("--text-max <n>","(--as json) Max chars of direct text kept per node (empty = unlimited)","").description("Page HTML (or scoped); use --as json for a {tag, attrs, text, children} tree")).action(x(async(e,t)=>{const o=String(t.as||"html").toLowerCase();if(o!=="html"&&o!=="json"){console.log(JSON.stringify({error:{code:"invalid_format",message:`--as must be "html" or "json", got "${t.as}"`}},null,2));process.exitCode=f.USAGE_ERROR;return}const r=String(t.max??"0");if(!/^\d+$/.test(r)){console.log(JSON.stringify({error:{code:"invalid_max",message:`--max must be a non-negative integer, got "${t.max}"`}},null,2));process.exitCode=f.USAGE_ERROR;return}const n=Number.parseInt(r,10);if(o==="json"){const u=(_,M)=>{const J=M===void 0||M===null?"":String(M);if(J==="")return null;if(!/^\d+$/.test(J))return{error:`${_} must be a non-negative integer, got "${J}"`};return Number.parseInt(J,10)},m=u("--depth",t.depth),g=u("--children-max",t.childrenMax),b=u("--text-max",t.textMax);for(const _ of[m,g,b])if(_&&typeof _==="object"&&"error"in _){console.log(JSON.stringify({error:{code:"invalid_budget",message:_.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const R=Wo({selector:t.selector??null,depth:m,childrenMax:g,textMax:b}),P=await e.evaluate(R);if(P&&typeof P==="object"&&"invalidSelector"in P&&P.invalidSelector){console.log(JSON.stringify({error:{code:"invalid_selector",message:`Selector "${t.selector}" is not a valid CSS selector: ${P.reason}`}},null,2));process.exitCode=f.USAGE_ERROR;return}const G=P;if(!G||G.matched===0){console.log(JSON.stringify({error:{code:"selector_not_found",message:t.selector?`Selector "${t.selector}" matched 0 elements.`:"Page has no documentElement."}},null,2));process.exitCode=f.USAGE_ERROR;return}console.log(JSON.stringify(G,null,2));return}const i=t.selector?JSON.stringify(t.selector):"null",s=await e.evaluate(`(() => {
|
|
94
|
+
})()`,s=await e.evaluate(i),d=(await e.getCookies({url:s.finalUrl||t}).catch(()=>[])).map((R)=>R.name).filter(Boolean),u=[...new Set([...s.cookieNames,...d])],m={requestedUrl:t,finalUrl:s.finalUrl,cookieNames:u,networkEntries:n,initialState:s.initialState,title:s.title},g=Xo(m,Ee()),b=resolveSitemapAvailabilityForUrl(s.finalUrl||t);console.log(JSON.stringify({...g,...b?{sitemap:b}:{}},null,2))}));const z=(e)=>e.option("--role <role>","Semantic role (button, link, textbox, option, etc.)").option("--name <text>","Accessible name contains text (aria-label, label, title, placeholder, or visible text)").option("--label <text>","Associated label contains text").option("--text <text>","Visible text contains text").option("--testid <id>","data-testid / data-test / test-id contains id"),ct=(e,t)=>e.option(`--${t}-role <role>`,`${t} semantic role`).option(`--${t}-name <text>`,`${t} accessible name contains text`).option(`--${t}-label <text>`,`${t} associated label contains text`).option(`--${t}-text <text>`,`${t} visible text contains text`).option(`--${t}-testid <id>`,`${t} data-testid / data-test / test-id contains id`),ve=(e)=>{const t={};for(const o of["role","name","label","text","testid"]){const r=e[o];if(typeof r==="string"&&r.trim())t[o]=r.trim()}return Object.keys(t).length>0?t:null},lt=(e,t)=>{const o={},r={role:`${t}Role`,name:`${t}Name`,label:`${t}Label`,text:`${t}Text`,testid:`${t}Testid`};for(const n of["role","name","label","text","testid"]){const i=e[r[n]];if(typeof i==="string"&&i.trim())o[n]=i.trim()}return Object.keys(o).length>0?o:null},dt=async(e,t,o)=>{const r=await e.evaluate(kt({...t,limit:6}));if(At(r))return r;if(o==="write"&&r.matches_n!==1)return{error:{code:"semantic_ambiguous",message:`Semantic locator matched ${r.matches_n} elements; write actions require a unique target.`,hint:"Add --name/--label/--text/--testid or use browser find with a narrower locator.",matches_n:r.matches_n,entries:r.entries}};const n=r.entries[0];if(!n)return{error:{code:"semantic_not_found",message:"Semantic locator matched 0 elements",hint:"Try browser state, --source ax, or relax the semantic locator."}};const i=String(n.ref);if(o==="read")return{target:i,...r.matches_n>1?{total_matches:r.matches_n}:{}};return i},to=async(e,t,o)=>{const r=ve(t);if(!r)return null;return dt(e,r,o)},Ge=async(e,t,o,r)=>{const n=typeof t==="string"&&t.trim()?t.trim():"",i=!!ve(o);if(n&&i)return{error:{code:"usage_error",message:"Pass either <target> or semantic locator flags, not both."}};if(n)return n;const s=await to(e,o,r);if(s)return s;return{error:{code:"usage_error",message:"Missing target. Pass a numeric ref/CSS selector, or semantic flags like --role button --name Submit."}}},de=(e)=>{console.log(JSON.stringify(e,null,2));process.exitCode=f.USAGE_ERROR},ue=async(e,t,o)=>{const r=await Ge(e,t,o,"write");if(typeof r==="string")return r;if("error"in r)de(r);return null},Fe=async(e,t,o,r,n)=>{const i=!!ve(r);if(i&&o!==void 0){de({error:{code:"usage_error",message:`When using semantic locator flags, pass only <${n}> as the positional argument.`}});return null}const s=i?t:o;if(s===void 0){de({error:{code:"usage_error",message:`Missing ${n}.`,hint:i?`With semantic locator flags, pass the ${n} as the only positional argument.`:`Pass both a target and ${n}.`}});return null}const d=await ue(e,i?void 0:t,r);if(!d)return null;return{target:d,value:String(s)}},ut=async(e,t,o,r,n)=>{const i=typeof t==="string"&&t.trim()?t.trim():"",s=lt(o,r);if(i&&s){de({error:{code:"usage_error",message:`Pass either <${n}> or --${r}-* semantic locator flags, not both.`}});return null}if(i)return i;if(s){const d=await dt(e,s,"write");if(typeof d==="string")return d;if("error"in d)de(d);return null}de({error:{code:"usage_error",message:`Missing ${n}. Pass a numeric ref/CSS selector, or --${r}-role/--${r}-name semantic flags.`}});return null};C(z(h.command("find")).option("--css <selector>","CSS selector; combine with --text to keep only matches whose text / aria-label / title / placeholder / value contains it").option("--visible","Keep only visible elements",!1).option("--limit <n>","Max entries returned","50").option("--text-max <n>","Max chars of trimmed text per entry","120").description("Find DOM elements by CSS or semantic locator — returns JSON {matches_n, visible_n, filtered_n?, entries[]}; visible entries are listed first")).action(x(async(e,t)=>{let o=ve(t),r;if(t.css&&o&&Object.keys(o).length===1&&typeof o.text==="string"){r=o.text;o=null}const n=t.visible===!0;if((!t.css||typeof t.css!=="string")&&!o){console.log(JSON.stringify({error:{code:"usage_error",message:"--css <selector> or a semantic locator flag is required",hint:'Examples: ppcli browser find --css ".btn.primary"; ppcli browser find --css button --visible --text 发布; ppcli browser find --role button --name Save'}},null,2));process.exitCode=f.USAGE_ERROR;return}if(t.css&&o){console.log(JSON.stringify({error:{code:"usage_error",message:"Pass either --css or semantic locator flags (--role/--name/--label/--testid), not both.",hint:"Only --text and --visible combine with --css."}},null,2));process.exitCode=f.USAGE_ERROR;return}const i=N(t.limit);if(i&&typeof i==="object"&&"error"in i){console.log(JSON.stringify({error:{code:"usage_error",message:i.error.replace("--nth","--limit")}},null,2));process.exitCode=f.USAGE_ERROR;return}const s=N(t.textMax);if(s&&typeof s==="object"&&"error"in s){console.log(JSON.stringify({error:{code:"usage_error",message:s.error.replace("--nth","--text-max")}},null,2));process.exitCode=f.USAGE_ERROR;return}const d=await e.evaluate(o?kt({...o,limit:i??void 0,textMax:s??void 0,visibleOnly:n,visibleFirst:!0}):Go(t.css,{limit:i??void 0,textMax:s??void 0,visibleOnly:n,visibleFirst:!0,textFilter:r}));if(At(d)){console.log(JSON.stringify(d,null,2));process.exitCode=f.GENERIC_ERROR;return}console.log(JSON.stringify(d,null,2))}));const me=h.command("get").description("Get page properties");C(me.command("title").description("Page title")).action(x(async(e)=>{console.log(await e.evaluate("document.title"))}));C(me.command("url").description("Current page URL")).action(x(async(e)=>{console.log(await e.getCurrentUrl?.()??await e.evaluate("location.href"))}));const Be=async(e,t,o,r,n)=>{const i=await Ge(e,t,o,"read");if(typeof i!=="string"&&"error"in i){console.log(JSON.stringify(i,null,2));process.exitCode=f.USAGE_ERROR;return}const s=typeof i==="string"?i:i.target,d=typeof i==="string"?void 0:i.total_matches,u=N(o.nth);if(u&&typeof u==="object"&&"error"in u){console.log(JSON.stringify({error:{code:"usage_error",message:u.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const{matches_n:m,match_level:g}=await E(e,s,{firstOnMulti:u===null,...typeof u==="number"?{nth:u}:{}}),b=await e.evaluate(r);let R;if(n==="attributes")try{R=b==null?{}:JSON.parse(String(b))}catch{R=b}else R=b??null;console.log(JSON.stringify({value:R,matches_n:m,match_level:g,...d&&d>1?{total_matches:d}:{}},null,2))};C(z(me.command("text")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","Pick the nth match (0-based) when <target> is a multi-match CSS selector").description("Element text content — JSON envelope {value, matches_n}")).action(x(async(e,t,o)=>Be(e,t,o??{},Io(),"text")));C(z(me.command("value")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","Pick the nth match (0-based) when <target> is a multi-match CSS selector").description("Input/textarea value — JSON envelope {value, matches_n}")).action(x(async(e,t,o)=>Be(e,t,o??{},Jo(),"value")));C(me.command("html").option("--selector <css>","CSS selector scope (first match)").option("--as <format>",'Output format: "html" (default) or "json" for structured tree',"html").option("--max <n>","Max characters of raw HTML to return (0 = unlimited)","0").option("--depth <n>","(--as json) Max tree depth below root (0 = root only, 0 disables = unlimited via empty)","").option("--children-max <n>","(--as json) Max element children kept per node (empty = unlimited)","").option("--text-max <n>","(--as json) Max chars of direct text kept per node (empty = unlimited)","").description("Page HTML (or scoped); use --as json for a {tag, attrs, text, children} tree")).action(x(async(e,t)=>{const o=String(t.as||"html").toLowerCase();if(o!=="html"&&o!=="json"){console.log(JSON.stringify({error:{code:"invalid_format",message:`--as must be "html" or "json", got "${t.as}"`}},null,2));process.exitCode=f.USAGE_ERROR;return}const r=String(t.max??"0");if(!/^\d+$/.test(r)){console.log(JSON.stringify({error:{code:"invalid_max",message:`--max must be a non-negative integer, got "${t.max}"`}},null,2));process.exitCode=f.USAGE_ERROR;return}const n=Number.parseInt(r,10);if(o==="json"){const u=(_,M)=>{const J=M===void 0||M===null?"":String(M);if(J==="")return null;if(!/^\d+$/.test(J))return{error:`${_} must be a non-negative integer, got "${J}"`};return Number.parseInt(J,10)},m=u("--depth",t.depth),g=u("--children-max",t.childrenMax),b=u("--text-max",t.textMax);for(const _ of[m,g,b])if(_&&typeof _==="object"&&"error"in _){console.log(JSON.stringify({error:{code:"invalid_budget",message:_.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const R=Ho({selector:t.selector??null,depth:m,childrenMax:g,textMax:b}),P=await e.evaluate(R);if(P&&typeof P==="object"&&"invalidSelector"in P&&P.invalidSelector){console.log(JSON.stringify({error:{code:"invalid_selector",message:`Selector "${t.selector}" is not a valid CSS selector: ${P.reason}`}},null,2));process.exitCode=f.USAGE_ERROR;return}const G=P;if(!G||G.matched===0){console.log(JSON.stringify({error:{code:"selector_not_found",message:t.selector?`Selector "${t.selector}" matched 0 elements.`:"Page has no documentElement."}},null,2));process.exitCode=f.USAGE_ERROR;return}console.log(JSON.stringify(G,null,2));return}const i=t.selector?JSON.stringify(t.selector):"null",s=await e.evaluate(`(() => {
|
|
95
95
|
const s = ${i};
|
|
96
96
|
if (s) {
|
|
97
97
|
try {
|
|
@@ -103,7 +103,7 @@ Examples:
|
|
|
103
103
|
}
|
|
104
104
|
return { kind: 'ok', html: document.documentElement ? document.documentElement.outerHTML : null };
|
|
105
105
|
})()`);if(s.kind==="invalid_selector"){console.log(JSON.stringify({error:{code:"invalid_selector",message:`Selector "${t.selector}" is not a valid CSS selector: ${s.reason}`}},null,2));process.exitCode=f.USAGE_ERROR;return}const d=s.html;if(d===null){if(t.selector){console.log(JSON.stringify({error:{code:"selector_not_found",message:`Selector "${t.selector}" matched 0 elements.`}},null,2));process.exitCode=f.USAGE_ERROR;return}console.log("(empty)");return}if(n>0&&d.length>n){console.log(`<!-- ppcli: truncated ${n} of ${d.length} chars; re-run without --max (or --max 0) for full -->
|
|
106
|
-
${d.slice(0,n)}`);return}console.log(d)}));C(z(me.command("attributes")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","Pick the nth match (0-based) when <target> is a multi-match CSS selector").description("Element attributes — JSON envelope {value, matches_n}")).action(x(async(e,t,o)=>Be(e,t,o??{},Jo(),"attributes")));function ee(e){const t=N(e);if(t&&typeof t==="object"&&"error"in t)return t;if(typeof t==="number")return{opts:{nth:t}};return{opts:{}}}function to(e){const t=Array.isArray(e)?e:[];if(t.length===0)return{error:{code:"usage_error",message:"At least one file path is required.",hint:'Example: ppcli browser upload "input[type=file]" ./receipt.pdf'}};const o=[];for(const r of t){const n=String(r),i=n==="~"||n.startsWith(`~${k.sep}`)?k.join(ge.homedir(),n.slice(2)):n,s=k.resolve(i);if(!A.existsSync(s))return{error:{code:"file_not_found",message:`File not found: ${s}`}};if(!A.statSync(s).isFile())return{error:{code:"not_a_file",message:`Not a regular file: ${s}`}};o.push(s)}return{files:o}}function Le(e,t){const o=N(e);if(o&&typeof o==="object"&&"error"in o)return{error:o.error.replace("--nth",t)};if(typeof o==="number")return{opts:{nth:o}};return{opts:{}}}C(z(h.command("click")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Click element — JSON envelope {clicked, target, matches_n}")).action(x(async(e,t,o)=>{const r=await Ge(e,t,o??{},"write");if(typeof r!=="string"){console.log(JSON.stringify(r,null,2));process.exitCode=f.USAGE_ERROR;return}const n=ee(o?.nth);if("error"in n){console.log(JSON.stringify({error:{code:"usage_error",message:n.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const{matches_n:i,match_level:s,click_method:d,hit:u,retargeted:m}=await e.click(r,n.opts);console.log(JSON.stringify({clicked:!0,target:r,matches_n:i,match_level:s,...d&&{click_method:d},...u&&{hit:u},...m&&{retargeted:m}},null,2))}));C(z(h.command("type")).argument("[targetOrText]","Numeric ref/CSS target, or text when using --role/--name/etc.").argument("[text]","Text to type").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Click element, then type text — JSON envelope {typed, text, target, matches_n, autocomplete}")).action(x(async(e,t,o,r)=>{const n=await Fe(e,t,o,r??{},"text");if(!n)return;const i=ee(r?.nth);if("error"in i){console.log(JSON.stringify({error:{code:"usage_error",message:i.error}},null,2));process.exitCode=f.USAGE_ERROR;return}await e.click(n.target,i.opts);await e.wait(0.3);const{matches_n:s,match_level:d}=await e.typeText(n.target,n.value,i.opts),u=await e.evaluate(Mo());if(u)await e.wait(0.4);console.log(JSON.stringify({typed:!0,text:n.value,target:n.target,matches_n:s,match_level:d,autocomplete:!!u},null,2))}));C(z(h.command("hover")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Move the mouse over an element — JSON envelope {hovered, target, matches_n}")).action(x(async(e,t,o)=>{if(typeof e.hover!=="function")throw Error("browser hover is not supported by this browser backend");const r=await ue(e,t,o??{});if(!r)return;const n=ee(o?.nth);if("error"in n){console.log(JSON.stringify({error:{code:"usage_error",message:n.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const{matches_n:i,match_level:s}=await e.hover(r,n.opts);console.log(JSON.stringify({hovered:!0,target:r,matches_n:i,match_level:s},null,2))}));C(z(h.command("focus")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Focus an element — JSON envelope {focused, target, matches_n}")).action(x(async(e,t,o)=>{if(typeof e.focus!=="function")throw Error("browser focus is not supported by this browser backend");const r=await ue(e,t,o??{});if(!r)return;const n=ee(o?.nth);if("error"in n){console.log(JSON.stringify({error:{code:"usage_error",message:n.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const{focused:i,matches_n:s,match_level:d}=await e.focus(r,n.opts);console.log(JSON.stringify({focused:i,target:r,matches_n:s,match_level:d},null,2))}));C(z(h.command("dblclick")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Double-click element — JSON envelope {dblclicked, target, matches_n}")).action(x(async(e,t,o)=>{if(typeof e.dblClick!=="function")throw Error("browser dblclick is not supported by this browser backend");const r=await ue(e,t,o??{});if(!r)return;const n=ee(o?.nth);if("error"in n){console.log(JSON.stringify({error:{code:"usage_error",message:n.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const{matches_n:i,match_level:s}=await e.dblClick(r,n.opts);console.log(JSON.stringify({dblclicked:!0,target:r,matches_n:i,match_level:s},null,2))}));const ut=async(e,t,o,r)=>{if(typeof e.setChecked!=="function")throw Error(`browser ${r?"check":"uncheck"} is not supported by this browser backend`);const n=await ue(e,t,o);if(!n)return;const i=ee(o?.nth);if("error"in i){console.log(JSON.stringify({error:{code:"usage_error",message:i.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const s=await e.setChecked(n,r,i.opts);console.log(JSON.stringify({checked:s.checked,changed:s.changed,target:n,matches_n:s.matches_n,match_level:s.match_level,...s.kind?{kind:s.kind}:{}},null,2))};C(z(h.command("check")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Ensure a checkbox/radio/aria-checked control is checked — JSON envelope {checked, changed, target, matches_n}")).action(x(async(e,t,o)=>{await ut(e,t,o??{},!0)}));C(z(h.command("uncheck")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Ensure a checkbox/aria-checked control is unchecked — JSON envelope {checked, changed, target, matches_n}")).action(x(async(e,t,o)=>{await ut(e,t,o??{},!1)}));C(z(h.command("upload")).argument("[targetOrFile]","Numeric ref/CSS target, or first file when using --role/--name/etc.").argument("[files...]","Local file path(s) to attach").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Attach local files to a file input — JSON envelope {uploaded, files, file_names, target, matches_n}")).action(x(async(e,t,o,r)=>{if(typeof e.uploadFiles!=="function")throw Error("browser upload is not supported by this browser backend");const n=!!ve(r??{}),s=await ue(e,n?void 0:t,r??{});if(!s)return;const d=ee(r?.nth);if("error"in d){console.log(JSON.stringify({error:{code:"usage_error",message:d.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const u=n?[t,...Array.isArray(o)?o:[]].filter((b)=>b!==void 0):o,m=to(u);if("error"in m){console.log(JSON.stringify({error:m.error},null,2));process.exitCode=f.USAGE_ERROR;return}const g=await e.uploadFiles(s,m.files,d.opts);console.log(JSON.stringify(g,null,2))}));C(at(at(h.command("drag"),"from"),"to").argument("[source]","Numeric ref/CSS selector to drag from, or omit with --from-role/--from-name/etc.").argument("[target]","Numeric ref/CSS selector to drop onto, or omit with --to-role/--to-name/etc. or --dx/--dy").option("--from-nth <n>","When <source> is a multi-match CSS selector, pick the nth match (0-based)").option("--to-nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").option("--dx <px>","Drag by this many CSS pixels horizontally from the source centre instead of onto a target (negative = left)").option("--dy <px>","Drag by this many CSS pixels vertically from the source centre instead of onto a target (negative = up)").option("--steps <n>","Pointer-move samples between press and release (default: 2 for element drops, ~1 per 12px for --dx/--dy)").description("Drag one element onto another, or by a pixel offset with --dx/--dy (sliders) — JSON envelope {dragged, source, target | dx, dy, from, to}")).action(x(async(e,t,o,r)=>{if(typeof e.drag!=="function")throw Error("browser drag is not supported by this browser backend");const n=(_,M)=>{console.log(JSON.stringify({error:{code:"usage_error",message:_,...M?{hint:M}:{}}},null,2));process.exitCode=f.USAGE_ERROR},i=(_,M)=>{if(_===void 0||_===null||_==="")return null;const J=Number(String(_).trim().replace(/px$/i,""));if(!Number.isFinite(J))return{error:`${M} must be a number of CSS pixels, got "${String(_)}"`};return J},s=i(r?.dx,"--dx"),d=i(r?.dy,"--dy");for(const _ of[s,d])if(_&&typeof _==="object")return n(_.error);let u;if(r?.steps!==void 0){u=Number(r.steps);if(!Number.isInteger(u)||u<1||u>200)return n(`--steps must be an integer between 1 and 200, got "${String(r.steps)}"`)}const m=s!==null||d!==null;if(m){if(typeof o==="string"&&o.length>0||ct(r??{},"to")!==null||r?.toNth!==void 0)return n("Pass either a drop target or --dx/--dy, not both.","Element drop: browser drag <source> <target>. Pixel drag: browser drag <source> --dx 180.");if((s??0)===0&&(d??0)===0)return n("--dx/--dy are both 0 — nothing to drag.");if(typeof e.dragBy!=="function")throw Error("browser drag --dx/--dy is not supported by this browser backend")}const g=await dt(e,t,r??{},"from","source");if(!g)return;if(m){const _=Le(r?.fromNth,"--from-nth");if("error"in _)return n(_.error);const M=await e.dragBy(g,{dx:s??0,dy:d??0},{from:_.opts,...u!==void 0?{steps:u}:{}});console.log(JSON.stringify(M,null,2));return}const b=await dt(e,o,r??{},"to","target");if(!b)return;const R=Le(r?.fromNth,"--from-nth");if("error"in R){console.log(JSON.stringify({error:{code:"usage_error",message:R.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const P=Le(r?.toNth,"--to-nth");if("error"in P){console.log(JSON.stringify({error:{code:"usage_error",message:P.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const G=await e.drag(g,b,{from:R.opts,to:P.opts,...u!==void 0?{steps:u}:{}});console.log(JSON.stringify(G,null,2))}));C(z(h.command("fill")).argument("[targetOrText]","Numeric ref/CSS target, or text when using --role/--name/etc.").argument("[text]","Text to set exactly").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Set input/textarea/contenteditable text exactly and verify the value — JSON envelope {filled, verified, text, actual}")).action(x(async(e,t,o,r)=>{const n=await Fe(e,t,o,r??{},"text");if(!n)return;const i=ee(r?.nth);if("error"in i){console.log(JSON.stringify({error:{code:"usage_error",message:i.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const s=await e.fillText(n.target,n.value,i.opts);if(!s.verified)process.exitCode=f.GENERIC_ERROR;console.log(JSON.stringify({filled:s.filled,verified:s.verified,target:n.target,text:n.value,actual:s.actual,length:s.length,matches_n:s.matches_n,match_level:s.match_level,...s.mode?{mode:s.mode}:{}},null,2))}));C(z(h.command("select")).argument("[targetOrOption]","Numeric ref/CSS target, or option text when using --role/--name/etc.").argument("[option]","Option text (or value) to select").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Select dropdown option — JSON envelope {selected, target, matches_n}")).action(x(async(e,t,o,r)=>{const n=await Fe(e,t,o,r??{},"option");if(!n)return;const i=ee(r?.nth);if("error"in i){console.log(JSON.stringify({error:{code:"usage_error",message:i.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const{matches_n:s,match_level:d}=await E(e,n.target,i.opts),u=await e.evaluate(jo(n.value));if(u?.error){console.log(JSON.stringify({error:{code:u.error==="Not a <select>"?"not_a_select":"option_not_found",message:u.error,...u.available&&{available:u.available},matches_n:s}},null,2));process.exitCode=f.GENERIC_ERROR;return}console.log(JSON.stringify({selected:u?.selected??n.value,target:n.target,matches_n:s,match_level:d},null,2))}));C(h.command("keys").argument("<key>","Key to press (Enter, Escape, Tab, Control+a)")).description("Press keyboard key").action(x(async(e,t)=>{await e.pressKey(t);console.log(`Pressed: ${t}`)}));const mt=h.command("dialog").description("Handle a blocking JavaScript alert/confirm/prompt dialog");C(mt.command("accept").option("--text <text>","Prompt text to submit for prompt() dialogs").description("Accept the currently open JavaScript dialog")).action(x(async(e,t)=>{if(!e.handleJavaScriptDialog)throw Error("This browser session does not support JavaScript dialog handling");try{await e.handleJavaScriptDialog(!0,t?.text)}catch(o){if(U(o).toLowerCase().includes("no dialog")){console.log(JSON.stringify({error:{code:"no_javascript_dialog",message:"No JavaScript dialog is currently open."}},null,2));process.exitCode=f.USAGE_ERROR;return}throw o}console.log(JSON.stringify({handled:!0,action:"accept",...t?.text!==void 0&&{text:t.text}},null,2))}));C(mt.command("dismiss").description("Dismiss the currently open JavaScript dialog")).action(x(async(e)=>{if(!e.handleJavaScriptDialog)throw Error("This browser session does not support JavaScript dialog handling");try{await e.handleJavaScriptDialog(!1)}catch(t){if(U(t).toLowerCase().includes("no dialog")){console.log(JSON.stringify({error:{code:"no_javascript_dialog",message:"No JavaScript dialog is currently open."}},null,2));process.exitCode=f.USAGE_ERROR;return}throw t}console.log(JSON.stringify({handled:!0,action:"dismiss"},null,2))}));C(h.command("wait")).argument("<type>","selector, text, time, xhr, or download").argument("[value]","CSS selector, text string, seconds, XHR URL regex, or download filename/URL pattern").option("--timeout <ms>","Timeout in milliseconds","10000").description('Wait for selector, text, time, matching XHR, or browser download (e.g. wait selector ".loaded", wait text "Success", wait time 3, wait xhr "/api/search", wait download receipt.pdf)').action(x(async(e,t,o,r)=>{const n=parseInt(r.timeout,10);if(t==="time"){const i=parseFloat(o??"2");await e.wait(i);console.log(`Waited ${i}s`)}else if(t==="selector"){if(!o){console.error("Missing CSS selector");process.exitCode=f.USAGE_ERROR;return}await e.wait({selector:o,timeout:n/1000});console.log(`Element "${o}" appeared`)}else if(t==="text"){if(!o){console.error("Missing text");process.exitCode=f.USAGE_ERROR;return}await e.wait({text:o,timeout:n/1000});console.log(`Text "${o}" appeared`)}else if(t==="xhr"){if(!o){console.error("Missing XHR URL regex");process.exitCode=f.USAGE_ERROR;return}let i;try{i=new RegExp(o)}catch(g){console.error(`Invalid regex "${o}": ${g instanceof Error?g.message:String(g)}`);process.exitCode=f.USAGE_ERROR;return}if(!(await e.startNetworkCapture?.()??!1))try{await e.evaluate(Ae)}catch{}await ye(e);const d=Date.now()+n,u=400;let m=null;while(Date.now()<d&&!m){m=(await ye(e)).find((b)=>i.test(b.url))??null;if(!m)await new Promise((b)=>setTimeout(b,u))}if(!m){console.log(JSON.stringify({error:{code:"xhr_not_seen",message:`No captured XHR matched /${o}/ within ${n}ms`,hint:"Check the pattern against `browser network` output; the endpoint may not have fired yet, or capture is disabled."}},null,2));process.exitCode=f.GENERIC_ERROR;return}console.log(JSON.stringify({matched:{url:m.url,status:m.status,contentType:m.ct}},null,2))}else if(t==="download"){if(typeof e.waitForDownload!=="function"){console.log(JSON.stringify({error:{code:"download_wait_unavailable",message:"The active browser backend does not support download lifecycle waits.",hint:"Use the Browser Bridge extension version 1.0.8 or newer, then retry the command."}},null,2));process.exitCode=f.GENERIC_ERROR;return}const i=await e.waitForDownload(String(o??""),n);if(!i.downloaded){const s=i.state==="interrupted"&&i.id!==void 0?"download_failed":"download_not_seen";console.log(JSON.stringify({error:{code:s,message:i.error??`No download matched "${o??"*"}" within ${n}ms`,hint:"Check the pattern against the expected filename or URL; use a longer --timeout if the download starts slowly."},download:i},null,2));process.exitCode=f.GENERIC_ERROR;return}console.log(JSON.stringify(i,null,2))}else{console.error(`Unknown wait type "${t}". Use: selector, text, time, xhr, or download`);process.exitCode=f.USAGE_ERROR}}));C(h.command("eval").argument("<js>","JavaScript code").option("--frame <index>",'Cross-origin iframe index from "browser frames"').description("Execute JS in page context, return result (top-level await is supported; the trailing expression is the result)")).action(x(async(e,t,o)=>{const r=Do(t);let n;if(o.frame!==void 0){const i=Number.parseInt(o.frame,10);if(!Number.isInteger(i)||i<0){console.error(`Invalid frame index "${o.frame}". Use a 0-based index from "browser frames".`);process.exitCode=f.USAGE_ERROR;return}if(!e.evaluateInFrame)throw Error("This browser session does not support frame-targeted evaluation");n=await e.evaluateInFrame(r,i)}else n=await e.evaluate(r);if(typeof n==="string")console.log(n);else console.log(JSON.stringify(n,null,2))}));C(h.command("extract").option("--selector <css>","CSS selector scope; defaults to <main>/<article>/<body>").option("--chunk-size <chars>","Target chunk size in chars","20000").option("--start <char>","Start offset (use next_start_char from a previous extract)","0").description("Extract page content as markdown, paragraph-aware chunks for long pages")).action(x(async(e,t)=>{const o=String(t.chunkSize??"20000");if(!/^\d+$/.test(o)||Number.parseInt(o,10)<=0){console.log(JSON.stringify({error:{code:"invalid_chunk_size",message:`--chunk-size must be a positive integer, got "${t.chunkSize}"`}},null,2));process.exitCode=f.USAGE_ERROR;return}const r=String(t.start??"0");if(!/^\d+$/.test(r)){console.log(JSON.stringify({error:{code:"invalid_start",message:`--start must be a non-negative integer, got "${t.start}"`}},null,2));process.exitCode=f.USAGE_ERROR;return}const n=Number.parseInt(o,10),i=Number.parseInt(r,10),s=typeof t.selector==="string"&&t.selector.length>0?t.selector:null,d=Ho(s),u=await e.evaluate(d);if(!u){console.log(JSON.stringify({error:{code:"extract_failed",message:"Page returned no root element."}},null,2));process.exitCode=f.USAGE_ERROR;return}if("invalidSelector"in u){console.log(JSON.stringify({error:{code:"invalid_selector",message:`Selector "${s}" is not a valid CSS selector: ${u.reason}`}},null,2));process.exitCode=f.USAGE_ERROR;return}if("notFound"in u){console.log(JSON.stringify({error:{code:"selector_not_found",message:s?`Selector "${s}" matched 0 elements.`:"Page has no body/main/article element."}},null,2));process.exitCode=f.USAGE_ERROR;return}const m=qo({html:u.html,url:u.url,title:u.title,selector:s,start:i,chunkSize:n});console.log(JSON.stringify(m,null,2))}));C(h.command("network")).option("--detail <key>","Emit full body for the entry with this key").option("--all","Include static resources (js/css/images/telemetry)").option("--raw","Emit full bodies for every entry (skip shape preview)").option("--filter <fields>","Comma-separated field names; keep only entries whose body shape has ALL names as path segments").option("--since <duration>","Only include entries from the last duration (for example: 30s, 2m)").option("--until <duration>","Only include entries older than the duration from now").option("--follow","Continuously print new matching entries as JSON lines",!1).option("--failed","Only include failed HTTP requests (status 0 or >= 400)",!1).option("--max-body <chars>","With --detail: cap the emitted body at N chars (0 = unlimited, default)","0").option("--ttl <ms>","Cache TTL in ms for --detail lookups",String(Pt)).description("Capture network requests as shape previews; retrieve full bodies by key").action(x(async(e,t)=>{const o=Zt(t.ttl,"ttl",Pt),r=st(e),n=typeof t.detail==="string"&&t.detail.length>0,i=typeof t.filter==="string",s=Te(t.since,"since"),d=Te(t.until,"until");if(s&&typeof s==="object"){Z("invalid_since",s.error);return}if(d&&typeof d==="object"){Z("invalid_until",d.error);return}if(n&&i){Z("invalid_args","--filter and --detail cannot be used together (one narrows a list, the other fetches a specific entry).");return}let u=null;if(i){const w=Lo(t.filter);if("reason"in w){Z("invalid_filter",w.reason);return}u=w.fields}if(n&&t.follow){Z("invalid_args","--follow cannot be used with --detail.");return}if(n){const w=Fo(r,{ttlMs:o});if(w.status==="missing"){Z("cache_missing",`No cached capture. Run "browser network" first (in session "${r}").`);return}if(w.status==="expired"){Z("cache_expired",`Cache is stale (age ${w.ageMs}ms > ttl ${o}ms). Re-run "browser network" to refresh.`);return}if(w.status==="corrupt"||!w.file){Z("cache_corrupt",'Cache file is malformed; re-run "browser network" to regenerate.');return}const O=Go(w.file,t.detail);if(!O){Z("key_not_found",`Key "${t.detail}" not in cache.`,{available_keys:w.file.entries.map((H)=>H.key)});return}const y=String(t.maxBody??"0");if(!/^\d+$/.test(y)){Z("invalid_max_body",`--max-body must be a non-negative integer, got "${t.maxBody}"`);return}const S=Number.parseInt(y,10);let D=O.body,B=!1;if(S>0&&typeof O.body==="string"&&O.body.length>S){D=O.body.slice(0,S);B=!0}const ae=O.body_truncated===!0,q={key:O.key,url:O.url,method:O.method,status:O.status,ct:O.ct,size:O.size,...typeof O.timestamp==="number"?{timestamp:we(O.timestamp)}:{},shape:At(O.body),body:D};if(ae||B){q.body_truncated=!0;q.body_full_size=O.body_full_size??O.size;q.body_truncation_reason=ae?"capture-limit":"max-body"}console.log(JSON.stringify(q,null,2));return}if(t.follow){if(!await e.startNetworkCapture?.())try{await e.evaluate(Ae)}catch{}while(!0){const w=await ye(e).catch((S)=>{Z("capture_failed",`Could not read network capture: ${S.message}`);return[]});let O=t.all?w:Gt(w);O=ot(O,{sinceMs:s,untilMs:d});if(t.failed)O=O.filter((S)=>S.status===0||S.status>=400);const y=Tt(O);for(const S of y){const D=S.channel==="websocket"||S.channel==="eventsource";console.log(JSON.stringify({key:S.key,timestamp:we(S.timestamp),method:S.method,status:S.status,url:S.url,ct:S.ct,size:S.size,...D?{channel:S.channel,direction:S.direction}:{},...S.eventName?{event_name:S.eventName}:{},...D?{body:hr(S.body)}:{},...S.bodyTruncated?{body_truncated:!0}:{}}))}await new Promise((S)=>setTimeout(S,Dt))}}let m;try{m=await ye(e)}catch(w){Z("capture_failed",`Could not read network capture: ${w.message}`);return}let g=t.all?m:Gt(m);g=ot(g,{sinceMs:s,untilMs:d});if(t.failed)g=g.filter((w)=>w.status===0||w.status>=400);const b=m.length-g.length,P=Tt(g).map((w)=>({key:w.key,url:w.url,method:w.method,status:w.status,size:w.size,ct:w.ct,body:w.body,...typeof w.timestamp==="number"?{timestamp:w.timestamp}:{},...w.bodyTruncated?{body_truncated:!0}:{},...w.bodyTruncated&&typeof w.bodyFullSize==="number"?{body_full_size:w.bodyFullSize}:{}}));let G=null;try{Bo(r,P)}catch(w){G=`Could not persist capture cache: ${w.message}. --detail lookups may miss this capture.`}const _=P.map((w)=>({entry:w,shape:At(w.body)})),M=u?_.filter((w)=>zo(w.shape,u)):_,J=u?_.length-M.length:0,W={session:r,captured_at:new Date().toISOString(),count:M.length,filtered_out:b};if(u){W.filter=u;W.filter_dropped=J}if(G)W.cache_warning=G;const ne=M.filter((w)=>w.entry.body_truncated).length;if(ne>0){W.body_truncated_count=ne;W.body_truncated_hint="Some bodies exceeded the capture limit; their `shape` reflects only the captured prefix."}if(t.raw)W.entries=M.map((w)=>({...w.entry,...typeof w.entry.timestamp==="number"?{timestamp:we(w.entry.timestamp)}:{}}));else{W.entries=M.map((w)=>({key:w.entry.key,method:w.entry.method,...typeof w.entry.timestamp==="number"?{timestamp:we(w.entry.timestamp)}:{},status:w.entry.status,url:w.entry.url,ct:w.entry.ct,size:w.entry.size,shape:w.shape,...w.entry.body_truncated?{body_truncated:!0}:{}}));W.detail_hint='Run "browser network --detail <key>" for full body.'}console.log(JSON.stringify(W,null,2))}));h.command("init").argument("<name>","Adapter name in site/command format (e.g. hn/top)").description("Generate adapter scaffold in ~/.ppcli/clis/").action(async(e)=>{try{const t=e.split("/");if(t.length!==2||!t[0]||!t[1]){console.error("Name must be site/command format (e.g. hn/top)");process.exitCode=f.USAGE_ERROR;return}const[o,r]=t;if(!/^[a-zA-Z0-9_-]+$/.test(o)||!/^[a-zA-Z0-9_-]+$/.test(r)){console.error("Name parts must be alphanumeric/dash/underscore only");process.exitCode=f.USAGE_ERROR;return}const n=await import("node:os"),i=await import("node:fs"),s=await import("node:path"),d=s.join(n.homedir(),".ppcli","clis",o),u=s.join(d,`${r}.js`);if(i.existsSync(u)){console.log(`Adapter already exists: ${u}`);return}const g=`import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
106
|
+
${d.slice(0,n)}`);return}console.log(d)}));C(z(me.command("attributes")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","Pick the nth match (0-based) when <target> is a multi-match CSS selector").description("Element attributes — JSON envelope {value, matches_n}")).action(x(async(e,t,o)=>Be(e,t,o??{},jo(),"attributes")));function ee(e){const t=N(e);if(t&&typeof t==="object"&&"error"in t)return t;if(typeof t==="number")return{opts:{nth:t}};return{opts:{}}}function oo(e){const t=Array.isArray(e)?e:[];if(t.length===0)return{error:{code:"usage_error",message:"At least one file path is required.",hint:'Example: ppcli browser upload "input[type=file]" ./receipt.pdf'}};const o=[];for(const r of t){const n=String(r),i=n==="~"||n.startsWith(`~${k.sep}`)?k.join(ge.homedir(),n.slice(2)):n,s=k.resolve(i);if(!A.existsSync(s))return{error:{code:"file_not_found",message:`File not found: ${s}`}};if(!A.statSync(s).isFile())return{error:{code:"not_a_file",message:`Not a regular file: ${s}`}};o.push(s)}return{files:o}}function Le(e,t){const o=N(e);if(o&&typeof o==="object"&&"error"in o)return{error:o.error.replace("--nth",t)};if(typeof o==="number")return{opts:{nth:o}};return{opts:{}}}C(z(h.command("click")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Click element — JSON envelope {clicked, target, matches_n}")).action(x(async(e,t,o)=>{const r=await Ge(e,t,o??{},"write");if(typeof r!=="string"){console.log(JSON.stringify(r,null,2));process.exitCode=f.USAGE_ERROR;return}const n=ee(o?.nth);if("error"in n){console.log(JSON.stringify({error:{code:"usage_error",message:n.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const{matches_n:i,match_level:s,click_method:d,hit:u,retargeted:m}=await e.click(r,n.opts);console.log(JSON.stringify({clicked:!0,target:r,matches_n:i,match_level:s,...d&&{click_method:d},...u&&{hit:u},...m&&{retargeted:m}},null,2))}));C(z(h.command("type")).argument("[targetOrText]","Numeric ref/CSS target, or text when using --role/--name/etc.").argument("[text]","Text to type").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Click element, then type text — JSON envelope {typed, text, target, matches_n, autocomplete}")).action(x(async(e,t,o,r)=>{const n=await Fe(e,t,o,r??{},"text");if(!n)return;const i=ee(r?.nth);if("error"in i){console.log(JSON.stringify({error:{code:"usage_error",message:i.error}},null,2));process.exitCode=f.USAGE_ERROR;return}await e.click(n.target,i.opts);await e.wait(0.3);const{matches_n:s,match_level:d}=await e.typeText(n.target,n.value,i.opts),u=await e.evaluate(Do());if(u)await e.wait(0.4);console.log(JSON.stringify({typed:!0,text:n.value,target:n.target,matches_n:s,match_level:d,autocomplete:!!u},null,2))}));C(z(h.command("hover")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Move the mouse over an element — JSON envelope {hovered, target, matches_n}")).action(x(async(e,t,o)=>{if(typeof e.hover!=="function")throw Error("browser hover is not supported by this browser backend");const r=await ue(e,t,o??{});if(!r)return;const n=ee(o?.nth);if("error"in n){console.log(JSON.stringify({error:{code:"usage_error",message:n.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const{matches_n:i,match_level:s}=await e.hover(r,n.opts);console.log(JSON.stringify({hovered:!0,target:r,matches_n:i,match_level:s},null,2))}));C(z(h.command("focus")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Focus an element — JSON envelope {focused, target, matches_n}")).action(x(async(e,t,o)=>{if(typeof e.focus!=="function")throw Error("browser focus is not supported by this browser backend");const r=await ue(e,t,o??{});if(!r)return;const n=ee(o?.nth);if("error"in n){console.log(JSON.stringify({error:{code:"usage_error",message:n.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const{focused:i,matches_n:s,match_level:d}=await e.focus(r,n.opts);console.log(JSON.stringify({focused:i,target:r,matches_n:s,match_level:d},null,2))}));C(z(h.command("dblclick")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Double-click element — JSON envelope {dblclicked, target, matches_n}")).action(x(async(e,t,o)=>{if(typeof e.dblClick!=="function")throw Error("browser dblclick is not supported by this browser backend");const r=await ue(e,t,o??{});if(!r)return;const n=ee(o?.nth);if("error"in n){console.log(JSON.stringify({error:{code:"usage_error",message:n.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const{matches_n:i,match_level:s}=await e.dblClick(r,n.opts);console.log(JSON.stringify({dblclicked:!0,target:r,matches_n:i,match_level:s},null,2))}));const mt=async(e,t,o,r)=>{if(typeof e.setChecked!=="function")throw Error(`browser ${r?"check":"uncheck"} is not supported by this browser backend`);const n=await ue(e,t,o);if(!n)return;const i=ee(o?.nth);if("error"in i){console.log(JSON.stringify({error:{code:"usage_error",message:i.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const s=await e.setChecked(n,r,i.opts);console.log(JSON.stringify({checked:s.checked,changed:s.changed,target:n,matches_n:s.matches_n,match_level:s.match_level,...s.kind?{kind:s.kind}:{}},null,2))};C(z(h.command("check")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Ensure a checkbox/radio/aria-checked control is checked — JSON envelope {checked, changed, target, matches_n}")).action(x(async(e,t,o)=>{await mt(e,t,o??{},!0)}));C(z(h.command("uncheck")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Ensure a checkbox/aria-checked control is unchecked — JSON envelope {checked, changed, target, matches_n}")).action(x(async(e,t,o)=>{await mt(e,t,o??{},!1)}));C(z(h.command("upload")).argument("[targetOrFile]","Numeric ref/CSS target, or first file when using --role/--name/etc.").argument("[files...]","Local file path(s) to attach").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Attach local files to a file input — JSON envelope {uploaded, files, file_names, target, matches_n}")).action(x(async(e,t,o,r)=>{if(typeof e.uploadFiles!=="function")throw Error("browser upload is not supported by this browser backend");const n=!!ve(r??{}),s=await ue(e,n?void 0:t,r??{});if(!s)return;const d=ee(r?.nth);if("error"in d){console.log(JSON.stringify({error:{code:"usage_error",message:d.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const u=n?[t,...Array.isArray(o)?o:[]].filter((b)=>b!==void 0):o,m=oo(u);if("error"in m){console.log(JSON.stringify({error:m.error},null,2));process.exitCode=f.USAGE_ERROR;return}const g=await e.uploadFiles(s,m.files,d.opts);console.log(JSON.stringify(g,null,2))}));C(ct(ct(h.command("drag"),"from"),"to").argument("[source]","Numeric ref/CSS selector to drag from, or omit with --from-role/--from-name/etc.").argument("[target]","Numeric ref/CSS selector to drop onto, or omit with --to-role/--to-name/etc. or --dx/--dy").option("--from-nth <n>","When <source> is a multi-match CSS selector, pick the nth match (0-based)").option("--to-nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").option("--dx <px>","Drag by this many CSS pixels horizontally from the source centre instead of onto a target (negative = left)").option("--dy <px>","Drag by this many CSS pixels vertically from the source centre instead of onto a target (negative = up)").option("--steps <n>","Pointer-move samples between press and release (default: 2 for element drops, ~1 per 12px for --dx/--dy)").description("Drag one element onto another, or by a pixel offset with --dx/--dy (sliders) — JSON envelope {dragged, source, target | dx, dy, from, to}")).action(x(async(e,t,o,r)=>{if(typeof e.drag!=="function")throw Error("browser drag is not supported by this browser backend");const n=(_,M)=>{console.log(JSON.stringify({error:{code:"usage_error",message:_,...M?{hint:M}:{}}},null,2));process.exitCode=f.USAGE_ERROR},i=(_,M)=>{if(_===void 0||_===null||_==="")return null;const J=Number(String(_).trim().replace(/px$/i,""));if(!Number.isFinite(J))return{error:`${M} must be a number of CSS pixels, got "${String(_)}"`};return J},s=i(r?.dx,"--dx"),d=i(r?.dy,"--dy");for(const _ of[s,d])if(_&&typeof _==="object")return n(_.error);let u;if(r?.steps!==void 0){u=Number(r.steps);if(!Number.isInteger(u)||u<1||u>200)return n(`--steps must be an integer between 1 and 200, got "${String(r.steps)}"`)}const m=s!==null||d!==null;if(m){if(typeof o==="string"&&o.length>0||lt(r??{},"to")!==null||r?.toNth!==void 0)return n("Pass either a drop target or --dx/--dy, not both.","Element drop: browser drag <source> <target>. Pixel drag: browser drag <source> --dx 180.");if((s??0)===0&&(d??0)===0)return n("--dx/--dy are both 0 — nothing to drag.");if(typeof e.dragBy!=="function")throw Error("browser drag --dx/--dy is not supported by this browser backend")}const g=await ut(e,t,r??{},"from","source");if(!g)return;if(m){const _=Le(r?.fromNth,"--from-nth");if("error"in _)return n(_.error);const M=await e.dragBy(g,{dx:s??0,dy:d??0},{from:_.opts,...u!==void 0?{steps:u}:{}});console.log(JSON.stringify(M,null,2));return}const b=await ut(e,o,r??{},"to","target");if(!b)return;const R=Le(r?.fromNth,"--from-nth");if("error"in R){console.log(JSON.stringify({error:{code:"usage_error",message:R.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const P=Le(r?.toNth,"--to-nth");if("error"in P){console.log(JSON.stringify({error:{code:"usage_error",message:P.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const G=await e.drag(g,b,{from:R.opts,to:P.opts,...u!==void 0?{steps:u}:{}});console.log(JSON.stringify(G,null,2))}));C(z(h.command("fill")).argument("[targetOrText]","Numeric ref/CSS target, or text when using --role/--name/etc.").argument("[text]","Text to set exactly").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Set input/textarea/contenteditable text exactly and verify the value — JSON envelope {filled, verified, text, actual}")).action(x(async(e,t,o,r)=>{const n=await Fe(e,t,o,r??{},"text");if(!n)return;const i=ee(r?.nth);if("error"in i){console.log(JSON.stringify({error:{code:"usage_error",message:i.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const s=await e.fillText(n.target,n.value,i.opts);if(!s.verified)process.exitCode=f.GENERIC_ERROR;console.log(JSON.stringify({filled:s.filled,verified:s.verified,target:n.target,text:n.value,actual:s.actual,length:s.length,matches_n:s.matches_n,match_level:s.match_level,...s.mode?{mode:s.mode}:{}},null,2))}));C(z(h.command("select")).argument("[targetOrOption]","Numeric ref/CSS target, or option text when using --role/--name/etc.").argument("[option]","Option text (or value) to select").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Select dropdown option — JSON envelope {selected, target, matches_n}")).action(x(async(e,t,o,r)=>{const n=await Fe(e,t,o,r??{},"option");if(!n)return;const i=ee(r?.nth);if("error"in i){console.log(JSON.stringify({error:{code:"usage_error",message:i.error}},null,2));process.exitCode=f.USAGE_ERROR;return}const{matches_n:s,match_level:d}=await E(e,n.target,i.opts),u=await e.evaluate(Mo(n.value));if(u?.error){console.log(JSON.stringify({error:{code:u.error==="Not a <select>"?"not_a_select":"option_not_found",message:u.error,...u.available&&{available:u.available},matches_n:s}},null,2));process.exitCode=f.GENERIC_ERROR;return}console.log(JSON.stringify({selected:u?.selected??n.value,target:n.target,matches_n:s,match_level:d},null,2))}));C(h.command("keys").argument("<key>","Key to press (Enter, Escape, Tab, Control+a)")).description("Press keyboard key").action(x(async(e,t)=>{await e.pressKey(t);console.log(`Pressed: ${t}`)}));const pt=h.command("dialog").description("Handle a blocking JavaScript alert/confirm/prompt dialog");C(pt.command("accept").option("--text <text>","Prompt text to submit for prompt() dialogs").description("Accept the currently open JavaScript dialog")).action(x(async(e,t)=>{if(!e.handleJavaScriptDialog)throw Error("This browser session does not support JavaScript dialog handling");try{await e.handleJavaScriptDialog(!0,t?.text)}catch(o){if(U(o).toLowerCase().includes("no dialog")){console.log(JSON.stringify({error:{code:"no_javascript_dialog",message:"No JavaScript dialog is currently open."}},null,2));process.exitCode=f.USAGE_ERROR;return}throw o}console.log(JSON.stringify({handled:!0,action:"accept",...t?.text!==void 0&&{text:t.text}},null,2))}));C(pt.command("dismiss").description("Dismiss the currently open JavaScript dialog")).action(x(async(e)=>{if(!e.handleJavaScriptDialog)throw Error("This browser session does not support JavaScript dialog handling");try{await e.handleJavaScriptDialog(!1)}catch(t){if(U(t).toLowerCase().includes("no dialog")){console.log(JSON.stringify({error:{code:"no_javascript_dialog",message:"No JavaScript dialog is currently open."}},null,2));process.exitCode=f.USAGE_ERROR;return}throw t}console.log(JSON.stringify({handled:!0,action:"dismiss"},null,2))}));C(h.command("wait")).argument("<type>","selector, text, time, xhr, or download").argument("[value]","CSS selector, text string, seconds, XHR URL regex, or download filename/URL pattern").option("--timeout <ms>","Timeout in milliseconds","10000").description('Wait for selector, text, time, matching XHR, or browser download (e.g. wait selector ".loaded", wait text "Success", wait time 3, wait xhr "/api/search", wait download receipt.pdf)').action(x(async(e,t,o,r)=>{const n=parseInt(r.timeout,10);if(t==="time"){const i=parseFloat(o??"2");await e.wait(i);console.log(`Waited ${i}s`)}else if(t==="selector"){if(!o){console.error("Missing CSS selector");process.exitCode=f.USAGE_ERROR;return}await e.wait({selector:o,timeout:n/1000});console.log(`Element "${o}" appeared`)}else if(t==="text"){if(!o){console.error("Missing text");process.exitCode=f.USAGE_ERROR;return}await e.wait({text:o,timeout:n/1000});console.log(`Text "${o}" appeared`)}else if(t==="xhr"){if(!o){console.error("Missing XHR URL regex");process.exitCode=f.USAGE_ERROR;return}let i;try{i=new RegExp(o)}catch(g){console.error(`Invalid regex "${o}": ${g instanceof Error?g.message:String(g)}`);process.exitCode=f.USAGE_ERROR;return}if(!(await e.startNetworkCapture?.()??!1))try{await e.evaluate(Ae)}catch{}await ye(e);const d=Date.now()+n,u=400;let m=null;while(Date.now()<d&&!m){m=(await ye(e)).find((b)=>i.test(b.url))??null;if(!m)await new Promise((b)=>setTimeout(b,u))}if(!m){console.log(JSON.stringify({error:{code:"xhr_not_seen",message:`No captured XHR matched /${o}/ within ${n}ms`,hint:"Check the pattern against `browser network` output; the endpoint may not have fired yet, or capture is disabled."}},null,2));process.exitCode=f.GENERIC_ERROR;return}console.log(JSON.stringify({matched:{url:m.url,status:m.status,contentType:m.ct}},null,2))}else if(t==="download"){if(typeof e.waitForDownload!=="function"){console.log(JSON.stringify({error:{code:"download_wait_unavailable",message:"The active browser backend does not support download lifecycle waits.",hint:"Use the Browser Bridge extension version 1.0.8 or newer, then retry the command."}},null,2));process.exitCode=f.GENERIC_ERROR;return}const i=await e.waitForDownload(String(o??""),n);if(!i.downloaded){const s=i.state==="interrupted"&&i.id!==void 0?"download_failed":"download_not_seen";console.log(JSON.stringify({error:{code:s,message:i.error??`No download matched "${o??"*"}" within ${n}ms`,hint:"Check the pattern against the expected filename or URL; use a longer --timeout if the download starts slowly."},download:i},null,2));process.exitCode=f.GENERIC_ERROR;return}console.log(JSON.stringify(i,null,2))}else{console.error(`Unknown wait type "${t}". Use: selector, text, time, xhr, or download`);process.exitCode=f.USAGE_ERROR}}));C(h.command("eval").argument("<js>","JavaScript code").option("--frame <index>",'Cross-origin iframe index from "browser frames"').description("Execute JS in page context, return result (top-level await is supported; the trailing expression is the result)")).action(x(async(e,t,o)=>{const r=Uo(t);let n;if(o.frame!==void 0){const i=Number.parseInt(o.frame,10);if(!Number.isInteger(i)||i<0){console.error(`Invalid frame index "${o.frame}". Use a 0-based index from "browser frames".`);process.exitCode=f.USAGE_ERROR;return}if(!e.evaluateInFrame)throw Error("This browser session does not support frame-targeted evaluation");n=await e.evaluateInFrame(r,i)}else n=await e.evaluate(r);if(typeof n==="string")console.log(n);else console.log(JSON.stringify(n,null,2))}));C(h.command("extract").option("--selector <css>","CSS selector scope; defaults to <main>/<article>/<body>").option("--chunk-size <chars>","Target chunk size in chars","20000").option("--start <char>","Start offset (use next_start_char from a previous extract)","0").description("Extract page content as markdown, paragraph-aware chunks for long pages")).action(x(async(e,t)=>{const o=String(t.chunkSize??"20000");if(!/^\d+$/.test(o)||Number.parseInt(o,10)<=0){console.log(JSON.stringify({error:{code:"invalid_chunk_size",message:`--chunk-size must be a positive integer, got "${t.chunkSize}"`}},null,2));process.exitCode=f.USAGE_ERROR;return}const r=String(t.start??"0");if(!/^\d+$/.test(r)){console.log(JSON.stringify({error:{code:"invalid_start",message:`--start must be a non-negative integer, got "${t.start}"`}},null,2));process.exitCode=f.USAGE_ERROR;return}const n=Number.parseInt(o,10),i=Number.parseInt(r,10),s=typeof t.selector==="string"&&t.selector.length>0?t.selector:null,d=qo(s),u=await e.evaluate(d);if(!u){console.log(JSON.stringify({error:{code:"extract_failed",message:"Page returned no root element."}},null,2));process.exitCode=f.USAGE_ERROR;return}if("invalidSelector"in u){console.log(JSON.stringify({error:{code:"invalid_selector",message:`Selector "${s}" is not a valid CSS selector: ${u.reason}`}},null,2));process.exitCode=f.USAGE_ERROR;return}if("notFound"in u){console.log(JSON.stringify({error:{code:"selector_not_found",message:s?`Selector "${s}" matched 0 elements.`:"Page has no body/main/article element."}},null,2));process.exitCode=f.USAGE_ERROR;return}const m=Vo({html:u.html,url:u.url,title:u.title,selector:s,start:i,chunkSize:n});console.log(JSON.stringify(m,null,2))}));C(h.command("network")).option("--detail <key>","Emit full body for the entry with this key").option("--all","Include static resources (js/css/images/telemetry)").option("--raw","Emit full bodies for every entry (skip shape preview)").option("--filter <fields>","Comma-separated field names; keep only entries whose body shape has ALL names as path segments").option("--since <duration>","Only include entries from the last duration (for example: 30s, 2m)").option("--until <duration>","Only include entries older than the duration from now").option("--follow","Continuously print new matching entries as JSON lines",!1).option("--failed","Only include failed HTTP requests (status 0 or >= 400)",!1).option("--max-body <chars>","With --detail: cap the emitted body at N chars (0 = unlimited, default)","0").option("--ttl <ms>","Cache TTL in ms for --detail lookups",String(It)).description("Capture network requests as shape previews; retrieve full bodies by key").action(x(async(e,t)=>{const o=Yt(t.ttl,"ttl",It),r=at(e),n=typeof t.detail==="string"&&t.detail.length>0,i=typeof t.filter==="string",s=Te(t.since,"since"),d=Te(t.until,"until");if(s&&typeof s==="object"){Z("invalid_since",s.error);return}if(d&&typeof d==="object"){Z("invalid_until",d.error);return}if(n&&i){Z("invalid_args","--filter and --detail cannot be used together (one narrows a list, the other fetches a specific entry).");return}let u=null;if(i){const w=zo(t.filter);if("reason"in w){Z("invalid_filter",w.reason);return}u=w.fields}if(n&&t.follow){Z("invalid_args","--follow cannot be used with --detail.");return}if(n){const w=Bo(r,{ttlMs:o});if(w.status==="missing"){Z("cache_missing",`No cached capture. Run "browser network" first (in session "${r}").`);return}if(w.status==="expired"){Z("cache_expired",`Cache is stale (age ${w.ageMs}ms > ttl ${o}ms). Re-run "browser network" to refresh.`);return}if(w.status==="corrupt"||!w.file){Z("cache_corrupt",'Cache file is malformed; re-run "browser network" to regenerate.');return}const O=Fo(w.file,t.detail);if(!O){Z("key_not_found",`Key "${t.detail}" not in cache.`,{available_keys:w.file.entries.map((H)=>H.key)});return}const y=String(t.maxBody??"0");if(!/^\d+$/.test(y)){Z("invalid_max_body",`--max-body must be a non-negative integer, got "${t.maxBody}"`);return}const S=Number.parseInt(y,10);let D=O.body,B=!1;if(S>0&&typeof O.body==="string"&&O.body.length>S){D=O.body.slice(0,S);B=!0}const ae=O.body_truncated===!0,q={key:O.key,url:O.url,method:O.method,status:O.status,ct:O.ct,size:O.size,...typeof O.timestamp==="number"?{timestamp:we(O.timestamp)}:{},shape:Tt(O.body),body:D};if(ae||B){q.body_truncated=!0;q.body_full_size=O.body_full_size??O.size;q.body_truncation_reason=ae?"capture-limit":"max-body"}console.log(JSON.stringify(q,null,2));return}if(t.follow){if(!await e.startNetworkCapture?.())try{await e.evaluate(Ae)}catch{}while(!0){const w=await ye(e).catch((S)=>{Z("capture_failed",`Could not read network capture: ${S.message}`);return[]});let O=t.all?w:Bt(w);O=ot(O,{sinceMs:s,untilMs:d});if(t.failed)O=O.filter((S)=>S.status===0||S.status>=400);const y=Pt(O);for(const S of y){const D=S.channel==="websocket"||S.channel==="eventsource";console.log(JSON.stringify({key:S.key,timestamp:we(S.timestamp),method:S.method,status:S.status,url:S.url,ct:S.ct,size:S.size,...D?{channel:S.channel,direction:S.direction}:{},...S.eventName?{event_name:S.eventName}:{},...D?{body:yr(S.body)}:{},...S.bodyTruncated?{body_truncated:!0}:{}}))}await new Promise((S)=>setTimeout(S,Gt))}}let m;try{m=await ye(e)}catch(w){Z("capture_failed",`Could not read network capture: ${w.message}`);return}let g=t.all?m:Bt(m);g=ot(g,{sinceMs:s,untilMs:d});if(t.failed)g=g.filter((w)=>w.status===0||w.status>=400);const b=m.length-g.length,P=Pt(g).map((w)=>({key:w.key,url:w.url,method:w.method,status:w.status,size:w.size,ct:w.ct,body:w.body,...typeof w.timestamp==="number"?{timestamp:w.timestamp}:{},...w.bodyTruncated?{body_truncated:!0}:{},...w.bodyTruncated&&typeof w.bodyFullSize==="number"?{body_full_size:w.bodyFullSize}:{}}));let G=null;try{Lo(r,P)}catch(w){G=`Could not persist capture cache: ${w.message}. --detail lookups may miss this capture.`}const _=P.map((w)=>({entry:w,shape:Tt(w.body)})),M=u?_.filter((w)=>Wo(w.shape,u)):_,J=u?_.length-M.length:0,W={session:r,captured_at:new Date().toISOString(),count:M.length,filtered_out:b};if(u){W.filter=u;W.filter_dropped=J}if(G)W.cache_warning=G;const ne=M.filter((w)=>w.entry.body_truncated).length;if(ne>0){W.body_truncated_count=ne;W.body_truncated_hint="Some bodies exceeded the capture limit; their `shape` reflects only the captured prefix."}if(t.raw)W.entries=M.map((w)=>({...w.entry,...typeof w.entry.timestamp==="number"?{timestamp:we(w.entry.timestamp)}:{}}));else{W.entries=M.map((w)=>({key:w.entry.key,method:w.entry.method,...typeof w.entry.timestamp==="number"?{timestamp:we(w.entry.timestamp)}:{},status:w.entry.status,url:w.entry.url,ct:w.entry.ct,size:w.entry.size,shape:w.shape,...w.entry.body_truncated?{body_truncated:!0}:{}}));W.detail_hint='Run "browser network --detail <key>" for full body.'}console.log(JSON.stringify(W,null,2))}));h.command("init").argument("<name>","Adapter name in site/command format (e.g. hn/top)").description("Generate adapter scaffold in ~/.ppcli/clis/").action(async(e)=>{try{const t=e.split("/");if(t.length!==2||!t[0]||!t[1]){console.error("Name must be site/command format (e.g. hn/top)");process.exitCode=f.USAGE_ERROR;return}const[o,r]=t;if(!/^[a-zA-Z0-9_-]+$/.test(o)||!/^[a-zA-Z0-9_-]+$/.test(r)){console.error("Name parts must be alphanumeric/dash/underscore only");process.exitCode=f.USAGE_ERROR;return}const n=await import("node:os"),i=await import("node:fs"),s=await import("node:path"),d=s.join(n.homedir(),".ppcli","clis",o),u=s.join(d,`${r}.js`);if(i.existsSync(u)){console.log(`Adapter already exists: ${u}`);return}const g=`import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
107
107
|
|
|
108
108
|
cli({
|
|
109
109
|
site: '${o}',
|
|
@@ -133,10 +133,10 @@ cli({
|
|
|
133
133
|
→ ${H.length} row${H.length===1?"":"s"}`);const ce=g(H);if(ce.length>0){console.log(`
|
|
134
134
|
✗ Adapter output violates row shape conventions:`);for(const j of ce.slice(0,20)){const V=j.rowIndex!==void 0?`row[${j.rowIndex}] `:"";console.log(` - [${j.rule}] ${V}${j.detail}`)}if(ce.length>20)console.log(` ... and ${ce.length-20} more failure(s)`);console.log(`
|
|
135
135
|
Keep rows agent-native: <=12 top-level keys, nesting depth <=1, and id-shaped fields at top level.`);process.exitCode=f.GENERIC_ERROR;return}if(t.writeFixture||t.updateFixture)if(J&&!t.updateFixture){console.log(`
|
|
136
|
-
Fixture already exists at ${b(r,n)}.`);console.log(" Use --update-fixture to overwrite.")}else{const V=u(H,O!==void 0?O:ne?{limit:3}:void 0),
|
|
137
|
-
${J?"↻ Updated":"✎ Wrote"} fixture: ${
|
|
136
|
+
Fixture already exists at ${b(r,n)}.`);console.log(" Use --update-fixture to overwrite.")}else{const V=u(H,O!==void 0?O:ne?{limit:3}:void 0),po=d(r,n,V);console.log(`
|
|
137
|
+
${J?"↻ Updated":"✎ Wrote"} fixture: ${po}`);console.log(" Review and hand-tune the derived expectations (add patterns / notEmpty, tighten rowCount).");J=V}if(!J){console.log(`
|
|
138
138
|
✓ Adapter runs. (No fixture at ${b(r,n)} — consider --write-fixture to seed one.)`);const j=checkSiteMemory(r);printSiteMemoryReport(j,t.strictMemory);if(!j.ok&&t.strictMemory)process.exitCode=f.GENERIC_ERROR;return}const fe=m(H,J);if(fe.length===0){console.log(`
|
|
139
139
|
✓ Adapter matches fixture (${b(r,n)}).`);const j=checkSiteMemory(r);printSiteMemoryReport(j,t.strictMemory);if(!j.ok&&t.strictMemory)process.exitCode=f.GENERIC_ERROR;return}console.log(`
|
|
140
|
-
✗ Adapter output does not match fixture:`);for(const j of fe.slice(0,20)){const V=j.rowIndex!==void 0?`row[${j.rowIndex}] `:"";console.log(` - [${j.rule}] ${V}${j.detail}`)}if(fe.length>20)console.log(` ... and ${fe.length-20} more failure(s)`);process.exitCode=f.GENERIC_ERROR}catch(o){console.error(`Error: ${o instanceof Error?o.message:String(o)}`);process.exitCode=f.GENERIC_ERROR}});h.command("close").description("Release the current browser session tab lease").action(x(async(e)=>{await e.closeWindow?.();console.log("Browser session tab lease released")}));a.command("doctor").description("Diagnose ppcli browser bridge connectivity").option("-v, --verbose","Debug output").action(async(e)=>{
|
|
140
|
+
✗ Adapter output does not match fixture:`);for(const j of fe.slice(0,20)){const V=j.rowIndex!==void 0?`row[${j.rowIndex}] `:"";console.log(` - [${j.rule}] ${V}${j.detail}`)}if(fe.length>20)console.log(` ... and ${fe.length-20} more failure(s)`);process.exitCode=f.GENERIC_ERROR}catch(o){console.error(`Error: ${o instanceof Error?o.message:String(o)}`);process.exitCode=f.GENERIC_ERROR}});h.command("close").description("Release the current browser session tab lease").action(x(async(e)=>{await e.closeWindow?.();console.log("Browser session tab lease released")}));a.command("doctor").description("Diagnose ppcli browser bridge connectivity").option("-v, --verbose","Debug output").action(async(e)=>{Ar(e);const{runBrowserDoctor:t,renderBrowserDoctorReport:o}=await import("./doctor.js"),r=await t({cliVersion:ke});console.log(o(r))});a.command("completion").description("Output shell completion script").argument("<shell>","Shell type: bash, zsh, or fish").action((e)=>{xo(e)});const se=a.command("plugin").description("Manage ppcli plugins"),ro=se.description();se.command("install").description("Install a plugin from a git repository").argument("<source>","Plugin source (e.g. github:user/repo)").action(async(e)=>{const{installPlugin:t}=await import("./plugin.js"),{discoverPlugins:o}=await import("./discovery.js");try{const r=t(e);await o();if(Array.isArray(r))if(r.length===0)console.log("No plugins were installed (all skipped or incompatible).");else console.log(`✅ Installed ${r.length} plugin(s) from monorepo: ${r.join(", ")}`);else console.log(`✅ Plugin "${r}" installed successfully. Commands are ready to use.`)}catch(r){console.error(`Error: ${U(r)}`);process.exitCode=f.GENERIC_ERROR}});se.command("uninstall").description("Uninstall a plugin").argument("<name>","Plugin name").action(async(e)=>{const{uninstallPlugin:t}=await import("./plugin.js");try{t(e);console.log(`✅ Plugin "${e}" uninstalled.`)}catch(o){console.error(`Error: ${U(o)}`);process.exitCode=f.GENERIC_ERROR}});se.command("update").description("Update a plugin (or all plugins) to the latest version").argument("[name]","Plugin name (required unless --all is passed)").option("--all","Update all installed plugins").action(async(e,t)=>{if(!e&&!t.all){console.error("Error: Please specify a plugin name or use the --all flag.");process.exitCode=f.USAGE_ERROR;return}if(e&&t.all){console.error("Error: Cannot specify both a plugin name and --all.");process.exitCode=f.USAGE_ERROR;return}const{updatePlugin:o,updateAllPlugins:r}=await import("./plugin.js"),{discoverPlugins:n}=await import("./discovery.js");if(t.all){const i=r();if(i.length>0)await n();let s=!1;console.log(" Update Results:");for(const d of i){if(d.success){console.log(` ✓ ${d.name}`);continue}s=!0;console.log(` ✗ ${d.name} — ${String(d.error)}`)}if(i.length===0){console.log(" No plugins installed.");return}console.log();if(s){console.error("Completed with some errors.");process.exitCode=f.GENERIC_ERROR}else console.log("✅ All plugins updated successfully.");return}try{o(e);await n();console.log(`✅ Plugin "${e}" updated successfully.`)}catch(i){console.error(`Error: ${U(i)}`);process.exitCode=f.GENERIC_ERROR}});se.command("list").description("List installed plugins").option("-f, --format <fmt>","Output format: table, json","table").action(async(e)=>{const{listPlugins:t}=await import("./plugin.js"),o=t();if(o.length===0){console.log(" No plugins installed.");console.log(" Install one with: ppcli plugin install github:user/repo");return}if(e.format==="json"){_e(o,{fmt:"json",columns:["name","commands","source"],title:"ppcli/plugins",source:"ppcli plugin list"});return}console.log();console.log(" Installed plugins");console.log();const r=o.filter((i)=>!i.monorepoName),n=new Map;for(const i of o){if(!i.monorepoName)continue;const s=n.get(i.monorepoName)??[];s.push(i);n.set(i.monorepoName,s)}for(const i of r){const s=i.version?` @${i.version}`:"",d=i.description?` — ${i.description}`:"",u=i.commands.length>0?` (${i.commands.join(", ")})`:"",m=i.source?` ← ${i.source}`:"";console.log(` ${i.name}${s}${d}${u}${m}`)}for(const[i,s]of n){console.log();console.log(` \uD83D\uDCE6 ${i} (monorepo)`);for(const d of s){const u=d.version?` @${d.version}`:"",m=d.description?` — ${d.description}`:"",g=d.commands.length>0?` (${d.commands.join(", ")})`:"";console.log(` ${d.name}${u}${m}${g}`)}}console.log();console.log(` ${o.length} plugin(s) installed`);console.log()});se.command("create").description("Create a new plugin scaffold").argument("<name>","Plugin name (lowercase, hyphens allowed)").option("-d, --dir <path>","Output directory (default: ./<name>)").option("--description <text>","Plugin description").action(async(e,t)=>{const{createPluginScaffold:o}=await import("./plugin-scaffold.js");try{const r=o(e,{dir:t.dir,description:t.description});console.log(`✅ Plugin scaffold created at ${r.dir}`);console.log();console.log(" Files created:");for(const n of r.files)console.log(` ${n}`);console.log();console.log(" Next steps:");console.log(` cd ${r.dir}`);console.log(` ppcli plugin install file://${r.dir}`);console.log(` ppcli ${e} hello`)}catch(r){console.error(`Error: ${U(r)}`);process.exitCode=f.GENERIC_ERROR}});const Re=a.command("adapter").description("Manage CLI adapters"),no=Re.description();Re.command("status").description("Show which sites have local overrides vs using official baseline").action(async()=>{const e=await import("node:os"),t=k.join(e.homedir(),".ppcli","clis"),o=c;try{const n=(await A.promises.readdir(t,{withFileTypes:!0})).filter((s)=>s.isDirectory()).map((s)=>s.name).sort();let i=[];try{i=(await A.promises.readdir(o,{withFileTypes:!0})).filter((d)=>d.isDirectory()).map((d)=>d.name).sort()}catch{}if(n.length===0){console.log("No local adapter overrides. All sites use the official baseline.");return}console.log(`Local overrides in ~/.ppcli/clis/ (${n.length} sites):
|
|
141
141
|
`);for(const s of n){const u=i.includes(s)?"override":"custom";console.log(` ${s} [${u}]`)}console.log(`
|
|
142
|
-
Official baseline: ${i.length} sites in package`)}catch{console.log("No local adapter overrides. All sites use the official baseline.")}});Re.command("eject").description("Copy an official adapter to ~/.ppcli/clis/ for local editing").argument("<site>","Site name (e.g. twitter, bilibili)").action(async(e)=>{const t=await import("node:os"),o=k.join(t.homedir(),".ppcli","clis"),r=k.join(c,e),n=k.join(o,e);try{await A.promises.access(r)}catch{console.error(`Error: Site "${e}" not found in official adapters.`);process.exitCode=f.USAGE_ERROR;return}try{await A.promises.access(n);console.error(`Site "${e}" already exists in ~/.ppcli/clis/. Use "ppcli adapter reset ${e}" first to restore official version.`);process.exitCode=f.USAGE_ERROR;return}catch{}A.cpSync(r,n,{recursive:!0});console.log(`✅ Ejected "${e}" to ~/.ppcli/clis/${e}/`);console.log("You can now edit the adapter files. Changes take effect immediately.");console.log("Note: Official updates to this adapter will overwrite your changes.")});Re.command("reset").description("Remove local override and restore official adapter version").argument("[site]","Site name (e.g. twitter, bilibili)").option("--all","Reset all local overrides").action(async(e,t)=>{const o=await import("node:os"),r=k.join(o.homedir(),".ppcli","clis");if(t.all){try{const d=(await A.promises.readdir(r,{withFileTypes:!0})).filter((u)=>u.isDirectory());if(d.length===0){console.log("No local sites to reset.");return}for(const u of d)A.rmSync(k.join(r,u.name),{recursive:!0,force:!0});console.log(`✅ Reset ${d.length} site(s). All adapters now use official baseline.`)}catch{console.log("No local sites to reset.")}return}if(!e){console.error("Error: Please specify a site name or use --all.");process.exitCode=f.USAGE_ERROR;return}const n=k.join(r,e);try{await A.promises.access(n)}catch{console.error(`Site "${e}" has no local override.`);return}const i=A.existsSync(k.join(c,e));A.rmSync(n,{recursive:!0,force:!0});console.log(i?`✅ Reset "${e}". Now using official baseline.`:`✅ Removed custom site "${e}".`)});const L=a.command("profile").description("Manage Browser Bridge Chrome profiles"),no=L.description();L.command("list").description("List Chrome profiles: extension-connected and managed (dedicated Chrome instances)").option("-f, --format <fmt>","Output format: text, json","text").action(async(e)=>{const t=await or(),o=rr(),r=t?.profiles??[],n=!!t&&!fr(t,ke)&&Array.isArray(t.profiles),i=await Promise.all(Ke().map(async({name:u,entry:m})=>({name:u,port:m.port,userDataDir:m.userDataDir,running:await cr(m),isDefault:o.defaultContextId===u,createdAt:m.createdAt??"",label:m.label??"",proxy:m.proxy?{scheme:m.proxy.scheme,host:m.proxy.host,port:m.proxy.port,hasAuth:!!m.proxy.username,username:m.proxy.username??""}:null,fingerprint:m.fingerprint?.enabled?{enabled:!0,timezone:m.fingerprint.timezone??"",locale:m.fingerprint.locale??"",hardwareConcurrency:m.fingerprint.hardwareConcurrency??0,deviceMemory:m.fingerprint.deviceMemory??0,screen:m.fingerprint.screen??null,webglVendor:m.fingerprint.webglVendor??"",webglRenderer:m.fingerprint.webglRenderer??"",canvasNoise:m.fingerprint.canvasNoise===!0}:{enabled:!1},webrtc:m.webrtc!==!1,clean:m.clean!==!1})));if(e.format==="json"){const u=n?r.map((m)=>({contextId:m.contextId,alias:Jt(o,m.contextId)??"",connected:!0,version:m.extensionVersion??"",isDefault:o.defaultContextId===m.contextId})):[];console.log(JSON.stringify({managed:i,extension:u,daemonRunning:!!t,daemonStale:!!t&&!n,defaultProfile:o.defaultContextId??""},null,2));return}if(i.length>0){console.log("Managed profiles (dedicated Chrome instances)");console.log();for(const u of i){const m=u.isDefault?" default":"";console.log(` ${u.name}${m} — ${u.running?`running on port ${u.port}`:"stopped"}`)}console.log()}if(!t){console.log("Daemon is not running. Run ppcli doctor after opening Chrome.");return}if(!n){console.log(`Daemon ${pr(t)} is stale for CLI v${ke}.`);console.log("Run: ppcli daemon restart");return}if(r.length===0){console.log("No Browser Bridge profiles connected.");console.log("Open a Chrome profile with the ppcli extension installed, then run ppcli profile list again.");return}const s=new Set(r.map((u)=>u.contextId));console.log("Connected Browser Bridge profiles");console.log();for(const u of r){const m=Jt(o,u.contextId),g=o.defaultContextId===u.contextId?" default":"",b=m?` ${m}`:"",R=u.extensionVersion?` v${u.extensionVersion}`:" version unknown";console.log(` ${u.contextId}${b}${g} — connected${R}`)}if(i.some((u)=>u.running)){console.log();console.log("注意:托管 Chrome 也装了同一个扩展,上面的某个 contextId 可能就是它,");console.log(" 无法从这份列表分辨。写操作请显式带 --profile <名字>,不要依赖默认路由。")}const d=Object.entries(o.aliases).filter(([,u])=>!s.has(u));if(d.length>0||o.defaultContextId&&!s.has(o.defaultContextId)){console.log();console.log("Disconnected saved profiles:");const u=new Set;for(const[m,g]of d){u.add(g);console.log(` ${g} ${m} — not connected`)}if(o.defaultContextId&&!u.has(o.defaultContextId)&&!s.has(o.defaultContextId))console.log(` ${o.defaultContextId} — default, not connected`)}});const pt=(e)=>{if(e.config){const r=JSON.parse(e.config);if(typeof r.proxy==="string")r.proxy=Ye(r.proxy);return r}const t={};if(e.label)t.label=e.label;if(e.proxy)t.proxy=Ye(e.proxy);if(e.webrtc===!1)t.webrtc=!1;if(e.clean===!1)t.clean=!1;const o=e.fingerprint;if(o&&o!=="off")t.fingerprint={enabled:!0,spoof:o!=="lite",...e.fpTimezone?{timezone:e.fpTimezone}:{},...e.fpLocale?{locale:e.fpLocale}:{},...e.fpUa?{userAgent:e.fpUa}:{},...e.fpPlatform?{platform:e.fpPlatform}:{},...e.fpSeed?{seed:Number(e.fpSeed)}:{}};return t},ft=(e)=>e?{scheme:e.scheme,host:e.host,port:e.port,hasAuth:!!e.username,username:e.username??""}:null,gt=(e)=>e?.enabled?{enabled:!0,seed:e.seed,timezone:e.timezone??"",locale:e.locale??"",userAgent:e.userAgent??"",platform:e.platform??"",hardwareConcurrency:e.hardwareConcurrency??0,deviceMemory:e.deviceMemory??0,screen:e.screen??null,webglVendor:e.webglVendor??"",webglRenderer:e.webglRenderer??"",canvasNoise:e.canvasNoise===!0}:{enabled:!1},ht=(e)=>e.option("--label <text>","Display label / note for the environment").option("--proxy <url>","Per-env proxy: scheme://[user:pass@]host:port (scheme = http/https/socks5)").option("--fingerprint <mode>","Fingerprint mode: off | on (spoof soft signals) | lite (timezone/locale follow only)").option("--fp-timezone <iana>","Fingerprint timezone, e.g. America/New_York (must be set explicitly)").option("--fp-locale <locale>","Fingerprint locale, e.g. en-US (must be set explicitly)").option("--fp-ua <ua>","Override navigator.userAgent (use with care)").option("--fp-platform <p>","Override navigator.platform, e.g. Win32").option("--fp-seed <n>","Fixed fingerprint seed (for reproducible fingerprint)").option("--no-webrtc","Disable WebRTC leak protection (on by default)").option("--no-clean","Disable lean launch flags / telemetry suppression (on by default)").option("--config <json>","Full config as JSON: { label, proxy, fingerprint } — overrides granular flags");L.command("create").description("Create a managed profile: a dedicated Chrome instance with its own user data dir (for a second account)").argument("<name>","Profile name, e.g. work / brand2").option("--browser-path <path>","Explicit browser executable (defaults to auto-discovered Chrome)").option("--no-start","Create only; do not launch Chrome now").option("-f, --format <fmt>","Output format: text, json","text").action(async(e,t)=>{try{const o=pt(t),{opts:r}=await Mt({...o,browserPath:t.browserPath}),n=await ar(e,{...r,browserPath:t.browserPath});if(t.start!==!1)await Ze(e,n);if(t.format==="json"){console.log(JSON.stringify({ok:!0,name:e,port:n.port,userDataDir:n.userDataDir,label:n.label??"",proxy:ft(n.proxy),fingerprint:gt(n.fingerprint),webrtc:n.webrtc!==!1,clean:n.clean!==!1,started:t.start!==!1},null,2));return}console.log(`✅ 托管 profile "${e}" 已创建(端口 ${n.port})`);console.log(` 数据目录:${n.userDataDir}`);if(n.proxy)console.log(` 代理:${n.proxy.scheme}://${n.proxy.host}:${n.proxy.port}`);if(n.fingerprint?.enabled)console.log(` 指纹:已开启${n.fingerprint.timezone?` 时区 ${n.fingerprint.timezone}`:""}${n.fingerprint.locale?` 语言 ${n.fingerprint.locale}`:""}`);if(t.start!==!1)console.log(` Chrome 已启动。用 ppcli <site> login --profile ${e} 在新窗口里登录账号。`)}catch(o){console.error(`Error: ${U(o)}`);process.exitCode=f.USAGE_ERROR}});ht(L.commands[L.commands.length-1]);L.command("start").description("Launch the dedicated Chrome instance of a managed profile").argument("<name>","Managed profile name").action(async(e)=>{try{const t=Ke().find((r)=>r.name===e.trim());if(!t)throw Error(`托管 profile "${e}" 不存在,先 ppcli profile create ${e}`);const o=await Ze(t.name,t.entry);console.log(`✅ profile "${e}" 运行中(${o})`)}catch(t){console.error(`Error: ${U(t)}`);process.exitCode=f.USAGE_ERROR}});L.command("stop").description("Gracefully close the dedicated Chrome instance of a managed profile").argument("<name>","Managed profile name").action(async(e)=>{try{const t=Ke().find((r)=>r.name===e.trim());if(!t)throw Error(`托管 profile "${e}" 不存在`);const o=await dr(t.entry);console.log(o?`✅ profile "${e}" 已关闭`:`profile "${e}" 本来就没在运行`)}catch(t){console.error(`Error: ${U(t)}`);process.exitCode=f.USAGE_ERROR}});L.command("remove").description("Remove a managed profile (closes Chrome and deletes its login data)").argument("<name>","Managed profile name").option("--keep-data","Keep the user data dir on disk",!1).action(async(e,t)=>{try{await lr(e,{keepData:t.keepData===!0});console.log(`✅ 托管 profile "${e}" 已删除${t.keepData?"(保留数据目录)":"(含登录数据)"}`)}catch(o){console.error(`Error: ${U(o)}`);process.exitCode=f.USAGE_ERROR}});L.command("update").description("Reconfigure a managed profile (label / proxy / fingerprint); takes effect on next start").argument("<name>","Managed profile name").option("--clear-proxy","Remove the proxy from this environment").option("--restart","Relaunch the environment after updating",!1).option("-f, --format <fmt>","Output format: text, json","text").action(async(e,t)=>{try{const o=pt(t),{opts:r}=await Mt(o),n={};if(o.label!==void 0)n.label=o.label??null;if(t.clearProxy)n.proxy=null;else if(r.proxy)n.proxy=r.proxy;if(t.fingerprint==="off")n.fingerprint=null;else if(r.fingerprint)n.fingerprint=r.fingerprint;if(o.webrtc!==void 0)n.webrtc=o.webrtc;if(o.clean!==void 0)n.clean=o.clean;const{entry:i,wasRunning:s}=await ur(e,n);if(t.restart&&s)await Ze(e,i);if(t.format==="json"){console.log(JSON.stringify({ok:!0,name:e,label:i.label??"",proxy:ft(i.proxy),fingerprint:gt(i.fingerprint),webrtc:i.webrtc!==!1,clean:i.clean!==!1,wasRunning:s,restarted:!!(t.restart&&s)},null,2));return}console.log(`✅ 托管 profile "${e}" 配置已更新${s?"(已关闭,重启后生效)":""}`)}catch(o){console.error(`Error: ${U(o)}`);process.exitCode=f.USAGE_ERROR}});ht(L.commands[L.commands.length-1]);L.command("test-proxy").description("Test proxy connectivity and detect exit IP / country / timezone").argument("<url>","Proxy: scheme://[user:pass@]host:port (scheme = http/https/socks5)").option("-f, --format <fmt>","Output format: text, json","text").action(async(e,t)=>{try{const o=Ye(e),r=await mr(o);if(t.format==="json"){console.log(JSON.stringify(r,null,2));return}if(r.ok){console.log(`✅ 代理可用 — 出口 IP ${r.ip}`);console.log(` 位置:${r.country??"?"}${r.city?" / "+r.city:""}`);console.log(` 时区:${r.timezone??"?"} 语言:${r.locale??"?"}`)}else{console.log(`❌ 代理不可用:${r.error}`);process.exitCode=f.USAGE_ERROR}}catch(o){console.error(`Error: ${U(o)}`);process.exitCode=f.USAGE_ERROR}});L.command("rename").description("Assign a local alias to a connected Browser Bridge profile").argument("<contextId>","Profile contextId from ppcli profile list").argument("<alias>","Local alias, e.g. work or personal").action((e,t)=>{try{nr(e,t);console.log(`Profile ${e} is now aliased as ${t}.`)}catch(o){console.error(`Error: ${U(o)}`);process.exitCode=f.USAGE_ERROR}});L.command("use").description("Set the default Browser Bridge profile for future commands").argument("<profile>","Profile alias or contextId").action((e)=>{try{const t=sr(e);console.log(`Default Browser Bridge profile: ${t.defaultContextId??e}`)}catch(t){console.error(`Error: ${U(t)}`);process.exitCode=f.USAGE_ERROR}});const pe=a.command("daemon").description("Manage the ppcli daemon"),io=pe.description();pe.command("status").description("Show daemon status").option("--json","Machine-readable status (includes bridgeReady)").action(async(e)=>{await Zo(e)});pe.command("stop").description("Stop the daemon").action(async()=>{await Yo()});pe.command("restart").description("Restart the daemon").action(async()=>{await Ko()});pe.command("warm").description("Pre-open the background automation window (keeps a tab alive for reuse)").option("--url <url>","http(s) info page to keep in the warm tab (falls back to about:blank)").action(async(e)=>{await Qo(e.url)});const Ce=qe(),ze=a.command("external").description("Manage external CLI passthrough commands");ze.command("install").description("Install an external CLI").argument("<name>","Name of the external CLI").action((e)=>{const t=Ce.find((o)=>o.name===e);if(!t){console.error(`External CLI '${e}' not found in registry.`);process.exitCode=f.USAGE_ERROR;return}So(t)});ze.command("register").description("Register an external CLI").argument("<name>","Name of the CLI").option("--binary <bin>","Binary name if different from name").option("--install <cmd>","Auto-install command").option("--desc <text>","Description").action((e,t)=>{vo(e,{binary:t.binary,install:t.install,description:t.desc})});ze.command("list").description("List registered external CLIs").option("-f, --format <fmt>","Output format: table, json, yaml, md, csv","table").action((e)=>{const t=qe().map((o)=>({name:o.name,package:o.package??"",binary:o.binary,installed:Ve(o.binary),description:o.description??"",homepage:o.homepage??"",tags:o.tags?.join(", ")??""}));_e(t,{fmt:e.format,columns:["name","package","binary","installed","description","homepage","tags"],title:"ppcli/external/list",source:"ppcli external list"})});function so(e,t){const o=t??(()=>{const r=process.argv.indexOf(e);return process.argv.slice(r+1)})();try{xo(e,o,Ce)}catch(r){if(r instanceof Xe){process.stderr.write(go.dump(Ao(r),{sortKeys:!1,lineWidth:120,noRefs:!0}));process.exitCode=r.exitCode}else{console.error(`Error: ${U(r)}`);process.exitCode=f.GENERIC_ERROR}}}for(const e of Ce){if(a.commands.some((t)=>t.name()===e.name))continue;a.command(e.name).description(`(External) ${e.description||e.name}`).argument("[args...]").allowUnknownOption().passThroughOptions().helpOption(!1).action((t)=>so(e.name,t))}const wt=a.command("antigravity").description("antigravity commands");wt.command("serve").description("Start Anthropic-compatible API proxy for Antigravity").option("--port <port>","Server port (default: 8082)","8082").option("--timeout <seconds>","Maximum time to wait for a reply (default: 120s)").action(async(e)=>{const{startServe:t}=await import("../../clis/antigravity/serve.js");await t({port:parseInt(e.port,10),timeout:e.timeout?Zt(e.timeout,"--timeout",120):void 0})});const yt=new Map;yt.set("antigravity",wt);const bt=Eo(a,yt);Ar(a);const ao=Ce.map((e)=>e.name),co=Ce.map((e)=>({name:e.name,label:_t(e)})),We=new Map;for(const[,e]of Ee())if(!We.has(e.site))We.set(e.site,e.domain);const xt=[],St=[];for(const e of bt)if(Ot(We.get(e))==="app")xt.push(e);else St.push(e);const vt={external:co,apps:xt,sites:St},lo=new Set([...ao,...bt]);he(h,{globalCommand:a,description:T});he(v,{globalCommand:a,description:"Inspect website login status"});he(pe,{globalCommand:a,description:io});he(se,{globalCommand:a,description:oo});he(Re,{globalCommand:a,description:ro});he(L,{globalCommand:a,description:no});a.configureHelp({visibleCommands:(e)=>e.commands.filter((t)=>e!==a||!lo.has(t.name()))});const uo=(e)=>{const t=[];let o=e.parent;while(o){const r=$o(o);t.unshift(r?`${o.name()} ${r}`:o.name());o=o.parent}return[...t,e.name(),e.usage()].filter(Boolean).join(" ").trim()};function Rt(e){e.configureHelp({commandUsage:uo});for(const t of e.commands)Rt(t)}Rt(h);Oo(a,()=>No(a,vt),()=>_o(vt));a.on("command:*",(e)=>{const t=e[0];console.error(`error: unknown command '${t}'`);if(Ve(t))console.error(` Tip: '${t}' exists on your PATH. Use 'ppcli external register ${t}' to add it as an external CLI.`);a.outputHelp();process.exitCode=f.USAGE_ERROR});return a}export function runCli(c,l){createProgram(c,l).parse()}export{findPackageRoot};export function resolveBrowserVerifyInvocation(c={}){const l=c.platform??process.platform,a=c.fileExists??A.existsSync,p=c.readFile??((E)=>A.readFileSync(E,"utf-8")),v=c.projectRoot??findPackageRoot(et,a);for(const E of ho(v,p))if(a(E))return{binary:process.execPath,args:[E],cwd:v};const h=k.join(v,"src","main.ts");if(!a(h))throw Error(`Could not find ppcli entrypoint under ${v}. Expected built entry from package.json or src/main.ts.`);const T=k.join(v,"node_modules",".bin",l==="win32"?"tsx.cmd":"tsx");if(a(T))return{binary:T,args:[h],cwd:v,...l==="win32"?{shell:!0}:{}};return{binary:l==="win32"?"npx.cmd":"npx",args:["tsx",h],cwd:v,...l==="win32"?{shell:!0}:{}}}
|
|
142
|
+
Official baseline: ${i.length} sites in package`)}catch{console.log("No local adapter overrides. All sites use the official baseline.")}});Re.command("eject").description("Copy an official adapter to ~/.ppcli/clis/ for local editing").argument("<site>","Site name (e.g. twitter, bilibili)").action(async(e)=>{const t=await import("node:os"),o=k.join(t.homedir(),".ppcli","clis"),r=k.join(c,e),n=k.join(o,e);try{await A.promises.access(r)}catch{console.error(`Error: Site "${e}" not found in official adapters.`);process.exitCode=f.USAGE_ERROR;return}try{await A.promises.access(n);console.error(`Site "${e}" already exists in ~/.ppcli/clis/. Use "ppcli adapter reset ${e}" first to restore official version.`);process.exitCode=f.USAGE_ERROR;return}catch{}A.cpSync(r,n,{recursive:!0});console.log(`✅ Ejected "${e}" to ~/.ppcli/clis/${e}/`);console.log("You can now edit the adapter files. Changes take effect immediately.");console.log("Note: Official updates to this adapter will overwrite your changes.")});Re.command("reset").description("Remove local override and restore official adapter version").argument("[site]","Site name (e.g. twitter, bilibili)").option("--all","Reset all local overrides").action(async(e,t)=>{const o=await import("node:os"),r=k.join(o.homedir(),".ppcli","clis");if(t.all){try{const d=(await A.promises.readdir(r,{withFileTypes:!0})).filter((u)=>u.isDirectory());if(d.length===0){console.log("No local sites to reset.");return}for(const u of d)A.rmSync(k.join(r,u.name),{recursive:!0,force:!0});console.log(`✅ Reset ${d.length} site(s). All adapters now use official baseline.`)}catch{console.log("No local sites to reset.")}return}if(!e){console.error("Error: Please specify a site name or use --all.");process.exitCode=f.USAGE_ERROR;return}const n=k.join(r,e);try{await A.promises.access(n)}catch{console.error(`Site "${e}" has no local override.`);return}const i=A.existsSync(k.join(c,e));A.rmSync(n,{recursive:!0,force:!0});console.log(i?`✅ Reset "${e}". Now using official baseline.`:`✅ Removed custom site "${e}".`)});const L=a.command("profile").description("Manage Browser Bridge Chrome profiles"),io=L.description();L.command("list").description("List Chrome profiles: extension-connected and managed (dedicated Chrome instances)").option("-f, --format <fmt>","Output format: text, json","text").action(async(e)=>{const t=await rr(),o=nr(),r=t?.profiles??[],n=!!t&&!gr(t,ke)&&Array.isArray(t.profiles),i=await Promise.all(Ke().map(async({name:u,entry:m})=>({name:u,port:m.port,userDataDir:m.userDataDir,running:await lr(m),isDefault:o.defaultContextId===u,createdAt:m.createdAt??"",label:m.label??"",proxy:m.proxy?{scheme:m.proxy.scheme,host:m.proxy.host,port:m.proxy.port,hasAuth:!!m.proxy.username,username:m.proxy.username??""}:null,fingerprint:m.fingerprint?.enabled?{enabled:!0,timezone:m.fingerprint.timezone??"",locale:m.fingerprint.locale??"",hardwareConcurrency:m.fingerprint.hardwareConcurrency??0,deviceMemory:m.fingerprint.deviceMemory??0,screen:m.fingerprint.screen??null,webglVendor:m.fingerprint.webglVendor??"",webglRenderer:m.fingerprint.webglRenderer??"",canvasNoise:m.fingerprint.canvasNoise===!0}:{enabled:!1},webrtc:m.webrtc!==!1,clean:m.clean!==!1})));if(e.format==="json"){const u=n?r.map((m)=>({contextId:m.contextId,alias:jt(o,m.contextId)??"",connected:!0,version:m.extensionVersion??"",isDefault:o.defaultContextId===m.contextId})):[];console.log(JSON.stringify({managed:i,extension:u,daemonRunning:!!t,daemonStale:!!t&&!n,defaultProfile:o.defaultContextId??""},null,2));return}if(i.length>0){console.log("Managed profiles (dedicated Chrome instances)");console.log();for(const u of i){const m=u.isDefault?" default":"";console.log(` ${u.name}${m} — ${u.running?`running on port ${u.port}`:"stopped"}`)}console.log()}if(!t){console.log("Daemon is not running. Run ppcli doctor after opening Chrome.");return}if(!n){console.log(`Daemon ${fr(t)} is stale for CLI v${ke}.`);console.log("Run: ppcli daemon restart");return}if(r.length===0){console.log("No Browser Bridge profiles connected.");console.log("Open a Chrome profile with the ppcli extension installed, then run ppcli profile list again.");return}const s=new Set(r.map((u)=>u.contextId));console.log("Connected Browser Bridge profiles");console.log();for(const u of r){const m=jt(o,u.contextId),g=o.defaultContextId===u.contextId?" default":"",b=m?` ${m}`:"",R=u.extensionVersion?` v${u.extensionVersion}`:" version unknown";console.log(` ${u.contextId}${b}${g} — connected${R}`)}if(i.some((u)=>u.running)){console.log();console.log("注意:托管 Chrome 也装了同一个扩展,上面的某个 contextId 可能就是它,");console.log(" 无法从这份列表分辨。写操作请显式带 --profile <名字>,不要依赖默认路由。")}const d=Object.entries(o.aliases).filter(([,u])=>!s.has(u));if(d.length>0||o.defaultContextId&&!s.has(o.defaultContextId)){console.log();console.log("Disconnected saved profiles:");const u=new Set;for(const[m,g]of d){u.add(g);console.log(` ${g} ${m} — not connected`)}if(o.defaultContextId&&!u.has(o.defaultContextId)&&!s.has(o.defaultContextId))console.log(` ${o.defaultContextId} — default, not connected`)}});const ft=(e)=>{if(e.config){const r=JSON.parse(e.config);if(typeof r.proxy==="string")r.proxy=Ye(r.proxy);return r}const t={};if(e.label)t.label=e.label;if(e.proxy)t.proxy=Ye(e.proxy);if(e.webrtc===!1)t.webrtc=!1;if(e.clean===!1)t.clean=!1;const o=e.fingerprint;if(o&&o!=="off")t.fingerprint={enabled:!0,spoof:o!=="lite",...e.fpTimezone?{timezone:e.fpTimezone}:{},...e.fpLocale?{locale:e.fpLocale}:{},...e.fpUa?{userAgent:e.fpUa}:{},...e.fpPlatform?{platform:e.fpPlatform}:{},...e.fpSeed?{seed:Number(e.fpSeed)}:{}};return t},gt=(e)=>e?{scheme:e.scheme,host:e.host,port:e.port,hasAuth:!!e.username,username:e.username??""}:null,ht=(e)=>e?.enabled?{enabled:!0,seed:e.seed,timezone:e.timezone??"",locale:e.locale??"",userAgent:e.userAgent??"",platform:e.platform??"",hardwareConcurrency:e.hardwareConcurrency??0,deviceMemory:e.deviceMemory??0,screen:e.screen??null,webglVendor:e.webglVendor??"",webglRenderer:e.webglRenderer??"",canvasNoise:e.canvasNoise===!0}:{enabled:!1},wt=(e)=>e.option("--label <text>","Display label / note for the environment").option("--proxy <url>","Per-env proxy: scheme://[user:pass@]host:port (scheme = http/https/socks5)").option("--fingerprint <mode>","Fingerprint mode: off | on (spoof soft signals) | lite (timezone/locale follow only)").option("--fp-timezone <iana>","Fingerprint timezone, e.g. America/New_York (must be set explicitly)").option("--fp-locale <locale>","Fingerprint locale, e.g. en-US (must be set explicitly)").option("--fp-ua <ua>","Override navigator.userAgent (use with care)").option("--fp-platform <p>","Override navigator.platform, e.g. Win32").option("--fp-seed <n>","Fixed fingerprint seed (for reproducible fingerprint)").option("--no-webrtc","Disable WebRTC leak protection (on by default)").option("--no-clean","Disable lean launch flags / telemetry suppression (on by default)").option("--config <json>","Full config as JSON: { label, proxy, fingerprint } — overrides granular flags");L.command("create").description("Create a managed profile: a dedicated Chrome instance with its own user data dir (for a second account)").argument("<name>","Profile name, e.g. work / brand2").option("--browser-path <path>","Explicit browser executable (defaults to auto-discovered Chrome)").option("--no-start","Create only; do not launch Chrome now").option("-f, --format <fmt>","Output format: text, json","text").action(async(e,t)=>{try{const o=ft(t),{opts:r}=await Dt({...o,browserPath:t.browserPath}),n=await cr(e,{...r,browserPath:t.browserPath});if(t.start!==!1)await Ze(e,n);if(t.format==="json"){console.log(JSON.stringify({ok:!0,name:e,port:n.port,userDataDir:n.userDataDir,label:n.label??"",proxy:gt(n.proxy),fingerprint:ht(n.fingerprint),webrtc:n.webrtc!==!1,clean:n.clean!==!1,started:t.start!==!1},null,2));return}console.log(`✅ 托管 profile "${e}" 已创建(端口 ${n.port})`);console.log(` 数据目录:${n.userDataDir}`);if(n.proxy)console.log(` 代理:${n.proxy.scheme}://${n.proxy.host}:${n.proxy.port}`);if(n.fingerprint?.enabled)console.log(` 指纹:已开启${n.fingerprint.timezone?` 时区 ${n.fingerprint.timezone}`:""}${n.fingerprint.locale?` 语言 ${n.fingerprint.locale}`:""}`);if(t.start!==!1)console.log(` Chrome 已启动。用 ppcli <site> login --profile ${e} 在新窗口里登录账号。`)}catch(o){console.error(`Error: ${U(o)}`);process.exitCode=f.USAGE_ERROR}});wt(L.commands[L.commands.length-1]);L.command("start").description("Launch the dedicated Chrome instance of a managed profile").argument("<name>","Managed profile name").action(async(e)=>{try{const t=Ke().find((r)=>r.name===e.trim());if(!t)throw Error(`托管 profile "${e}" 不存在,先 ppcli profile create ${e}`);const o=await Ze(t.name,t.entry);console.log(`✅ profile "${e}" 运行中(${o})`)}catch(t){console.error(`Error: ${U(t)}`);process.exitCode=f.USAGE_ERROR}});L.command("stop").description("Gracefully close the dedicated Chrome instance of a managed profile").argument("<name>","Managed profile name").action(async(e)=>{try{const t=Ke().find((r)=>r.name===e.trim());if(!t)throw Error(`托管 profile "${e}" 不存在`);const o=await ur(t.entry);console.log(o?`✅ profile "${e}" 已关闭`:`profile "${e}" 本来就没在运行`)}catch(t){console.error(`Error: ${U(t)}`);process.exitCode=f.USAGE_ERROR}});L.command("remove").description("Remove a managed profile (closes Chrome and deletes its login data)").argument("<name>","Managed profile name").option("--keep-data","Keep the user data dir on disk",!1).action(async(e,t)=>{try{await dr(e,{keepData:t.keepData===!0});console.log(`✅ 托管 profile "${e}" 已删除${t.keepData?"(保留数据目录)":"(含登录数据)"}`)}catch(o){console.error(`Error: ${U(o)}`);process.exitCode=f.USAGE_ERROR}});L.command("update").description("Reconfigure a managed profile (label / proxy / fingerprint); takes effect on next start").argument("<name>","Managed profile name").option("--clear-proxy","Remove the proxy from this environment").option("--restart","Relaunch the environment after updating",!1).option("-f, --format <fmt>","Output format: text, json","text").action(async(e,t)=>{try{const o=ft(t),{opts:r}=await Dt(o),n={};if(o.label!==void 0)n.label=o.label??null;if(t.clearProxy)n.proxy=null;else if(r.proxy)n.proxy=r.proxy;if(t.fingerprint==="off")n.fingerprint=null;else if(r.fingerprint)n.fingerprint=r.fingerprint;if(o.webrtc!==void 0)n.webrtc=o.webrtc;if(o.clean!==void 0)n.clean=o.clean;const{entry:i,wasRunning:s}=await mr(e,n);if(t.restart&&s)await Ze(e,i);if(t.format==="json"){console.log(JSON.stringify({ok:!0,name:e,label:i.label??"",proxy:gt(i.proxy),fingerprint:ht(i.fingerprint),webrtc:i.webrtc!==!1,clean:i.clean!==!1,wasRunning:s,restarted:!!(t.restart&&s)},null,2));return}console.log(`✅ 托管 profile "${e}" 配置已更新${s?"(已关闭,重启后生效)":""}`)}catch(o){console.error(`Error: ${U(o)}`);process.exitCode=f.USAGE_ERROR}});wt(L.commands[L.commands.length-1]);L.command("test-proxy").description("Test proxy connectivity and detect exit IP / country / timezone").argument("<url>","Proxy: scheme://[user:pass@]host:port (scheme = http/https/socks5)").option("-f, --format <fmt>","Output format: text, json","text").action(async(e,t)=>{try{const o=Ye(e),r=await pr(o);if(t.format==="json"){console.log(JSON.stringify(r,null,2));return}if(r.ok){console.log(`✅ 代理可用 — 出口 IP ${r.ip}`);console.log(` 位置:${r.country??"?"}${r.city?" / "+r.city:""}`);console.log(` 时区:${r.timezone??"?"} 语言:${r.locale??"?"}`)}else{console.log(`❌ 代理不可用:${r.error}`);process.exitCode=f.USAGE_ERROR}}catch(o){console.error(`Error: ${U(o)}`);process.exitCode=f.USAGE_ERROR}});L.command("rename").description("Assign a local alias to a connected Browser Bridge profile").argument("<contextId>","Profile contextId from ppcli profile list").argument("<alias>","Local alias, e.g. work or personal").action((e,t)=>{try{ir(e,t);console.log(`Profile ${e} is now aliased as ${t}.`)}catch(o){console.error(`Error: ${U(o)}`);process.exitCode=f.USAGE_ERROR}});L.command("use").description("Set the default Browser Bridge profile for future commands").argument("<profile>","Profile alias or contextId").action((e)=>{try{const t=ar(e);console.log(`Default Browser Bridge profile: ${t.defaultContextId??e}`)}catch(t){console.error(`Error: ${U(t)}`);process.exitCode=f.USAGE_ERROR}});const pe=a.command("daemon").description("Manage the ppcli daemon"),so=pe.description();pe.command("status").description("Show daemon status").option("--json","Machine-readable status (includes bridgeReady)").action(async(e)=>{await Yo(e)});pe.command("stop").description("Stop the daemon").action(async()=>{await Qo()});pe.command("restart").description("Restart the daemon").action(async()=>{await Zo()});pe.command("warm").description("Pre-open the background automation window (keeps a tab alive for reuse)").option("--url <url>","http(s) info page to keep in the warm tab (falls back to about:blank)").action(async(e)=>{await er(e.url)});const Ce=qe(),ze=a.command("external").description("Manage external CLI passthrough commands");ze.command("install").description("Install an external CLI").argument("<name>","Name of the external CLI").action((e)=>{const t=Ce.find((o)=>o.name===e);if(!t){console.error(`External CLI '${e}' not found in registry.`);process.exitCode=f.USAGE_ERROR;return}vo(t)});ze.command("register").description("Register an external CLI").argument("<name>","Name of the CLI").option("--binary <bin>","Binary name if different from name").option("--install <cmd>","Auto-install command").option("--desc <text>","Description").action((e,t)=>{Ro(e,{binary:t.binary,install:t.install,description:t.desc})});ze.command("list").description("List registered external CLIs").option("-f, --format <fmt>","Output format: table, json, yaml, md, csv","table").action((e)=>{const t=qe().map((o)=>({name:o.name,package:o.package??"",binary:o.binary,installed:Ve(o.binary),description:o.description??"",homepage:o.homepage??"",tags:o.tags?.join(", ")??""}));_e(t,{fmt:e.format,columns:["name","package","binary","installed","description","homepage","tags"],title:"ppcli/external/list",source:"ppcli external list"})});function ao(e,t){const o=t??(()=>{const r=process.argv.indexOf(e);return process.argv.slice(r+1)})();try{So(e,o,Ce)}catch(r){if(r instanceof Xe){process.stderr.write(ho.dump(To(r),{sortKeys:!1,lineWidth:120,noRefs:!0}));process.exitCode=r.exitCode}else{console.error(`Error: ${U(r)}`);process.exitCode=f.GENERIC_ERROR}}}for(const e of Ce){if(a.commands.some((t)=>t.name()===e.name))continue;a.command(e.name).description(`(External) ${e.description||e.name}`).argument("[args...]").allowUnknownOption().passThroughOptions().helpOption(!1).action((t)=>ao(e.name,t))}const yt=a.command("antigravity").description("antigravity commands");yt.command("serve").description("Start Anthropic-compatible API proxy for Antigravity").option("--port <port>","Server port (default: 8082)","8082").option("--timeout <seconds>","Maximum time to wait for a reply (default: 120s)").action(async(e)=>{const{startServe:t}=await import("../../clis/antigravity/serve.js");await t({port:parseInt(e.port,10),timeout:e.timeout?Yt(e.timeout,"--timeout",120):void 0})});const bt=new Map;bt.set("antigravity",yt);const xt=_o(a,bt);Pr(a);const co=Ce.map((e)=>e.name),lo=Ce.map((e)=>({name:e.name,label:Ot(e)})),We=new Map;for(const[,e]of Ee())if(!We.has(e.site))We.set(e.site,e.domain);const St=[],vt=[];for(const e of xt)if($t(We.get(e))==="app")St.push(e);else vt.push(e);const Rt={external:lo,apps:St,sites:vt},uo=new Set([...co,...xt]);he(h,{globalCommand:a,description:T});he(v,{globalCommand:a,description:"Inspect website login status"});he(pe,{globalCommand:a,description:so});he(se,{globalCommand:a,description:ro});he(Re,{globalCommand:a,description:no});he(L,{globalCommand:a,description:io});a.configureHelp({visibleCommands:(e)=>e.commands.filter((t)=>e!==a||!uo.has(t.name()))});const mo=(e)=>{const t=[];let o=e.parent;while(o){const r=No(o);t.unshift(r?`${o.name()} ${r}`:o.name());o=o.parent}return[...t,e.name(),e.usage()].filter(Boolean).join(" ").trim()};function Ct(e){e.configureHelp({commandUsage:mo});for(const t of e.commands)Ct(t)}Ct(h);$o(a,()=>ko(a,Rt),()=>Oo(Rt));a.on("command:*",(e)=>{const t=e[0];console.error(`error: unknown command '${t}'`);if(Ve(t))console.error(` Tip: '${t}' exists on your PATH. Use 'ppcli external register ${t}' to add it as an external CLI.`);a.outputHelp();process.exitCode=f.USAGE_ERROR});return a}export function runCli(c,l){createProgram(c,l).parse()}export{findPackageRoot};export function resolveBrowserVerifyInvocation(c={}){const l=c.platform??process.platform,a=c.fileExists??A.existsSync,p=c.readFile??((E)=>A.readFileSync(E,"utf-8")),v=c.projectRoot??findPackageRoot(et,a);for(const E of wo(v,p))if(a(E))return{binary:process.execPath,args:[E],cwd:v};const h=k.join(v,"src","main.ts");if(!a(h))throw Error(`Could not find ppcli entrypoint under ${v}. Expected built entry from package.json or src/main.ts.`);const T=k.join(v,"node_modules",".bin",l==="win32"?"tsx.cmd":"tsx");if(a(T))return{binary:T,args:[h],cwd:v,...l==="win32"?{shell:!0}:{}};return{binary:l==="win32"?"npx.cmd":"npx",args:["tsx",h],cwd:v,...l==="win32"?{shell:!0}:{}}}
|
package/package.json
CHANGED
|
@@ -130,7 +130,7 @@ Error envelope always includes `error.code` and `error.message`. Target errors (
|
|
|
130
130
|
|
|
131
131
|
| command | purpose |
|
|
132
132
|
|---------|---------|
|
|
133
|
-
| `browser state` | Snapshot: text tree with `[N]` refs, scroll hints, hidden-interactive hints, `compounds (N):` sidecar for date/select/file refs. |
|
|
133
|
+
| `browser state [--max-chars N]` | Snapshot: text tree with `[N]` refs, scroll hints, hidden-interactive hints, `compounds (N):` sidecar for date/select/file refs. Output is capped at 12000 characters by default; when cut, a `… truncated:` line says how much is left — the unprinted refs still work. Reach them with `find --text/--role/--css`, `scroll` + `state`, or `extract`; `--max-chars 0` prints everything. |
|
|
134
134
|
| `browser state --source ax` | Opt-in accessibility-tree snapshot. Use when custom controls, portals, or iframe contents are hard to identify in normal `state`. AX refs can recover stale React re-renders by role/name/nth and can route same-origin iframe refs. Cross-origin iframe refs are best-effort because Chrome may not expose attachable OOPIF targets to extensions. |
|
|
135
135
|
| `browser state --compare-sources` | Metrics-only DOM vs AX comparison for deciding whether AX should become default. It prints counts and sizes, not page text, so it is safer to share for validation. |
|
|
136
136
|
| `browser find --css <sel> [--limit N] [--text-max N]` | Run a CSS query and return one entry per match with `{nth, ref, tag, role, text, attrs, visible, compound?}`. Allocates refs for matches the prior snapshot didn't tag. Cheap alternative to `state` when you already know the selector. |
|
|
@@ -341,7 +341,7 @@ Think about payload size per call. Budgets exist for a reason.
|
|
|
341
341
|
|
|
342
342
|
| command | rough cost | when to use |
|
|
343
343
|
|---------|-----------|-------------|
|
|
344
|
-
| `state` | medium (
|
|
344
|
+
| `state` | medium (≤ 12000 chars by default, `--max-chars`) | First call on any page, after every nav, when you need refs. |
|
|
345
345
|
| `find --css <sel>` | small | You already know the selector — one query, compact entries. |
|
|
346
346
|
| `get title` / `get url` | tiny | Sanity checks between steps. |
|
|
347
347
|
| `get text/value/attributes` | tiny per call | Verifying one specific field. |
|