publishport-opencli 1.0.45 → 1.0.46

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/dist/src/cli.js CHANGED
@@ -1,5 +1,5 @@
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 J,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 Io,getTextResolvedJs as Po,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 It}from"./browser/network-key.js";import{DEFAULT_TTL_MS as Pt,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,resolveChromeExecutable as pr}from"./browser/managed-chrome.js";import{testProxy as fr,parseProxyUrl as Ye}from"./browser/proxy-test.js";import{formatDaemonVersion as gr,isDaemonStale as hr}from"./browser/daemon-version.js";import{DEFAULT_BROWSER_CONNECT_TIMEOUT as Qe}from"./browser/config.js";import{budgetStateText as wr,DEFAULT_STATE_MAX_CHARS as Ut}from"./browser/state-budget.js";const et=fo(import.meta.url),yr='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 br(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 xr={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=xr[c]??f.GENERIC_ERROR}const Sr="Site sitemap available. For navigation context, use the opencli-browser-sitemap skill; treat browser state as truth if it disagrees.";function vr(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 Rr(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 vr(c,v)){const E=Rr(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:Sr,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 Cr(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 Er(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;Cr(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((P)=>Object.keys(P)))),T=h.slice(0,p),E=c.slice(0,a),N=(P)=>{if(P===null||P===void 0)return"";return(typeof P==="object"?JSON.stringify(P):String(P)).replace(/\s+/g," ").slice(0,v)},te=T.map((P)=>Math.max(P.length,...E.map((K)=>N(K[P]).length))),Y=(P)=>P.map((K,x)=>K.padEnd(te[x])).join(" "),X=[];X.push(` ${Y(T)}`);X.push(` ${te.map((P)=>"-".repeat(P)).join(" ")}`);for(const P of E)X.push(` ${Y(T.map((K)=>N(P[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 _r(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)&&_r(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 Ie(c,l){return l?`${l}:${c}`:c}async function Or(c,l){const a=Vt(l)?.defaultPage?.trim();if(!a)return;return rt(c,a,{scope:l,source:"saved"})}async function $r(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=Ie(p.adapter?`adapter:${c}`:c,a),P=l?await rt(Y,l,{scope:X,source:"explicit"}):await Or(Y,X);if(P){if(!Y.setActivePage)throw Error("This browser session does not support explicit tab targeting");Y.setActivePage(P)}return{page:Y,close:()=>E.close()}}function nt(c,l){const a=Pe(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>",yr)}function Nr(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 Pe(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=Pe(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=Pe(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 Ie(at(c),typeof l==="string"&&l.trim()?l.trim():void 0)}function kr(c){return typeof c==="string"?c:JSON.stringify(c,null,2)}function Ar(c,l){const a=kr(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 Ar(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 Tr(c){if(c.verbose)process.env.OPENCLI_VERBOSE="1"}function Ir(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=Ir(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),I=R==="public"?"[public]":`[${R}]`,G=b.aliases?.length?` (aliases: ${b.aliases.join(", ")})`:"";console.log(` ${b.name} ${I}${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: ${J(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(`
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 J,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 Io,getTextResolvedJs as Po,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 It}from"./browser/network-key.js";import{DEFAULT_TTL_MS as Pt,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,resolveChromeExecutable as pr}from"./browser/managed-chrome.js";import{testProxy as fr,parseProxyUrl as Ye}from"./browser/proxy-test.js";import{formatDaemonVersion as gr,isDaemonStale as hr}from"./browser/daemon-version.js";import{DEFAULT_BROWSER_CONNECT_TIMEOUT as Qe}from"./browser/config.js";import{isRemoteExecution as wr}from"./runtime.js";import{budgetStateText as yr,DEFAULT_STATE_MAX_CHARS as Ut}from"./browser/state-budget.js";const et=fo(import.meta.url),br='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 xr(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 Sr={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=Sr[c]??f.GENERIC_ERROR}const vr="Site sitemap available. For navigation context, use the opencli-browser-sitemap skill; treat browser state as truth if it disagrees.";function Rr(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 Cr(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 Rr(c,v)){const E=Cr(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:vr,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 Er(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 _r(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;Er(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((P)=>Object.keys(P)))),T=h.slice(0,p),E=c.slice(0,a),N=(P)=>{if(P===null||P===void 0)return"";return(typeof P==="object"?JSON.stringify(P):String(P)).replace(/\s+/g," ").slice(0,v)},te=T.map((P)=>Math.max(P.length,...E.map((K)=>N(K[P]).length))),Y=(P)=>P.map((K,x)=>K.padEnd(te[x])).join(" "),X=[];X.push(` ${Y(T)}`);X.push(` ${te.map((P)=>"-".repeat(P)).join(" ")}`);for(const P of E)X.push(` ${Y(T.map((K)=>N(P[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 Or(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)&&Or(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 Ie(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 Nr(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=Ie(p.adapter?`adapter:${c}`:c,a),P=l?await rt(Y,l,{scope:X,source:"explicit"}):await $r(Y,X);if(P){if(!Y.setActivePage)throw Error("This browser session does not support explicit tab targeting");Y.setActivePage(P)}return{page:Y,close:()=>E.close()}}function nt(c,l){if(wr())return"background";const a=Pe(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>",br)}function kr(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 Pe(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=Pe(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=Pe(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 Ie(at(c),typeof l==="string"&&l.trim()?l.trim():void 0)}function Ar(c){return typeof c==="string"?c:JSON.stringify(c,null,2)}function Tr(c,l){const a=Ar(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 Tr(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 Ir(c){if(c.verbose)process.env.OPENCLI_VERBOSE="1"}function Pr(c){return[...new Set(c.commands.map((l)=>l.name()))].sort((l,a)=>l.localeCompare(a)).join(", ")}function Jr(c){for(const l of c.commands){if(l.commands.length===0)continue;const a=Pr(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),I=R==="public"?"[public]":`[${R}]`,G=b.aliases?.length?` (aliases: ${b.aliases.join(", ")})`:"";console.log(` ${b.name} ${I}${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: ${J(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
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.
@@ -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(Io(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 P(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=Nr(r),i=it(r),s=Xt(r),d=nt(r,"background");o=await $r(i,n,s,{windowMode:d,profile:st(r),adapter:Pe(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 P(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=J(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){P(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,Ie(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,Ie(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=Er(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(wr(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`(() => {
14
+ `),T=h.description();async function E(e,t,o={}){const r=await e.evaluate(Io(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 P(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=kr(r),i=it(r),s=Xt(r),d=nt(r,"background");o=await Nr(i,n,s,{windowMode:d,profile:st(r),adapter:Pe(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 P(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=J(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){P(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,Ie(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,Ie(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=_r(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(yr(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++) {
@@ -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 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=(_,D)=>{console.log(JSON.stringify({error:{code:"usage_error",message:_,...D?{hint:D}:{}}},null,2));process.exitCode=f.USAGE_ERROR},i=(_,D)=>{if(_===void 0||_===null||_==="")return null;const j=Number(String(_).trim().replace(/px$/i,""));if(!Number.isFinite(j))return{error:`${D} 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 D=await e.dragBy(g,{dx:s??0,dy:d??0},{from:_.opts,...u!==void 0?{steps:u}:{}});console.log(JSON.stringify(D,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 I=Le(r?.toNth,"--to-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 G=await e.drag(g,b,{from:R.opts,to:I.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(J(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(J(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(Pt)).description("Capture network requests as shape previews; retrieve full bodies by key").action(x(async(e,t)=>{const o=Yt(t.ttl,"ttl",Pt),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 U=O.body,L=!1;if(S>0&&typeof O.body==="string"&&O.body.length>S){U=O.body.slice(0,S);L=!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:U};if(ae||L){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=It(O);for(const S of y){const U=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,...U?{channel:S.channel,direction:S.direction}:{},...S.eventName?{event_name:S.eventName}:{},...U?{body:br(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,I=It(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,I)}catch(w){G=`Could not persist capture cache: ${w.message}. --detail lookups may miss this capture.`}const _=I.map((w)=>({entry:w,shape:Tt(w.body)})),D=u?_.filter((w)=>Wo(w.shape,u)):_,j=u?_.length-D.length:0,W={session:r,captured_at:new Date().toISOString(),count:D.length,filtered_out:b};if(u){W.filter=u;W.filter_dropped=j}if(G)W.cache_warning=G;const ne=D.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=D.map((w)=>({...w.entry,...typeof w.entry.timestamp==="number"?{timestamp:we(w.entry.timestamp)}:{}}));else{W.entries=D.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=(_,D)=>{console.log(JSON.stringify({error:{code:"usage_error",message:_,...D?{hint:D}:{}}},null,2));process.exitCode=f.USAGE_ERROR},i=(_,D)=>{if(_===void 0||_===null||_==="")return null;const j=Number(String(_).trim().replace(/px$/i,""));if(!Number.isFinite(j))return{error:`${D} 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 D=await e.dragBy(g,{dx:s??0,dy:d??0},{from:_.opts,...u!==void 0?{steps:u}:{}});console.log(JSON.stringify(D,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 I=Le(r?.toNth,"--to-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 G=await e.drag(g,b,{from:R.opts,to:I.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(J(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(J(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(Pt)).description("Capture network requests as shape previews; retrieve full bodies by key").action(x(async(e,t)=>{const o=Yt(t.ttl,"ttl",Pt),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 U=O.body,L=!1;if(S>0&&typeof O.body==="string"&&O.body.length>S){U=O.body.slice(0,S);L=!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:U};if(ae||L){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=It(O);for(const S of y){const U=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,...U?{channel:S.channel,direction:S.direction}:{},...S.eventName?{event_name:S.eventName}:{},...U?{body:xr(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,I=It(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,I)}catch(w){G=`Could not persist capture cache: ${w.message}. --detail lookups may miss this capture.`}const _=I.map((w)=>({entry:w,shape:Tt(w.body)})),D=u?_.filter((w)=>Wo(w.shape,u)):_,j=u?_.length-D.length:0,W={session:r,captured_at:new Date().toISOString(),count:D.length,filtered_out:b};if(u){W.filter=u;W.filter_dropped=j}if(G)W.cache_warning=G;const ne=D.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=D.map((w)=>({...w.entry,...typeof w.entry.timestamp==="number"?{timestamp:we(w.entry.timestamp)}:{}}));else{W.entries=D.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}',
@@ -137,6 +137,6 @@ cli({
137
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 M=checkSiteMemory(r);printSiteMemoryReport(M,t.strictMemory);if(!M.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 M=checkSiteMemory(r);printSiteMemoryReport(M,t.strictMemory);if(!M.ok&&t.strictMemory)process.exitCode=f.GENERIC_ERROR;return}console.log(`
140
- ✗ Adapter output does not match fixture:`);for(const M of fe.slice(0,20)){const V=M.rowIndex!==void 0?`row[${M.rowIndex}] `:"";console.log(` - [${M.rule}] ${V}${M.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)=>{Tr(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: ${J(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: ${J(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: ${J(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: ${J(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):
140
+ ✗ Adapter output does not match fixture:`);for(const M of fe.slice(0,20)){const V=M.rowIndex!==void 0?`row[${M.rowIndex}] `:"";console.log(` - [${M.rule}] ${V}${M.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)=>{Ir(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: ${J(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: ${J(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: ${J(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: ${J(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 B=a.command("profile").description("Manage Browser Bridge Chrome profiles"),io=B.description();B.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&&!hr(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 ${gr(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");B.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: ${J(o)}`);process.exitCode=f.USAGE_ERROR}});wt(B.commands[B.commands.length-1]);B.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: ${J(t)}`);process.exitCode=f.USAGE_ERROR}});B.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: ${J(t)}`);process.exitCode=f.USAGE_ERROR}});B.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: ${J(o)}`);process.exitCode=f.USAGE_ERROR}});B.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: ${J(o)}`);process.exitCode=f.USAGE_ERROR}});wt(B.commands[B.commands.length-1]);B.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 fr(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: ${J(o)}`);process.exitCode=f.USAGE_ERROR}});B.command("prepare-browser").description("Make sure managed profiles have a browser to launch; downloads Chrome for Testing from npmmirror when no Chrome / Edge is installed").option("-f, --format <fmt>","Output format: text, json","text").action(async(e)=>{try{const t=await pr({});if(e.format==="json")console.log(JSON.stringify({ok:!0,executable:t},null,2));else console.log(t)}catch(t){if(e.format==="json")console.log(JSON.stringify({ok:!1,error:J(t)},null,2));else console.error(`Error: ${J(t)}`);process.exitCode=f.GENERIC_ERROR}});B.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: ${J(o)}`);process.exitCode=f.USAGE_ERROR}});B.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: ${J(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: ${J(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(B,{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}:{}}}
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 B=a.command("profile").description("Manage Browser Bridge Chrome profiles"),io=B.description();B.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&&!hr(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 ${gr(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");B.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: ${J(o)}`);process.exitCode=f.USAGE_ERROR}});wt(B.commands[B.commands.length-1]);B.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: ${J(t)}`);process.exitCode=f.USAGE_ERROR}});B.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: ${J(t)}`);process.exitCode=f.USAGE_ERROR}});B.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: ${J(o)}`);process.exitCode=f.USAGE_ERROR}});B.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: ${J(o)}`);process.exitCode=f.USAGE_ERROR}});wt(B.commands[B.commands.length-1]);B.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 fr(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: ${J(o)}`);process.exitCode=f.USAGE_ERROR}});B.command("prepare-browser").description("Make sure managed profiles have a browser to launch; downloads Chrome for Testing from npmmirror when no Chrome / Edge is installed").option("-f, --format <fmt>","Output format: text, json","text").action(async(e)=>{try{const t=await pr({});if(e.format==="json")console.log(JSON.stringify({ok:!0,executable:t},null,2));else console.log(t)}catch(t){if(e.format==="json")console.log(JSON.stringify({ok:!1,error:J(t)},null,2));else console.error(`Error: ${J(t)}`);process.exitCode=f.GENERIC_ERROR}});B.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: ${J(o)}`);process.exitCode=f.USAGE_ERROR}});B.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: ${J(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: ${J(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);Jr(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(B,{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}:{}}}
@@ -1,3 +1,3 @@
1
1
  import{mkdir as W,readFile as D,rename as le,writeFile as U}from"node:fs/promises";import{homedir as H}from"node:os";import{dirname as V,join as z}from"node:path";import{pathToFileURL as fe}from"node:url";import{InvalidArgumentError as _,Option as de}from"commander";import{AuthRequiredError as L,CliError as G,getErrorMessage as R}from"../errors.js";import{executeCommand as w}from"../execution.js";import{ensureBrowserBridgeReady as he}from"../browser/daemon-lifecycle.js";import{resolveProfile as ge}from"../browser/profile.js";import{isElectronApp as E}from"../electron-apps.js";import{fullName as me,getRegistry as J}from"../registry.js";import{render as K}from"../output.js";const x=1,T=86400000;function k(e,t,r){if(e===void 0||e===null||e==="")return r;const i=Number(e);if(!Number.isInteger(i)||i<=0)throw new _(`${t} must be a positive integer. Received: "${String(e)}"`);return i}function Q(e,t,r){if(e===void 0||e===null||e==="")return r;const i=Number(e);if(!Number.isInteger(i)||i<0)throw new _(`${t} must be a non-negative integer. Received: "${String(e)}"`);return i}function Y(e){if(!e||!e.trim())return null;const t=e.split(",").map((r)=>r.trim()).filter(Boolean);return t.length>0?new Set(t):null}function pe(){return z(H(),".ppcli","auth-refresh.json")}const C=1,ye=600000;function _e(){return z(H(),".ppcli","auth-state.json")}async function we(e){try{const t=JSON.parse(await D(e,"utf8"));if(t&&t.version===C&&t.profiles&&typeof t.profiles==="object")return{version:C,profiles:t.profiles}}catch{}return{version:C,profiles:{}}}async function ke(e,t){await W(V(e),{recursive:!0});const r=`${e}.${process.pid}.tmp`;await U(r,`${JSON.stringify(t,null,2)}
2
2
  `,"utf8");await le(r,e)}function X(e,t){return e!==void 0&&t>=e&&t-e<ye}function Se(e,t,r){return{site:e,status:t.status,logged_in:t.logged_in,identity:t.identity,checked:"cached",error:t.error,checked_at:new Date(t.at).toISOString(),verified:X(t.verifiedAt,r)}}function be(e,t,r,i){return{status:e.status,logged_in:e.logged_in,identity:e.identity,checked:e.checked,error:e.error,at:t,...r!==void 0?{verifiedAt:r}:{},...i!==void 0?{verifyAttemptAt:i}:{}}}function Ae(){return{version:x,sites:{}}}async function ve(e){try{const t=JSON.parse(await D(e,"utf8"));if(t&&t.version===x&&t.sites&&typeof t.sites==="object")return{version:x,sites:t.sites}}catch(t){if(t.code!=="ENOENT")throw t}return Ae()}async function Re(e,t){await W(V(e),{recursive:!0});await U(e,`${JSON.stringify(t,null,2)}
3
- `,"utf8")}function Z(e){if(!e?.last_touched_at)return null;const t=Date.parse(e.last_touched_at);return Number.isFinite(t)?t:null}function Ee(e,t){const r=Z(e);return r!==null&&t.getTime()-r<T}function h(e){const t=Z(e);return t===null?"":new Date(t+T).toISOString()}function ee(){const e=new Set;return[...J().values()].filter((t)=>{if(e.has(t))return!1;e.add(t);return t.name==="whoami"&&t.access==="read"}).sort((t,r)=>t.site.localeCompare(r.site))}async function O(e){const t=e;if(!t._lazy||!t._modulePath)return e;await import(fe(t._modulePath).href);return J().get(me(e))??e}function S(e,t){const r=e.args.some((i)=>i.name==="timeout");return{...e,args:r?e.args:[...e.args,{name:"timeout",type:"int",default:t,help:"Per-site auth command timeout in seconds"}]}}function xe(e,t){if(e.browser!==!0||typeof e.authStatus?.quickCheck!=="function")return null;return S({...e,func:e.authStatus.quickCheck,navigateBefore:!1,siteSession:"ephemeral",defaultWindowMode:"background"},t)}function te(e){if(typeof e==="boolean")return e;if(e&&typeof e==="object"&&!Array.isArray(e)){const t=e.logged_in;if(typeof t==="boolean")return t}return null}function Te(e){const t=Array.isArray(e)?e[0]:e;if(t&&typeof t==="object"&&!Array.isArray(t)){const r=t.logged_in;if(typeof r==="boolean")return r}return null}function Ce(e){const t=Array.isArray(e)?e[0]:e;if(t&&typeof t==="object"&&!Array.isArray(t)){const r=t.error;if(typeof r==="string"&&r.trim())return r.trim()}return""}function re(e){if(e===void 0||e===null)return"";if(typeof e==="string"||typeof e==="number"||typeof e==="boolean")return String(e);return""}function ne(e){const t=Array.isArray(e)?e[0]:e;if(!t||typeof t!=="object"||Array.isArray(t))return"";const r=t,i=/(?:email|phone|real.?name|first.?name|last.?name|cookie|token|session|secret|password|csrf|jwt|bearer|wt2)/i;for(const n of["username","handle","user_id","id","name","nickname","user_type","url"]){if(i.test(n))continue;const s=re(r[n]);if(s)return s}for(const[n,s]of Object.entries(r)){if(n==="site"||n==="logged_in"||i.test(n))continue;const a=re(s);if(a)return a}return""}function I(e){return e instanceof L||typeof e==="object"&&e!==null&&e.code==="AUTH_REQUIRED"}function ie(e,t,r){if(I(r))return{site:e,status:"not_logged_in",logged_in:!1,identity:"",checked:t,error:""};const i=r instanceof G?r.code:"",n=R(r);return{site:e,status:"error",logged_in:"",identity:"",checked:t,error:i?`${i}: ${n}`:n}}function Oe(e,t){if(e.browser!==!0)return null;let r=e.authStatus?.refresh;if(typeof r!=="function"){const i=e.authStatus?.quickCheck;if(typeof i!=="function"||!e.domain)return null;const n=e.domain.startsWith("http://")||e.domain.startsWith("https://")?e.domain:`https://${e.domain}`;r=async(s,a,u)=>{await s.goto(n);await s.wait(1);if(te(await i(s,a,u))!==!0)throw new L(e.domain??e.site,`Auth refresh quickCheck failed for ${e.site}`);return{status:"touched"}}}return S({...e,func:r,navigateBefore:!1,siteSession:"persistent",defaultWindowMode:"background"},t)}function Ie(e){if(e&&typeof e==="object"&&!Array.isArray(e)){const t=e;if(t.status==="refreshed"||t.refreshed===!0)return"refreshed"}return"touched"}function qe(e,t,r){if(I(r))return{site:e,status:"not_logged_in",last_touched_at:t?.last_touched_at??"",next_refresh_at:h(t),error:""};const i=r instanceof G?r.code:"",n=R(r);return{site:e,status:"error",last_touched_at:t?.last_touched_at??"",next_refresh_at:h(t),error:i?`${i}: ${n}`:n}}function oe(){return process.env.PUBLISHPORT_SITE_SESSION==="ephemeral"?{siteSession:"ephemeral",keepTab:"false"}:{siteSession:"persistent",keepTab:"true"}}async function Ne(e,t){try{const r=await O(e);if(r.browser!==!0){const a=S(r,t.timeoutSeconds),u=await w(a,{timeout:t.timeoutSeconds},!1,{...t.profile?{profile:t.profile}:{}});return{site:e.site,status:"logged_in",logged_in:!0,identity:ne(u),checked:"quick",error:""}}if(r.authStatus?.quickCheckRequiresNavigation===!0)return{site:e.site,status:"unknown",logged_in:"",identity:"",checked:"navigation_required",error:""};const i=xe(r,t.timeoutSeconds);if(!i)return{site:e.site,status:"unknown",logged_in:"",identity:"",checked:"skipped",error:"quickCheck not implemented; use --full to run whoami"};const n=await w(i,{timeout:t.timeoutSeconds},!1,{...oe(),windowMode:"background",...t.profile?{profile:t.profile}:{}}),s=te(n);if(s===!0)return{site:e.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};if(s===!1)return{site:e.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"quick",error:""};return{site:e.site,status:"unknown",logged_in:"",identity:"",checked:"quick",error:"quickCheck returned no boolean logged_in signal"}}catch(r){return ie(e.site,"quick",r)}}async function Me(e,t){try{const r=await O(e),i=S(r,t.timeoutSeconds),n=await w(i,{timeout:t.timeoutSeconds},!1,{...oe(),windowMode:"background",...t.profile?{profile:t.profile}:{}});if(Te(n)===!1)return{site:e.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"full",error:Ce(n)};return{site:e.site,status:"logged_in",logged_in:!0,identity:ne(n),checked:"full",error:""}}catch(r){return ie(e.site,"full",r)}}function Fe(e,t){return t?`${e}@${t}`:e}async function $e(e,t){const r=Fe(e.site,t.profile),i=t.state.sites[r];if(!t.force&&Ee(i,t.now))return{site:e.site,status:"skipped",last_touched_at:i?.last_touched_at??"",next_refresh_at:h(i),error:""};const n=t.now.toISOString();try{const s=await O(e),a=Oe(s,t.timeoutSeconds);if(!a){t.state.sites[r]={...i,last_attempt_at:n,last_status:"unsupported"};return{site:e.site,status:"unsupported",last_touched_at:i?.last_touched_at??"",next_refresh_at:h(i),error:"refresh probe is not available for this site"}}const u=await w(a,{timeout:t.timeoutSeconds},!1,{siteSession:"persistent",keepTab:"true",windowMode:"background",...t.profile?{profile:t.profile}:{}}),d=Ie(u);t.state.sites[r]={...i,last_attempt_at:n,last_touched_at:n,last_status:d};return{site:e.site,status:d,last_touched_at:n,next_refresh_at:new Date(t.now.getTime()+T).toISOString(),error:""}}catch(s){const a=I(s)?"not_logged_in":"error";t.state.sites[r]={...i,last_attempt_at:n,last_status:a};return qe(e.site,i,s)}}async function se(e,t){const r=ge(t);if(r.kind!=="extension")return null;if(!e.some((n)=>n.browser===!0&&!E(n.site)))return null;try{await he({timeoutSeconds:15,contextId:r.contextId,verbose:!1});return null}catch(n){return R(n)}}function Pe(e,t,r){return{site:e,status:"error",logged_in:"",identity:"",checked:t,error:`BROWSER_CONNECT: ${r}`}}async function ae(e,t,r){const i=Array(e.length);let n=0;const s=Array.from({length:Math.min(t,e.length)},async()=>{while(n<e.length){const a=n++;i[a]=await r(e[a])}});await Promise.all(s);return i}export async function collectAuthStatus(e){const t=Y(e.sites),r=e.full?"full":"quick",i=k(e.concurrency,"--concurrency",r==="full"?3:8),n=k(e.timeout,"--timeout",r==="full"?20:8),s=String(e.only??"all");if(!["all","logged-in","not-logged-in","unknown","error"].includes(s))throw new _("--only must be one of: all, logged-in, not-logged-in, unknown, error");if(r==="full"&&!t)throw new _("--full 会对每个站点真实打开页面,不允许全量使用。"+"要实证某个站点请点名:auth status --site <site> --full;"+"想看全部站点就用默认的 quick(纯 cookie,不开任何页面)。");const a=Q(e.maxAge,"--max-age",0)*1000,u=Q(e.verify,"--verify",0),d=e.statePath??_e(),f=(e.now??new Date).getTime(),l=e.profile??"",b=ee().filter((o)=>!t||t.has(o.site)),A=await we(d),g=A.profiles[l]??{},v=new Map,m=[];for(const o of b){const c=g[o.site];if(r!=="full"&&a>0&&c&&f>=c.at&&f-c.at<a)v.set(o.site,Se(o.site,c,f));else m.push(o)}const q=m.length>0?await se(m,e.profile):null,ue=await ae(m,i,(o)=>q&&o.browser===!0&&!E(o.site)?Promise.resolve(Pe(o.site,r,q)):r==="full"?Me(o,{timeoutSeconds:n,profile:e.profile}):Ne(o,{timeoutSeconds:n,profile:e.profile})),je=new Map(b.map((o)=>[o.site,o])),p=new Map(v),N=new Map,M=new Map;for(const o of ue){p.set(o.site,o);if(r==="full"){M.set(o.site,f);if(o.status!=="error")N.set(o.site,f)}}const F=(o)=>{const c=N.get(o.site);if(c!==void 0)return c;const y=g[o.site];return y&&y.status===o.status&&X(y.verifiedAt,f)?y.verifiedAt:void 0},ce=(o)=>M.get(o.site)??g[o.site]?.verifyAttemptAt,P={...g};for(const o of p.keys()){if(v.has(o))continue;const c=p.get(o);if(!c)continue;P[o]=be(c,f,F(c),ce(c))}A.profiles[l]=P;await ke(d,A).catch(()=>{});const j=b.map((o)=>p.get(o.site)).filter((o)=>o!==void 0).map((o)=>({...o,checked_at:o.checked_at??new Date(f).toISOString(),verified:o.verified??F(o)!==void 0})),B=s.replace(/-/g,"_");return B==="all"?j:j.filter((o)=>o.status===B)}export async function collectAuthRefresh(e){const t=Y(e.sites),r=k(e.concurrency,"--concurrency",3),i=k(e.timeout,"--timeout",20),n=e.statePath??pe(),s=e.now??new Date,a=await ve(n),u=ee().filter((l)=>!t||t.has(l.site)),d=await se(u,e.profile),f=await ae(u,r,(l)=>d&&l.browser===!0&&!E(l.site)?Promise.resolve({site:l.site,status:"error",last_touched_at:a.sites[l.site]?.last_touched_at??"",next_refresh_at:h(a.sites[l.site]),error:`BROWSER_CONNECT: ${d}`}):$e(l,{timeoutSeconds:i,profile:e.profile,now:s,state:a,force:e.all===!0}));await Re(n,a);return f}export function registerAuthCommands(e){const t=e.command("auth").description("Inspect website login status"),r=t.command("status").description("Show login status for sites with auth adapters").option("--site <sites>","Comma-separated site names to check, e.g. github,chatgpt").option("--full","Run full per-site whoami probes instead of quick no-navigation checks",!1).option("--concurrency <n>","Maximum sites to check at once").option("--timeout <seconds>","Per-site timeout in seconds").option("--max-age <seconds>","Reuse a local cached verdict younger than this instead of probing").option("--verify <n>","Deprecated and ignored: batch auth status never navigates. Use --site <s> --full to confirm one site with a real whoami").addOption(new de("--only <status>","Filter rows by status").choices(["all","logged-in","not-logged-in","unknown","error"]).default("all")).option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(n)=>{const s=typeof r.optsWithGlobals==="function"?r.optsWithGlobals():{},a=await collectAuthStatus({sites:n.site,full:n.full===!0,concurrency:n.concurrency,timeout:n.timeout,only:n.only,maxAge:n.maxAge,verify:n.verify,profile:typeof s.profile==="string"&&s.profile.trim()?s.profile.trim():void 0}),u=typeof n.format==="string"?n.format:"table";K(a,{fmt:u,fmtExplicit:r.getOptionValueSource("format")==="cli",columns:["site","status","identity","checked","error"],title:"ppcli/auth status",source:n.full?"full whoami probe":"quick auth check"})}),i=t.command("refresh").description("Touch logged-in site sessions to keep browser auth fresh").option("--site <sites>","Comma-separated site names to refresh, e.g. github,claude").option("--all","Ignore the 24h refresh throttle and force every selected site",!1).option("--concurrency <n>","Maximum sites to refresh at once").option("--timeout <seconds>","Per-site timeout in seconds").option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(n)=>{const s=typeof i.optsWithGlobals==="function"?i.optsWithGlobals():{},a=await collectAuthRefresh({sites:n.site,all:n.all===!0,concurrency:n.concurrency,timeout:n.timeout,profile:typeof s.profile==="string"&&s.profile.trim()?s.profile.trim():void 0}),u=typeof n.format==="string"?n.format:"table";K(a,{fmt:u,fmtExplicit:i.getOptionValueSource("format")==="cli",columns:["site","status","last_touched_at","next_refresh_at","error"],title:"ppcli/auth refresh",source:n.all?"forced persistent touch":"persistent touch with 24h throttle"})});return t}
3
+ `,"utf8")}function Z(e){if(!e?.last_touched_at)return null;const t=Date.parse(e.last_touched_at);return Number.isFinite(t)?t:null}function Ee(e,t){const r=Z(e);return r!==null&&t.getTime()-r<T}function h(e){const t=Z(e);return t===null?"":new Date(t+T).toISOString()}function ee(){const e=new Set;return[...J().values()].filter((t)=>{if(e.has(t))return!1;e.add(t);return t.name==="whoami"&&t.access==="read"}).sort((t,r)=>t.site.localeCompare(r.site))}async function O(e){const t=e;if(!t._lazy||!t._modulePath)return e;await import(fe(t._modulePath).href);return J().get(me(e))??e}function S(e,t){const r=e.args.some((i)=>i.name==="timeout");return{...e,args:r?e.args:[...e.args,{name:"timeout",type:"int",default:t,help:"Per-site auth command timeout in seconds"}]}}function xe(e,t){if(e.browser!==!0||typeof e.authStatus?.quickCheck!=="function")return null;return S({...e,func:e.authStatus.quickCheck,navigateBefore:!1,siteSession:"ephemeral",defaultWindowMode:"background"},t)}function te(e){if(typeof e==="boolean")return e;if(e&&typeof e==="object"&&!Array.isArray(e)){const t=e.logged_in;if(typeof t==="boolean")return t}return null}function Te(e){const t=Array.isArray(e)?e[0]:e;if(t&&typeof t==="object"&&!Array.isArray(t)){const r=t.logged_in;if(typeof r==="boolean")return r}return null}function Ce(e){const t=Array.isArray(e)?e[0]:e;if(t&&typeof t==="object"&&!Array.isArray(t)){const r=t.error;if(typeof r==="string"&&r.trim())return r.trim()}return""}function re(e){if(e===void 0||e===null)return"";if(typeof e==="string"||typeof e==="number"||typeof e==="boolean")return String(e);return""}function ne(e){const t=Array.isArray(e)?e[0]:e;if(!t||typeof t!=="object"||Array.isArray(t))return"";const r=t,i=/(?:email|phone|real.?name|first.?name|last.?name|cookie|token|session|secret|password|csrf|jwt|bearer|wt2)/i;for(const n of["username","handle","user_id","id","name","nickname","user_type","url"]){if(i.test(n))continue;const s=re(r[n]);if(s)return s}for(const[n,s]of Object.entries(r)){if(n==="site"||n==="logged_in"||i.test(n))continue;const a=re(s);if(a)return a}return""}function I(e){return e instanceof L||typeof e==="object"&&e!==null&&e.code==="AUTH_REQUIRED"}function ie(e,t,r){if(I(r))return{site:e,status:"not_logged_in",logged_in:!1,identity:"",checked:t,error:""};const i=r instanceof G?r.code:"",n=R(r);return{site:e,status:"error",logged_in:"",identity:"",checked:t,error:i?`${i}: ${n}`:n}}function Oe(e,t){if(e.browser!==!0)return null;let r=e.authStatus?.refresh;if(typeof r!=="function"){const i=e.authStatus?.quickCheck;if(typeof i!=="function"||!e.domain)return null;const n=e.domain.startsWith("http://")||e.domain.startsWith("https://")?e.domain:`https://${e.domain}`;r=async(s,a,u)=>{await s.goto(n);await s.wait(1);if(te(await i(s,a,u))!==!0)throw new L(e.domain??e.site,`Auth refresh quickCheck failed for ${e.site}`);return{status:"touched"}}}return S({...e,func:r,navigateBefore:!1,siteSession:"persistent",defaultWindowMode:"background"},t)}function Ie(e){if(e&&typeof e==="object"&&!Array.isArray(e)){const t=e;if(t.status==="refreshed"||t.refreshed===!0)return"refreshed"}return"touched"}function qe(e,t,r){if(I(r))return{site:e,status:"not_logged_in",last_touched_at:t?.last_touched_at??"",next_refresh_at:h(t),error:""};const i=r instanceof G?r.code:"",n=R(r);return{site:e,status:"error",last_touched_at:t?.last_touched_at??"",next_refresh_at:h(t),error:i?`${i}: ${n}`:n}}function oe(){return process.env.PUBLISHPORT_SITE_SESSION==="ephemeral"?{siteSession:"ephemeral",keepTab:"false"}:{siteSession:"persistent",keepTab:"false"}}async function Ne(e,t){try{const r=await O(e);if(r.browser!==!0){const a=S(r,t.timeoutSeconds),u=await w(a,{timeout:t.timeoutSeconds},!1,{...t.profile?{profile:t.profile}:{}});return{site:e.site,status:"logged_in",logged_in:!0,identity:ne(u),checked:"quick",error:""}}if(r.authStatus?.quickCheckRequiresNavigation===!0)return{site:e.site,status:"unknown",logged_in:"",identity:"",checked:"navigation_required",error:""};const i=xe(r,t.timeoutSeconds);if(!i)return{site:e.site,status:"unknown",logged_in:"",identity:"",checked:"skipped",error:"quickCheck not implemented; use --full to run whoami"};const n=await w(i,{timeout:t.timeoutSeconds},!1,{...oe(),windowMode:"background",...t.profile?{profile:t.profile}:{}}),s=te(n);if(s===!0)return{site:e.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};if(s===!1)return{site:e.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"quick",error:""};return{site:e.site,status:"unknown",logged_in:"",identity:"",checked:"quick",error:"quickCheck returned no boolean logged_in signal"}}catch(r){return ie(e.site,"quick",r)}}async function Me(e,t){try{const r=await O(e),i=S(r,t.timeoutSeconds),n=await w(i,{timeout:t.timeoutSeconds},!1,{...oe(),windowMode:"background",...t.profile?{profile:t.profile}:{}});if(Te(n)===!1)return{site:e.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"full",error:Ce(n)};return{site:e.site,status:"logged_in",logged_in:!0,identity:ne(n),checked:"full",error:""}}catch(r){return ie(e.site,"full",r)}}function Fe(e,t){return t?`${e}@${t}`:e}async function $e(e,t){const r=Fe(e.site,t.profile),i=t.state.sites[r];if(!t.force&&Ee(i,t.now))return{site:e.site,status:"skipped",last_touched_at:i?.last_touched_at??"",next_refresh_at:h(i),error:""};const n=t.now.toISOString();try{const s=await O(e),a=Oe(s,t.timeoutSeconds);if(!a){t.state.sites[r]={...i,last_attempt_at:n,last_status:"unsupported"};return{site:e.site,status:"unsupported",last_touched_at:i?.last_touched_at??"",next_refresh_at:h(i),error:"refresh probe is not available for this site"}}const u=await w(a,{timeout:t.timeoutSeconds},!1,{siteSession:"persistent",keepTab:"false",windowMode:"background",...t.profile?{profile:t.profile}:{}}),d=Ie(u);t.state.sites[r]={...i,last_attempt_at:n,last_touched_at:n,last_status:d};return{site:e.site,status:d,last_touched_at:n,next_refresh_at:new Date(t.now.getTime()+T).toISOString(),error:""}}catch(s){const a=I(s)?"not_logged_in":"error";t.state.sites[r]={...i,last_attempt_at:n,last_status:a};return qe(e.site,i,s)}}async function se(e,t){const r=ge(t);if(r.kind!=="extension")return null;if(!e.some((n)=>n.browser===!0&&!E(n.site)))return null;try{await he({timeoutSeconds:15,contextId:r.contextId,verbose:!1});return null}catch(n){return R(n)}}function Pe(e,t,r){return{site:e,status:"error",logged_in:"",identity:"",checked:t,error:`BROWSER_CONNECT: ${r}`}}async function ae(e,t,r){const i=Array(e.length);let n=0;const s=Array.from({length:Math.min(t,e.length)},async()=>{while(n<e.length){const a=n++;i[a]=await r(e[a])}});await Promise.all(s);return i}export async function collectAuthStatus(e){const t=Y(e.sites),r=e.full?"full":"quick",i=k(e.concurrency,"--concurrency",r==="full"?3:8),n=k(e.timeout,"--timeout",r==="full"?20:8),s=String(e.only??"all");if(!["all","logged-in","not-logged-in","unknown","error"].includes(s))throw new _("--only must be one of: all, logged-in, not-logged-in, unknown, error");if(r==="full"&&!t)throw new _("--full 会对每个站点真实打开页面,不允许全量使用。"+"要实证某个站点请点名:auth status --site <site> --full;"+"想看全部站点就用默认的 quick(纯 cookie,不开任何页面)。");const a=Q(e.maxAge,"--max-age",0)*1000,u=Q(e.verify,"--verify",0),d=e.statePath??_e(),f=(e.now??new Date).getTime(),l=e.profile??"",b=ee().filter((o)=>!t||t.has(o.site)),A=await we(d),g=A.profiles[l]??{},v=new Map,m=[];for(const o of b){const c=g[o.site];if(r!=="full"&&a>0&&c&&f>=c.at&&f-c.at<a)v.set(o.site,Se(o.site,c,f));else m.push(o)}const q=m.length>0?await se(m,e.profile):null,ue=await ae(m,i,(o)=>q&&o.browser===!0&&!E(o.site)?Promise.resolve(Pe(o.site,r,q)):r==="full"?Me(o,{timeoutSeconds:n,profile:e.profile}):Ne(o,{timeoutSeconds:n,profile:e.profile})),je=new Map(b.map((o)=>[o.site,o])),p=new Map(v),N=new Map,M=new Map;for(const o of ue){p.set(o.site,o);if(r==="full"){M.set(o.site,f);if(o.status!=="error")N.set(o.site,f)}}const F=(o)=>{const c=N.get(o.site);if(c!==void 0)return c;const y=g[o.site];return y&&y.status===o.status&&X(y.verifiedAt,f)?y.verifiedAt:void 0},ce=(o)=>M.get(o.site)??g[o.site]?.verifyAttemptAt,P={...g};for(const o of p.keys()){if(v.has(o))continue;const c=p.get(o);if(!c)continue;P[o]=be(c,f,F(c),ce(c))}A.profiles[l]=P;await ke(d,A).catch(()=>{});const j=b.map((o)=>p.get(o.site)).filter((o)=>o!==void 0).map((o)=>({...o,checked_at:o.checked_at??new Date(f).toISOString(),verified:o.verified??F(o)!==void 0})),B=s.replace(/-/g,"_");return B==="all"?j:j.filter((o)=>o.status===B)}export async function collectAuthRefresh(e){const t=Y(e.sites),r=k(e.concurrency,"--concurrency",3),i=k(e.timeout,"--timeout",20),n=e.statePath??pe(),s=e.now??new Date,a=await ve(n),u=ee().filter((l)=>!t||t.has(l.site)),d=await se(u,e.profile),f=await ae(u,r,(l)=>d&&l.browser===!0&&!E(l.site)?Promise.resolve({site:l.site,status:"error",last_touched_at:a.sites[l.site]?.last_touched_at??"",next_refresh_at:h(a.sites[l.site]),error:`BROWSER_CONNECT: ${d}`}):$e(l,{timeoutSeconds:i,profile:e.profile,now:s,state:a,force:e.all===!0}));await Re(n,a);return f}export function registerAuthCommands(e){const t=e.command("auth").description("Inspect website login status"),r=t.command("status").description("Show login status for sites with auth adapters").option("--site <sites>","Comma-separated site names to check, e.g. github,chatgpt").option("--full","Run full per-site whoami probes instead of quick no-navigation checks",!1).option("--concurrency <n>","Maximum sites to check at once").option("--timeout <seconds>","Per-site timeout in seconds").option("--max-age <seconds>","Reuse a local cached verdict younger than this instead of probing").option("--verify <n>","Deprecated and ignored: batch auth status never navigates. Use --site <s> --full to confirm one site with a real whoami").addOption(new de("--only <status>","Filter rows by status").choices(["all","logged-in","not-logged-in","unknown","error"]).default("all")).option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(n)=>{const s=typeof r.optsWithGlobals==="function"?r.optsWithGlobals():{},a=await collectAuthStatus({sites:n.site,full:n.full===!0,concurrency:n.concurrency,timeout:n.timeout,only:n.only,maxAge:n.maxAge,verify:n.verify,profile:typeof s.profile==="string"&&s.profile.trim()?s.profile.trim():void 0}),u=typeof n.format==="string"?n.format:"table";K(a,{fmt:u,fmtExplicit:r.getOptionValueSource("format")==="cli",columns:["site","status","identity","checked","error"],title:"ppcli/auth status",source:n.full?"full whoami probe":"quick auth check"})}),i=t.command("refresh").description("Touch logged-in site sessions to keep browser auth fresh").option("--site <sites>","Comma-separated site names to refresh, e.g. github,claude").option("--all","Ignore the 24h refresh throttle and force every selected site",!1).option("--concurrency <n>","Maximum sites to refresh at once").option("--timeout <seconds>","Per-site timeout in seconds").option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(n)=>{const s=typeof i.optsWithGlobals==="function"?i.optsWithGlobals():{},a=await collectAuthRefresh({sites:n.site,all:n.all===!0,concurrency:n.concurrency,timeout:n.timeout,profile:typeof s.profile==="string"&&s.profile.trim()?s.profile.trim():void 0}),u=typeof n.format==="string"?n.format:"table";K(a,{fmt:u,fmtExplicit:i.getOptionValueSource("format")==="cli",columns:["site","status","last_touched_at","next_refresh_at","error"],title:"ppcli/auth refresh",source:n.all?"forced persistent touch":"persistent touch with 24h throttle"})});return t}
@@ -1,2 +1,2 @@
1
- import{getRegistry as ie,fullName as w}from"./registry.js";import{pathToFileURL as se}from"node:url";import*as ae from"node:crypto";import*as M from"node:fs";import*as j from"node:os";import*as N from"node:path";import{executePipeline as K}from"./pipeline/index.js";import{adapterLoadError as ue,ArgumentError as d,CommandExecutionError as A,SessionBusyError as ce,attachTraceReceipt as le,getErrorMessage as D}from"./errors.js";import{shouldUseBrowserSession as fe}from"./capabilityRouting.js";import{getBrowserFactory as me,browserSession as de,runWithTimeout as V,DEFAULT_BROWSER_COMMAND_TIMEOUT as pe}from"./runtime.js";import{withBrowserSessionLockIf as he}from"./browser-session-lock.js";import{resolveProfile as ge}from"./browser/profile.js";import{ensureManagedChrome as we}from"./browser/managed-chrome.js";import{generateFingerprintJs as ye}from"./browser/fingerprint.js";import{CDPBridge as Se}from"./browser/index.js";import{clearDaemonRunContext as ve,generateRunId as be,isUnknownOutcomeError as G,releaseSiteSessionLease as J,setDaemonCommandTimeoutSeconds as $e,setDaemonRunContext as Q}from"./browser/daemon-client.js";import{emitHook as k}from"./hooks.js";import{setContentBaseDir as Ee}from"./utils.js";import{log as X}from"./logger.js";import{isElectronApp as Pe}from"./electron-apps.js";import{probeCDP as Ae,resolveElectronEndpoint as xe}from"./launcher.js";import{ObservationSession as Ce,exportObservationSession as Re}from"./observation/index.js";import{resolveAdapterSourcePath as Ie}from"./adapter-source.js";const v=new Map,_=new Map,Te=`${j.homedir()}/.ppcli/clis/`;function Me(e){if(e===void 0||e===null||e===""||e==="off")return"off";if(e==="on"||e==="retain-on-failure")return e;throw new d(`--trace must be one of: off, on, retain-on-failure. Received: "${String(e)}"`)}export function coerceAndValidateArgs(e,r){const t={...r};for(const n of e){const o=t[n.name];if(n.required&&(o===void 0||o===null||o===""))throw new d(`Argument "${n.name}" is required.`,n.help??`Provide a value for --${n.name}`);if(o!==void 0&&o!==null){if(n.type==="int"||n.type==="number"){const l=Number(o);if(!Number.isFinite(l))throw new d(`Argument "${n.name}" must be a valid number. Received: "${o}"`);if(n.type==="int"&&!Number.isInteger(l))throw new d(`Argument "${n.name}" must be a valid integer. Received: "${o}"`);t[n.name]=l}else if(n.type==="boolean"||n.type==="bool")if(typeof o==="string"){const l=o.toLowerCase();if(l==="true"||l==="1")t[n.name]=!0;else if(l==="false"||l==="0")t[n.name]=!1;else throw new d(`Argument "${n.name}" must be a boolean (true/false). Received: "${o}"`)}else t[n.name]=Boolean(o);const s=t[n.name];if(n.choices&&n.choices.length>0){if(!n.choices.map(String).includes(String(s)))throw new d(`Argument "${n.name}" must be one of: ${n.choices.join(", ")}. Received: "${s}"`)}}else if(n.default!==void 0)t[n.name]=n.default}return t}async function B(e,r,t,n){const o=e;if(o._lazy&&o._modulePath){const s=o._modulePath;if(s.startsWith(Te)&&v.has(s))try{const m=M.statSync(s),i=_.get(s);if(i!==void 0&&m.mtimeMs!==i)v.delete(s)}catch{}if(!v.has(s)){const m=se(s).href,g=import(_.has(s)?`${m}?t=${Date.now()}`:m).then(()=>{try{_.set(s,M.statSync(s).mtimeMs)}catch{}},(p)=>{v.delete(s);throw ue(`Failed to load adapter module ${s}: ${D(p)}`,"Check that the adapter file exists and has no syntax errors.")});v.set(s,g)}await v.get(s);const f=ie().get(w(e));if(f?.func)return Y(f,r,t,n);if(f?.pipeline)return K(r,f.pipeline,{args:t,debug:n})}if(e.func)return Y(e,r,t,n);if(e.pipeline)return K(r,e.pipeline,{args:t,debug:n});throw new A(`Command ${w(e)} has no func or pipeline`,"This is likely a bug in the adapter definition. Please report this issue.")}function Y(e,r,t,n){if(e.browser===!1)return e.func(t,n);if(!r)throw new A(`Command ${w(e)} requires a browser session but none was provided`);return e.func(r,t,n)}function Ne(e){if(e.navigateBefore===!1)return null;if(typeof e.navigateBefore==="string")return e.navigateBefore;return null}function De(e,r){if(!e||!r)return!1;try{const t=new URL(e).hostname;return t===r||t.endsWith(`.${r}`)}catch{return!1}}function ke(e,r){if(!r)return!1;try{const t=new URL(e),n=t.hostname===r||t.hostname.endsWith(`.${r}`),o=t.pathname===""||t.pathname==="/";return n&&o&&t.search===""&&t.hash===""}catch{return!1}}async function _e(e,r,t,n){if(t!=="persistent"||!e.domain)return!0;if(!ke(n,e.domain))return!0;const o=await r.getCurrentUrl?.().catch(()=>null);return!De(o,e.domain)}export async function executeCommand(e,r,t=!1,n={}){let o;try{o=n.prepared?r:prepareCommandArgs(e,r)}catch(i){if(i instanceof d)throw i;throw new d(D(i))}const s=He(e,o);$e(s);const l=Me(n.trace),f={command:w(e),args:o,startedAt:Date.now()};await k("onBeforeExecute",f);let m;try{if(fe(e)){const i=Pe(e.site),g=ge(n.profile),p=!i&&g.kind==="managed"?g:null;process.env.PPCLI_ACTIVE_PROFILE=p?p.name:"";let P;const x=ze(e.defaultWindowMode??"background",n.windowMode);let F,W;if(p){P=await we(p.name,p.entry,{windowMode:x});const u=p.entry.fingerprint;if(u?.enabled)F=ye(u);const a=p.entry.proxy;if(a?.username)W={username:a.username,password:a.password??""}}else if(i){const u=process.env.OPENCLI_CDP_ENDPOINT;if(u){const a=Number(new URL(u).port);if(!await Ae(a))throw new A(`CDP not reachable at ${u}`,"Check that the app is running with --remote-debugging-port and the endpoint is correct.");P=u}else P=await xe(e.site)}const ne=p?Se:me(e.site),L=!p&&g.kind==="extension"?g.contextId:void 0,oe=e,b=Fe(e,n.siteSession),C=We(e,b),q=qe(b,n.keepTab),y=b==="persistent"&&e.access==="write"?{runId:be(),session:C}:null;if(y)Q({runId:y.runId,command:w(e),access:"write"});let z,H=!1,R=null;try{m=await he(x!=="foreground",()=>de(ne,async(u)=>{const a=l==="off"?null:new Ce({scope:{contextId:L??p?.name,session:C,target:u.getActivePage?.(),site:e.site,command:w(e),adapterSourcePath:Ie(oe)}});if(a){a.record({stream:"action",name:"command",phase:"start",data:{args:o}});await u.startNetworkCapture?.().catch(()=>!1)}const h=Ne(e);if(h&&await _e(e,u,b,h)){a?.record({stream:"action",name:"pre_navigate",phase:"start",data:{url:h}});try{await u.goto(h);a?.record({stream:"action",name:"pre_navigate",phase:"end",data:{url:h}})}catch(c){if(c instanceof ce)throw c;a?.record({stream:"action",name:"pre_navigate",phase:"error",data:{url:h,error:c instanceof Error?c.message:String(c)}});const S=new A(`Pre-navigation to ${h} failed: ${c instanceof Error?c.message:c}`,"Check that the site is reachable and the browser extension is running.");S.cause=c;if(a&&(l==="on"||l==="retain-on-failure")){a.record({stream:"error",message:S.message,stack:S.stack,code:S.code,hint:S.hint});await U(a,u).catch(()=>{});O(a,"failure",S,n.onTraceExport)}throw S}}const E=s!==null?s+ee:pe,I=B(e,u,o,t);R=I;let T=!1;I.then(()=>{T=!0},()=>{T=!0});try{const c=await V(I,{timeout:E,label:w(e),hint:re(e,s)});a?.record({stream:"action",name:"command",phase:"end"});if(a&&l==="on"){await U(a,u).catch(()=>{});O(a,"success",void 0,n.onTraceExport)}if(!q)await u.closeWindow?.().catch(()=>{});return c}catch(c){if(!T){H=!0;Be(c)}if(a){a.record({stream:"action",name:"command",phase:"error",data:{error:c instanceof Error?c.message:String(c)}});a.record({stream:"error",message:c instanceof Error?c.message:String(c),stack:c instanceof Error?c.stack:void 0});if(l==="on"||l==="retain-on-failure"){await U(a,u).catch(()=>{});O(a,"failure",c,n.onTraceExport)}}if(!q)await u.closeWindow?.().catch(()=>{});throw c}},{session:C,cdpEndpoint:P,contextId:L,windowMode:x,surface:"adapter",siteSession:b,initScript:F,proxyAuth:W}),p?.name)}catch(u){z=u;throw u}finally{if(y)if(H&&R){const{runId:u,session:a}=y,h=(E)=>{ve(u);if(!G(E))J({runId:u,session:a,surface:"adapter"})};R.then(()=>h(),(E)=>h(E))}else{Q(null);if(!G(z))await J({runId:y.runId,session:y.session,surface:"adapter"})}}}else if(s!==null){const i=s+ee;m=await V(B(e,null,o,t),{timeout:i,label:w(e),hint:re(e,s)})}else m=await B(e,null,o,t)}catch(i){f.error=i;f.finishedAt=Date.now();await k("onAfterExecute",f);throw i}f.finishedAt=Date.now();await k("onAfterExecute",f,m);return m}const Z=new WeakSet;function Be(e){if(e!==null&&typeof e==="object")Z.add(e)}export function isAdapterStillRunning(e){return e!==null&&typeof e==="object"&&Z.has(e)}async function U(e,r){const t=r.getActivePage?.()??e.scope.target,[n,o,s,l,f]=await Promise.all([r.getCurrentUrl?.().catch(()=>null)??Promise.resolve(null),r.snapshot().catch(()=>{return}),r.readNetworkCapture?.().catch(()=>[])??Promise.resolve([]),r.consoleMessages("all").catch(()=>[]),r.screenshot({format:"png"}).catch(()=>{return})]);if(o!==void 0||n!==void 0)e.record({stream:"state",url:n,target:t,snapshot:o,label:"final"});for(const m of Array.isArray(s)?s:[]){const i=m;e.record({stream:"network",url:String(i.url??""),method:typeof i.method==="string"?i.method:void 0,status:typeof i.responseStatus==="number"?i.responseStatus:void 0,contentType:typeof i.responseContentType==="string"?i.responseContentType:void 0,size:typeof i.responseBodyFullSize==="number"?i.responseBodyFullSize:void 0,requestHeaders:i.requestHeaders,responseHeaders:i.responseHeaders,requestBody:i.requestBodyPreview,responseBody:i.responsePreview,ts:typeof i.timestamp==="number"?i.timestamp:void 0})}for(const m of Array.isArray(l)?l:[])if(m&&typeof m==="object"){const i=m;e.record({stream:"console",level:String(i.type??i.level??"log"),text:String(i.text??i.message??""),ts:typeof i.timestamp==="number"?i.timestamp:void 0})}else e.record({stream:"console",level:"log",text:String(m)});if(typeof f==="string"&&f)e.record({stream:"screenshot",format:"png",data:f,label:"final"})}function O(e,r,t,n){try{const o=Re(e,{error:t,status:r});if(r==="failure"&&t!==void 0)le(t,o.receipt);else process.stderr.write(`ppcli trace artifact: ${o.dir}
2
- `);try{n?.(o)}catch(s){X.warn(`[trace] Trace export callback failed: ${s instanceof Error?s.message:String(s)}`)}return o}catch(o){X.warn(`[trace] Failed to export trace artifact: ${o instanceof Error?o.message:String(o)}`);return}}export function resolveFileBackedArg(e,r){const t=e.args.find((f)=>f.fromFile);if(!t)return;const n=typeof t.fromFile==="string"?t.fromFile:"file",o=t.positional?`<${t.name}>`:`--${t.name}`,s=r[n]==null?"":String(r[n]).trim(),l=r[t.name]==null?"":String(r[t.name]);if(s){if(l.trim())throw new d(`${o} 和 --${n} 只能二选一,不能同时给`);const f=N.resolve(s.startsWith("~/")?N.join(j.homedir(),s.slice(2)):s);let m;try{m=M.readFileSync(f,"utf-8")}catch(g){throw new d(`--${n} 指向的文件读不到: ${f}(${D(g)})`)}const i=m.trim();if(!i)throw new d(`--${n} 指向的文件内容为空: ${f}`);r[t.name]=i;Ee(N.dirname(f));return}if(!l.trim()&&t.requiredUnlessFile)throw new d(`缺少 ${t.name}:${o} 或 --${n} 给其一`)}export function prepareCommandArgs(e,r){const t=coerceAndValidateArgs(e.args,r);resolveFileBackedArg(e,t);e.validateArgs?.(t);return t}const ee=30;function Ue(e){if(e===void 0||e===null||e==="")return null;if(e==="ephemeral"||e==="persistent")return e;throw new d(`--site-session must be one of: ephemeral, persistent. Received: "${String(e)}"`)}function Oe(){const e=process.env.PUBLISHPORT_SITE_SESSION;if(e==="persistent"||e==="ephemeral")return e;return null}function Fe(e,r){return Ue(r)??Oe()??e.siteSession??"persistent"}function We(e,r){const t=typeof e.sessionKey==="string"&&e.sessionKey.trim()?`:${e.sessionKey.trim()}`:"";if(r==="persistent")return`site:${e.site}${t}`;return`site:${e.site}${t}:${ae.randomUUID()}`}function Le(e,r){if(r===void 0||r==="")return null;if(r==="true")return!0;if(r==="false")return!1;throw new d(`${e} must be one of: true, false. Received: "${String(r)}"`)}function qe(e,r){if(e==="persistent")return!0;return Le("--keep-tab",r)??!1}function te(e,r){if(r===void 0||r==="")return null;if(r==="foreground"||r==="background")return r;throw new d(`${e} must be one of: foreground, background. Received: "${String(r)}"`)}function ze(e="background",r){return te("--window",r)??te("OPENCLI_WINDOW",process.env.OPENCLI_WINDOW)??e}function re(e,r){const t=e.access==="write"?"⚠️ 这是写操作,超时不代表没写成功——请求可能已经到达平台。禁止直接重试:"+"先用该平台的列表/统计命令(stats / lists / articles 等)按标题回查,确认没写进去再重发。 ":"";if(r!==null)return`${t}Pass a higher --timeout value (currently ${r}s)`;if(e.args.some((n)=>n.name==="timeout"))return`${t}Try again, or pass a higher --timeout <seconds>`;return t+"This command has no --timeout option. Retry once; if it keeps timing out, "+"the page or browser bridge is likely stuck — have the user reopen the PublishPort client. "+"The global default can be raised with OPENCLI_BROWSER_COMMAND_TIMEOUT."}function He(e,r){if(!e.args.some((o)=>o.name==="timeout"))return null;const t=r.timeout;if(t===void 0||t===null||t==="")throw new d(`Argument "timeout" must be a positive integer. Received: "${String(t)}"`);const n=Number(t);if(!Number.isInteger(n)||n<=0)throw new d(`Argument "timeout" must be a positive integer. Received: "${String(t)}"`);return n}
1
+ import{getRegistry as ie,fullName as w}from"./registry.js";import{pathToFileURL as se}from"node:url";import*as ae from"node:crypto";import*as M from"node:fs";import*as j from"node:os";import*as N from"node:path";import{executePipeline as K}from"./pipeline/index.js";import{adapterLoadError as ue,ArgumentError as d,CommandExecutionError as A,SessionBusyError as ce,attachTraceReceipt as le,getErrorMessage as k}from"./errors.js";import{shouldUseBrowserSession as fe}from"./capabilityRouting.js";import{getBrowserFactory as me,browserSession as de,runWithTimeout as V,isRemoteExecution as pe,DEFAULT_BROWSER_COMMAND_TIMEOUT as he}from"./runtime.js";import{withBrowserSessionLockIf as ge}from"./browser-session-lock.js";import{resolveProfile as we}from"./browser/profile.js";import{ensureManagedChrome as ye}from"./browser/managed-chrome.js";import{generateFingerprintJs as Se}from"./browser/fingerprint.js";import{CDPBridge as ve}from"./browser/index.js";import{clearDaemonRunContext as be,generateRunId as $e,isUnknownOutcomeError as G,releaseSiteSessionLease as J,setDaemonCommandTimeoutSeconds as Ee,setDaemonRunContext as Q}from"./browser/daemon-client.js";import{emitHook as D}from"./hooks.js";import{setContentBaseDir as Pe}from"./utils.js";import{log as X}from"./logger.js";import{isElectronApp as Ae}from"./electron-apps.js";import{probeCDP as xe,resolveElectronEndpoint as Re}from"./launcher.js";import{ObservationSession as Ce,exportObservationSession as Ie}from"./observation/index.js";import{resolveAdapterSourcePath as Te}from"./adapter-source.js";const v=new Map,_=new Map,Me=`${j.homedir()}/.ppcli/clis/`;function Ne(e){if(e===void 0||e===null||e===""||e==="off")return"off";if(e==="on"||e==="retain-on-failure")return e;throw new d(`--trace must be one of: off, on, retain-on-failure. Received: "${String(e)}"`)}export function coerceAndValidateArgs(e,r){const t={...r};for(const n of e){const o=t[n.name];if(n.required&&(o===void 0||o===null||o===""))throw new d(`Argument "${n.name}" is required.`,n.help??`Provide a value for --${n.name}`);if(o!==void 0&&o!==null){if(n.type==="int"||n.type==="number"){const l=Number(o);if(!Number.isFinite(l))throw new d(`Argument "${n.name}" must be a valid number. Received: "${o}"`);if(n.type==="int"&&!Number.isInteger(l))throw new d(`Argument "${n.name}" must be a valid integer. Received: "${o}"`);t[n.name]=l}else if(n.type==="boolean"||n.type==="bool")if(typeof o==="string"){const l=o.toLowerCase();if(l==="true"||l==="1")t[n.name]=!0;else if(l==="false"||l==="0")t[n.name]=!1;else throw new d(`Argument "${n.name}" must be a boolean (true/false). Received: "${o}"`)}else t[n.name]=Boolean(o);const s=t[n.name];if(n.choices&&n.choices.length>0){if(!n.choices.map(String).includes(String(s)))throw new d(`Argument "${n.name}" must be one of: ${n.choices.join(", ")}. Received: "${s}"`)}}else if(n.default!==void 0)t[n.name]=n.default}return t}async function B(e,r,t,n){const o=e;if(o._lazy&&o._modulePath){const s=o._modulePath;if(s.startsWith(Me)&&v.has(s))try{const m=M.statSync(s),i=_.get(s);if(i!==void 0&&m.mtimeMs!==i)v.delete(s)}catch{}if(!v.has(s)){const m=se(s).href,g=import(_.has(s)?`${m}?t=${Date.now()}`:m).then(()=>{try{_.set(s,M.statSync(s).mtimeMs)}catch{}},(p)=>{v.delete(s);throw ue(`Failed to load adapter module ${s}: ${k(p)}`,"Check that the adapter file exists and has no syntax errors.")});v.set(s,g)}await v.get(s);const f=ie().get(w(e));if(f?.func)return Y(f,r,t,n);if(f?.pipeline)return K(r,f.pipeline,{args:t,debug:n})}if(e.func)return Y(e,r,t,n);if(e.pipeline)return K(r,e.pipeline,{args:t,debug:n});throw new A(`Command ${w(e)} has no func or pipeline`,"This is likely a bug in the adapter definition. Please report this issue.")}function Y(e,r,t,n){if(e.browser===!1)return e.func(t,n);if(!r)throw new A(`Command ${w(e)} requires a browser session but none was provided`);return e.func(r,t,n)}function ke(e){if(e.navigateBefore===!1)return null;if(typeof e.navigateBefore==="string")return e.navigateBefore;return null}function De(e,r){if(!e||!r)return!1;try{const t=new URL(e).hostname;return t===r||t.endsWith(`.${r}`)}catch{return!1}}function _e(e,r){if(!r)return!1;try{const t=new URL(e),n=t.hostname===r||t.hostname.endsWith(`.${r}`),o=t.pathname===""||t.pathname==="/";return n&&o&&t.search===""&&t.hash===""}catch{return!1}}async function Be(e,r,t,n){if(t!=="persistent"||!e.domain)return!0;if(!_e(n,e.domain))return!0;const o=await r.getCurrentUrl?.().catch(()=>null);return!De(o,e.domain)}export async function executeCommand(e,r,t=!1,n={}){let o;try{o=n.prepared?r:prepareCommandArgs(e,r)}catch(i){if(i instanceof d)throw i;throw new d(k(i))}const s=je(e,o);Ee(s);const l=Ne(n.trace),f={command:w(e),args:o,startedAt:Date.now()};await D("onBeforeExecute",f);let m;try{if(fe(e)){const i=Ae(e.site),g=we(n.profile),p=!i&&g.kind==="managed"?g:null;process.env.PPCLI_ACTIVE_PROFILE=p?p.name:"";let P;const x=He(e.defaultWindowMode??"background",n.windowMode);let F,W;if(p){P=await ye(p.name,p.entry,{windowMode:x});const u=p.entry.fingerprint;if(u?.enabled)F=Se(u);const a=p.entry.proxy;if(a?.username)W={username:a.username,password:a.password??""}}else if(i){const u=process.env.OPENCLI_CDP_ENDPOINT;if(u){const a=Number(new URL(u).port);if(!await xe(a))throw new A(`CDP not reachable at ${u}`,"Check that the app is running with --remote-debugging-port and the endpoint is correct.");P=u}else P=await Re(e.site)}const ne=p?ve:me(e.site),L=!p&&g.kind==="extension"?g.contextId:void 0,oe=e,b=We(e,n.siteSession),R=Le(e,b),q=ze(e,b,n.keepTab),y=b==="persistent"&&e.access==="write"?{runId:$e(),session:R}:null;if(y)Q({runId:y.runId,command:w(e),access:"write"});let z,H=!1,C=null;try{m=await ge(x!=="foreground",()=>de(ne,async(u)=>{const a=l==="off"?null:new Ce({scope:{contextId:L??p?.name,session:R,target:u.getActivePage?.(),site:e.site,command:w(e),adapterSourcePath:Te(oe)}});if(a){a.record({stream:"action",name:"command",phase:"start",data:{args:o}});await u.startNetworkCapture?.().catch(()=>!1)}const h=ke(e);if(h&&await Be(e,u,b,h)){a?.record({stream:"action",name:"pre_navigate",phase:"start",data:{url:h}});try{await u.goto(h);a?.record({stream:"action",name:"pre_navigate",phase:"end",data:{url:h}})}catch(c){if(c instanceof ce)throw c;a?.record({stream:"action",name:"pre_navigate",phase:"error",data:{url:h,error:c instanceof Error?c.message:String(c)}});const S=new A(`Pre-navigation to ${h} failed: ${c instanceof Error?c.message:c}`,"Check that the site is reachable and the browser extension is running.");S.cause=c;if(a&&(l==="on"||l==="retain-on-failure")){a.record({stream:"error",message:S.message,stack:S.stack,code:S.code,hint:S.hint});await U(a,u).catch(()=>{});O(a,"failure",S,n.onTraceExport)}throw S}}const E=s!==null?s+ee:he,I=B(e,u,o,t);C=I;let T=!1;I.then(()=>{T=!0},()=>{T=!0});try{const c=await V(I,{timeout:E,label:w(e),hint:re(e,s)});a?.record({stream:"action",name:"command",phase:"end"});if(a&&l==="on"){await U(a,u).catch(()=>{});O(a,"success",void 0,n.onTraceExport)}if(!q)await u.closeWindow?.().catch(()=>{});return c}catch(c){if(!T){H=!0;Ue(c)}if(a){a.record({stream:"action",name:"command",phase:"error",data:{error:c instanceof Error?c.message:String(c)}});a.record({stream:"error",message:c instanceof Error?c.message:String(c),stack:c instanceof Error?c.stack:void 0});if(l==="on"||l==="retain-on-failure"){await U(a,u).catch(()=>{});O(a,"failure",c,n.onTraceExport)}}if(!q)await u.closeWindow?.().catch(()=>{});throw c}},{session:R,cdpEndpoint:P,contextId:L,windowMode:x,surface:"adapter",siteSession:b,initScript:F,proxyAuth:W}),p?.name)}catch(u){z=u;throw u}finally{if(y)if(H&&C){const{runId:u,session:a}=y,h=(E)=>{be(u);if(!G(E))J({runId:u,session:a,surface:"adapter"})};C.then(()=>h(),(E)=>h(E))}else{Q(null);if(!G(z))await J({runId:y.runId,session:y.session,surface:"adapter"})}}}else if(s!==null){const i=s+ee;m=await V(B(e,null,o,t),{timeout:i,label:w(e),hint:re(e,s)})}else m=await B(e,null,o,t)}catch(i){f.error=i;f.finishedAt=Date.now();await D("onAfterExecute",f);throw i}f.finishedAt=Date.now();await D("onAfterExecute",f,m);return m}const Z=new WeakSet;function Ue(e){if(e!==null&&typeof e==="object")Z.add(e)}export function isAdapterStillRunning(e){return e!==null&&typeof e==="object"&&Z.has(e)}async function U(e,r){const t=r.getActivePage?.()??e.scope.target,[n,o,s,l,f]=await Promise.all([r.getCurrentUrl?.().catch(()=>null)??Promise.resolve(null),r.snapshot().catch(()=>{return}),r.readNetworkCapture?.().catch(()=>[])??Promise.resolve([]),r.consoleMessages("all").catch(()=>[]),r.screenshot({format:"png"}).catch(()=>{return})]);if(o!==void 0||n!==void 0)e.record({stream:"state",url:n,target:t,snapshot:o,label:"final"});for(const m of Array.isArray(s)?s:[]){const i=m;e.record({stream:"network",url:String(i.url??""),method:typeof i.method==="string"?i.method:void 0,status:typeof i.responseStatus==="number"?i.responseStatus:void 0,contentType:typeof i.responseContentType==="string"?i.responseContentType:void 0,size:typeof i.responseBodyFullSize==="number"?i.responseBodyFullSize:void 0,requestHeaders:i.requestHeaders,responseHeaders:i.responseHeaders,requestBody:i.requestBodyPreview,responseBody:i.responsePreview,ts:typeof i.timestamp==="number"?i.timestamp:void 0})}for(const m of Array.isArray(l)?l:[])if(m&&typeof m==="object"){const i=m;e.record({stream:"console",level:String(i.type??i.level??"log"),text:String(i.text??i.message??""),ts:typeof i.timestamp==="number"?i.timestamp:void 0})}else e.record({stream:"console",level:"log",text:String(m)});if(typeof f==="string"&&f)e.record({stream:"screenshot",format:"png",data:f,label:"final"})}function O(e,r,t,n){try{const o=Ie(e,{error:t,status:r});if(r==="failure"&&t!==void 0)le(t,o.receipt);else process.stderr.write(`ppcli trace artifact: ${o.dir}
2
+ `);try{n?.(o)}catch(s){X.warn(`[trace] Trace export callback failed: ${s instanceof Error?s.message:String(s)}`)}return o}catch(o){X.warn(`[trace] Failed to export trace artifact: ${o instanceof Error?o.message:String(o)}`);return}}export function resolveFileBackedArg(e,r){const t=e.args.find((f)=>f.fromFile);if(!t)return;const n=typeof t.fromFile==="string"?t.fromFile:"file",o=t.positional?`<${t.name}>`:`--${t.name}`,s=r[n]==null?"":String(r[n]).trim(),l=r[t.name]==null?"":String(r[t.name]);if(s){if(l.trim())throw new d(`${o} 和 --${n} 只能二选一,不能同时给`);const f=N.resolve(s.startsWith("~/")?N.join(j.homedir(),s.slice(2)):s);let m;try{m=M.readFileSync(f,"utf-8")}catch(g){throw new d(`--${n} 指向的文件读不到: ${f}(${k(g)})`)}const i=m.trim();if(!i)throw new d(`--${n} 指向的文件内容为空: ${f}`);r[t.name]=i;Pe(N.dirname(f));return}if(!l.trim()&&t.requiredUnlessFile)throw new d(`缺少 ${t.name}:${o} 或 --${n} 给其一`)}export function prepareCommandArgs(e,r){const t=coerceAndValidateArgs(e.args,r);resolveFileBackedArg(e,t);e.validateArgs?.(t);return t}const ee=30;function Oe(e){if(e===void 0||e===null||e==="")return null;if(e==="ephemeral"||e==="persistent")return e;throw new d(`--site-session must be one of: ephemeral, persistent. Received: "${String(e)}"`)}function Fe(){const e=process.env.PUBLISHPORT_SITE_SESSION;if(e==="persistent"||e==="ephemeral")return e;return null}function We(e,r){return Oe(r)??Fe()??e.siteSession??"persistent"}function Le(e,r){const t=typeof e.sessionKey==="string"&&e.sessionKey.trim()?`:${e.sessionKey.trim()}`:"";if(r==="persistent")return`site:${e.site}${t}`;return`site:${e.site}${t}:${ae.randomUUID()}`}function qe(e,r){if(r===void 0||r==="")return null;if(r==="true")return!0;if(r==="false")return!1;throw new d(`${e} must be one of: true, false. Received: "${String(r)}"`)}function ze(e,r,t){return qe("--keep-tab",t)??(r==="persistent"&&e.siteSession==="persistent")}function te(e,r){if(r===void 0||r==="")return null;if(r==="foreground"||r==="background")return r;throw new d(`${e} must be one of: foreground, background. Received: "${String(r)}"`)}function He(e="background",r){if(pe())return"background";return te("--window",r)??te("OPENCLI_WINDOW",process.env.OPENCLI_WINDOW)??e}function re(e,r){const t=e.access==="write"?"⚠️ 这是写操作,超时不代表没写成功——请求可能已经到达平台。禁止直接重试:"+"先用该平台的列表/统计命令(stats / lists / articles 等)按标题回查,确认没写进去再重发。 ":"";if(r!==null)return`${t}Pass a higher --timeout value (currently ${r}s)`;if(e.args.some((n)=>n.name==="timeout"))return`${t}Try again, or pass a higher --timeout <seconds>`;return t+"This command has no --timeout option. Retry once; if it keeps timing out, "+"the page or browser bridge is likely stuck — have the user reopen the PublishPort client. "+"The global default can be raised with OPENCLI_BROWSER_COMMAND_TIMEOUT."}function je(e,r){if(!e.args.some((o)=>o.name==="timeout"))return null;const t=r.timeout;if(t===void 0||t===null||t==="")throw new d(`Argument "timeout" must be a positive integer. Received: "${String(t)}"`);const n=Number(t);if(!Number.isInteger(n)||n<=0)throw new d(`Argument "timeout" must be a positive integer. Received: "${String(t)}"`);return n}
@@ -7,6 +7,13 @@ export { DEFAULT_BROWSER_COMMAND_TIMEOUT, DEFAULT_BROWSER_CONNECT_TIMEOUT };
7
7
  */
8
8
  export declare function getBrowserFactory(site?: string): new () => IBrowserFactory;
9
9
  export type BrowserWindowMode = 'foreground' | 'background';
10
+ /**
11
+ * [pp-only] 命令由云端 AI 经 PublishPort 中转发起(本机执行器为子进程注入
12
+ * PUBLISHPORT_REMOTE=1)。这类命令一律在后台窗口运行,忽略 `--window foreground`
13
+ * 与 OPENCLI_WINDOW:用户在做自己的事,远程任务不得抢占焦点。只有用户在桌面端
14
+ * 亲自点击的操作(登录等)才可能把浏览器带到前台。
15
+ */
16
+ export declare function isRemoteExecution(): boolean;
10
17
  export type BrowserSurface = 'browser' | 'adapter';
11
18
  /**
12
19
  * Timeout with seconds unit. Used for high-level command timeouts.
@@ -1 +1 @@
1
- import{BrowserBridge as s,CDPBridge as m}from"./browser/index.js";import{TimeoutError as a}from"./errors.js";import{isElectronApp as w}from"./electron-apps.js";import{DEFAULT_BROWSER_COMMAND_TIMEOUT,DEFAULT_BROWSER_CONNECT_TIMEOUT}from"./browser/config.js";export{DEFAULT_BROWSER_COMMAND_TIMEOUT,DEFAULT_BROWSER_CONNECT_TIMEOUT};export function getBrowserFactory(t){if(t&&w(t))return m;return s}export async function runWithTimeout(t,i){const e=i.label??"Operation";return withTimeoutMs(t,i.timeout*1000,()=>new a(e,i.timeout,i.hint))}export function withTimeoutMs(t,i,e="Operation timed out"){const o=typeof e==="string"?()=>Error(e):e;return new Promise((r,c)=>{const u=setTimeout(()=>c(o()),i);t.then((n)=>{clearTimeout(u);r(n)},(n)=>{clearTimeout(u);c(n)})})}export async function browserSession(t,i,e={}){const o=new t;try{const r=await o.connect({timeout:DEFAULT_BROWSER_CONNECT_TIMEOUT,session:e.session,cdpEndpoint:e.cdpEndpoint,contextId:e.contextId,idleTimeout:e.idleTimeout,windowMode:e.windowMode,surface:e.surface,siteSession:e.siteSession,initScript:e.initScript,proxyAuth:e.proxyAuth});return await i(r)}finally{await o.close().catch(()=>{})}}
1
+ import{BrowserBridge as s,CDPBridge as m}from"./browser/index.js";import{TimeoutError as a}from"./errors.js";import{isElectronApp as T}from"./electron-apps.js";import{DEFAULT_BROWSER_COMMAND_TIMEOUT,DEFAULT_BROWSER_CONNECT_TIMEOUT}from"./browser/config.js";export{DEFAULT_BROWSER_COMMAND_TIMEOUT,DEFAULT_BROWSER_CONNECT_TIMEOUT};export function getBrowserFactory(t){if(t&&T(t))return m;return s}export function isRemoteExecution(){return process.env.PUBLISHPORT_REMOTE==="1"}export async function runWithTimeout(t,i){const e=i.label??"Operation";return withTimeoutMs(t,i.timeout*1000,()=>new a(e,i.timeout,i.hint))}export function withTimeoutMs(t,i,e="Operation timed out"){const o=typeof e==="string"?()=>Error(e):e;return new Promise((n,c)=>{const u=setTimeout(()=>c(o()),i);t.then((r)=>{clearTimeout(u);n(r)},(r)=>{clearTimeout(u);c(r)})})}export async function browserSession(t,i,e={}){const o=new t;try{const n=await o.connect({timeout:DEFAULT_BROWSER_CONNECT_TIMEOUT,session:e.session,cdpEndpoint:e.cdpEndpoint,contextId:e.contextId,idleTimeout:e.idleTimeout,windowMode:e.windowMode,surface:e.surface,siteSession:e.siteSession,initScript:e.initScript,proxyAuth:e.proxyAuth});return await i(n)}finally{await o.close().catch(()=>{})}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.0.45",
3
+ "version": "1.0.46",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false