passenger-mode 0.3.0 → 0.3.1
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/lib/cli.js +9 -1
- package/dist/lib/runtime/index.d.ts +1 -0
- package/dist/lib/runtime/index.js +25 -0
- package/dist/lib/runtime/ui.js +4 -1
- package/dist/lib/server/claude-planner.d.ts +2 -0
- package/dist/lib/server/claude-planner.js +6 -2
- package/dist/passenger-mode.js +2 -2
- package/package.json +3 -2
package/dist/lib/cli.js
CHANGED
|
@@ -120,7 +120,15 @@ async function pushCmd(rest) {
|
|
|
120
120
|
console.error(`No manifest at ${file}. Run passenger-mode init (or crawl) first.`);
|
|
121
121
|
process.exit(2);
|
|
122
122
|
}
|
|
123
|
-
|
|
123
|
+
let res;
|
|
124
|
+
try {
|
|
125
|
+
res = await fetch(`${api}/api/v1/manifest`, { method: "POST", headers: { authorization: `Bearer ${secret}`, "content-type": "application/json" }, body });
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
const why = e?.cause?.code ?? e?.cause?.message ?? e?.message ?? "network error";
|
|
129
|
+
console.error(`Couldn't reach ${api} (${why}). Check your connection. Behind a proxy? Run with NODE_USE_ENV_PROXY=1 (Node 22.21+) so Node uses HTTPS_PROXY.`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
124
132
|
const out = await res.json().catch(() => ({}));
|
|
125
133
|
if (!res.ok) {
|
|
126
134
|
console.error(`Upload failed (${res.status}): ${out.error ?? "unknown error"}`);
|
|
@@ -5,6 +5,7 @@ import { RemotePlanner } from "./remote-planner.ts";
|
|
|
5
5
|
import { OverlayUI } from "./ui.ts";
|
|
6
6
|
export { Driver, RoutePlanner, RemotePlanner, OverlayUI };
|
|
7
7
|
export type { Planner, TripLog };
|
|
8
|
+
export type { Manifest, Action, ActionKind, Destination, Route, Selector, Input, Framework } from "../types.ts";
|
|
8
9
|
export declare const HOSTED = "https://passengermode.com";
|
|
9
10
|
export interface MountOptions {
|
|
10
11
|
/**
|
|
@@ -12,12 +12,37 @@ function framed(win) {
|
|
|
12
12
|
return true;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
const MOUNTED = "__passengerModeMounted";
|
|
15
16
|
export async function mount(opts) {
|
|
16
17
|
const win = opts.window ?? window;
|
|
17
18
|
if (framed(win) && !opts.allowInFrame) {
|
|
18
19
|
console.info("Passenger Mode is off inside frames.");
|
|
19
20
|
return null;
|
|
20
21
|
}
|
|
22
|
+
// One switch per page: a script tag plus the React component, or the tag pasted twice, must not
|
|
23
|
+
// put two drivers on the same page. Claimed before any await so concurrent mounts can't race.
|
|
24
|
+
const w = win;
|
|
25
|
+
if (w[MOUNTED]) {
|
|
26
|
+
console.info("Passenger Mode is already on this page; ignoring the second install.");
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
w[MOUNTED] = true;
|
|
30
|
+
try {
|
|
31
|
+
const handle = await mountOnce(opts, win);
|
|
32
|
+
if (!handle)
|
|
33
|
+
w[MOUNTED] = false;
|
|
34
|
+
else {
|
|
35
|
+
const destroy = handle.destroy;
|
|
36
|
+
handle.destroy = () => { destroy(); w[MOUNTED] = false; };
|
|
37
|
+
}
|
|
38
|
+
return handle;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
w[MOUNTED] = false;
|
|
42
|
+
throw e;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function mountOnce(opts, win) {
|
|
21
46
|
const api = (opts.apiBase ?? HOSTED).replace(/\/$/, "");
|
|
22
47
|
const keyed = opts.key ? `key=${encodeURIComponent(opts.key)}` : "";
|
|
23
48
|
// Hosted: the owner's kill switch and the free-tier cap are checked before the switch appears.
|
package/dist/lib/runtime/ui.js
CHANGED
|
@@ -181,7 +181,10 @@ export class OverlayUI {
|
|
|
181
181
|
const label = h(d, "label", { for: "pm-dest" }, "Where to?");
|
|
182
182
|
const title = h(d, "h2");
|
|
183
183
|
title.appendChild(label);
|
|
184
|
-
|
|
184
|
+
// The example comes from this site's own destinations, so it never suggests something the site can't do.
|
|
185
|
+
const example = (this.opts.suggestions ?? [])[0];
|
|
186
|
+
const placeholder = example ? `Say it your way, e.g. ${example.charAt(0).toLowerCase()}${example.slice(1)}` : "Say it your way";
|
|
187
|
+
const input = h(d, "input", { id: "pm-dest", name: "destination", autocomplete: "off", maxlength: "300", placeholder });
|
|
185
188
|
const btn = h(d, "button", { class: "primary", type: "submit" }, "Plan trip");
|
|
186
189
|
form.append(input, btn);
|
|
187
190
|
form.addEventListener("submit", (e) => { e.preventDefault(); if (input.value.trim())
|
|
@@ -41,3 +41,5 @@ export declare function planStep(body: any, opts: PlannerHandlerOptions): Promis
|
|
|
41
41
|
export declare function createPlannerHandler(opts: PlannerHandlerOptions): (req: Request) => Promise<Response>;
|
|
42
42
|
/** The model's answer is only a suggestion: anything outside the visible manifest actions becomes "stuck". */
|
|
43
43
|
export declare function checkStep(out: any, view: PageView): Step;
|
|
44
|
+
/** Visitors see plan steps: drop any "(/card/)" style path the model let slip through. */
|
|
45
|
+
export declare function plainStep(text: string): string;
|
|
@@ -32,7 +32,7 @@ const PLAN_TOOL = {
|
|
|
32
32
|
properties: {
|
|
33
33
|
possible: { type: "boolean" },
|
|
34
34
|
summary: { type: "string", description: "One line, e.g. 'Cancel your Pro plan: 3 steps, no charge'." },
|
|
35
|
-
steps: { type: "array", items: { type: "string" }, description: "2-6 plain steps. If the last step is irreversible, end it with 'You press it.'" },
|
|
35
|
+
steps: { type: "array", items: { type: "string" }, description: "2-6 plain steps in the visitor's words, naming buttons and pages by their labels. Never show URLs, paths or ids. If the last step is irreversible, end it with 'You press it.'" },
|
|
36
36
|
},
|
|
37
37
|
required: ["possible"],
|
|
38
38
|
},
|
|
@@ -123,7 +123,7 @@ export async function planStep(body, opts) {
|
|
|
123
123
|
if (body.op === "plan") {
|
|
124
124
|
const out = await askModel(opts, PLAN_TOOL, describe(view, opts.manifest, true));
|
|
125
125
|
const plan = out?.possible && out.summary
|
|
126
|
-
? { summary: String(out.summary).slice(0, 120), steps: (out.steps ?? []).slice(0, 8).map((s) => String(s).slice(0, 120)) }
|
|
126
|
+
? { summary: String(out.summary).slice(0, 120), steps: (out.steps ?? []).slice(0, 8).map((s) => plainStep(String(s)).slice(0, 120)) }
|
|
127
127
|
: null;
|
|
128
128
|
return { status: 200, body: { plan } };
|
|
129
129
|
}
|
|
@@ -157,3 +157,7 @@ export function checkStep(out, view) {
|
|
|
157
157
|
}
|
|
158
158
|
return { kind: "stuck", reason: out?.kind === "stuck" && out.reason ? String(out.reason).slice(0, 160) : "I don't know this road." };
|
|
159
159
|
}
|
|
160
|
+
/** Visitors see plan steps: drop any "(/card/)" style path the model let slip through. */
|
|
161
|
+
export function plainStep(text) {
|
|
162
|
+
return text.replace(/\s*\((?:https?:\/\/[^)\s]+|\/[^)\s]*)\)/g, "").replace(/\s{2,}/g, " ").trim();
|
|
163
|
+
}
|
package/dist/passenger-mode.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";var PassengerMode=(()=>{var x=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var U=Object.prototype.hasOwnProperty;var V=(r,t,e)=>t in r?x(r,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):r[t]=e;var z=(r,t)=>{for(var e in t)x(r,e,{get:t[e],enumerable:!0})},j=(r,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of q(t))!U.call(r,i)&&i!==e&&x(r,i,{get:()=>t[i],enumerable:!(n=W(t,i))||n.enumerable});return r};var G=r=>j(x({},"__esModule",{value:!0}),r);var h=(r,t,e)=>V(r,typeof t!="symbol"?t+"":t,e);var ut={};z(ut,{Driver:()=>b,HOSTED:()=>N,OverlayUI:()=>y,RemotePlanner:()=>f,RoutePlanner:()=>v,mount:()=>H});var _=[[/\b(pay|payment|purchase|checkout|check ?out|buy|place (your )?order|order now|confirm (order|purchase)|donate|subscribe|upgrade)\b/,"payment"],[/\b(cancel|end|stop)\b.{0,30}\b(plan|subscription|membership|club|trial|order|booking|reservation)\b|\bunsubscribe\b|\bdowngrade\b/,"cancels a plan or order"],[/\b(delete|destroy|erase|wipe|purge)\b|\bremove (account|user|member|card|payment)\b|\b(close|deactivate) (my |your )?account\b/,"deletion"],[/\b(change|update|reset|set)\b.{0,20}\b(password|e-?mail|2fa|passkey|two[- ]factor)\b|\bnew password\b/,"credential change"],[/\b(sign ?out|log ?out)\b/,"ends the session"]],Y=[[/\b(send|invite|share with)\b/,"sends to other people"],[/\b(publish|go live|make public|unpublish)\b/,"publishes"]],A=/[$€£¥₹]\s?\d|\d(?:[.,]\d{2})?\s?(?:usd|eur|gbp|dollars?)\b|\bper (?:month|year|seat)\b|\/\s?(?:mo|month|yr|year)\b/i,K=/\b(send|track|capture|gtag|analytics|log|report|emit|posthog|plausible|umami)\s*\(/gi;function E(r){return r.replace(/([a-z])([A-Z])/g,"$1 $2").replace(/[_\-./]+/g," ").toLowerCase()}function P(r,t=[]){let e=r.filter(Boolean).map(i=>E(i)).join(" | "),n=[e,...t.filter(Boolean).map(i=>E(i.replace(K,"(")))].join(" | ");for(let[i,s]of _)if(i.test(n))return s;for(let[i,s]of Y)if(i.test(e))return s;return r.some(i=>i&&A.test(i))?"payment":null}function D(r,t){let e=t??"";return/(log-?out|sign-?out|logoff|\/delete|\/destroy|\/remove|unsubscribe)/i.test(e)?P([r,e])??"acts on its own":/\b(sign ?out|log ?out)\b/i.test(E(r))?"ends the session":null}var C=['input[autocomplete^="cc-"]','input[name*="card" i]','input[name*="cvc" i]','input[name*="cvv" i]','input[id*="card-number" i]',"[data-stripe]",'iframe[src*="js.stripe.com"]','iframe[name^="__privateStripeFrame"]','iframe[src*="paypal.com"]','iframe[src*="braintree"]','iframe[src*="squareup.com"]','iframe[src*="adyen"]','iframe[title*="payment" i]'].join(","),J=r=>(r.textContent??"").replace(/\s+/g," ").trim();function I(r){let t=r.tagName==="FORM"?r.querySelector('button:not([type="button"]):not([type="reset"]), input[type="submit"]')??r:r,e=[t.getAttribute("aria-label"),t===r&&r.tagName==="FORM"?"":J(t),t.getAttribute("value"),t.getAttribute("title")].filter(Boolean).join(" ").slice(0,300);if(r.tagName==="A")return D(e,r.getAttribute("href"));let i=P([e]);if(i)return i;if(A.test(e))return"payment";let o=(r.tagName==="FORM"?r:r.closest("form"))??null;if(o){if(o.querySelector(C))return"payment";if(o.querySelector('input[type="password"]'))return"uses your password"}else if(r.ownerDocument?.querySelector(C))return"payment";return null}var m=r=>'"'+r.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"',L=r=>(r??"").replace(/\s+/g," ").trim().toLowerCase();function X(r){if(!r.isConnected||r.closest("[hidden],[inert],[aria-hidden='true']"))return!1;let t=r.ownerDocument?.defaultView;for(let e=r;e;e=e.parentElement){let n=t?.getComputedStyle(e);if(n&&(n.display==="none"||n.visibility==="hidden"))return!1}return!r.disabled}var Z={navigate:"a[href]",click:"button, [role='button'], input[type='submit'], input[type='button']",submit:"form"};function $(r,t,e){let n=s=>!!s&&X(s)&&!(e&&e.contains(s)),i=s=>{for(let o of Array.from(t.querySelectorAll(s))){let l=r.kind==="submit"&&o.tagName!=="FORM"?o.closest("form")??o:o;if(n(l))return l}return null};for(let s of r.selectors){let o=null;switch(s.by){case"testid":o=i(`[data-testid=${m(s.value)}],[data-test=${m(s.value)}],[data-cy=${m(s.value)}]`);break;case"id":o=i(`[id=${m(s.value)}]`);break;case"name":o=i(`[name=${m(s.value)}]`);break;case"aria-label":o=i(`[aria-label=${m(s.value)}]`);break;case"href":o=i(`a[href=${m(s.value)}]`);break;case"text":{if(s.value.startsWith("{"))break;let l=L(s.value),a=r.kind==="submit"?Array.from(t.querySelectorAll("form button, form input[type='submit']")).map(c=>({el:c.closest("form"),text:L(c.textContent||c.getAttribute("value"))})):Array.from(t.querySelectorAll(Z[r.kind])).map(c=>({el:c,text:L(c.getAttribute("aria-label")||c.textContent||c.getAttribute("value"))}));o=(a.find(c=>c.text===l&&n(c.el))??a.find(c=>c.text.startsWith(l)&&n(c.el)))?.el??null;break}}if(o)return o}return null}function M(r){return(r.getAttribute("aria-label")||r.textContent||r.getAttribute("value")||"").replace(/\s+/g," ").trim().slice(0,80)}function Q(r){let t=r.split("/").filter(Boolean).map(e=>/^\[\[\.\.\..+\]\]$/.test(e)?"(?:/.*)?":/^\[\.\.\..+\]$/.test(e)?"/.+":/^\[.+\]$/.test(e)||e.startsWith(":")||e.includes(":param")?"/[^/]+":e==="*"?"(?:/.*)?":"/"+e.replace(/[.*+?^${}()|\\]/g,"\\$&"));return new RegExp("^"+(t.join("")||"/?")+"/?$")}function tt(r){return r.split("/").filter(Boolean).reduce((t,e)=>e.startsWith("[[...")||e==="*"?t:e.startsWith("[")||e.startsWith(":")?t+1:t+3,0)}var et=/^(utm_\w+|fbclid|gclid|msclkid|ref|ttclid|mc_\w+)$/i;function S(r){let[t,e=""]=r.split("#")[0].split("?"),n=[...new URLSearchParams(e)].filter(([i])=>!et.test(i)).sort(([i],[s])=>i.localeCompare(s));return(t||"/")+(n.length?"?"+new URLSearchParams(n).toString():"")}function w(r,t){let[e,n=""]=r.split("#")[0].split("?"),i=new URLSearchParams(n),s=null,o=-1;for(let l of t){let[a,d]=l.path.split("?");if(!Q(a).test(e||"/"))continue;let c=d?[...new URLSearchParams(d)]:[];if(!c.every(([g,T])=>i.get(g)===T))continue;let u=tt(a)+c.length*2;u>o&&(s=l.path,o=u)}return s}function R(r,t){if(!r.startsWith("/")&&!r.startsWith("#/"))return null;let e=r.startsWith("#")?r.slice(1):r;return w(e.replace(/:param/g,"x"),t)}var k="passenger-mode:trip",it=300,nt=/\b(all|every|each|everyone|everybody)\b.{0,40}\b(users?|members?|people|contacts?|customers?|items?|posts?|messages?|accounts?|orders?|friends?|followers?)\b|\b\d{2,}\s+times\b|\bmass\b|\bbulk\b/i,rt={payment:"pay buttons","cancels a plan or order":"cancel buttons",deletion:"delete buttons","sends to other people":"send buttons",publishes:"publish buttons","credential change":"password or email changes","ends the session":"sign-out buttons","uses your password":"buttons that use your password"};function st(){let r=globalThis.crypto;return r?.randomUUID?r.randomUUID():"t"+Date.now().toString(36)+Math.random().toString(36).slice(2,10)}var ot={render(){},async moveCursor(){},ring(){},owns:()=>!1},b=class{constructor(t){h(this,"state","off");h(this,"plan",null);h(this,"log",null);h(this,"o");h(this,"ui");h(this,"run",0);h(this,"gateEl",null);h(this,"gateAction",null);h(this,"stepCount",0);h(this,"detach",[]);if(this.o={routeFrom:"path",isTrusted:e=>e.isTrusted,maxSteps:15,timeLimitMs:3*6e4,settleMs:250,...t},this.ui=t.ui??ot,this.o.storage===void 0)try{this.o.storage=t.window.sessionStorage}catch{this.o.storage=null}this.listen(),this.restore()}get doc(){return this.o.window.document}path(){let t=this.o.window.location;return typeof this.o.routeFrom=="function"?this.o.routeFrom():this.o.routeFrom==="hash"?t.hash.replace(/^#/,"")||"/":t.pathname+t.search}set(t,e={}){this.state=t,this.ui.render(t,{plan:this.plan,destination:this.log?.destination,step:this.stepCount,...e})}open(t){return this.o.isTrusted(t)?(this.state==="off"&&this.set("open"),!0):!1}close(){this.finish("ended"),this.plan=null,this.set("off")}async planTrip(t,e){if(!this.o.isTrusted(e))return null;let n=t.trim().slice(0,it);if(!n)return null;if(this.log={id:st(),destination:n,startedAt:Date.now(),steps:[]},this.stepCount=0,nt.test(n))return this.set("stuck",{message:"Passenger Mode does one thing per trip. Try a single destination."}),null;this.set("planning");let i=null,s=this.view();try{i=await this.o.planner.plan(s,this.o.manifest)}catch{return this.set("stuck",{message:"I can't plan trips right now. Try again in a moment."}),null}return i?(this.plan=i,this.set("ready"),i):(this.set("stuck",{message:"I don't know this road. Try saying it another way."}),null)}go(t){return!this.o.isTrusted(t)||this.state!=="ready"?!1:(this.log.startedAt=Date.now(),this.drive(),!0)}notThis(){this.plan=null,this.log=null,this.set("open")}takeWheel(){this.state==="driving"&&(this.run++,this.ui.ring(null),this.set("paused",{message:"Nothing moves until you resume."}))}resume(t){return!this.o.isTrusted(t)||this.state!=="paused"&&this.state!=="handoff"?!1:(this.drive(),!0)}end(){this.finish(this.state==="arrived"?"arrived":this.state==="paused"?"taken-over":"ended"),this.plan=null,this.set("open")}rate(t){this.log&&(this.log.rating=t?"up":"down",this.o.onTrip?.(this.log))}view(){let t=w(this.path(),this.o.manifest.routes),e=[];for(let n of this.o.manifest.actions){if(n.route!==null&&n.route!==t||n.route===null&&n.seenOn&&t!==null&&!n.seenOn.includes(t))continue;let i=this.find(n);if(!i)continue;let s=n.label.startsWith("{");e.push({id:n.id,label:s?M(i):n.label,kind:n.kind,irreversible:n.irreversible,runtimeLabel:s})}return{destination:this.log?.destination??"",route:t,path:this.path(),visible:e,history:(this.log?.steps??[]).map(n=>({actionId:n.actionId,label:n.label,route:n.route})),...this.plan?{approvedPlan:this.plan.steps.slice(0,8)}:{}}}find(t){let e=$(t,this.doc);return e&&!this.ui.owns(e)?e:null}drive(){let t=++this.run;this.set("driving",{caption:"Starting the trip"}),this.loop(t)}live(t){return t===this.run&&this.state==="driving"}async loop(t){await this.settle();let e=!1;for(;this.live(t);){if(this.stepCount>=this.o.maxSteps)return this.stuck("That's more steps than one trip allows, so I stopped.");if(Date.now()-this.log.startedAt>this.o.timeLimitMs)return this.stuck("This trip is taking too long, so I stopped.");let n=this.view(),i;try{i=await this.o.planner.next(n,this.o.manifest)}catch{return this.stuck("I lost the road. Try again in a moment.")}if(!this.live(t))return;if(i.kind==="arrived")return this.arrive(i.summary);if(i.kind==="stuck"){if(!e){e=!0,await this.settle(1500);continue}let u=this.log.steps.at(-1);return n.route===null&&u?.by==="driver"?this.arrive(`This is where \u201C${u.label}\u201D leads. I don't know this page, so take it from here.`):this.stuck(i.reason)}e=!1;let s=this.log.steps.slice(-2);if(s.length===2&&s.every(u=>u.actionId===i.actionId))return this.stuck("I'm going in circles, so I stopped. Try saying it another way.");let o=this.o.manifest.actions.find(u=>u.id===i.actionId);if(!o||!n.visible.some(u=>u.id===o.id))return this.stuck("I don't know this road.");let l=this.find(o);if(!l)return this.stuck("I don't know this road.");if(o.kind==="navigate"&&this.linksHere(l))return this.arrive(i.caption??"You're already here.");let a=o.label.startsWith("{")?M(l):o.label,d=o.irreversible?o.irreversibleReason:I(l),c=d?`Pull over: ${a} is yours to press`:i.caption??`Pressing ${a}`;if(l.scrollIntoView?.({block:"center",behavior:"smooth"}),this.ui.ring(l,d?"gate":"drive"),await this.ui.moveCursor(l,c),!this.live(t))return;if(d)return this.pullOver(l,o,a,d);if(o.kind==="submit"){let u=l.tagName==="FORM"?l:l.closest("form"),g=u?Array.from(u.querySelectorAll("input[required],select[required],textarea[required]")).filter(T=>!T.value):[];if(g.length){this.ui.ring(u,"handoff"),g[0].scrollIntoView?.({block:"center"}),g[0].focus?.({preventScroll:!0}),this.set("handoff",{message:`Your turn: fill in ${g.length===1?"the highlighted field":`the ${g.length} highlighted fields`}, then press Resume.`});return}this.record(o,a,"driver"),this.markNav(),u?.requestSubmit?u.requestSubmit():u?.submit()}else this.record(o,a,"driver"),this.markNav(),l.click();await this.settle(),this.clearNav()}}pullOver(t,e,n,i){this.gateEl=t,this.gateAction={action:e,label:n};let s=t.tagName==="FORM"?t.querySelector('button:not([type="button"]):not([type="reset"]), input[type="submit"]')??t:t;this.ui.ring(s,"gate"),s!==t&&this.ui.moveCursor(s,`Pull over: ${n} is yours to press`),this.set("gate",{caption:`Pull over: ${n} is yours to press`,message:`This one's yours. Passenger Mode never presses ${rt[i]??"buttons like this"}. Press ${n} to finish, or end the trip.`})}onGateClick(t){if(this.state!=="gate"||!this.gateEl||!this.gateAction||!this.o.isTrusted(t))return;let e=t.target;if(!e||!this.gateEl.contains(e))return;let{action:n,label:i}=this.gateAction;this.gateAction=null,this.record(n,i,"passenger"),this.gateEl=null,this.ui.ring(null),this.markNav();let s=++this.run;this.state="driving",this.ui.render("driving",{plan:this.plan,caption:"Checking you arrived",destination:this.log?.destination,step:this.stepCount}),this.settle().then(()=>(this.clearNav(),this.loop(s)))}record(t,e,n){this.stepCount++,this.log?.steps.push({actionId:t.id,label:e,route:w(this.path(),this.o.manifest.routes),by:n,at:Date.now()})}arrive(t){this.ui.ring(null),this.finish("arrived"),this.set("arrived",{message:t})}stuck(t){this.ui.ring(null),this.finish("stuck",t),this.set("stuck",{message:t})}finish(t,e){this.run++,this.gateEl=null,this.clearNav(!0),this.log&&!this.log.outcome&&(this.log.outcome=t,this.log.reason=e,this.log.endedAt=Date.now(),this.o.onTrip?.(this.log))}settle(t){let e=this.o.window,n=t??this.o.settleMs;return new Promise(i=>{let s,o=()=>{d?.disconnect(),clearTimeout(c),i()},l=()=>{clearTimeout(s),s=setTimeout(o,n)},a=e.MutationObserver,d=a?new a(l):null;d?.observe(e.document.documentElement,{subtree:!0,childList:!0,attributes:!0});let c=setTimeout(o,Math.max(2500,n*4));l()})}linksHere(t){let e=t.getAttribute("href");if(!e||e.startsWith("#")||this.o.routeFrom==="hash"||typeof this.o.routeFrom=="function")return!1;try{let n=this.o.window.location,i=new URL(e,n.href);return i.origin===n.origin&&S(i.pathname+i.search)===S(n.pathname+n.search)}catch{return!1}}markNav(){try{this.o.storage?.setItem(k,JSON.stringify({log:this.log,plan:this.plan,stepCount:this.stepCount,at:Date.now(),origin:this.o.window.location.origin}))}catch{}}clearNav(t=!1){if(t||this.state==="driving")try{this.o.storage?.removeItem(k)}catch{}}restore(){let t=null;try{t=JSON.parse(this.o.storage?.getItem(k)??"null")}catch{t=null}if(t){try{this.o.storage?.removeItem(k)}catch{}Date.now()-t.at>15e3||t.origin!==this.o.window.location.origin||!t.log||(this.log=t.log,this.plan=t.plan,this.stepCount=t.stepCount??0,setTimeout(()=>this.drive(),400))}}listen(){let t=this.o.window,e=i=>{this.state!=="driving"||!this.o.isTrusted(i)||this.ui.owns(i.target)||this.takeWheel()};for(let i of["pointerdown","mousedown","keydown","wheel","touchstart"])t.addEventListener(i,e,{capture:!0,passive:!0}),this.detach.push(()=>t.removeEventListener(i,e,{capture:!0}));let n=i=>this.onGateClick(i);t.addEventListener("click",n,{capture:!0}),t.addEventListener("submit",n,{capture:!0}),this.detach.push(()=>{t.removeEventListener("click",n,{capture:!0}),t.removeEventListener("submit",n,{capture:!0})})}destroy(){this.run++,this.detach.forEach(t=>t()),this.detach=[]}};var at=new Set("a an the my me i to of for on in and or please want would like can you how do go get help with this that it our".split(" ")),O=r=>r.toLowerCase().replace(/[{}]/g," ").split(/[^a-z0-9]+/).filter(t=>t.length>1&&!at.has(t)).map(t=>t.replace(/(ing|es|s)$/,""));function F(r,t){let e=new Set(O(t));return O(r).filter(n=>e.has(n)).length}var v=class r{constructor(){h(this,"target",new Map)}pickTarget(t,e){let n=this.target.get(t);if(n)return n;let i=null,s=(l,a)=>{a>0&&(!i||a>i.score)&&(i={id:l,score:a})};for(let l of e.destinations){let a=F(t,l.goal);if(a!==0)for(let d of l.actionIds){let c=e.actions.find(u=>u.id===d);c&&s(d,a*2+F(t,c.label)+(c.kind!=="navigate"?.5:0))}}for(let l of e.actions){let a=F(t,l.label);a>0&&s(l.id,a+(l.kind!=="navigate"?.25:0))}let o=i;return o&&this.target.set(t,o.id),o?o.id:null}static on(t,e){return t.route!==null?t.route===e:!t.seenOn||e!==null&&t.seenOn.includes(e)}path(t,e,n){if(r.on(e,t))return[];let i=[{route:t,via:[]}],s=new Set([t]);for(;i.length;){let{route:o,via:l}=i.shift();for(let a of n.actions){if(a.kind!=="navigate"||!a.href||!r.on(a,o))continue;let d=R(a.href,n.routes);if(!d||s.has(d))continue;let c=[...l,a.id];if(r.on(e,d))return c;s.add(d),i.push({route:d,via:c})}}return null}async plan(t,e){let n=this.pickTarget(t.destination,e);if(!n)return null;let i=e.actions.find(a=>a.id===n),s=this.path(t.route,i,e);if(s===null)return null;let o=s.map(a=>`Open ${e.actions.find(c=>c.id===a).label}`);o.push(i.irreversible?`Stop at ${i.label}. You press it.`:`${i.kind==="submit"?"Submit":"Press"} ${i.label}`);let l=o.length;return{summary:`${i.label}: ${l} step${l===1?"":"s"}`,steps:o}}async next(t,e){let n=this.pickTarget(t.destination,e);if(!n)return{kind:"stuck",reason:"I don't know this road."};let i=e.actions.find(a=>a.id===n);if(t.history.some(a=>a.actionId===n))return{kind:"arrived",summary:`Done: ${i.label}.`};if(t.visible.some(a=>a.id===n))return{kind:"act",actionId:n,caption:i.irreversible?`Pull over: ${i.label} is yours to press`:`${i.kind==="submit"?"Submitting":"Pressing"} ${i.label}`};let o=this.path(t.route,i,e)?.[0];if(!o||!t.visible.some(a=>a.id===o))return{kind:"stuck",reason:"I don't know this road."};let l=e.actions.find(a=>a.id===o);return{kind:"act",actionId:o,caption:`Opening ${l.label}`}}};var f=class{constructor(t,e=(...i)=>fetch(...i),n={}){h(this,"endpoint",t);h(this,"fetchImpl",e);h(this,"headers",n)}async call(t,e){let n=await this.fetchImpl(this.endpoint,{method:"POST",headers:{"content-type":"application/json",...this.headers},body:JSON.stringify({op:t,view:e}),credentials:"same-origin"});if(!n.ok)throw new Error(`planner ${n.status}`);return await n.json()}async plan(t,e){return(await this.call("plan",t)).plan}async next(t,e){return(await this.call("next",t)).step}};var lt=`
|
|
1
|
+
"use strict";var PassengerMode=(()=>{var k=Object.defineProperty;var U=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var V=Object.prototype.hasOwnProperty;var z=(r,t,e)=>t in r?k(r,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):r[t]=e;var j=(r,t)=>{for(var e in t)k(r,e,{get:t[e],enumerable:!0})},_=(r,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of q(t))!V.call(r,i)&&i!==e&&k(r,i,{get:()=>t[i],enumerable:!(n=U(t,i))||n.enumerable});return r};var G=r=>_(k({},"__esModule",{value:!0}),r);var h=(r,t,e)=>z(r,typeof t!="symbol"?t+"":t,e);var ht={};j(ht,{Driver:()=>v,HOSTED:()=>H,OverlayUI:()=>w,RemotePlanner:()=>m,RoutePlanner:()=>y,mount:()=>W});var K=[[/\b(pay|payment|purchase|checkout|check ?out|buy|place (your )?order|order now|confirm (order|purchase)|donate|subscribe|upgrade)\b/,"payment"],[/\b(cancel|end|stop)\b.{0,30}\b(plan|subscription|membership|club|trial|order|booking|reservation)\b|\bunsubscribe\b|\bdowngrade\b/,"cancels a plan or order"],[/\b(delete|destroy|erase|wipe|purge)\b|\bremove (account|user|member|card|payment)\b|\b(close|deactivate) (my |your )?account\b/,"deletion"],[/\b(change|update|reset|set)\b.{0,20}\b(password|e-?mail|2fa|passkey|two[- ]factor)\b|\bnew password\b/,"credential change"],[/\b(sign ?out|log ?out)\b/,"ends the session"]],Y=[[/\b(send|invite|share with)\b/,"sends to other people"],[/\b(publish|go live|make public|unpublish)\b/,"publishes"]],P=/[$€£¥₹]\s?\d|\d(?:[.,]\d{2})?\s?(?:usd|eur|gbp|dollars?)\b|\bper (?:month|year|seat)\b|\/\s?(?:mo|month|yr|year)\b/i,J=/\b(send|track|capture|gtag|analytics|log|report|emit|posthog|plausible|umami)\s*\(/gi;function A(r){return r.replace(/([a-z])([A-Z])/g,"$1 $2").replace(/[_\-./]+/g," ").toLowerCase()}function M(r,t=[]){let e=r.filter(Boolean).map(i=>A(i)).join(" | "),n=[e,...t.filter(Boolean).map(i=>A(i.replace(J,"(")))].join(" | ");for(let[i,s]of K)if(i.test(n))return s;for(let[i,s]of Y)if(i.test(e))return s;return r.some(i=>i&&P.test(i))?"payment":null}function C(r,t){let e=t??"";return/(log-?out|sign-?out|logoff|\/delete|\/destroy|\/remove|unsubscribe)/i.test(e)?M([r,e])??"acts on its own":/\b(sign ?out|log ?out)\b/i.test(A(r))?"ends the session":null}var I=['input[autocomplete^="cc-"]','input[name*="card" i]','input[name*="cvc" i]','input[name*="cvv" i]','input[id*="card-number" i]',"[data-stripe]",'iframe[src*="js.stripe.com"]','iframe[name^="__privateStripeFrame"]','iframe[src*="paypal.com"]','iframe[src*="braintree"]','iframe[src*="squareup.com"]','iframe[src*="adyen"]','iframe[title*="payment" i]'].join(","),X=r=>(r.textContent??"").replace(/\s+/g," ").trim();function $(r){let t=r.tagName==="FORM"?r.querySelector('button:not([type="button"]):not([type="reset"]), input[type="submit"]')??r:r,e=[t.getAttribute("aria-label"),t===r&&r.tagName==="FORM"?"":X(t),t.getAttribute("value"),t.getAttribute("title")].filter(Boolean).join(" ").slice(0,300);if(r.tagName==="A")return C(e,r.getAttribute("href"));let i=M([e]);if(i)return i;if(P.test(e))return"payment";let o=(r.tagName==="FORM"?r:r.closest("form"))??null;if(o){if(o.querySelector(I))return"payment";if(o.querySelector('input[type="password"]'))return"uses your password"}else if(r.ownerDocument?.querySelector(I))return"payment";return null}var f=r=>'"'+r.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"',L=r=>(r??"").replace(/\s+/g," ").trim().toLowerCase();function Z(r){if(!r.isConnected||r.closest("[hidden],[inert],[aria-hidden='true']"))return!1;let t=r.ownerDocument?.defaultView;for(let e=r;e;e=e.parentElement){let n=t?.getComputedStyle(e);if(n&&(n.display==="none"||n.visibility==="hidden"))return!1}return!r.disabled}var Q={navigate:"a[href]",click:"button, [role='button'], input[type='submit'], input[type='button']",submit:"form"};function R(r,t,e){let n=s=>!!s&&Z(s)&&!(e&&e.contains(s)),i=s=>{for(let o of Array.from(t.querySelectorAll(s))){let a=r.kind==="submit"&&o.tagName!=="FORM"?o.closest("form")??o:o;if(n(a))return a}return null};for(let s of r.selectors){let o=null;switch(s.by){case"testid":o=i(`[data-testid=${f(s.value)}],[data-test=${f(s.value)}],[data-cy=${f(s.value)}]`);break;case"id":o=i(`[id=${f(s.value)}]`);break;case"name":o=i(`[name=${f(s.value)}]`);break;case"aria-label":o=i(`[aria-label=${f(s.value)}]`);break;case"href":o=i(`a[href=${f(s.value)}]`);break;case"text":{if(s.value.startsWith("{"))break;let a=L(s.value),l=r.kind==="submit"?Array.from(t.querySelectorAll("form button, form input[type='submit']")).map(d=>({el:d.closest("form"),text:L(d.textContent||d.getAttribute("value"))})):Array.from(t.querySelectorAll(Q[r.kind])).map(d=>({el:d,text:L(d.getAttribute("aria-label")||d.textContent||d.getAttribute("value"))}));o=(l.find(d=>d.text===a&&n(d.el))??l.find(d=>d.text.startsWith(a)&&n(d.el)))?.el??null;break}}if(o)return o}return null}function S(r){return(r.getAttribute("aria-label")||r.textContent||r.getAttribute("value")||"").replace(/\s+/g," ").trim().slice(0,80)}function tt(r){let t=r.split("/").filter(Boolean).map(e=>/^\[\[\.\.\..+\]\]$/.test(e)?"(?:/.*)?":/^\[\.\.\..+\]$/.test(e)?"/.+":/^\[.+\]$/.test(e)||e.startsWith(":")||e.includes(":param")?"/[^/]+":e==="*"?"(?:/.*)?":"/"+e.replace(/[.*+?^${}()|\\]/g,"\\$&"));return new RegExp("^"+(t.join("")||"/?")+"/?$")}function et(r){return r.split("/").filter(Boolean).reduce((t,e)=>e.startsWith("[[...")||e==="*"?t:e.startsWith("[")||e.startsWith(":")?t+1:t+3,0)}var nt=/^(utm_\w+|fbclid|gclid|msclkid|ref|ttclid|mc_\w+)$/i;function F(r){let[t,e=""]=r.split("#")[0].split("?"),n=[...new URLSearchParams(e)].filter(([i])=>!nt.test(i)).sort(([i],[s])=>i.localeCompare(s));return(t||"/")+(n.length?"?"+new URLSearchParams(n).toString():"")}function T(r,t){let[e,n=""]=r.split("#")[0].split("?"),i=new URLSearchParams(n),s=null,o=-1;for(let a of t){let[l,c]=a.path.split("?");if(!tt(l).test(e||"/"))continue;let d=c?[...new URLSearchParams(c)]:[];if(!d.every(([g,b])=>i.get(g)===b))continue;let u=et(l)+d.length*2;u>o&&(s=a.path,o=u)}return s}function O(r,t){if(!r.startsWith("/")&&!r.startsWith("#/"))return null;let e=r.startsWith("#")?r.slice(1):r;return T(e.replace(/:param/g,"x"),t)}var E="passenger-mode:trip",it=300,rt=/\b(all|every|each|everyone|everybody)\b.{0,40}\b(users?|members?|people|contacts?|customers?|items?|posts?|messages?|accounts?|orders?|friends?|followers?)\b|\b\d{2,}\s+times\b|\bmass\b|\bbulk\b/i,st={payment:"pay buttons","cancels a plan or order":"cancel buttons",deletion:"delete buttons","sends to other people":"send buttons",publishes:"publish buttons","credential change":"password or email changes","ends the session":"sign-out buttons","uses your password":"buttons that use your password"};function ot(){let r=globalThis.crypto;return r?.randomUUID?r.randomUUID():"t"+Date.now().toString(36)+Math.random().toString(36).slice(2,10)}var at={render(){},async moveCursor(){},ring(){},owns:()=>!1},v=class{constructor(t){h(this,"state","off");h(this,"plan",null);h(this,"log",null);h(this,"o");h(this,"ui");h(this,"run",0);h(this,"gateEl",null);h(this,"gateAction",null);h(this,"stepCount",0);h(this,"detach",[]);if(this.o={routeFrom:"path",isTrusted:e=>e.isTrusted,maxSteps:15,timeLimitMs:3*6e4,settleMs:250,...t},this.ui=t.ui??at,this.o.storage===void 0)try{this.o.storage=t.window.sessionStorage}catch{this.o.storage=null}this.listen(),this.restore()}get doc(){return this.o.window.document}path(){let t=this.o.window.location;return typeof this.o.routeFrom=="function"?this.o.routeFrom():this.o.routeFrom==="hash"?t.hash.replace(/^#/,"")||"/":t.pathname+t.search}set(t,e={}){this.state=t,this.ui.render(t,{plan:this.plan,destination:this.log?.destination,step:this.stepCount,...e})}open(t){return this.o.isTrusted(t)?(this.state==="off"&&this.set("open"),!0):!1}close(){this.finish("ended"),this.plan=null,this.set("off")}async planTrip(t,e){if(!this.o.isTrusted(e))return null;let n=t.trim().slice(0,it);if(!n)return null;if(this.log={id:ot(),destination:n,startedAt:Date.now(),steps:[]},this.stepCount=0,rt.test(n))return this.set("stuck",{message:"Passenger Mode does one thing per trip. Try a single destination."}),null;this.set("planning");let i=null,s=this.view();try{i=await this.o.planner.plan(s,this.o.manifest)}catch{return this.set("stuck",{message:"I can't plan trips right now. Try again in a moment."}),null}return i?(this.plan=i,this.set("ready"),i):(this.set("stuck",{message:"I don't know this road. Try saying it another way."}),null)}go(t){return!this.o.isTrusted(t)||this.state!=="ready"?!1:(this.log.startedAt=Date.now(),this.drive(),!0)}notThis(){this.plan=null,this.log=null,this.set("open")}takeWheel(){this.state==="driving"&&(this.run++,this.ui.ring(null),this.set("paused",{message:"Nothing moves until you resume."}))}resume(t){return!this.o.isTrusted(t)||this.state!=="paused"&&this.state!=="handoff"?!1:(this.drive(),!0)}end(){this.finish(this.state==="arrived"?"arrived":this.state==="paused"?"taken-over":"ended"),this.plan=null,this.set("open")}rate(t){this.log&&(this.log.rating=t?"up":"down",this.o.onTrip?.(this.log))}view(){let t=T(this.path(),this.o.manifest.routes),e=[];for(let n of this.o.manifest.actions){if(n.route!==null&&n.route!==t||n.route===null&&n.seenOn&&t!==null&&!n.seenOn.includes(t))continue;let i=this.find(n);if(!i)continue;let s=n.label.startsWith("{");e.push({id:n.id,label:s?S(i):n.label,kind:n.kind,irreversible:n.irreversible,runtimeLabel:s})}return{destination:this.log?.destination??"",route:t,path:this.path(),visible:e,history:(this.log?.steps??[]).map(n=>({actionId:n.actionId,label:n.label,route:n.route})),...this.plan?{approvedPlan:this.plan.steps.slice(0,8)}:{}}}find(t){let e=R(t,this.doc);return e&&!this.ui.owns(e)?e:null}drive(){let t=++this.run;this.set("driving",{caption:"Starting the trip"}),this.loop(t)}live(t){return t===this.run&&this.state==="driving"}async loop(t){await this.settle();let e=!1;for(;this.live(t);){if(this.stepCount>=this.o.maxSteps)return this.stuck("That's more steps than one trip allows, so I stopped.");if(Date.now()-this.log.startedAt>this.o.timeLimitMs)return this.stuck("This trip is taking too long, so I stopped.");let n=this.view(),i;try{i=await this.o.planner.next(n,this.o.manifest)}catch{return this.stuck("I lost the road. Try again in a moment.")}if(!this.live(t))return;if(i.kind==="arrived")return this.arrive(i.summary);if(i.kind==="stuck"){if(!e){e=!0,await this.settle(1500);continue}let u=this.log.steps.at(-1);return n.route===null&&u?.by==="driver"?this.arrive(`This is where \u201C${u.label}\u201D leads. I don't know this page, so take it from here.`):this.stuck(i.reason)}e=!1;let s=this.log.steps.slice(-2);if(s.length===2&&s.every(u=>u.actionId===i.actionId))return this.stuck("I'm going in circles, so I stopped. Try saying it another way.");let o=this.o.manifest.actions.find(u=>u.id===i.actionId);if(!o||!n.visible.some(u=>u.id===o.id))return this.stuck("I don't know this road.");let a=this.find(o);if(!a)return this.stuck("I don't know this road.");if(o.kind==="navigate"&&this.linksHere(a))return this.arrive(i.caption??"You're already here.");let l=o.label.startsWith("{")?S(a):o.label,c=o.irreversible?o.irreversibleReason:$(a),d=c?`Pull over: ${l} is yours to press`:i.caption??`Pressing ${l}`;if(a.scrollIntoView?.({block:"center",behavior:"smooth"}),this.ui.ring(a,c?"gate":"drive"),await this.ui.moveCursor(a,d),!this.live(t))return;if(c)return this.pullOver(a,o,l,c);if(o.kind==="submit"){let u=a.tagName==="FORM"?a:a.closest("form"),g=u?Array.from(u.querySelectorAll("input[required],select[required],textarea[required]")).filter(b=>!b.value):[];if(g.length){this.ui.ring(u,"handoff"),g[0].scrollIntoView?.({block:"center"}),g[0].focus?.({preventScroll:!0}),this.set("handoff",{message:`Your turn: fill in ${g.length===1?"the highlighted field":`the ${g.length} highlighted fields`}, then press Resume.`});return}this.record(o,l,"driver"),this.markNav(),u?.requestSubmit?u.requestSubmit():u?.submit()}else this.record(o,l,"driver"),this.markNav(),a.click();await this.settle(),this.clearNav()}}pullOver(t,e,n,i){this.gateEl=t,this.gateAction={action:e,label:n};let s=t.tagName==="FORM"?t.querySelector('button:not([type="button"]):not([type="reset"]), input[type="submit"]')??t:t;this.ui.ring(s,"gate"),s!==t&&this.ui.moveCursor(s,`Pull over: ${n} is yours to press`),this.set("gate",{caption:`Pull over: ${n} is yours to press`,message:`This one's yours. Passenger Mode never presses ${st[i]??"buttons like this"}. Press ${n} to finish, or end the trip.`})}onGateClick(t){if(this.state!=="gate"||!this.gateEl||!this.gateAction||!this.o.isTrusted(t))return;let e=t.target;if(!e||!this.gateEl.contains(e))return;let{action:n,label:i}=this.gateAction;this.gateAction=null,this.record(n,i,"passenger"),this.gateEl=null,this.ui.ring(null),this.markNav();let s=++this.run;this.state="driving",this.ui.render("driving",{plan:this.plan,caption:"Checking you arrived",destination:this.log?.destination,step:this.stepCount}),this.settle().then(()=>(this.clearNav(),this.loop(s)))}record(t,e,n){this.stepCount++,this.log?.steps.push({actionId:t.id,label:e,route:T(this.path(),this.o.manifest.routes),by:n,at:Date.now()})}arrive(t){this.ui.ring(null),this.finish("arrived"),this.set("arrived",{message:t})}stuck(t){this.ui.ring(null),this.finish("stuck",t),this.set("stuck",{message:t})}finish(t,e){this.run++,this.gateEl=null,this.clearNav(!0),this.log&&!this.log.outcome&&(this.log.outcome=t,this.log.reason=e,this.log.endedAt=Date.now(),this.o.onTrip?.(this.log))}settle(t){let e=this.o.window,n=t??this.o.settleMs;return new Promise(i=>{let s,o=()=>{c?.disconnect(),clearTimeout(d),i()},a=()=>{clearTimeout(s),s=setTimeout(o,n)},l=e.MutationObserver,c=l?new l(a):null;c?.observe(e.document.documentElement,{subtree:!0,childList:!0,attributes:!0});let d=setTimeout(o,Math.max(2500,n*4));a()})}linksHere(t){let e=t.getAttribute("href");if(!e||e.startsWith("#")||this.o.routeFrom==="hash"||typeof this.o.routeFrom=="function")return!1;try{let n=this.o.window.location,i=new URL(e,n.href);return i.origin===n.origin&&F(i.pathname+i.search)===F(n.pathname+n.search)}catch{return!1}}markNav(){try{this.o.storage?.setItem(E,JSON.stringify({log:this.log,plan:this.plan,stepCount:this.stepCount,at:Date.now(),origin:this.o.window.location.origin}))}catch{}}clearNav(t=!1){if(t||this.state==="driving")try{this.o.storage?.removeItem(E)}catch{}}restore(){let t=null;try{t=JSON.parse(this.o.storage?.getItem(E)??"null")}catch{t=null}if(t){try{this.o.storage?.removeItem(E)}catch{}Date.now()-t.at>15e3||t.origin!==this.o.window.location.origin||!t.log||(this.log=t.log,this.plan=t.plan,this.stepCount=t.stepCount??0,setTimeout(()=>this.drive(),400))}}listen(){let t=this.o.window,e=i=>{this.state!=="driving"||!this.o.isTrusted(i)||this.ui.owns(i.target)||this.takeWheel()};for(let i of["pointerdown","mousedown","keydown","wheel","touchstart"])t.addEventListener(i,e,{capture:!0,passive:!0}),this.detach.push(()=>t.removeEventListener(i,e,{capture:!0}));let n=i=>this.onGateClick(i);t.addEventListener("click",n,{capture:!0}),t.addEventListener("submit",n,{capture:!0}),this.detach.push(()=>{t.removeEventListener("click",n,{capture:!0}),t.removeEventListener("submit",n,{capture:!0})})}destroy(){this.run++,this.detach.forEach(t=>t()),this.detach=[]}};var lt=new Set("a an the my me i to of for on in and or please want would like can you how do go get help with this that it our".split(" ")),B=r=>r.toLowerCase().replace(/[{}]/g," ").split(/[^a-z0-9]+/).filter(t=>t.length>1&&!lt.has(t)).map(t=>t.replace(/(ing|es|s)$/,""));function D(r,t){let e=new Set(B(t));return B(r).filter(n=>e.has(n)).length}var y=class r{constructor(){h(this,"target",new Map)}pickTarget(t,e){let n=this.target.get(t);if(n)return n;let i=null,s=(a,l)=>{l>0&&(!i||l>i.score)&&(i={id:a,score:l})};for(let a of e.destinations){let l=D(t,a.goal);if(l!==0)for(let c of a.actionIds){let d=e.actions.find(u=>u.id===c);d&&s(c,l*2+D(t,d.label)+(d.kind!=="navigate"?.5:0))}}for(let a of e.actions){let l=D(t,a.label);l>0&&s(a.id,l+(a.kind!=="navigate"?.25:0))}let o=i;return o&&this.target.set(t,o.id),o?o.id:null}static on(t,e){return t.route!==null?t.route===e:!t.seenOn||e!==null&&t.seenOn.includes(e)}path(t,e,n){if(r.on(e,t))return[];let i=[{route:t,via:[]}],s=new Set([t]);for(;i.length;){let{route:o,via:a}=i.shift();for(let l of n.actions){if(l.kind!=="navigate"||!l.href||!r.on(l,o))continue;let c=O(l.href,n.routes);if(!c||s.has(c))continue;let d=[...a,l.id];if(r.on(e,c))return d;s.add(c),i.push({route:c,via:d})}}return null}async plan(t,e){let n=this.pickTarget(t.destination,e);if(!n)return null;let i=e.actions.find(l=>l.id===n),s=this.path(t.route,i,e);if(s===null)return null;let o=s.map(l=>`Open ${e.actions.find(d=>d.id===l).label}`);o.push(i.irreversible?`Stop at ${i.label}. You press it.`:`${i.kind==="submit"?"Submit":"Press"} ${i.label}`);let a=o.length;return{summary:`${i.label}: ${a} step${a===1?"":"s"}`,steps:o}}async next(t,e){let n=this.pickTarget(t.destination,e);if(!n)return{kind:"stuck",reason:"I don't know this road."};let i=e.actions.find(l=>l.id===n);if(t.history.some(l=>l.actionId===n))return{kind:"arrived",summary:`Done: ${i.label}.`};if(t.visible.some(l=>l.id===n))return{kind:"act",actionId:n,caption:i.irreversible?`Pull over: ${i.label} is yours to press`:`${i.kind==="submit"?"Submitting":"Pressing"} ${i.label}`};let o=this.path(t.route,i,e)?.[0];if(!o||!t.visible.some(l=>l.id===o))return{kind:"stuck",reason:"I don't know this road."};let a=e.actions.find(l=>l.id===o);return{kind:"act",actionId:o,caption:`Opening ${a.label}`}}};var m=class{constructor(t,e=(...i)=>fetch(...i),n={}){h(this,"endpoint",t);h(this,"fetchImpl",e);h(this,"headers",n)}async call(t,e){let n=await this.fetchImpl(this.endpoint,{method:"POST",headers:{"content-type":"application/json",...this.headers},body:JSON.stringify({op:t,view:e}),credentials:"same-origin"});if(!n.ok)throw new Error(`planner ${n.status}`);return await n.json()}async plan(t,e){return(await this.call("plan",t)).plan}async next(t,e){return(await this.call("next",t)).step}};var pt=`
|
|
2
2
|
:host{all:initial}
|
|
3
3
|
*{box-sizing:border-box;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
4
4
|
.layer{position:fixed;inset:0;pointer-events:none;z-index:2147483646}
|
|
@@ -49,4 +49,4 @@ ol li{display:flex;gap:10px;align-items:center}
|
|
|
49
49
|
[hidden]{display:none!important}
|
|
50
50
|
@media (prefers-reduced-motion:reduce){.cursor,.ring,.knob,.track{transition:none}.ring[data-tone="gate"]{animation:none}}
|
|
51
51
|
@media (max-width:600px){.cursor svg{display:none}.panel{left:12px;right:12px;width:auto}.bar{left:12px;right:12px;transform:none;max-width:none}}
|
|
52
|
-
`,
|
|
52
|
+
`,ct='<svg width="30" height="34" viewBox="0 0 30 34" aria-hidden="true"><path d="M3 2 L26 19 L15 21 L10 32 Z" fill="#2D5BFF" fill-opacity=".6" stroke="#fff" stroke-width="2" stroke-linejoin="round"/></svg>',N='<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="#111418" stroke-width="1.6" stroke-linejoin="round" aria-hidden="true"><path d="M6 9v8H3V9zM6 9l3.5-6.5c1.2 0 2 .9 1.8 2.1L10.8 8H16a1.5 1.5 0 0 1 1.5 1.8l-1.2 6A1.5 1.5 0 0 1 14.8 17H6"/></svg>';function p(r,t,e={},n){let i=r.createElement(t);for(let[s,o]of Object.entries(e))i.setAttribute(s,o);return n!==void 0&&(i.textContent=n),i}var w=class{constructor(t,e,n={}){h(this,"on",e);h(this,"opts",n);h(this,"host");h(this,"root");h(this,"layer");h(this,"switchBtn");h(this,"panel");h(this,"bar");h(this,"cursor");h(this,"cap");h(this,"ringEl");h(this,"ringTarget",null);h(this,"raf",0);h(this,"doc");h(this,"win");h(this,"destination","");this.win=t,this.doc=t.document;let i=this.doc;this.host=p(i,"div",{id:"passenger-mode","data-passenger-mode":""}),this.root=this.host.attachShadow({mode:"open"});let s=p(i,"style");s.textContent=pt,this.layer=p(i,"div",{class:"layer"}),n.offset?.right!=null&&this.layer.style.setProperty("--pm-right",`${n.offset.right}px`),n.offset?.bottom!=null&&this.layer.style.setProperty("--pm-bottom",`${n.offset.bottom}px`),this.ringEl=p(i,"div",{class:"ring",hidden:""}),this.cursor=p(i,"div",{class:"cursor",hidden:"","aria-hidden":"true"}),this.cursor.innerHTML=ct,this.cap=p(i,"span",{class:"cap"}),this.cursor.appendChild(this.cap),this.switchBtn=p(i,"button",{class:"switch auto",type:"button",role:"switch","aria-checked":"false"}),this.switchBtn.innerHTML=`<span class="t"><b>Passenger mode</b><span>Say where you're going. We'll do the clicking.</span></span><span class="track"><span class="knob"></span></span>`,this.switchBtn.addEventListener("click",o=>this.switchBtn.getAttribute("aria-checked")==="true"?this.on.close():this.on.open(o)),this.panel=p(i,"div",{class:"panel auto",role:"dialog","aria-label":"Passenger mode",hidden:""}),this.bar=p(i,"div",{class:"bar auto",role:"status","aria-live":"polite",hidden:""}),this.layer.append(this.ringEl,this.cursor,this.panel,this.bar,this.switchBtn),n.badge&&this.layer.appendChild(p(i,"span",{class:"badge"},"by Passenger Mode")),this.root.append(s,this.layer),i.body.appendChild(this.host)}owns(t){return t?t===this.host||this.host.contains(t)?!0:t.getRootNode?.()===this.root:!1}destroy(){this.win.cancelAnimationFrame?.(this.raf),this.host.remove()}render(t,e){let n=this.doc;e.destination&&(this.destination=e.destination),this.switchBtn.setAttribute("aria-checked",t==="off"?"false":"true");let i=t==="driving"||t==="paused"||t==="gate"||t==="handoff";if(this.cursor.hidden=!i||t==="handoff",i||this.ring(null),this.panel.replaceChildren(),this.panel.hidden=!(t==="open"||t==="planning"||t==="ready"||t==="stuck"||t==="arrived"),t==="open"&&this.renderOpen(),t==="planning"&&this.panel.append(p(n,"h2",{},"Planning the route\u2026"),p(n,"p",{class:"small"},this.destination)),t==="ready"&&e.plan&&this.renderPlan(e.plan),t==="stuck"){let s=p(n,"button",{class:"primary",type:"button"},"Try another destination");s.addEventListener("click",()=>this.on.notThis()),this.panel.append(p(n,"h2",{},"Pulled over"),p(n,"p",{},e.message??"I don't know this road."),s)}if(t==="arrived"&&this.renderArrived(e.message??""),this.bar.replaceChildren(),this.bar.hidden=!i,i){this.bar.dataset.tone=t==="driving"?"drive":t;let s=t==="driving"?"Passenger mode is driving":t==="paused"?"You have the wheel":t==="gate"?"Pulled over":"Your turn";this.bar.append(p(n,"span",{class:"dot"}),p(n,"b",{},s));let o=t==="driving"?e.caption:e.message;if(o&&this.bar.append(p(n,"span",{class:"msg"},o)),t==="driving"){let l=p(n,"button",{type:"button"},"Take the wheel");l.addEventListener("click",()=>this.on.takeWheel()),this.bar.append(l)}if(t==="paused"||t==="handoff"){let l=p(n,"button",{type:"button",class:"resume"},"Resume");l.addEventListener("click",c=>this.on.resume(c)),this.bar.append(l)}let a=p(n,"button",{type:"button"},"End trip");a.addEventListener("click",()=>this.on.end()),this.bar.append(a)}t==="driving"&&e.caption&&(this.cap.textContent=e.caption)}renderOpen(){let t=this.doc,e=p(t,"form",{class:"row"}),n=p(t,"label",{for:"pm-dest"},"Where to?"),i=p(t,"h2");i.appendChild(n);let s=(this.opts.suggestions??[])[0],o=s?`Say it your way, e.g. ${s.charAt(0).toLowerCase()}${s.slice(1)}`:"Say it your way",a=p(t,"input",{id:"pm-dest",name:"destination",autocomplete:"off",maxlength:"300",placeholder:o}),l=p(t,"button",{class:"primary",type:"submit"},"Plan trip");e.append(a,l),e.addEventListener("submit",d=>{d.preventDefault(),a.value.trim()&&this.on.plan(a.value,d)}),this.panel.append(i,e);let c=(this.opts.suggestions??[]).slice(0,4);if(c.length){let d=p(t,"div",{class:"row"});for(let u of c){let g=p(t,"button",{class:"chip",type:"button"},u);g.addEventListener("click",b=>this.on.plan(u,b)),d.appendChild(g)}this.panel.append(p(t,"p",{class:"small"},"Popular here"),d)}this.panel.append(p(t,"p",{class:"small"},"Nothing moves until you press Go. Any click takes the wheel.")),setTimeout(()=>a.focus(),0)}renderPlan(t){let e=this.doc;this.panel.append(p(e,"p",{class:"small"},`Where to: ${this.destination}`),p(e,"h2",{},t.summary));let n=p(e,"ol");t.steps.forEach((a,l)=>{let c=p(e,"li"),d=/you press it/i.test(a);c.append(p(e,"span",{class:d?"n gate":"n"},String(l+1)),e.createTextNode(a)),n.appendChild(c)});let i=p(e,"button",{class:"primary go",type:"button"},"Go");i.addEventListener("click",a=>this.on.go(a));let s=p(e,"button",{class:"ghost",type:"button"},"Not this");s.addEventListener("click",()=>this.on.notThis());let o=p(e,"div",{class:"row"});o.append(i,s),this.panel.append(n,o),setTimeout(()=>i.focus(),0)}renderArrived(t){let e=this.doc,n=p(e,"div",{class:"done"});n.innerHTML='<svg width="28" height="28" viewBox="0 0 28 28" aria-hidden="true"><circle cx="14" cy="14" r="13" fill="#2D5BFF"/><path d="M8.5 14.5l3.5 3.5 7.5-8" stroke="#fff" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>',n.appendChild(p(e,"h2",{},"You're here"));let i=p(e,"div",{class:"row"}),s=a=>{let l=p(e,"button",{class:"thumb",type:"button","aria-label":a?"Good trip":"Bad trip","aria-pressed":"false"});return l.innerHTML=a?N:N.replace("<svg ",'<svg style="transform:rotate(180deg)" '),l.addEventListener("click",()=>{this.on.rate(a),i.querySelectorAll(".thumb").forEach(c=>c.setAttribute("aria-pressed","false")),l.setAttribute("aria-pressed","true")}),l},o=p(e,"button",{class:"ghost",type:"button"},"Done");o.addEventListener("click",()=>this.on.end()),i.append(p(e,"span",{class:"small"},"How was the trip?"),s(!0),s(!1),o),this.panel.append(n,p(e,"p",{},t),i)}moveCursor(t,e){this.cap.textContent=e,this.cursor.hidden=!1;let n=t.getBoundingClientRect(),i=Math.max(4,Math.min(n.left+Math.min(n.width*.6,60),(this.win.innerWidth||1024)-40)),s=Math.max(4,n.top+Math.min(n.height*.6,24));this.cursor.style.transform=`translate(${i}px, ${s}px)`;let o=this.win.matchMedia?.("(prefers-reduced-motion: reduce)").matches;return new Promise(a=>setTimeout(a,o?300:900))}ring(t,e="drive"){if(this.ringTarget=t,this.ringEl.hidden=!t,!t){this.win.cancelAnimationFrame?.(this.raf);return}this.ringEl.dataset.tone=e;let n=()=>{if(!this.ringTarget)return;let i=this.ringTarget.getBoundingClientRect();Object.assign(this.ringEl.style,{left:`${i.left-4}px`,top:`${i.top-4}px`,width:`${i.width+8}px`,height:`${i.height+8}px`}),this.raf=this.win.requestAnimationFrame?.(n)??0};n()}};var H="https://passengermode.com";function ut(r){try{return r.top!==r.self}catch{return!0}}var x="__passengerModeMounted";async function W(r){let t=r.window??window;if(ut(t)&&!r.allowInFrame)return console.info("Passenger Mode is off inside frames."),null;let e=t;if(e[x])return console.info("Passenger Mode is already on this page; ignoring the second install."),null;e[x]=!0;try{let n=await dt(r,t);if(!n)e[x]=!1;else{let i=n.destroy;n.destroy=()=>{i(),e[x]=!1}}return n}catch(n){throw e[x]=!1,n}}async function dt(r,t){let e=(r.apiBase??H).replace(/\/$/,""),n=r.key?`key=${encodeURIComponent(r.key)}`:"";if(r.key)try{let u=await(await t.fetch(`${e}/api/v1/status?${n}`,{credentials:"omit"})).json();if(!u?.on)return console.info(`Passenger Mode is off for this site${u?.reason?`: ${u.reason}`:""}.`),null}catch{return null}let i=r.manifest??(r.key?`${e}/api/v1/manifest?${n}`:void 0);if(!i)throw new Error("Passenger Mode needs a manifest or a key.");let s=typeof i=="string"?await(await t.fetch(i,{credentials:r.key?"omit":"same-origin"})).json():i,o=r.planner??(r.key?new m(`${e}/api/v1/plan`,void 0,{"x-passenger-key":r.key}):r.endpoint?new m(r.endpoint):new y),a=r.key?u=>{try{t.fetch(`${e}/api/v1/trips`,{method:"POST",headers:{"content-type":"application/json","x-passenger-key":r.key},body:JSON.stringify(u),credentials:"omit",keepalive:!0}).catch(()=>{})}catch{}}:null,l=u=>{a?.(u),r.onTrip?.(u)},c,d=new w(t,{open:u=>c.open(u),close:()=>c.close(),plan:(u,g)=>{c.planTrip(u,g)},go:u=>c.go(u),notThis:()=>c.notThis(),takeWheel:()=>c.takeWheel(),resume:u=>c.resume(u),end:()=>c.end(),rate:u=>c.rate(u)},{suggestions:s.destinations.map(u=>u.goal),badge:r.badge??!0,offset:r.offset});return c=new v({manifest:s,planner:o,window:t,ui:d,routeFrom:r.routeFrom,onTrip:l}),{driver:c,ui:d,destroy(){c.destroy(),d.destroy()}}}if(typeof document<"u"){let r=document.currentScript;if(r?.dataset.manifest||r?.dataset.key){let t=n=>n!=null&&n!==""&&!Number.isNaN(Number(n))?Number(n):void 0,e=()=>{W({key:r.dataset.key,apiBase:r.dataset.api,manifest:r.dataset.manifest,endpoint:r.dataset.endpoint,routeFrom:r.dataset.route??"path",badge:r.dataset.badge!=="false",offset:{right:t(r.dataset.right),bottom:t(r.dataset.bottom)}})};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",e):e()}}return G(ht);})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "passenger-mode",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Say where you're going; the site drives itself there.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"navigation",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"types": "./dist/lib/scan.d.ts",
|
|
47
47
|
"default": "./dist/lib/scan.js"
|
|
48
48
|
},
|
|
49
|
-
"./script": "./dist/passenger-mode.js"
|
|
49
|
+
"./script": "./dist/passenger-mode.js",
|
|
50
|
+
"./package.json": "./package.json"
|
|
50
51
|
},
|
|
51
52
|
"files": [
|
|
52
53
|
"dist",
|