website-api 1.0.5 → 1.1.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.
Files changed (63) hide show
  1. package/dist/bin/cli.js +1 -1
  2. package/dist/src/capabilities/browser.d.ts +23 -0
  3. package/dist/src/capabilities/browser.js +1 -0
  4. package/dist/src/capabilities/cookies.d.ts +24 -0
  5. package/dist/src/capabilities/cookies.js +1 -0
  6. package/dist/src/capabilities/download.d.ts +11 -0
  7. package/dist/src/capabilities/download.js +1 -0
  8. package/dist/src/capabilities/fingerprint.d.ts +14 -0
  9. package/dist/src/capabilities/fingerprint.js +1 -0
  10. package/dist/src/capabilities/http.d.ts +18 -0
  11. package/dist/src/capabilities/http.js +1 -0
  12. package/dist/src/capabilities/login/login-helper.d.ts +29 -0
  13. package/dist/src/capabilities/login/login-helper.js +1 -0
  14. package/dist/src/capabilities/login/login-strategy.d.ts +15 -0
  15. package/dist/src/capabilities/login/login-strategy.js +1 -0
  16. package/dist/src/core/context.d.ts +24 -0
  17. package/dist/src/core/context.js +1 -0
  18. package/dist/src/core/define-site.d.ts +9 -0
  19. package/dist/src/core/define-site.js +1 -0
  20. package/dist/src/core/loader.d.ts +21 -0
  21. package/dist/src/core/loader.js +1 -0
  22. package/dist/src/core/runtime.d.ts +20 -0
  23. package/dist/src/core/runtime.js +1 -0
  24. package/dist/src/sites/chase.com/download-helper.d.ts +34 -0
  25. package/dist/src/sites/chase.com/download-helper.js +1 -0
  26. package/dist/src/sites/chase.com/index.d.ts +2 -0
  27. package/dist/src/sites/chase.com/index.js +1 -0
  28. package/dist/src/sites/chatgpt.com/index.d.ts +10 -0
  29. package/dist/src/sites/chatgpt.com/index.js +1 -0
  30. package/dist/src/sites/cursor.com/index.d.ts +6 -0
  31. package/dist/src/sites/cursor.com/index.js +1 -0
  32. package/dist/src/sites/gemini.google.com/index.d.ts +5 -0
  33. package/dist/src/sites/gemini.google.com/index.js +1 -0
  34. package/dist/src/sites/google.com/google-helpers.d.ts +24 -0
  35. package/dist/src/sites/google.com/google-helpers.js +1 -0
  36. package/dist/src/sites/google.com/index.d.ts +2 -0
  37. package/dist/src/sites/google.com/index.js +1 -0
  38. package/dist/src/sites/ollama.com/index.d.ts +9 -0
  39. package/dist/src/sites/ollama.com/index.js +1 -0
  40. package/dist/src/sites/perplexity.ai/index.d.ts +50 -0
  41. package/dist/src/sites/perplexity.ai/index.js +1 -0
  42. package/dist/src/sites/pseg.com/index.d.ts +2 -0
  43. package/dist/src/sites/pseg.com/index.js +1 -0
  44. package/dist/src/sites/pseg.com/pseg-helpers.d.ts +13 -0
  45. package/dist/src/sites/pseg.com/pseg-helpers.js +1 -0
  46. package/dist/src/types.d.ts +219 -31
  47. package/dist/src/util/args-parser.d.ts +13 -0
  48. package/dist/src/util/args-parser.js +1 -0
  49. package/dist/src/util/google-json.d.ts +32 -0
  50. package/dist/src/util/google-json.js +1 -0
  51. package/dist/src/website-api.d.ts +7 -34
  52. package/dist/src/website-api.js +1 -1
  53. package/package.json +14 -4
  54. package/dist/src/base-adapter.d.ts +0 -19
  55. package/dist/src/base-adapter.js +0 -1
  56. package/dist/src/universal-adapter.d.ts +0 -10
  57. package/dist/src/universal-adapter.js +0 -1
  58. package/dist/src/website/chatgpt.com/chatgpt-adapter.d.ts +0 -11
  59. package/dist/src/website/chatgpt.com/chatgpt-adapter.js +0 -1
  60. package/dist/src/website/cursor.com/cursor-adapter.d.ts +0 -6
  61. package/dist/src/website/cursor.com/cursor-adapter.js +0 -1
  62. package/dist/src/website/ollama.com/ollama-adapter.d.ts +0 -2
  63. package/dist/src/website/ollama.com/ollama-adapter.js +0 -1
package/dist/bin/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as e}from"node:fs";import{dirname as o,join as r}from"node:path";import{fileURLToPath as n}from"node:url";import{program as t}from"commander";import s from"chalk";import i from"cli-table3";import{getDefaultChromeDir as c}from"chrome-tools";import{queryWebsite as l,websites as a,loadAdapters as p}from"../src/website-api.js";const d=r(o(n(import.meta.url)),"..","..","package.json"),{version:m}=JSON.parse(e(d,"utf8"));process.on("unhandledRejection",e=>{console.error(e instanceof Error?e.message:"command not found"),process.exit(1)}),t.name("website-api").description("CLI to query website APIs using decrypted Chrome cookies on macOS").version(m),t.option("--profile <name>","specific Chrome profile directory (e.g., 'Default', 'Profile 1')").option("--current-profile","Show the currently resolved/selected Chrome profile directory and name").option("-u, --user-agent <string>","custom User-Agent header for HTTP requests"),t.option("--debug","Print full HTTP request and response bodies for debugging"),t.command("list").description("List all supported website API adapters").action(async()=>{await p(),console.log(s.bold.green("\n🌐 Supported Website APIs:\n"));const e=new i({head:[s.bold.cyan("ID"),s.bold.cyan("Name"),s.bold.cyan("Domain"),s.bold.cyan("Description")],colWidths:[18,25,20,50],wordWrap:!0,style:{head:[],border:[]}});for(const o of a)e.push([s.yellow(o.id),o.name,s.underline(o.domain),o.description]);console.log(e.toString()),console.log(`\nTo run an API query, execute: ${s.bold.cyan("npx website-api <id>")}\n`)}),t.argument("[website]","website ID or domain to query (e.g. 'chatgpt.com')").action(async e=>{const o=t.opts();if(o.currentProfile){const e=process.env.PROFILE_PATH||process.env.CHROME_PROFILE_PATH||c(),r=o.profile||process.env.PROFILE_NAME||"Default";return console.log(s.bold.green("\nšŸ‘¤ Currently Resolved Profile:\n")),console.log(` ${s.bold("Path:")} ${e}`),void console.log(` ${s.bold("Name:")} ${r}\n`)}if(e)try{const r=await l(e,{profile:o.profile,userAgent:o.userAgent,debug:Boolean(o.debug)});console.log(JSON.stringify(r,null,2))}catch(e){console.error(e instanceof Error?e.message:"command not found"),process.exit(1)}else t.outputHelp()}),t.parse(process.argv);
2
+ import{readFileSync as e,writeFileSync as o}from"node:fs";import{dirname as n,join as s}from"node:path";import{fileURLToPath as t}from"node:url";import{program as r}from"commander";import i from"chalk";import l from"cli-table3";import{getDefaultChromeDir as a}from"chrome-tools";import{queryWebsite as c,sites as p,loadSites as d,getSite as u}from"../src/website-api.js";import{parseArgsForWebsite as g}from"../src/util/args-parser.js";const f=s(n(t(import.meta.url)),"..","..","package.json"),{version:m}=JSON.parse(e(f,"utf8"));process.on("unhandledRejection",e=>{console.error(e instanceof Error?e.message:"command not found"),process.exit(1)}),async function(){const e=process.argv.slice(2),n=e.find(e=>!e.startsWith("-"));if(n&&"list"!==n){await d();const s=u(n);if(s){const t=e.filter((o,s)=>s!==e.indexOf(n));let r;try{r=g(s.positionals,s.parameters,t)}catch(e){console.error(i.red(`Error: ${e instanceof Error?e.message:String(e)}`)),console.log(`Run ${i.cyan(`npx website-api ${s.id} --help`)} for usage details.`),process.exit(1)}if(r.helpRequested)return void function(e){console.log(i.bold.green(`\n🌐 Website API: ${i.white(e.name)} (${i.yellow(e.id)})\n`)),console.log(` ${i.italic(e.description)}\n`);let o=`npx website-api ${e.id}`;if(e.positionals&&e.positionals.length>0)for(const n of e.positionals)o+=n.required?` <${n.name}>`:` [${n.name}]`;if(o+=" [options]",console.log(`${i.bold("Usage:")} ${i.cyan(o)}\n`),e.positionals&&e.positionals.length>0){console.log(i.bold("Positional Arguments:"));for(const o of e.positionals)console.log(` ${i.cyan(o.name.padEnd(15))} ${o.description}`);console.log()}console.log(i.bold("Options:"));const n=[...e.parameters||[],{name:"profile",type:"string",description:"specific Chrome profile directory (e.g., 'Default')"},{name:"user-agent",type:"string",description:"custom User-Agent header for HTTP requests",short:"u"},{name:"debug",type:"boolean",description:"Print full HTTP request and response bodies for debugging"},{name:"keep-open",type:"boolean",description:"Leave the browser tab open after running (preserve the logged-in session)"},{name:"help",type:"boolean",description:"Show help for this website site",short:"h"}];for(const e of n){let o=`--${e.name}`;"boolean"!==e.type&&(o+=" <value>"),o=e.short?`-${e.short}, ${o}`:` ${o}`;const n=void 0!==e.default?` (default: ${e.default})`:"";console.log(` ${i.yellow(o.padEnd(28))} ${e.description}${i.gray(n)}`)}console.log()}(s);try{const e=await c(s.id,r.options);let n;if(r.options.text&&e&&"object"==typeof e){const o=e.answer||e.text;n=void 0!==o?String(o):JSON.stringify(e,null,2)}else n="string"==typeof e?e:JSON.stringify(e,null,2);r.options.out?(o(r.options.out,n+"\n","utf8"),console.log(i.green(`Success! Decoded response written to ${r.options.out}`))):console.log(n)}catch(e){console.error(i.red(e instanceof Error?e.message:"command not found")),process.exit(1)}return}}r.name("website-api").description("CLI to query website APIs using decrypted Chrome cookies on macOS").version(m),r.option("--profile <name>","specific Chrome profile directory (e.g., 'Default', 'Profile 1')").option("--current-profile","Show the currently resolved/selected Chrome profile directory and name").option("-u, --user-agent <string>","custom User-Agent header for HTTP requests"),r.option("--debug","Print full HTTP request and response bodies for debugging"),r.command("list").description("List all supported website API sites").action(async()=>{await d(),console.log(i.bold.green("\n🌐 Supported Website APIs:\n"));const e=new l({head:[i.bold.cyan("ID"),i.bold.cyan("Name"),i.bold.cyan("Domain"),i.bold.cyan("Description")],colWidths:[18,25,20,50],wordWrap:!0,style:{head:[],border:[]}});for(const o of p){const n=[];"browser"===o.transport&&n.push(i.magenta("[p]")),o.auth&&n.push(i.red("[l]")),"extension"===o.origin&&n.push(i.blue("[x]"));const s=n.length?`${o.name} ${n.join(" ")}`:o.name;e.push([i.yellow(o.id),s,i.underline(o.domain),o.description])}console.log(e.toString()),console.log(`\n${i.magenta("[p]")} requires a running Chrome (Playwright) ${i.red("[l]")} requires login ${i.blue("[x]")} user extension`),console.log(`\nTo run an API query, execute: ${i.bold.cyan("npx website-api <id>")}\n`)}),r.argument("[website]","website ID or domain to query (e.g. 'chatgpt.com')").action(async e=>{const o=r.opts();if(o.currentProfile){const e=process.env.PROFILE_PATH||process.env.CHROME_PROFILE_PATH||a(),n=o.profile||process.env.PROFILE_NAME||"Default";return console.log(i.bold.green("\nšŸ‘¤ Currently Resolved Profile:\n")),console.log(` ${i.bold("Path:")} ${e}`),void console.log(` ${i.bold("Name:")} ${n}\n`)}e?(console.error(i.red(`Error: website adapter "${e}" not found.`)),console.log(`Run ${i.cyan("npx website-api list")} to see all supported adapters.`),process.exit(1)):r.outputHelp()}),r.parse(process.argv)}().catch(e=>{console.error(e instanceof Error?e.message:"command not found"),process.exit(1)});
@@ -0,0 +1,23 @@
1
+ import { type Browser, type Page } from "playwright-core";
2
+ export interface BrowserOptions {
3
+ /** CDP endpoint of a running Chrome. Defaults to env or localhost:9222. */
4
+ cdpEndpoint?: string;
5
+ /** Close a tab opened by this session on dispose. Defaults to true. */
6
+ close?: boolean;
7
+ debug?: boolean;
8
+ }
9
+ export interface BrowserSession {
10
+ page: Page;
11
+ browser: Browser;
12
+ /** Whether this session opened a brand-new tab (vs. reusing one). */
13
+ opened: boolean;
14
+ dispose(): Promise<void>;
15
+ }
16
+ /** A connector function — injectable so the context/tests can swap it out. */
17
+ export type BrowserConnector = (targetUrl: string, options: BrowserOptions) => Promise<BrowserSession>;
18
+ /**
19
+ * Connects to an existing Chrome over CDP and reuses (or opens) a tab for the
20
+ * target URL. Returns a session with an explicit `dispose()` the runtime calls
21
+ * during teardown — sites never manage the connection themselves.
22
+ */
23
+ export declare const connectChrome: BrowserConnector;
@@ -0,0 +1 @@
1
+ import{chromium as t}from"playwright-core";export const connectChrome=async(e,o={})=>{const n=o.cdpEndpoint||process.env.CDP_ENDPOINT||"http://localhost:9222",r=!!o.debug,a=await t.connectOverCDP(n),c=a.contexts()[0];if(!c)throw new Error("No active browser context found. Is Chrome running with remote debugging enabled?");let s=!1,i=c.pages().find(t=>{try{const o=new URL(e).hostname.replace("www.","");return new URL(t.url()).hostname.endsWith(o)||t.url().startsWith(e)}catch{return t.url().startsWith(e)}});return i?r&&console.log(`Reusing existing tab for ${e}`):(r&&console.log(`Opening a new tab for ${e}`),i=await c.newPage(),await i.goto(e,{waitUntil:"domcontentloaded"}),s=!0),{page:i,browser:a,opened:s,async dispose(){if(s&&!1!==o.close)try{await i.close()}catch{}try{await a.close()}catch{}}}};
@@ -0,0 +1,24 @@
1
+ import { getCookies as realGetCookies, getPasswords as realGetPasswords, type CookieEntry } from "chrome-tools";
2
+ import type { Credentials, QueryOptions } from "../types.js";
3
+ export declare const DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36";
4
+ /**
5
+ * Injectable Chrome accessors. Defaults to the real `chrome-tools` functions;
6
+ * tests substitute fakes so the capability is exercised without a real Chrome.
7
+ */
8
+ export interface CookieProviders {
9
+ getCookies?: typeof realGetCookies;
10
+ getPasswords?: typeof realGetPasswords;
11
+ env?: NodeJS.ProcessEnv;
12
+ }
13
+ export declare function resolveUserAgent(options: QueryOptions, env?: NodeJS.ProcessEnv): string;
14
+ export declare function buildCookieString(cookies: CookieEntry[]): string;
15
+ /**
16
+ * Resolves saved Chrome credentials for a domain. Searches the full domain
17
+ * first, then falls back to the registrable name (e.g. "pseg" from "pseg.com").
18
+ */
19
+ export declare function resolveCredentials(domain: string, options: QueryOptions, providers?: CookieProviders): Credentials;
20
+ /**
21
+ * Resolves decrypted Chrome cookies for a domain. When `required` is false a
22
+ * missing login yields an empty array instead of throwing.
23
+ */
24
+ export declare function resolveCookies(domain: string, options: QueryOptions, required: boolean, providers?: CookieProviders): CookieEntry[];
@@ -0,0 +1 @@
1
+ import{getCookies as e,getPasswords as r}from"chrome-tools";export const DEFAULT_USER_AGENT="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36";function o(e,r){return e.profilePath||r.PROFILE_PATH||r.CHROME_PROFILE_PATH||void 0}function n(e,r){return e.profile||r.PROFILE_NAME||void 0}export function resolveUserAgent(e,r=process.env){return e.userAgent||r.userAgent||r.USER_AGENT||DEFAULT_USER_AGENT}export function buildCookieString(e){return e.map(e=>`${e.name}=${e.value}`).join("; ")}export function resolveCredentials(e,t,s={}){const i=s.env??process.env,a=s.getPasswords??r,c=o(t,i),l=n(t,i);let p=a({chromeDir:c,profile:l,search:e});if(!p||0===p.length){const r=e.split(".");p=a({chromeDir:c,profile:l,search:r[r.length-2]||e})}if(!p||0===p.length)throw new Error(`No saved passwords found in Chrome for '${e}'`);const{username:u,password:f}=p[0];if(!u||!f)throw new Error(`Found credentials for '${e}' but username or password was empty`);return{username:u,password:f}}export function resolveCookies(r,t,s,i={}){const a=i.env??process.env,c=i.getCookies??e,l=o(t,a),p=n(t,a);let u=[];try{u=c({chromeDir:l,profile:p,domain:r,decrypt:!0})}catch{if(s)throw new Error("No login found in browser")}if((!u||0===u.length)&&s)throw new Error("No login found in browser");return u??[]}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Builds a `save(filename, content)` function bound to a target directory.
3
+ * Creates the directory on first write and returns the absolute path written.
4
+ * `outDir` of null means "current working directory".
5
+ */
6
+ export declare function createSaver(outDir: string | null, cwd?: string): (filename: string, content: string | Buffer) => Promise<string>;
7
+ /**
8
+ * Guards a downloaded payload that should be data but may be an HTML error page
9
+ * (a common symptom of an expired session). Throws a clear, actionable error.
10
+ */
11
+ export declare function assertNotHtml(text: string, label: string): string;
@@ -0,0 +1 @@
1
+ import t from"node:fs/promises";import e from"node:path";export function createSaver(r,o=process.cwd()){const n=e.resolve(o,r??".");let i=!1;return async function(r,o){i||(await t.mkdir(n,{recursive:!0}),i=!0);const s=e.join(n,e.basename(r));return await t.writeFile(s,o),s}}export function assertNotHtml(t,e){const r=String(t??"").trimStart();if(/^<!doctype html/i.test(r)||/^<html/i.test(r))throw new Error(`Download for ${e} returned HTML instead of data. The session may have expired.`);return t}
@@ -0,0 +1,14 @@
1
+ import type { Page } from "playwright-core";
2
+ import type { FingerprintConfig, FingerprintOption } from "../types.js";
3
+ /**
4
+ * Builds the init script that shapes the page's fingerprint. Pure and exported
5
+ * so it can be unit-tested without a real browser.
6
+ */
7
+ export declare function buildFingerprintScript(config: FingerprintConfig): string;
8
+ /** Resolves the effective fingerprint config from the site's option. */
9
+ export declare function resolveFingerprint(option: FingerprintOption, userAgent?: string): FingerprintConfig | null;
10
+ /**
11
+ * Applies the fingerprint to a page via an init script (runs before page
12
+ * scripts on the next navigation). No-op when fingerprinting is disabled.
13
+ */
14
+ export declare function applyFingerprint(page: Page, option: FingerprintOption, userAgent?: string): Promise<void>;
@@ -0,0 +1 @@
1
+ const n={languages:["en-US","en"]};export function buildFingerprintScript(n){return`(() => {\n const cfg = ${JSON.stringify(n)};\n const def = (obj, prop, get) => {\n try { Object.defineProperty(obj, prop, { get, configurable: true }); } catch (_) {}\n };\n // Hide the automation flag.\n def(navigator, "webdriver", () => undefined);\n if (cfg.languages) {\n def(navigator, "languages", () => cfg.languages);\n }\n if (cfg.platform) def(navigator, "platform", () => cfg.platform);\n if (typeof cfg.hardwareConcurrency === "number") {\n def(navigator, "hardwareConcurrency", () => cfg.hardwareConcurrency);\n }\n if (typeof cfg.deviceMemory === "number") {\n def(navigator, "deviceMemory", () => cfg.deviceMemory);\n }\n // Present a non-empty plugins list, as headless/automation often reports none.\n try {\n if (!navigator.plugins || navigator.plugins.length === 0) {\n def(navigator, "plugins", () => [1, 2, 3, 4, 5]);\n }\n } catch (_) {}\n // Ensure window.chrome exists, as some sites probe for it.\n try {\n if (!window.chrome) { window.chrome = { runtime: {} }; }\n } catch (_) {}\n })();`}export function resolveFingerprint(e,r){if(!1===e)return null;const t="stealth"===e?{...n}:{...n,...e};return r&&!t.userAgent&&(t.userAgent=r),t}export async function applyFingerprint(n,e,r){const t=resolveFingerprint(e,r);t&&await n.addInitScript(buildFingerprintScript(t))}
@@ -0,0 +1,18 @@
1
+ import type { HttpCapability } from "../types.js";
2
+ /**
3
+ * Dependencies the HTTP capability needs from the surrounding context. Kept
4
+ * narrow so tests can drive it with a fake fetch + static session values.
5
+ */
6
+ export interface HttpDeps {
7
+ fetchImpl?: typeof fetch;
8
+ cookieString: () => string;
9
+ userAgent: () => string;
10
+ debug?: boolean;
11
+ }
12
+ /** Parses an SSE body into the JSON payloads of its `data:` frames. */
13
+ export declare function parseSSE(raw: string): any[];
14
+ /**
15
+ * Builds the HTTP capability. Every request merges the resolved Chrome cookie
16
+ * string and User-Agent unless the caller already set those headers.
17
+ */
18
+ export declare function createHttp(deps: HttpDeps): HttpCapability;
@@ -0,0 +1 @@
1
+ export function parseSSE(t){const e=[],s=t.replace(/\r\n/g,"\n");for(const t of s.split("\n\n")){const s=[];for(const e of t.split("\n"))e.startsWith("data:")&&s.push(e.slice(5).trimStart());if(s.length)try{const t=JSON.parse(s.join("\n"));t&&("object"!=typeof t||Object.keys(t).length>0)&&e.push(t)}catch{}}return e}export function createHttp(t){const e=t.fetchImpl??fetch;function s(e){const s=new Headers(e?.headers);if(!s.has("Cookie")){const e=t.cookieString();e&&s.set("Cookie",e)}return s.has("User-Agent")||s.set("User-Agent",t.userAgent()),s}async function n(n,a){const r={...a,headers:s(a)};t.debug&&console.log("[debug] Request:",{url:n,init:r});const o=await e(n,r),c=await o.text();if(t.debug&&console.log("[debug] Response:",{url:n,status:o.status,statusText:o.statusText,headers:Array.from(o.headers.entries()),body:c}),!o.ok)throw new Error(`HTTP ${o.status}: ${o.statusText}`);return{response:o,text:c}}return{raw:n,text:async(t,e)=>(await n(t,e)).text,async html(t,e){const s=new Headers(e?.headers);return s.has("Accept")||s.set("Accept","text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"),(await n(t,{...e,headers:s})).text},async json(t,e){const s=new Headers(e?.headers);s.has("Accept")||s.set("Accept","application/json, text/plain, */*");const{text:a}=await n(t,{...e,headers:s});try{return JSON.parse(a)}catch{return{response:a}}},async sse(t,e){const s=new Headers(e?.headers);s.has("Accept")||s.set("Accept","text/event-stream");const{response:a,text:r}=await n(t,{...e,headers:s});return{status:a.status,contentType:a.headers.get("content-type")||"",frames:parseSSE(r),raw:r}}}}
@@ -0,0 +1,29 @@
1
+ import type { Page } from "playwright-core";
2
+ export interface LoginOptions {
3
+ page: Page;
4
+ emailSelector: string;
5
+ passwordSelector: string;
6
+ submitButtonSelector: string;
7
+ usernameSelectors?: string[];
8
+ passwordSelectors?: string[];
9
+ submitSelectors?: string[];
10
+ emailValue?: string;
11
+ passwordValue?: string;
12
+ getCredentials?: () => {
13
+ username: string;
14
+ password: string;
15
+ };
16
+ delayMs?: number;
17
+ intendedUrl?: string;
18
+ loginUrl?: string;
19
+ expectedRedirectUrlPattern?: string;
20
+ debug?: boolean;
21
+ pwdSelector?: string;
22
+ dashboardSelectors?: string[];
23
+ }
24
+ /**
25
+ * Automates a form login via Playwright: detects whether login is required,
26
+ * fills credentials, waits, submits, and awaits redirection. Idempotent — when
27
+ * a dashboard is already showing it skips login and returns false.
28
+ */
29
+ export declare function performFormLogin(options: LoginOptions): Promise<boolean>;
@@ -0,0 +1 @@
1
+ const t=['input[type="password"]'],e=['button[type="submit"]','input[type="submit"]'];async function i(t,e){for(const i of e)try{if(await t.locator(i).first().isVisible())return i}catch{}return null}function o(t,...e){const i=[t,...e.flatMap(t=>t??[])];return Array.from(new Set(i.filter(Boolean)))}export async function performFormLogin(a){const{page:r,emailSelector:n,passwordSelector:l,submitButtonSelector:s,delayMs:c=1e3,intendedUrl:w,expectedRedirectUrlPattern:f=w,debug:g=!1,pwdSelector:d='input[type="password"]',dashboardSelectors:u=[]}=a;if(w){if(!r.url().includes(w)){g&&console.log(`[LoginHelper] Navigating to intended URL: ${w}`);try{await r.goto(w,{waitUntil:"domcontentloaded"})}catch(t){g&&console.warn(`[LoginHelper] Warning navigating to ${w}:`,t)}}}try{await r.waitForLoadState("domcontentloaded")}catch{}let p=!1,m=r;g&&console.log("[LoginHelper] Waiting for session state to settle...");const b=Date.now();let y=!1;for(;Date.now()-b<15e3;){let t=!1;for(const e of u)try{if(await r.locator(e).first().isVisible()){p=!1,t=!0;break}}catch{}if(t){y=!0;break}try{if(await r.locator(d).first().isVisible()){p=!0,m=r,y=!0;break}}catch{}let e=!1;for(const t of r.frames())try{if(await t.locator(d).first().isVisible()){p=!0,m=t,e=!0;break}}catch{}if(e){y=!0;break}await r.waitForTimeout(500)}if(!y)try{await r.waitForSelector(n,{state:"visible",timeout:1e3}),p=!0,m=r}catch{try{await r.waitForSelector(l,{state:"visible",timeout:1e3}),p=!0,m=r}catch{p=!1}}if(!p)return g&&console.log("[LoginHelper] Already logged in. Skipping login flow."),!1;let L=a.emailValue,h=a.passwordValue;if((!L||!h)&&a.getCredentials){const t=a.getCredentials();L=t.username,h=t.password}if(!L||!h)throw new Error("[LoginHelper] Login required but no credentials were provided");const S=await i(m,o(n,a.usernameSelectors))||n,k=await i(m,o(l,a.passwordSelectors,t))||l,H=await i(m,o(s,a.submitSelectors,e))||s;if(g&&console.log(`[LoginHelper] Login form detected. email='${S}', password='${k}', submit='${H}'`),await m.fill(S,L),await m.fill(k,h),c>0&&(g&&console.log(`[LoginHelper] Waiting ${c}ms before submission...`),await m.waitForTimeout(c)),g&&console.log(`[LoginHelper] Clicking submit '${H}'...`),await m.click(H),f){g&&console.log(`[LoginHelper] Waiting for redirection matching '${f}'...`);try{await r.waitForURL(f,{timeout:15e3})}catch{try{await r.waitForNavigation({waitUntil:"networkidle",timeout:8e3})}catch{}}}return!0}
@@ -0,0 +1,15 @@
1
+ import type { FormLoginConfig, LoginContext, LoginStrategy } from "../../types.js";
2
+ /**
3
+ * Standard username/password form login. Wraps the universal `performFormLogin`
4
+ * engine, which auto-detects whether the form is present and skips login when a
5
+ * dashboard is already showing.
6
+ */
7
+ export declare class FormLoginStrategy implements LoginStrategy {
8
+ private readonly config;
9
+ constructor(config: FormLoginConfig);
10
+ ensureLoggedIn(ctx: LoginContext): Promise<boolean>;
11
+ }
12
+ /** Type guard: distinguishes a ready strategy from a plain config object. */
13
+ export declare function isLoginStrategy(value: unknown): value is LoginStrategy;
14
+ /** Normalizes `auth` (config or strategy) into a LoginStrategy. */
15
+ export declare function toLoginStrategy(auth: LoginStrategy | FormLoginConfig): LoginStrategy;
@@ -0,0 +1 @@
1
+ import{performFormLogin as e}from"./login-helper.js";export class FormLoginStrategy{config;constructor(e){this.config=e}ensureLoggedIn(t){return e({...this.config,page:t.page,debug:t.debug,getCredentials:t.getCredentials})}}export function isLoginStrategy(e){return"object"==typeof e&&null!==e&&"function"==typeof e.ensureLoggedIn}export function toLoginStrategy(e){return isLoginStrategy(e)?e:new FormLoginStrategy(e)}
@@ -0,0 +1,24 @@
1
+ import type { QueryOptions, Site, SiteContext } from "../types.js";
2
+ import { type CookieProviders } from "../capabilities/cookies.js";
3
+ import { type BrowserConnector } from "../capabilities/browser.js";
4
+ /**
5
+ * Injectable dependencies. In production all default to the real
6
+ * implementations; tests pass fakes to exercise a site or capability without a
7
+ * real Chrome, network, or filesystem.
8
+ */
9
+ export interface ContextProviders extends CookieProviders {
10
+ fetchImpl?: typeof fetch;
11
+ connectBrowser?: BrowserConnector;
12
+ cwd?: string;
13
+ }
14
+ /** A context plus the teardown hook the runtime calls in `finally`. */
15
+ export interface ManagedContext {
16
+ ctx: SiteContext;
17
+ dispose(): Promise<void>;
18
+ }
19
+ /**
20
+ * Builds the lazy capability context handed to `site.run(ctx)`. Nothing
21
+ * expensive happens until a capability is touched: cookies are read on first
22
+ * `cookies()`/HTTP call, and Chrome is launched on first `browser()`.
23
+ */
24
+ export declare function createContext(site: Site, options?: QueryOptions, providers?: ContextProviders): ManagedContext;
@@ -0,0 +1 @@
1
+ import{resolve as e}from"node:path";import{buildCookieString as i,resolveCookies as o,resolveCredentials as t,resolveUserAgent as r}from"../capabilities/cookies.js";import{createHttp as a}from"../capabilities/http.js";import{applyFingerprint as s}from"../capabilities/fingerprint.js";import{connectChrome as n}from"../capabilities/browser.js";import{createSaver as c}from"../capabilities/download.js";export function createContext(p,d={},l={}){const u=l.env??process.env,g=!!d.debug,m="required"===p.cookies;let w;const b=()=>(void 0===w&&(w=o(p.domain,d,m,l)),w),f=()=>i(b()),h=()=>r(d,u),k=()=>t(p.domain,d,l),v=a({fetchImpl:l.fetchImpl,cookieString:f,userAgent:h,debug:g}),D=l.connectBrowser??n;let j,y;const I=void 0!==d.close?!!d.close:!(d.keepOpen||p.keepBrowserOpen),x=()=>(y||(y=(async()=>(j=await D(p.landingUrl,{cdpEndpoint:u.CDP_ENDPOINT,close:I,debug:g}),await s(j.page,p.fingerprint,h()),p.auth&&await p.auth.ensureLoggedIn({page:j.page,debug:g,getCredentials:k}),j.page))()),y),C=d.outDir?e(l.cwd??process.cwd(),d.outDir):null,O=c(d.outDir??null,l.cwd);return{ctx:{site:p,domain:p.domain,options:d,debug:g,outDir:C,cookies:b,cookieString:f,credentials:k,userAgent:h,http:v,browser:x,eval:async e=>(await x()).evaluate(e),save:O},async dispose(){if(j)try{await j.dispose()}catch{}}}}
@@ -0,0 +1,9 @@
1
+ import type { Site, SiteDef } from "../types.js";
2
+ /** Detects an already-normalized site so loading is idempotent. */
3
+ export declare function isSite(value: unknown): value is Site;
4
+ /**
5
+ * Normalizes a minimal {@link SiteDef} into a fully-defaulted {@link Site}.
6
+ * Accepts a plain object (the common external-extension case), an already
7
+ * normalized site, or a class instance / factory result with the same fields.
8
+ */
9
+ export declare function defineSite(def: SiteDef): Site;
@@ -0,0 +1 @@
1
+ import{toLoginStrategy as t}from"../capabilities/login/login-strategy.js";export function isSite(t){return"object"==typeof t&&null!==t&&!0===t.__site}export function defineSite(e){if(isSite(e))return e;for(const t of["id","name","domain","description"])if(!e[t]||"string"!=typeof e[t])throw new Error(`Site is missing required string field "${t}"`);const n=Array.isArray(e.endpoints)&&e.endpoints.length>0;if(!e.run&&!n)throw new Error(`Site "${e.id}" must define either "endpoints" or "run"`);const i=e.run?async t=>e.run(t):(r=e.endpoints,async t=>{const e=r[0],n="html"===e.responseType?"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8":"text"===e.responseType?"text/plain,*/*;q=0.8":"application/json, text/plain, */*",i={method:e.method||"GET",headers:{Accept:n,...e.headers}};let o;return o="html"===e.responseType||"text"===e.responseType?await t.http.text(e.url,i):await t.http.json(e.url,i),e.transform?e.transform(o,t):o});var r;return{id:e.id,name:e.name,domain:e.domain,description:e.description,transport:e.transport??"http",cookies:e.cookies??"required",fingerprint:e.fingerprint??"stealth",keepBrowserOpen:e.keepBrowserOpen??!1,auth:e.auth?t(e.auth):void 0,parameters:e.parameters??[],positionals:e.positionals??[],landingUrl:e.endpoints?.[0]?.url??`https://${e.domain}`,run:i,__site:!0}}
@@ -0,0 +1,21 @@
1
+ import type { Site } from "../types.js";
2
+ /** Bundled sites ship inside the package (dist/src/sites). */
3
+ export declare const BUNDLED_SITES_DIR: string;
4
+ /**
5
+ * Resolves the extension roots searched in addition to bundled sites:
6
+ * 1. <config>/website-api/extensions (XDG_CONFIG_HOME or ~/.config)
7
+ * 2. each dir in $WEBSITE_API_EXTENSIONS (colon-separated)
8
+ *
9
+ * These hold the user's own sites, kept entirely outside the source tree.
10
+ */
11
+ export declare function extensionRoots(env?: NodeJS.ProcessEnv): string[];
12
+ /**
13
+ * Discovers and loads all sites from the bundled dir and every extension root.
14
+ * Later roots override earlier ones by `id`, so a user extension can shadow a
15
+ * bundled site. Failures in one file never abort the whole load.
16
+ */
17
+ export declare function discoverSites(options?: {
18
+ bundledDir?: string;
19
+ roots?: string[];
20
+ env?: NodeJS.ProcessEnv;
21
+ }): Promise<Site[]>;
@@ -0,0 +1 @@
1
+ import{existsSync as t,readdirSync as o,statSync as n}from"node:fs";import{homedir as r}from"node:os";import{dirname as e,join as s}from"node:path";import{fileURLToPath as i,pathToFileURL as c}from"node:url";import{defineSite as f,isSite as u}from"./define-site.js";const d=e(i(import.meta.url));export const BUNDLED_SITES_DIR=s(d,"..","sites");export function extensionRoots(t=process.env){const o=[],n=t.XDG_CONFIG_HOME||s(r(),".config");if(o.push(s(n,"website-api","extensions")),t.WEBSITE_API_EXTENSIONS)for(const n of t.WEBSITE_API_EXTENSIONS.split(":"))n.trim()&&o.push(n.trim());return o}function p(t){return!(!t.endsWith(".js")&&!t.endsWith(".mjs"))&&(!t.includes(".test.")&&!t.includes(".d.")&&!/(^|[-.])helper(s)?\.(m?js)$/i.test(t))}function a(t){const n=o(t);for(const o of["index.mjs","index.js"])if(n.includes(o))return s(t,o);const r=n.find(p);return r?s(t,r):null}function l(t){const r=[];let e;try{e=o(t)}catch{return r}for(const o of e){const e=s(t,o);let i;try{i=n(e)}catch{continue}if(i.isDirectory()){const t=a(e);t&&r.push(t)}else p(o)&&r.push(e)}return r}async function m(t,o){const n=await import(c(t).href);let r=n.default??n.site??n.sites;if(null==r)return[];if("function"==typeof r&&!u(r))try{r=new r}catch{try{r=r()}catch{return[]}}const e=Array.isArray(r)?r:[r],s=[];for(const t of e){if(!t||"object"!=typeof t)continue;const n=f(t);n.origin=o,s.push(n)}return s}export async function discoverSites(o={}){const n=o.env??process.env,r=[{dir:o.bundledDir??BUNDLED_SITES_DIR,origin:"bundled"},...(o.roots??extensionRoots(n)).map(t=>({dir:t,origin:"extension"}))],e=new Map;for(const{dir:o,origin:s}of r)if(t(o))for(const t of l(o))try{for(const o of await m(t,s))e.set(o.id,o)}catch(o){n.WEBSITE_API_DEBUG&&console.error(`[loader] failed to load ${t}:`,o)}return Array.from(e.values())}
@@ -0,0 +1,20 @@
1
+ import type { QueryOptions, Site } from "../types.js";
2
+ import { type ContextProviders } from "./context.js";
3
+ /** The loaded site registry, populated by {@link loadSites}. */
4
+ export declare let sites: Site[];
5
+ /** Auto-discovers and loads all sites (bundled + extensions). Idempotent. */
6
+ export declare function loadSites(force?: boolean): Promise<Site[]>;
7
+ /** Test/embedding hook: replace the registry with an explicit set. */
8
+ export declare function setSites(next: Site[]): void;
9
+ /** Finds a site by exact id/domain or a `.com`-insensitive match. */
10
+ export declare function getSite(id: string): Site | null;
11
+ /**
12
+ * Creates a fallback site for any URL/domain without a dedicated definition:
13
+ * a single GET to the URL with optional cookies.
14
+ */
15
+ export declare function createUniversalSite(websiteId: string): Site | null;
16
+ /**
17
+ * Resolves a site (dedicated or universal), runs it with a fresh capability
18
+ * context, and guarantees teardown of any browser session.
19
+ */
20
+ export declare function queryWebsite(websiteId: string, options?: QueryOptions, providers?: ContextProviders): Promise<unknown>;
@@ -0,0 +1 @@
1
+ import{defineSite as t}from"./define-site.js";import{discoverSites as e}from"./loader.js";import{createContext as o}from"./context.js";export let sites=[];let r=!1;export async function loadSites(t=!1){return r&&!t||(sites=await e(),r=!0),sites}export function setSites(t){sites=t,r=!0}export function getSite(t){if(!t)return null;const e=t.toLowerCase().trim();return sites.find(t=>t.id.toLowerCase()===e||t.id.toLowerCase().replace(".com","")===e||t.domain.toLowerCase()===e||t.domain.toLowerCase().replace(".com","")===e)??null}export function createUniversalSite(e){let o=e;o.startsWith("http://")||o.startsWith("https://")||(o="https://"+o);try{const r=new URL(o);return t({id:e,name:e,domain:r.hostname,description:`Universal site for ${r.hostname}`,cookies:"optional",endpoints:[{url:r.href}]})}catch{return null}}export async function queryWebsite(t,e={},r={}){await loadSites();let i=getSite(t);if(!i){if(!function(t){return t.startsWith("http://")||t.startsWith("https://")||t.includes(".")||t.includes("/")||t.includes(":")}(t))throw new Error("command not found");if(i=createUniversalSite(t),!i)throw new Error("command not found")}const{ctx:n,dispose:s}=o(i,e,r);try{return await i.run(n)}finally{await s()}}
@@ -0,0 +1,34 @@
1
+ import type { Page } from "playwright-core";
2
+ import type { SiteContext } from "../../types.js";
3
+ export interface ChaseDownloadOptions {
4
+ list?: boolean;
5
+ download?: boolean;
6
+ limit?: number | null;
7
+ first?: boolean;
8
+ filename?: string | null;
9
+ activity?: string;
10
+ range?: string;
11
+ from?: string | null;
12
+ to?: string | null;
13
+ accounts?: string | null;
14
+ }
15
+ export interface ChaseDownloadAccount {
16
+ id: string;
17
+ summaryType: "CARD" | "DDA";
18
+ detailType: string;
19
+ nickname: string;
20
+ mask: string;
21
+ }
22
+ export declare function normalizeActivity(value?: string): {
23
+ key: string;
24
+ label: string;
25
+ };
26
+ export declare function fetchDownloadOptions(page: Page): Promise<any>;
27
+ export declare function collectAccounts(payload: any): ChaseDownloadAccount[];
28
+ export declare function formatAccountLabel(account: ChaseDownloadAccount): string;
29
+ export declare function accountFileName(account: ChaseDownloadAccount): string;
30
+ export declare function summarizeAccounts(accounts: ChaseDownloadAccount[]): string;
31
+ export declare function selectAccounts(accounts: ChaseDownloadAccount[], opts: ChaseDownloadOptions): ChaseDownloadAccount[];
32
+ export declare function fetchAccountCsv(page: Page, account: ChaseDownloadAccount, opts: ChaseDownloadOptions, activityKey: string): Promise<string>;
33
+ /** Discovers, selects, downloads, and saves (or prints) Chase account CSVs. */
34
+ export declare function downloadAccounts(page: Page, ctx: SiteContext): Promise<string>;
@@ -0,0 +1 @@
1
+ import{assertNotHtml as t}from"../../capabilities/download.js";const e={CARD:{mode:"cardGet",count:"/svc/rr/accounts/secure/gateway/credit-card/transactions/inquiry-maintenance/digital-transaction-activity/v1/transaction-counts",csv:"/svc/rr/accounts/secure/gateway/credit-card/transactions/inquiry-maintenance/digital-transaction-activity/v1/transaction-activities"},DDA:{mode:"formPost",count:"/svc/rr/accounts/secure/v1/account/activity/download/count/dda/list",csv:"/svc/rr/accounts/secure/v1/account/activity/download/dda/list"}},n=new Map([["current",{key:"current",label:"Current display, including filters"}],["current-display",{key:"current",label:"Current display, including filters"}],["all",{key:"all",label:"All transactions"}],["all-transactions",{key:"all",label:"All transactions"}],["date-range",{key:"date-range",label:"Choose a date range"}],["date range",{key:"date-range",label:"Choose a date range"}],["choose-date-range",{key:"date-range",label:"Choose a date range"}]]);export function normalizeActivity(t){const e=String(t||"").trim().toLowerCase(),a=n.get(e||"all");if(!a)throw new Error(`Unknown activity option: ${t}`);return a}export async function fetchDownloadOptions(t){t.url().includes("secure.chase.com")||await t.goto("https://secure.chase.com/web/auth/dashboard#/dashboard/overview",{waitUntil:"domcontentloaded"});const e=await t.evaluate(async t=>{const e=await fetch(t.url,{method:"POST",credentials:"include",headers:t.headers,body:""});return{status:e.status,text:await e.text()}},{url:"/svc/rr/accounts/secure/v1/account/activity/download/options/list",headers:{"content-type":"application/x-www-form-urlencoded; charset=UTF-8","x-jpmc-channel":"id=C30","x-jpmc-csrf-token":"NONE"}});if(e.status<200||e.status>=300)throw new Error("Download options request failed with status "+e.status);return JSON.parse(e.text)}export function collectAccounts(t){return(t.downloadAccountActivityOptions||[]).map(t=>({id:String(t.accountId||"").trim(),summaryType:String(t.summaryType||"").trim(),detailType:String(t.detailType||"").trim(),nickname:String(t.nickName||"").trim(),mask:String(t.mask||"").trim()})).filter(t=>t.id&&("CARD"===t.summaryType||"DDA"===t.summaryType)&&t.detailType)}export function formatAccountLabel(t){const e=[];return t.nickname&&e.push(t.nickname),t.mask&&e.push(`****${t.mask}`),e.length||e.push(`${t.summaryType}/${t.detailType}/${t.id}`),e.join(" ")}export function accountFileName(t){return`${[t.nickname||"account",t.summaryType,t.detailType,t.id].map(t=>String(t).trim().replace(/[^A-Za-z0-9._-]+/g,"-")).filter(Boolean).join("-").replace(/-+/g,"-").replace(/^-|-$/g,"")||"account"}.csv`}export function summarizeAccounts(t){const e=[`Found ${t.length} downloadable account${1===t.length?"":"s"}:`];for(const[n,a]of t.entries())e.push(`${n+1}. ${formatAccountLabel(a)} | ${a.summaryType},${a.detailType},${a.id}`);return e.join("\n")}export function selectAccounts(t,e){let n=[];if(e.accounts&&(n=e.accounts.split(/[\s,]+/).map(Number).filter(t=>!isNaN(t))),!n.length){const n=void 0!==e.limit?e.limit:e.first?1:null;return t.slice(0,n||void 0)}const a=[],o=new Set;for(const e of n){if(e<1||e>t.length)throw new Error(`Account number ${e} is out of range. Run list to see 1-${t.length}.`);o.has(e)||(o.add(e),a.push(t[e-1]))}return a}function a(t){return`${t.getFullYear()}${String(t.getMonth()+1).padStart(2,"0")}${String(t.getDate()).padStart(2,"0")}`}function o(t){const e=String(t||"").match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);if(!e)throw new Error(`Invalid date "${t}". Use mm/dd/yyyy format.`);const[,n,a,o]=e;return`${o}${n.padStart(2,"0")}${a.padStart(2,"0")}`}function r(t,e,n){if("CARD"===t.summaryType){const r=new Date,c=new Date(r);c.setFullYear(r.getFullYear()-2);const s={"account-activity-download-type-code":"CSV","digital-account-identifier":t.id};if("date-range"===n){if(!e.from||!e.to)throw new Error("date-range requires from and to in mm/dd/yyyy format");s["start-date"]=o(e.from),s["end-date"]=o(e.to)}else"all"===n&&(s["start-date"]=a(c),s["end-date"]=a(r),s["eligibility-indicator"]="true");return s}const r={transactionType:"ALL",filterTranType:"ALL",statementPeriodId:"ALL",downloadType:"CSV",accountId:t.id};if("all"===n&&(r.dateOption="LAST_24_MONTHS"),"date-range"===n){if(!e.from||!e.to)throw new Error("date-range requires from and to in mm/dd/yyyy format");r.dateOption="DATE_RANGE",r.dateLo=e.from,r.dateHi=e.to}return r}export async function fetchAccountCsv(t,n,a,o){const c=function(t){const n=e[t.summaryType];if(!n)throw new Error(`Unsupported account type for ${formatAccountLabel(t)}: ${t.summaryType}`);return n}(n),s={body:r(n,a,o),countUrl:c.count,csvUrl:c.csv,csrfUrl:"/svc/rl/accounts/secure/v1/csrf/token/list",mode:c.mode,headers:{"content-type":"application/x-www-form-urlencoded; charset=UTF-8","x-jpmc-channel":"id=C30","x-jpmc-csrf-token":"NONE"}},i=await t.evaluate(async t=>{const e=t=>new URLSearchParams(t).toString(),n=(n,a)=>fetch(n+"?"+e(a),{method:"GET",credentials:"include",headers:t.headers}),a=(n,a,o=t.headers)=>fetch(n,{method:"POST",credentials:"include",headers:o,body:e(a)}),o="cardGet"===t.mode?await n(t.countUrl,t.body):await a(t.countUrl,t.body);if(!o.ok)throw new Error("Download count request failed with status "+o.status);const r=await fetch(t.csrfUrl,{method:"POST",credentials:"include",headers:t.headers,body:""});if(!r.ok)throw new Error("CSRF token request failed with status "+r.status);const c=await r.json(),s=c.csrfToken||c.response?.csrfToken;if(!s)throw new Error("CSRF token was not present in Chase token response");const i={...t.body,csrftoken:s,submit:"Submit"},d="cardGet"===t.mode?await n(t.csvUrl,i):await a(t.csvUrl,i,{"content-type":"application/x-www-form-urlencoded"});return{status:d.status,contentType:d.headers.get("content-type")||"",text:await d.text()}},s);if(i.status<200||i.status>=300)throw new Error(`Download request failed with status ${i.status} for ${formatAccountLabel(n)}`);if(i.contentType&&!/csv|text|octet-stream/i.test(i.contentType))throw new Error(`Download for ${formatAccountLabel(n)} returned ${i.contentType||"unknown content type"}`);return i.text}export async function downloadAccounts(e,n){const a=n.options,o=normalizeActivity(a.activity||a.range);if(!("date-range"!==o.key||a.from&&a.to))throw new Error("activity date-range requires from and to in mm/dd/yyyy format");const r=collectAccounts(await fetchDownloadOptions(e));if(!r.length)throw new Error("No downloadable accounts were found in the Chase download options response.");if(a.list)return summarizeAccounts(r);const c=selectAccounts(r,a),s=[];for(const[r,i]of c.entries()){const d=formatAccountLabel(i),u=t(await fetchAccountCsv(e,i,a,o.key),d);if(a.download){const t=a.filename&&1===c.length?a.filename:accountFileName(i),e=await n.save(t,u);s.push(`Saved: ${e}`)}else s.push(`\n===== ${r+1}/${c.length}: ${d} =====\n`+(u.endsWith("\n")?u:`${u}\n`))}return s.join("\n")}
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../types.js").Site;
2
+ export default _default;
@@ -0,0 +1 @@
1
+ import{defineSite as e}from"../../core/define-site.js";import{downloadAccounts as t}from"./download-helper.js";export default e({id:"chase",name:"Chase Bank",domain:"chase.com",description:"Logs into Chase, lists downloadable accounts, and downloads statement/transaction CSV files.",transport:"browser",cookies:"optional",keepBrowserOpen:!0,auth:{intendedUrl:"https://secure.chase.com/web/auth/dashboard#/dashboard/overview",emailSelector:'input[name="username"]',passwordSelector:'input[name="password"]',submitButtonSelector:"#signin-button",delayMs:1e3,pwdSelector:'input[type="password"], input[name="password"], input[id*="password"]',usernameSelectors:["#userId-input-field-input","input#userId-input","input#userId",'input[name="usr_name"]'],passwordSelectors:["#password-input-field-input","input#password-input","input#password"],submitSelectors:["#signin-button",'button[type="submit"]'],dashboardSelectors:[".accounts-group-container-bc","#account-groups-component-bc",'[data-testid="accounts-group-container"]',".innerTile","#DDA_ACCOUNTS"]},positionals:[{name:"accounts",description:"Account indexes to select (e.g. 1 3). Leave empty for all.",required:!1,variadic:!0}],parameters:[{name:"list",type:"boolean",description:"List downloadable accounts only",short:"l",default:!1},{name:"download",type:"boolean",description:"Save selected account CSV file(s) to cwd or --out-dir",short:"d",default:!1},{name:"limit",type:"number",description:"Only process the first n accounts"},{name:"first",type:"boolean",description:"Shortcut for --limit 1",default:!1},{name:"filename",type:"string",description:"Save one selected account to this filename (requires --download and one account)"},{name:"activity",type:"string",description:"Activity: current-display, all-transactions, date-range (default: all)"},{name:"range",type:"string",description:"Alias for --activity"},{name:"from",type:"string",description:"Start date for date-range (mm/dd/yyyy)"},{name:"to",type:"string",description:"End date for date-range (mm/dd/yyyy)"},{name:"out-dir",type:"string",description:"Write each CSV to <dir> instead of cwd"}],run:async e=>{const n=await e.browser();return await n.waitForTimeout(3e3),e.debug&&console.log("[chase] Running statement downloader flow..."),t(n,e)}});
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ChatGPT / Codex usage. A two-step flow:
3
+ * 1. Exchange Chrome cookies for a Bearer JWT via the Next-Auth session endpoint.
4
+ * 2. Use the JWT to query the private wham/usage endpoint.
5
+ *
6
+ * Cookie + User-Agent injection is handled by ctx.http, so the site only
7
+ * describes the two requests.
8
+ */
9
+ declare const _default: import("../../types.js").Site;
10
+ export default _default;
@@ -0,0 +1 @@
1
+ import{defineSite as t}from"../../core/define-site.js";export default t({id:"codex-usage",name:"ChatGPT / Codex Usage",domain:"chatgpt.com",description:"Fetches ChatGPT rate limit usage and quota details from the private wham/usage API.",run:async t=>{const a=await t.http.json("https://chatgpt.com/api/auth/session");if(!a?.accessToken)throw new Error("No ChatGPT login found in browser");return t.http.json("https://chatgpt.com/backend-api/wham/usage",{headers:{authorization:`Bearer ${a.accessToken}`}})}});
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Cursor.com — active usage summary. Pure declarative single-endpoint site;
3
+ * the runtime injects cookies + User-Agent and parses JSON.
4
+ */
5
+ declare const _default: import("../../types.js").Site;
6
+ export default _default;
@@ -0,0 +1 @@
1
+ import{defineSite as r}from"../../core/define-site.js";export default r({id:"cursor-usage",name:"Cursor Usage",domain:"cursor.com",description:"Fetches the active Cursor usage summary from the private usage-summary API.",endpoints:[{url:"https://cursor.com/api/usage-summary"}]});
@@ -0,0 +1,5 @@
1
+ import { type UniversalGoogleSchema } from "../../util/google-json.js";
2
+ /** Schema mapping Gemini's batchexecute response indices to a usage summary. */
3
+ export declare const GEMINI_USAGE_SCHEMA: UniversalGoogleSchema;
4
+ declare const _default: import("../../types.js").Site;
5
+ export default _default;
@@ -0,0 +1 @@
1
+ import{defineSite as e}from"../../core/define-site.js";import{decodeGoogleJsonWithSchema as t}from"../../util/google-json.js";export const GEMINI_USAGE_SCHEMA={planCode:{path:[0]},planName:{path:[0],transform:e=>2===e?"Gemini Pro":1===e?"Gemini Free":`Unknown (${e})`},limits:{path:[1],items:{rawLimit:{path:[0]},percentageUsed:{path:[1],transform:e=>parseFloat((100*e).toFixed(2))},tierName:{path:[2],transform:e=>1===e?"Hourly Usage Limit":2===e?"Weekly Usage Limit":`Unknown Tier (${e})`},resetTime:{path:[3,0,0],transform:e=>e?new Date(1e3*e).toISOString():null}}}};export default e({id:"gemini-usage",name:"Gemini Usage",domain:"gemini.google.com",description:"Fetches Gemini account usage/quota details via browser-attached Playwright.",transport:"browser",cookies:"optional",endpoints:[{url:"https://gemini.google.com/usage"}],run:async e=>{const o=await e.browser(),i=new Promise((e,t)=>{const i=setTimeout(()=>t(new Error("Timeout waiting for Gemini usage RPC payload. Make sure you are logged into gemini.google.com in Chrome.")),15e3);o.on("response",async t=>{if(t.url().includes("jSf9Qc"))try{const o=await t.text();clearTimeout(i),e(o)}catch{}})});e.debug&&console.log("Reloading to capture Gemini usage network request..."),await o.reload({waitUntil:"domcontentloaded"});const a=await i;return t(a,"jSf9Qc",GEMINI_USAGE_SCHEMA)}});
@@ -0,0 +1,24 @@
1
+ export declare function formEncode(value: any): string;
2
+ export declare function utf8Bytes(codePoint: number): number[];
3
+ export declare function utf8String(bytes: number[]): string;
4
+ export declare function percentDecode(value: any): string;
5
+ export declare function parseQueryString(search: string): Record<string, string>;
6
+ export declare function buildQueryString(query: Record<string, any>): string;
7
+ export declare function googlePath(url: string): string | null;
8
+ export declare function stripXssi(text: string): string;
9
+ export declare function parseJsonMaybe(text: string): any;
10
+ export declare function parseGoogleRecordStream(text: string): any[];
11
+ export declare function decodeGoogleBody({ body, contentType }: {
12
+ body: string;
13
+ contentType?: string;
14
+ }): {
15
+ format: string;
16
+ xssiPrefixed: boolean;
17
+ parsed: any;
18
+ records: any[];
19
+ };
20
+ export declare function cleanText(s: any, limit?: number): string;
21
+ export declare function extractAnswerFromText(text: string): string | null;
22
+ export declare function cleanHtml(html: string): string;
23
+ export declare function findHtmlInObject(obj: any): string | null;
24
+ export declare function extractAnswerFromRecordStream(text: string): string | null;
@@ -0,0 +1 @@
1
+ export function formEncode(t){let e="";const r=String(t??"");for(let t=0;t<r.length;t++){const n=r.codePointAt(t);if(void 0===n)continue;const o=String.fromCodePoint(n);if(n>65535&&t++," "===o)e+="+";else if("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~".includes(o))e+=o;else for(const t of utf8Bytes(n))e+=`%${t.toString(16).toUpperCase().padStart(2,"0")}`}return e}export function utf8Bytes(t){return t<=127?[t]:t<=2047?[192|t>>6,128|63&t]:t<=65535?[224|t>>12,128|t>>6&63,128|63&t]:[240|t>>18,128|t>>12&63,128|t>>6&63,128|63&t]}export function utf8String(t){let e="";for(let r=0;r<t.length;r++){const n=t[r];let o=n;192==(224&n)?o=(31&n)<<6|63&t[++r]:224==(240&n)?o=(15&n)<<12|(63&t[++r])<<6|63&t[++r]:240==(248&n)&&(o=(7&n)<<18|(63&t[++r])<<12|(63&t[++r])<<6|63&t[++r]),e+=String.fromCodePoint(o)}return e}export function percentDecode(t){const e=String(t??"").replace(/\+/g," ");let r="";for(let t=0;t<e.length;t++){if("%"!==e[t]||!/[0-9a-fA-F]{2}/.test(e.slice(t+1,t+3))){r+=e[t];continue}const n=[];for(;"%"===e[t]&&/[0-9a-fA-F]{2}/.test(e.slice(t+1,t+3));)n.push(Number.parseInt(e.slice(t+1,t+3),16)),t+=3;t--;try{r+=utf8String(n)}catch{r+=n.map(t=>`%${t.toString(16).toUpperCase().padStart(2,"0")}`).join("")}}return r}export function parseQueryString(t){const e={};for(const r of String(t??"").replace(/^\?/,"").split("&")){if(!r)continue;const t=r.indexOf("="),n=-1===t?r:r.slice(0,t),o=-1===t?"":r.slice(t+1);e[percentDecode(n)]=percentDecode(o)}return e}export function buildQueryString(t){return Object.entries(t).filter(([,t])=>null!=t&&""!==t).map(([t,e])=>`${formEncode(t)}=${formEncode(e)}`).join("&")}export function googlePath(t){const e=/^https?:\/\/([^/]+)(\/[^?#]*)?/i.exec(String(t??""));return e&&/(^|\.)google\.[^/]+$/i.test(e[1])?e[2]||"/":null}export function stripXssi(t){return t.replace(/^\s*\)\]\}'\s*\n?/,"")}export function parseJsonMaybe(t){try{return JSON.parse(t)}catch{return}}export function parseGoogleRecordStream(t){const e=[];for(const r of stripXssi(t).split(/\r?\n/)){const t=r.trim();if(!t)continue;const n=/^([a-zA-Z0-9_-]+);(.*)$/.exec(t);if(!n)continue;const o=n[2].trim();e.push({id:n[1],value:parseJsonMaybe(o)??o})}return e}export function decodeGoogleBody({body:t,contentType:e=""}){const r=String(t??""),n=stripXssi(r).trimStart(),o=/^\s*\)\]\}'/.test(r),i=/^[\[{]/.test(n)?parseJsonMaybe(n):void 0,s=void 0===i?parseGoogleRecordStream(r):[];let c="text";return(e.includes("html")||/^\s*</.test(n))&&(c="html"),o&&void 0!==i?c="google-xssi-json":o&&s.length?c="google-xssi-record-stream":void 0!==i?c="json":s.length&&(c="google-record-stream"),{format:c,xssiPrefixed:o,parsed:i,records:s}}export function cleanText(t,e=4e3){return String(t??"").replace(/\u0000/g,"").replace(/[ \t]+\n/g,"\n").trim().slice(0,e)}export function extractAnswerFromText(t){const e=String(t??"").split("\n").map(t=>t.trim()).filter(Boolean),r=e.findIndex(t=>/AI Mode response is ready/i.test(t));if(r>0)return e[r-1];const n=/^(Skip to main content|Accessibility help|Accessibility feedback|AI Mode|All|Images|Videos|News|More|Search Results|Sources|Related)$/i;return e.find(t=>!n.test(t))??null}export function cleanHtml(t){let e=t.replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&amp;/g,"&").replace(/&nbsp;/g," ");return e=function(t){let e="",r=0;for(;r<t.length;){const n=t.toLowerCase().indexOf("<style",r);if(-1===n){e+=t.slice(r);break}e+=t.slice(r,n);const o=t.toLowerCase().indexOf("</style>",n);if(-1===o)break;r=o+8}let n="";for(r=0;r<e.length;){const t=e.toLowerCase().indexOf("<script",r);if(-1===t){n+=e.slice(r);break}n+=e.slice(r,t);const o=e.toLowerCase().indexOf("<\/script>",t);if(-1===o)break;r=o+9}return n}(e),e=e.replace(/<[^>]+>/g," "),e.replace(/\s+/g," ").trim()}export function findHtmlInObject(t){if(!t)return null;if("string"==typeof t){const e=t.trim();return e.startsWith("<")||e.includes("class=")||e.includes("id=")?t:null}if(Array.isArray(t))for(const e of t){const t=findHtmlInObject(e);if(t)return t}else if("object"==typeof t){if("string"==typeof t.html)return t.html;if("string"==typeof t.aimc_block?.html)return t.aimc_block.html;if("string"==typeof t.value&&(t.value.startsWith("<")||t.value.includes("class=")))return t.value;for(const e of Object.values(t)){const t=findHtmlInObject(e);if(t)return t}}return null}export function extractAnswerFromRecordStream(t){try{const e=stripXssi(t).trimStart();if(e.startsWith("<")||e.includes("class=")||e.includes("id="))return cleanHtml(e);for(const e of parseGoogleRecordStream(t)){const t=findHtmlInObject(e.value);if(t)return cleanHtml(t)}}catch{}return null}
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../types.js").Site;
2
+ export default _default;
@@ -0,0 +1 @@
1
+ import{defineSite as e}from"../../core/define-site.js";import{buildQueryString as t,cleanText as o,decodeGoogleBody as n,extractAnswerFromRecordStream as r,extractAnswerFromText as s,formEncode as i,googlePath as a,parseQueryString as l}from"./google-helpers.js";function u(e,t){return e.map(e=>{const o=null==e.body?null:n({body:e.body,contentType:e.mimeType});return{method:e.method,type:e.type,url:e.url,status:e.status,mimeType:e.mimeType,decodedFormat:o?.format??null,recordCount:o?.records?.length??0,bodyPrefix:null==e.body?null:e.body.slice(0,t)}})}export default e({id:"google-ai",name:"Google AI Overview",domain:"google.com",description:"Fetches Google's AI Overview and AI Mode answers using browser-attached Playwright.",transport:"browser",cookies:"optional",endpoints:[{url:"https://www.google.com"}],positionals:[{name:"question",description:"The search query or question to ask Google",required:!0,variadic:!0}],parameters:[{name:"raw-limit",type:"number",description:"Max raw response chars to include",default:12e3},{name:"timeout",type:"number",description:"Playwright timeout in milliseconds",default:9e4},{name:"text",type:"boolean",description:"Print only the extracted AI Overview answer text",short:"t"}],run:async e=>{const d=e.options.question,c=void 0!==e.options.rawLimit?Number(e.options.rawLimit):12e3,m=void 0!==e.options.timeout?Number(e.options.timeout):9e4,p=await e.browser(),y=await p.context().newCDPSession(p);await y.send("Network.enable",{maxTotalBufferSize:1e8,maxResourceBufferSize:1e8});const w=[],g=new Map;y.on("Network.requestWillBeSent",e=>{const t=e.request||{};(e=>{const t=a(e);return"/search"===t||t?.startsWith("/async/")||t?.includes("batchexecute")})(t.url)&&(g.set(e.requestId,w.length),w.push({id:e.requestId,type:e.type,method:t.method,url:t.url,postData:t.postData||null,status:null,mimeType:null,body:null}))}),y.on("Network.responseReceived",e=>{const t=g.get(e.requestId);null!=t&&(w[t].status=e.response.status,w[t].mimeType=e.response.mimeType)}),y.on("Network.loadingFinished",async e=>{const t=g.get(e.requestId);if(null!=t)try{const o=w[t].mimeType||"";if(!/text|json|html|javascript|x-protobuf/.test(o))return;const n=await y.send("Network.getResponseBody",{requestId:e.requestId});w[t].body=n.base64Encoded?null:(n.body||"").slice(0,c)}catch{}});const f=`https://www.google.com/search?${t({q:d,udm:"50"})}`;e.debug&&console.log(`Navigating to Google Search: ${f}`),await p.goto(f,{waitUntil:"domcontentloaded"});const h=await async function(e,t){const o=Date.now();for(;Date.now()-o<Math.min(t,45e3);){await e.waitForTimeout(750);const t=await e.evaluate(()=>{const e=Array.from(document.querySelectorAll('[jsname="KFl8ub"], [data-attrid], .kp-wholepage')).map(e=>e.innerText?.trim()).filter(Boolean).find(e=>!/^(Sources|Related|AI Mode response is ready)$/i.test(e));if(e)return e;const t=(document.body?.innerText||"").split("\n").map(e=>e.trim()).filter(Boolean),o=t.findIndex(e=>/AI Mode response is ready/i.test(e));return o>0?t[o-1]:null});if(t)return t}return null}(p,m),b=await async function(e,t){return e.evaluate(e=>{const t=document.body?.innerText||"",o=document.documentElement?.outerHTML||"";return{title:document.title,url:document.location.href,bodyText:t.slice(0,e),htmlPrefix:o.slice(0,e)}},t)}(p,c),x=await async function(e){const o=await e.evaluate(()=>{const e=document.querySelector("[data-garc][data-lro-token][data-lro-signature][data-ei]");if(!e)return{url:null,error:"Missing AI Mode token container"};const t=document.getElementById("rKxeg")?.getAttribute("data-stkp")||null;return{origin:document.location.origin,search:document.location.search,stkp:t,fmt:document.querySelector("[data-madl]")?"madl":"adl",tokens:{ei:e.dataset.ei,garc:e.dataset.garc,lroToken:e.dataset.lroToken,lroSignature:e.dataset.lroSignature,xsrfFolwrToken:e.dataset.xsrfFolwrToken||null,srtst:e.dataset.srtst||null}}});if(!o?.origin)return o;const n=l(o.search),r={},s=["q","udm","mstk","csuir","mtid","ved","vet","sei","dpr","hl","gl","source","vsrid","lns_img","cinpts"];for(const e of s)n[e]&&(r[e]=n[e]);o.tokens.srtst&&(r.srtst=o.tokens.srtst),r.garc=o.tokens.garc,r.mlro=o.tokens.lroToken,r.mlros=o.tokens.lroSignature,r.ei=o.tokens.ei,o.stkp&&(r.stkp=o.stkp);const a={_fmt:o.fmt};o.tokens.xsrfFolwrToken&&(a._xsrf=o.tokens.xsrfFolwrToken);const u=t(r),d=Object.entries(a).map(([e,t])=>`${i(e)}:${i(t)}`).join(",");return{url:`${o.origin}/async/folwr?${u}&async=${d}`,tokens:o.tokens}}(p);let k=null;if(x?.url){e.debug&&console.log(`Discovered folwr endpoint: ${x.url}`);try{const e=await p.evaluate(async e=>(await fetch(e)).text(),x.url),t=r(e),o=s(e),i=!o||o.includes("<")||o.includes("class=")?null:o;k={title:b.title,url:x.url,bodyText:e,htmlPrefix:e.slice(0,c),answer:t||i,decoded:n({body:e,contentType:"text/plain"})}}catch(t){e.debug&&console.warn("Failed to query folwr endpoint in-page:",t)}}await p.waitForTimeout(500);const T=k?.answer||h||s(b.bodyText);return{question:d,answer:o(T,c)||null,finalUrl:p.url(),endpoint:x||null,searchPage:{title:b.title,url:b.url,bodyText:o(b.bodyText,c)},endpointResult:k?{title:k.title,url:k.url,bodyText:o(k.bodyText,c),htmlPrefix:k.htmlPrefix,decoded:k.decoded}:null,requests:u(w,c)}}});
@@ -0,0 +1,9 @@
1
+ export declare function extractPlan(html: string): string;
2
+ export declare function extractUsage(html: string, label: string): {
3
+ usage: string;
4
+ reset: string;
5
+ };
6
+ /** Parses the Ollama settings HTML into a usage summary. Pure + testable. */
7
+ export declare function parseOllamaUsage(html: string): Record<string, string>;
8
+ declare const _default: import("../../types.js").Site;
9
+ export default _default;
@@ -0,0 +1 @@
1
+ import{defineSite as e}from"../../core/define-site.js";export function extractPlan(e){return e.match(/Cloud Usage[\s\S]*?<\/span>[\s\S]*?<span[^>]*>([\s\S]*?)<\/span/i)?.[1]?.trim()??"unknown"}export function extractUsage(e,a){const s=a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");const t=new RegExp(`<div[\\s\\S]*?<span[^>]*>\\s*${s}\\s*<\\/span>[\\s\\S]*?aria-label="${s}\\s+([^"]+)"[\\s\\S]*?data-time="([^"]+)"`,"i"),n=e.match(t);return{usage:n?.[1]?.replace(/\s+used$/i,"").trim()??"unknown",reset:n?.[2]?.trim()??"unknown"}}export function parseOllamaUsage(e){const a=extractUsage(e,"Session usage"),s=extractUsage(e,"Weekly usage");return{time:(new Date).toISOString(),Plan:extractPlan(e),"Session Usage":a.usage,"Session Reset":a.reset,"Weekly Usage":s.usage,"Weekly Reset":s.reset}}export default e({id:"ollama-usage",name:"Ollama Usage",domain:"ollama.com",description:"Fetches Ollama plan and usage details from the authenticated settings page.",endpoints:[{url:"https://ollama.com/settings",responseType:"html",transform:e=>parseOllamaUsage("string"==typeof e?e:String(e))}]});
@@ -0,0 +1,50 @@
1
+ export declare function applyPatchOne(doc: any, patch: any): any;
2
+ export declare function applyBlockState(state: any, data: any): void;
3
+ export declare function extractAnswer(final: any, state: any): any;
4
+ /** Reduces a list of parsed SSE frames into the final answer + state. */
5
+ export declare function reduceFrames(frames: any[]): {
6
+ state: any;
7
+ final: any;
8
+ answer: any;
9
+ chunks: any[];
10
+ };
11
+ export declare function makeDefaultBody(): {
12
+ params: {
13
+ attachments: never[];
14
+ language: string;
15
+ timezone: string;
16
+ search_focus: string;
17
+ sources: string[];
18
+ frontend_uuid: `${string}-${string}-${string}-${string}-${string}`;
19
+ mode: string;
20
+ model_preference: string;
21
+ is_related_query: boolean;
22
+ is_sponsored: boolean;
23
+ frontend_context_uuid: `${string}-${string}-${string}-${string}-${string}`;
24
+ prompt_source: string;
25
+ query_source: string;
26
+ is_incognito: boolean;
27
+ time_from_first_type: number;
28
+ local_search_enabled: boolean;
29
+ use_schematized_api: boolean;
30
+ send_back_text_in_streaming_api: boolean;
31
+ supported_block_use_cases: string[];
32
+ client_coordinates: null;
33
+ mentions: never[];
34
+ dsl_query: string;
35
+ skip_search_enabled: boolean;
36
+ is_nav_suggestions_disabled: boolean;
37
+ source: string;
38
+ always_search_override: boolean;
39
+ override_no_search: boolean;
40
+ should_ask_for_mcp_tool_confirmation: boolean;
41
+ browser_agent_allow_once_from_toggle: boolean;
42
+ force_enable_browser_agent: boolean;
43
+ supported_features: string[];
44
+ extended_context: boolean;
45
+ version: string;
46
+ };
47
+ query_str: string;
48
+ };
49
+ declare const _default: import("../../types.js").Site;
50
+ export default _default;
@@ -0,0 +1 @@
1
+ import{randomUUID as e}from"node:crypto";import{defineSite as t}from"../../core/define-site.js";const r="https://www.perplexity.ai/rest/sse/perplexity_ask";function n(e){return e.replace(/~1/g,"/").replace(/~0/g,"~")}export function applyPatchOne(e,t){const r=t.op,o=t.path??"";if(""===o){if("replace"===r||"add"===r)return t.value;if("remove"===r)return null}const s=o.split("/").slice(1).map(n);null==e&&(e=/^\d+$/.test(s[0]??"")?[]:{});let i=e;for(let e=0;e<s.length-1;e++){const t=s[e],r=/^\d+$/.test(s[e+1]??"");if(Array.isArray(i)){const e=Number(t);for(;i.length<=e;)i.push(r?[]:{});i=i[e]}else i[t]??=r?[]:{},i=i[t]}const a=s.at(-1);if(null==a)return e;if(Array.isArray(i)){const e="-"===a?i.length:Number(a);"add"===r?i.splice(e,0,t.value):"replace"===r?i[e]=t.value:"remove"===r&&i.splice(e,1)}else"remove"===r?delete i[a]:"add"!==r&&"replace"!==r||(i[a]=t.value);return e}export function applyBlockState(e,t){for(const r of t?.blocks??[]){const t=r.intended_usage;for(const[n,o]of Object.entries(r))"intended_usage"!==n&&"diff_block"!==n&&t&&(e[t]=o);const n=r.diff_block;if(n?.field){e[n.field]??=null;for(const t of n.patches??[])e[n.field]=applyPatchOne(e[n.field],t)}}}export function extractAnswer(e,t){for(const t of e?.blocks??[]){const e=t.markdown_block;if(null!=e?.answer)return e.answer;if(Array.isArray(e?.chunks))return e.chunks.join("")}for(const e of["ask_text","ask_text_0_markdown","markdown_block"]){const r=t[e];if(null!=r?.answer)return r.answer;if(Array.isArray(r?.chunks))return r.chunks.join("")}return null}export function reduceFrames(e){const t={};let r=null;for(const n of e)applyBlockState(t,n),(n.final||"COMPLETED"===n.status||n.text_completed)&&(r=n);return!r&&e.length&&(r=e[e.length-1]),{state:t,final:r,answer:extractAnswer(r,t),chunks:e}}export function makeDefaultBody(){return{params:{attachments:[],language:"en-US",timezone:Intl.DateTimeFormat().resolvedOptions().timeZone||"America/New_York",search_focus:"internet",sources:["web"],frontend_uuid:e(),mode:"copilot",model_preference:"claude46sonnet",is_related_query:!1,is_sponsored:!1,frontend_context_uuid:e(),prompt_source:"user",query_source:"home",is_incognito:!1,time_from_first_type:1,local_search_enabled:!1,use_schematized_api:!0,send_back_text_in_streaming_api:!1,supported_block_use_cases:["answer_modes","media_items","knowledge_cards","inline_entity_cards","place_widgets","finance_widgets","prediction_market_widgets","sports_widgets","flight_status_widgets","news_widgets","shopping_widgets","jobs_widgets","search_result_widgets","inline_images","inline_assets","placeholder_cards","diff_blocks","inline_knowledge_cards","entity_group_v2","refinement_filters","canvas_mode","maps_preview","answer_tabs","price_comparison_widgets","preserve_latex","generic_onboarding_widgets","in_context_suggestions","pending_followups","inline_claims","unified_assets","workflow_steps","background_agents"],client_coordinates:null,mentions:[],dsl_query:"",skip_search_enabled:!0,is_nav_suggestions_disabled:!1,source:"default",always_search_override:!1,override_no_search:!1,should_ask_for_mcp_tool_confirmation:!0,browser_agent_allow_once_from_toggle:!1,force_enable_browser_agent:!1,supported_features:["browser_agent_permission_banner_v1.1"],extended_context:!1,version:"2.18"},query_str:""}}export default t({id:"perplexity",name:"Perplexity AI Ask",domain:"perplexity.ai",description:"Fetches live streaming answers from Perplexity AI using its private REST/SSE API.",positionals:[{name:"question",description:"The query or question to ask Perplexity AI",required:!0,variadic:!0}],parameters:[{name:"model",type:"string",description:"Model preference (e.g. 'claude46sonnet')",default:"claude46sonnet",short:"m"},{name:"out",type:"string",description:"Write decoded response JSON to file instead of stdout"},{name:"timeout",type:"number",description:"Request timeout in milliseconds",default:75e3},{name:"text",type:"boolean",description:"Print only the extracted text answer",short:"t"}],run:async t=>{const n=t.options.question,o=t.options.model||"claude46sonnet",s=void 0!==t.options.timeout?Number(t.options.timeout):75e3;if(0===t.cookies().length)throw new Error("No login found in browser. Please log in to perplexity.ai in Google Chrome.");const i=makeDefaultBody();let a;i.params.frontend_uuid=e(),i.params.frontend_context_uuid=e(),i.params.model_preference=o,i.params.dsl_query=n,i.params.time_from_first_type=1,i.query_str=n;try{a=await t.http.sse(r,{method:"POST",headers:{"content-type":"application/json",origin:"https://www.perplexity.ai",referer:"https://www.perplexity.ai/","x-perplexity-request-endpoint":r,"x-request-id":i.params.frontend_uuid},body:JSON.stringify(i),signal:AbortSignal.timeout(s)})}catch(e){if(e instanceof Error&&("AbortError"===e.name||"TimeoutError"===e.name))throw new Error(`Perplexity request timed out after ${s}ms.`);throw e}const l=reduceFrames(a.frames);return{endpoint:r,http_code:a.status,content_type:a.contentType,request:{query:n,model_preference:o,frontend_uuid:i.params.frontend_uuid,frontend_context_uuid:i.params.frontend_context_uuid},answer:l.answer,state:l.state,final:l.final,chunks:l.chunks}}});
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../types.js").Site;
2
+ export default _default;
@@ -0,0 +1 @@
1
+ import{defineSite as t}from"../../core/define-site.js";import{formatPropertyLabel as e,inferServiceTypeFromPropertyTitle as o,intervalToValue as r,normalizeInterval as a}from"./pseg-helpers.js";const i="https://mysmartenergy.nj.pseg.com/Dashboard";async function n(t,e){if(!e.url().toLowerCase().startsWith("https://mysmartenergy.nj.pseg.com/dashboard")){t.debug&&console.log(`Navigating to Dashboard from '${e.url()}'...`),await e.goto(i,{waitUntil:"domcontentloaded"});await e.locator("#LoginEmail").isVisible().catch(()=>!1)&&(t.debug&&console.log("Session expired or redirected to login. Re-authenticating..."),t.site.auth&&await t.site.auth.ensureLoggedIn({page:e,debug:t.debug,getCredentials:t.credentials}))}}async function s(t,e){return await async function(t,e){if(await n(t,e),!await e.locator('.selectPropertyContainer input[placeholder="Search"]').isVisible().catch(()=>!1)){const t=e.getByRole("link",{name:"Select Property"});await t.waitFor({state:"visible",timeout:5e3}).catch(()=>{});try{await t.click()}catch{await e.locator('a:has-text("Select Property")').first().click()}await e.waitForSelector(".selectPropertyContainer",{state:"visible",timeout:5e3})}}(t,e),e.evaluate(()=>Array.from(document.querySelectorAll(".selectPropertyContainer li")).map(t=>{const e=Array.from(t.querySelectorAll("h4"));return{propertyId:t.getAttribute("data-property-id")||"",propertyType:t.getAttribute("data-property-type")||"",title:t.querySelector("h2")?.textContent?.trim()||"",owner:e[0]?.textContent?.trim()||"",address:e[1]?.textContent?.trim()||"",isCurrent:t.classList.contains("current")}}))}async function l(t,e,o){return t.evaluate(({selector:t,value:e})=>{const o=document.querySelector(t);if(!o)throw new Error(`${t} select not found`);return o.value=e,o.dispatchEvent(new Event("change",{bubbles:!0})),{value:o.value}},{selector:`select[name="${e}"]`,value:o})}async function c(t,e,o){const a="Gas"===e?"4":"1";if("Gas"===e&&"Billing"!==o)throw new Error("Gas usage only supports the Billing interval.");const i=r(o);if(!i)throw new Error(`Unsupported interval mapping: ${o}`);await t.waitForSelector('select[name="SelectedServiceType"]',{state:"attached",timeout:5e3});const n=await l(t,"SelectedServiceType",a);if(n.value!==a)throw new Error(`Failed to set service type: ${JSON.stringify(n)}`);await t.waitForTimeout(500),await t.waitForSelector('select[name="SelectedInterval"]',{state:"attached",timeout:5e3});const s=await async function(t,e){return t.evaluate(t=>{const e=document.querySelector(t);if(!e)throw new Error(`${t} select not found`);return{value:e.value,options:Array.from(e.options).map(t=>({value:t.value,text:t.textContent?.trim()||""}))}},`select[name="${e}"]`)}(t,"SelectedInterval");if(!new Set(s.options.map(t=>t.value)).has(i)){const t=s.options.map(t=>t.text).join(", ");throw new Error(`Interval ${o} is not available for ${e}. Available: ${t}`)}const c=s.value===i?s:await l(t,"SelectedInterval",i);if(c.value!==i)throw new Error(`Failed to set interval: ${JSON.stringify(c)}`);return o}export default t({id:"pseg-usage",name:"PSEG Usage",domain:"mysmartenergy.nj.pseg.com",description:"Downloads PSEG Smart Energy usage data (CSV) or lists available properties.",transport:"browser",cookies:"optional",keepBrowserOpen:!0,auth:{intendedUrl:i,emailSelector:"#LoginEmail",passwordSelector:"#LoginPassword",submitButtonSelector:"button.loginBtn",delayMs:1e3},positionals:[{name:"property",description:"Property name (e.g. '100 Electric') or 1-based index.",required:!1}],parameters:[{name:"interval",type:"string",description:"15, 30, hourly, daily, weekly, monthly, billing",default:"billing",short:"i"},{name:"list",type:"boolean",description:"List all downloadable properties instead of downloading",short:"l"}],run:async t=>{const r=await t.browser();if(t.options.list){const o=await s(t,r),a=["Downloadable properties:"];for(const t of o){const o=t.isCurrent?" [current]":"";a.push(`${e(t)}${o} | ${t.address} | ${t.owner}`)}return a.join("\n")}const i=t.options.property;if(!i)throw new Error("Missing required argument: <property> or --list");const l=a(t.options.interval),u=await s(t,r),d=String(i).trim();let w;if(/^\d+$/.test(d)){const t=Number(d);if(t<1||t>u.length)throw new Error(`Property index ${t} is out of range. Use 1-${u.length}.`);w=u[t-1]}else{const t=d.toLowerCase();if(w=u.find(e=>e.title.trim().toLowerCase()===t)??u.find(o=>e(o).toLowerCase()===t),!w){const t=u.map(t=>e(t)).filter(Boolean).join(", ");throw new Error(`Property not found: ${i}. Available properties: ${t}`)}}const p=await async function(t,r,a){return await r.goto(`https://mysmartenergy.nj.pseg.com/Dashboard/SetMeterGroup?meterGroupId=${a.propertyId}`,{waitUntil:"domcontentloaded"}),await r.waitForTimeout(1e3),{label:e(a),title:a.title,serviceType:o(a.title)}}(0,r,w);return t.debug&&console.log(`Downloading ${p.label} usage CSV for ${l}...`),await n(t,r),await async function(t,e){const o="#downloadOptions";if(!await t.locator(o).isVisible().catch(()=>!1)){e&&console.log("Clicking 'Data' link...");try{await t.getByRole("link",{name:"Data"}).click()}catch{await t.locator('a:has-text("Data")').first().click()}try{return void await t.waitForSelector(o,{state:"visible",timeout:5e3})}catch{}try{await t.getByRole("link",{name:"download"}).click()}catch{await t.locator('a:has-text("download")').first().click()}await t.waitForSelector(o,{state:"visible",timeout:1e4})}}(r,t.debug),await c(r,p.serviceType,l),async function(t){return t.evaluate(async()=>{const t=document.querySelector("#downloadOptions");if(!t)throw new Error("Download form not found");const e=await fetch(t.action,{method:"POST",body:new FormData(t),credentials:"same-origin"});if(!e.ok)throw new Error(`Download request failed with status ${e.status} ${e.statusText}`);return e.text()})}(r)}});
@@ -0,0 +1,13 @@
1
+ export interface PsegProperty {
2
+ propertyId: string;
3
+ propertyType: string;
4
+ title: string;
5
+ owner: string;
6
+ address: string;
7
+ isCurrent: boolean;
8
+ }
9
+ export declare function normalizeInterval(value: string): string;
10
+ export declare function intervalToValue(interval: string): string;
11
+ export declare function inferServiceTypeFromPropertyTitle(title: string): "Gas" | "Electric";
12
+ export declare function extractAddressNumber(address: string): string;
13
+ export declare function formatPropertyLabel(property: Omit<PsegProperty, "isCurrent">): string;
@@ -0,0 +1 @@
1
+ const e={15:"15-Minute","15-minute":"15-Minute",30:"30-Minute","30-minute":"30-Minute",hourly:"Hourly",daily:"Daily",weekly:"Weekly",monthly:"Monthly",billing:"Billing"},r={"15-Minute":"3","30-Minute":"4",Hourly:"5",Daily:"6",Weekly:"8",Monthly:"9",Billing:"7"};export function normalizeInterval(r){const t=String(r||"").trim().toLowerCase(),n=e[t];if(!n)throw new Error(`Invalid interval: ${r}. Use 15, 30, hourly, daily, weekly, monthly, or billing.`);return n}export function intervalToValue(e){return r[e]}export function inferServiceTypeFromPropertyTitle(e){if(/\bgas\b/i.test(e))return"Gas";if(/\belectric\b/i.test(e))return"Electric";throw new Error(`Could not infer service type from property title: ${e}`)}export function extractAddressNumber(e){const r=String(e||"").match(/^(\d+)/);return r?r[1]:""}export function formatPropertyLabel(e){const r=String(e.title||"").trim();if(r)return r;const t=e.propertyType||inferServiceTypeFromPropertyTitle(e.title);return`${extractAddressNumber(e.address)||e.propertyId||"Unknown"} ${t}`}
@@ -1,6 +1,160 @@
1
1
  import type { CookieEntry } from "chrome-tools";
2
+ import type { Page } from "playwright-core";
3
+ export interface ParameterDefinition {
4
+ /** Option name (e.g. "model", "timeout"). Matches option flag `--<name>`. */
5
+ name: string;
6
+ /** Value type. */
7
+ type: "string" | "boolean" | "number";
8
+ /** Description for CLI usage help. */
9
+ description: string;
10
+ /** Default value if not provided. */
11
+ default?: any;
12
+ /** Whether the option is strictly required. */
13
+ required?: boolean;
14
+ /** Short character flag (e.g. "m"). */
15
+ short?: string;
16
+ }
17
+ export interface PositionalDefinition {
18
+ /** Name of the positional argument (e.g. "question"). */
19
+ name: string;
20
+ /** Description for CLI usage help. */
21
+ description: string;
22
+ /** Whether the positional is strictly required. */
23
+ required?: boolean;
24
+ /** If true, collects all remaining positional arguments. */
25
+ variadic?: boolean;
26
+ }
27
+ /** Resolved Chrome credentials for the target site. */
28
+ export interface Credentials {
29
+ username: string;
30
+ password: string;
31
+ }
32
+ /**
33
+ * Context handed to a login strategy by the framework.
34
+ *
35
+ * `getCredentials` is a lazy thunk: it is only invoked once the strategy decides
36
+ * a login is actually required, so an already-authenticated session never
37
+ * triggers a credential lookup (and never fails when no password is saved).
38
+ */
39
+ export interface LoginContext {
40
+ page: Page;
41
+ debug?: boolean;
42
+ getCredentials: () => Credentials;
43
+ }
44
+ /**
45
+ * A pluggable authentication approach. Implementations must be idempotent:
46
+ * detect the current session state and only log in when needed.
47
+ */
48
+ export interface LoginStrategy {
49
+ /** @returns true if a login was performed, false if already logged in. */
50
+ ensureLoggedIn(ctx: LoginContext): Promise<boolean>;
51
+ }
52
+ /**
53
+ * Declarative configuration for a standard single-page username/password form.
54
+ * Only selectors / URLs — no credentials, no business logic. A site may pass
55
+ * this plain object as `auth`; the framework wraps it in a FormLoginStrategy.
56
+ */
57
+ export interface FormLoginConfig {
58
+ /** Discriminator so a config object is distinguishable from a strategy. */
59
+ type?: "form";
60
+ /** URL to land on before checking session state (e.g. the dashboard). */
61
+ intendedUrl?: string;
62
+ /** Optional explicit login page URL. */
63
+ loginUrl?: string;
64
+ /** Primary username/email input selector. */
65
+ emailSelector: string;
66
+ /** Primary password input selector. */
67
+ passwordSelector: string;
68
+ /** Submit button selector. */
69
+ submitButtonSelector: string;
70
+ /** Site-specific fallback selectors, tried after the primary if not visible. */
71
+ usernameSelectors?: string[];
72
+ passwordSelectors?: string[];
73
+ submitSelectors?: string[];
74
+ /** Selector used to detect a visible login form (defaults to a password input). */
75
+ pwdSelector?: string;
76
+ /** Selectors that, when visible, indicate an already-authenticated dashboard. */
77
+ dashboardSelectors?: string[];
78
+ /** Delay before submitting, in ms. */
79
+ delayMs?: number;
80
+ /** URL pattern to await after submit (defaults to intendedUrl). */
81
+ expectedRedirectUrlPattern?: string;
82
+ }
83
+ export interface FingerprintConfig {
84
+ /** Override navigator.userAgent / UA string used by the browser session. */
85
+ userAgent?: string;
86
+ /** navigator.languages (e.g. ["en-US", "en"]). */
87
+ languages?: string[];
88
+ /** navigator.platform (e.g. "MacIntel"). */
89
+ platform?: string;
90
+ /** Hardware concurrency reported to the page. */
91
+ hardwareConcurrency?: number;
92
+ /** Device memory (GB) reported to the page. */
93
+ deviceMemory?: number;
94
+ }
95
+ /**
96
+ * `"stealth"` applies the default anti-bot evasion profile, `false` disables all
97
+ * fingerprint shaping, and an object customizes individual fields.
98
+ */
99
+ export type FingerprintOption = "stealth" | false | FingerprintConfig;
100
+ export interface SSEResult {
101
+ status: number;
102
+ contentType: string;
103
+ /** Parsed JSON payload of every `data:` frame (non-JSON frames are skipped). */
104
+ frames: any[];
105
+ /** The raw, undecoded stream body. */
106
+ raw: string;
107
+ }
108
+ export interface HttpCapability {
109
+ /** GET (or `init.method`) and parse JSON. Cookie + User-Agent auto-injected. */
110
+ json(url: string, init?: RequestInit): Promise<any>;
111
+ /** Fetch and return the response body as text. */
112
+ text(url: string, init?: RequestInit): Promise<string>;
113
+ /** Alias of `text`, semantically for HTML documents. */
114
+ html(url: string, init?: RequestInit): Promise<string>;
115
+ /** Fetch a Server-Sent-Events stream and collect its frames. */
116
+ sse(url: string, init?: RequestInit): Promise<SSEResult>;
117
+ /** Escape hatch: the raw Response plus its already-read text body. */
118
+ raw(url: string, init?: RequestInit): Promise<{
119
+ response: Response;
120
+ text: string;
121
+ }>;
122
+ }
123
+ /**
124
+ * The single object every site's `run()` receives. Each capability is lazy:
125
+ * touch `ctx.browser()` and Chrome is launched; never touch it and it isn't.
126
+ * The runtime owns setup and teardown — sites never manage lifecycle.
127
+ */
128
+ export interface SiteContext {
129
+ /** The site being run. */
130
+ readonly site: Site;
131
+ /** Cookie/credential domain for this site. */
132
+ readonly domain: string;
133
+ /** The parsed query options (CLI flags + positionals). */
134
+ readonly options: QueryOptions;
135
+ /** Convenience for `!!options.debug`. */
136
+ readonly debug: boolean;
137
+ /** Resolved output directory for `save()`, or null to use cwd. */
138
+ readonly outDir: string | null;
139
+ /** Resolve decrypted Chrome cookies for `domain` (lazy, memoized). */
140
+ cookies(): CookieEntry[];
141
+ /** Cookies as a `name=value; ...` header string. */
142
+ cookieString(): string;
143
+ /** Resolve a saved Chrome username/password for `domain`. */
144
+ credentials(): Credentials;
145
+ /** Resolve the User-Agent (options → env → default). */
146
+ userAgent(): string;
147
+ /** HTTP capability with cookie + User-Agent auto-injection. */
148
+ readonly http: HttpCapability;
149
+ /** Connect to Chrome over CDP, applying fingerprint + auth. Memoized. */
150
+ browser(): Promise<Page>;
151
+ /** Sugar for `(await browser()).evaluate(fn)`. */
152
+ eval<T>(fn: () => T | Promise<T>): Promise<T>;
153
+ /** Write a file to `outDir` (or cwd) and return its absolute path. */
154
+ save(filename: string, content: string | Buffer): Promise<string>;
155
+ }
2
156
  /**
3
- * Defines an API endpoint to be fetched by an adapter.
157
+ * Defines an API endpoint for the default declarative fetch flow.
4
158
  */
5
159
  export interface Endpoint {
6
160
  /** Full URL of the API endpoint. */
@@ -9,56 +163,90 @@ export interface Endpoint {
9
163
  method?: string;
10
164
  /** Additional headers to include in the request. */
11
165
  headers?: Record<string, string>;
12
- /**
13
- * Expected response body type. Defaults to auto-detect from content-type.
14
- * Use "html" for text/html responses that need parsing.
15
- */
166
+ /** Expected response body type. Defaults to auto-detect from content-type. */
16
167
  responseType?: "auto" | "json" | "text" | "html";
17
168
  /** Optional post-processing step for the parsed response body. */
18
- transform?: (this: WebsiteAdapter, body: unknown, cookies: CookieEntry[], options: QueryOptions) => Promise<unknown> | unknown;
169
+ transform?: (body: unknown, ctx: SiteContext) => unknown | Promise<unknown>;
19
170
  }
20
171
  /**
21
- * Configuration for defining a website adapter.
22
- * Simple adapters declare `endpoints` and get automatic fetch handling.
23
- * Complex adapters override `fetchData` for custom multi-step flows.
172
+ * The minimal, declarative shape an author writes. A site is either:
173
+ * • declarative — set `endpoints` and get an automatic fetch flow, or
174
+ * • imperative — set `run(ctx)` and drive capabilities directly.
175
+ *
176
+ * External sites in the user's extensions folder export a plain object of this
177
+ * shape as their default export — no imports from the package are required.
24
178
  */
25
- export interface AdapterConfig {
26
- /** Unique identifier, typically the domain (e.g. "chatgpt.com"). */
179
+ export interface SiteDef {
180
+ /** Unique identifier, typically a slug (e.g. "cursor-usage"). */
27
181
  id: string;
28
- /** Human-readable name (e.g. "ChatGPT / Codex Usage"). */
182
+ /** Human-readable name. */
29
183
  name: string;
30
- /** Cookie domain to filter Chrome cookies for this site. */
184
+ /** Cookie/credential domain (e.g. "cursor.com"). */
31
185
  domain: string;
32
- /** Short description of what data this adapter fetches. */
186
+ /** Short description of what data this site fetches. */
33
187
  description: string;
34
- /** Endpoints for simple single-fetch adapters. First endpoint is used by default fetchData. */
188
+ /** Transport used to reach the site. Defaults to "http". */
189
+ transport?: "http" | "browser";
190
+ /** Whether valid cookies are required. Defaults to "required". */
191
+ cookies?: "required" | "optional";
192
+ /** Browser fingerprint profile (browser transport only). Defaults to "stealth". */
193
+ fingerprint?: FingerprintOption;
194
+ /**
195
+ * Keep a tab opened by this site open after the run finishes, instead of
196
+ * closing it on teardown. Use for login sites so the authenticated session
197
+ * (and any 2FA state) stays warm for the next command. Defaults to false.
198
+ * The `--keep-open` CLI flag overrides this per-invocation.
199
+ */
200
+ keepBrowserOpen?: boolean;
201
+ /** Declarative login config or a custom strategy (browser transport). */
202
+ auth?: LoginStrategy | FormLoginConfig;
203
+ /** Declares custom CLI flags for this site. */
204
+ parameters?: ParameterDefinition[];
205
+ /** Declares expected positional arguments for this site. */
206
+ positionals?: PositionalDefinition[];
207
+ /** Declarative single-fetch flow. First endpoint is used. */
35
208
  endpoints?: Endpoint[];
36
- /** Override for complex multi-step flows (e.g. session token → API call). */
37
- fetchData?: (this: WebsiteAdapter, cookies: CookieEntry[], options: QueryOptions) => Promise<unknown>;
209
+ /** Imperative flow. Receives the full capability context. */
210
+ run?: (ctx: SiteContext) => unknown | Promise<unknown>;
38
211
  }
39
212
  /**
40
- * A fully-instantiated website adapter with shared helper methods.
213
+ * A normalized, fully-defaulted site produced by `defineSite()`.
41
214
  */
42
- export interface WebsiteAdapter extends AdapterConfig {
43
- /** Converts a cookie array into a "name=value; ..." header string. */
44
- buildCookieString(cookies: CookieEntry[]): string;
45
- /** Resolves the User-Agent from options → env → default. */
46
- resolveUserAgent(options: QueryOptions): string;
47
- /** Fetches a URL and returns parsed JSON, with error handling. */
48
- fetchJson(url: string, init?: RequestInit): Promise<any>;
49
- /** Fetches a URL and returns the raw response text, with error handling. */
50
- fetchText(url: string, init?: RequestInit): Promise<string>;
51
- /** Fetches an HTML document as text, with error handling. */
52
- fetchHtml(url: string, init?: RequestInit): Promise<string>;
215
+ export interface Site {
216
+ id: string;
217
+ name: string;
218
+ domain: string;
219
+ description: string;
220
+ transport: "http" | "browser";
221
+ cookies: "required" | "optional";
222
+ fingerprint: FingerprintOption;
223
+ keepBrowserOpen: boolean;
224
+ auth?: LoginStrategy;
225
+ parameters: ParameterDefinition[];
226
+ positionals: PositionalDefinition[];
227
+ /** URL the browser transport lands on (from endpoints[0] or https://domain). */
228
+ landingUrl: string;
229
+ run: (ctx: SiteContext) => Promise<unknown>;
230
+ /** Set by the loader: where this site was discovered. */
231
+ origin?: "bundled" | "extension";
232
+ /** Marker used to detect an already-normalized site. */
233
+ readonly __site: true;
53
234
  }
54
235
  /**
55
- * Options passed when querying a website API.
236
+ * Options passed when querying a site. CLI flags and positionals land here as
237
+ * camelCase keys (e.g. `--out-dir` → `outDir`).
56
238
  */
57
239
  export interface QueryOptions {
58
240
  /** Chrome profile directory name (e.g. "Default", "Profile 1"). */
59
241
  profile?: string;
242
+ /** Chrome profile path override. */
243
+ profilePath?: string;
60
244
  /** Custom User-Agent header for HTTP requests. */
61
245
  userAgent?: string;
62
- /** When true, print full HTTP request and response details for debugging. */
246
+ /** When true, print full HTTP request/response details for debugging. */
63
247
  debug?: boolean;
248
+ /** Directory to write downloads to. */
249
+ outDir?: string;
250
+ /** Allows passing custom site-specific parameters. */
251
+ [key: string]: any;
64
252
  }
@@ -0,0 +1,13 @@
1
+ import type { ParameterDefinition, PositionalDefinition } from "../types.js";
2
+ /**
3
+ * Parses raw command-line arguments based on positional and parameter schemas of a website adapter.
4
+ * Handles defaults, types, validation, variadic trailing arguments, and maps kebab-case CLI options to camelCase.
5
+ *
6
+ * @param positionalDefs Expected positional arguments schema
7
+ * @param parameterDefs Expected custom parameter option flags schema
8
+ * @param argv Command line arguments (excluding node, cli paths and website id)
9
+ */
10
+ export declare function parseArgsForWebsite(positionalDefs: PositionalDefinition[] | undefined, parameterDefs: ParameterDefinition[] | undefined, argv: string[]): {
11
+ options: Record<string, any>;
12
+ helpRequested: boolean;
13
+ };
@@ -0,0 +1 @@
1
+ const e=[{name:"profile",type:"string",description:"specific Chrome profile directory (e.g., 'Default')"},{name:"user-agent",type:"string",description:"custom User-Agent header for HTTP requests",short:"u"},{name:"debug",type:"boolean",description:"Print full HTTP request and response bodies for debugging"},{name:"keep-open",type:"boolean",description:"Leave the browser tab open after running (preserve the logged-in session)"},{name:"out",type:"string",description:"Write decoded response JSON/text to file instead of stdout",short:"o"}];export function parseArgsForWebsite(r=[],n=[],o){let i=!1;const s={},a=[...n,...e];for(const e of a){const r=t(e.name);void 0!==e.default?s[r]=e.default:"boolean"===e.type&&(s[r]=!1)}const c={};for(const e of r)c[e.name]=[];let p=0;for(let e=0;e<o.length;e++){const n=o[e];if("--help"!==n&&"-h"!==n)if(n.startsWith("-")){let r;const i=!n.startsWith("--"),c=i?n.slice(1):n.slice(2);if(r=i?a.find(e=>e.short===c):a.find(e=>e.name===c),!r)throw new Error(`Unknown option: ${n}`);const p=t(r.name);if("boolean"===r.type)s[p]=!0;else if("string"===r.type){const t=o[e+1];if(void 0===t||t.startsWith("-"))throw new Error(`Option ${n} requires a value`);s[p]=t,e++}else if("number"===r.type){const t=o[e+1];if(void 0===t||t.startsWith("-"))throw new Error(`Option ${n} requires a numeric value`);const r=Number(t);if(isNaN(r))throw new Error(`Option ${n} requires a valid numeric value, received: "${t}"`);s[p]=r,e++}}else if(p<r.length){const e=r[p];c[e.name].push(n),e.variadic||p++}else{const e=r[r.length-1];if(!e?.variadic)throw new Error(`Unexpected extra argument: "${n}"`);c[e.name].push(n)}else i=!0}for(const e of r){const r=c[e.name];if(e.required&&0===r.length&&!i)throw new Error(`Missing required argument: <${e.name}>`);s[t(e.name)]=r.length>0?r.join(" "):null}for(const e of a){const r=t(e.name);if(e.required&&void 0===s[r]&&!i)throw new Error(`Missing required option: --${e.name}`)}return{options:s,helpRequested:i}}function t(e){return e.replace(/-([a-z])/g,(e,t)=>t.toUpperCase())}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Universal recursive schema defining how to extract fields from nested arrays.
3
+ */
4
+ export interface SchemaField {
5
+ /** Array of indices specifying the path to traverse (e.g. [1, 0, 0]) */
6
+ path: number[];
7
+ /** Optional function to transform or format the extracted value */
8
+ transform?: (value: any) => any;
9
+ /** Optional nested schema to apply if this field contains an array of items */
10
+ items?: Record<string, SchemaField>;
11
+ }
12
+ export type UniversalGoogleSchema = Record<string, SchemaField>;
13
+ /**
14
+ * Parses and transforms Google batchexecute payload array using a universal schema mapping.
15
+ *
16
+ * @param payload The decoded RPC payload array.
17
+ * @param schema The universal schema mapping fields to array paths and options.
18
+ * @returns The normalized JSON structure.
19
+ */
20
+ export declare function parseGoogleJsonWithSchema(payload: any, schema: UniversalGoogleSchema): any;
21
+ /**
22
+ * Decodes raw Google batchexecute response envelope and extracts the payload for a given RPC ID.
23
+ *
24
+ * @param rawResponse The raw HTTP response body from Google's batchexecute endpoint.
25
+ * @param rpcId The Google RPC ID to extract (e.g. 'jSf9Qc').
26
+ * @returns The parsed JSON payload for the requested RPC ID.
27
+ */
28
+ export declare function decodeGoogleJson(rawResponse: string, rpcId: string): any;
29
+ export declare const decode_google_json: typeof decodeGoogleJson;
30
+ export declare const parse_google_json_with_schema: typeof parseGoogleJsonWithSchema;
31
+ export declare const decodeGoogleJsonWithSchema: (rawResponse: string, rpcId: string, schema: UniversalGoogleSchema) => any;
32
+ export declare const decode_google_json_with_schema: (rawResponse: string, rpcId: string, schema: UniversalGoogleSchema) => any;
@@ -0,0 +1 @@
1
+ function o(o,e){let r=o;for(const o of e){if(null==r||!Array.isArray(r))return;r=r[o]}return r}export function parseGoogleJsonWithSchema(e,r){const n={};for(const[t,s]of Object.entries(r)){const r=o(e,s.path);s.items&&Array.isArray(r)?n[t]=r.map(o=>parseGoogleJsonWithSchema(o,s.items)):n[t]=s.transform?s.transform(r):r}return n}export function decodeGoogleJson(o,e){let r=o.trim();r.startsWith(")]}'")&&(r=r.substring(4).trim());const n=r.indexOf("\n");if(-1===n)throw new Error("Invalid Google JSON response format: No newline found");const t=r.substring(0,n).trim(),s=parseInt(t,10);if(isNaN(s))throw new Error(`Invalid Google JSON response chunk length: "${t}"`);let i=r.substring(n+1,n+1+s);const a=i.lastIndexOf("]]");-1!==a&&(i=i.substring(0,a+2));const c=JSON.parse(i).find(o=>Array.isArray(o)&&o[1]===e);if(!c)throw new Error(`RPC ID "${e}" payload not found in Google JSON response`);return JSON.parse(c[2])}export const decode_google_json=decodeGoogleJson;export const parse_google_json_with_schema=parseGoogleJsonWithSchema;export const decodeGoogleJsonWithSchema=(o,e,r)=>parseGoogleJsonWithSchema(decodeGoogleJson(o,e),r);export const decode_google_json_with_schema=decodeGoogleJsonWithSchema;
@@ -1,34 +1,7 @@
1
- import type { QueryOptions, WebsiteAdapter } from "./types.js";
2
- /**
3
- * All loaded website adapters, populated by `loadAdapters()`.
4
- */
5
- export declare let websites: WebsiteAdapter[];
6
- /**
7
- * Auto-discovers and loads all website adapters from the `website/` directory.
8
- *
9
- * Scans each subdirectory for a file matching `*-adapter.js` and dynamically
10
- * imports it. Each adapter module must have a default export that is a
11
- * WebsiteAdapter (created via `defineAdapter()`).
12
- *
13
- * This is called once on first use — no manual imports needed when adding
14
- * new adapters.
15
- */
16
- export declare function loadAdapters(): Promise<void>;
17
- /**
18
- * Finds an adapter by ID or partial domain match.
19
- *
20
- * @param id - Website identifier (e.g. "chatgpt.com", "chatgpt", "cursor")
21
- * @returns The matching adapter or null
22
- */
23
- export declare function getWebsite(id: string): WebsiteAdapter | null;
24
- /**
25
- * Queries a registered website API using decrypted Google Chrome cookies on macOS.
26
- *
27
- * Cookie retrieval is simplified to a single `getCookies({ domain })` call —
28
- * no manual extraction or domain matching needed.
29
- *
30
- * @param websiteId - The domain or identifier of the website (e.g. "chatgpt.com")
31
- * @param options - Retrieval options (profile, userAgent)
32
- * @returns The structured website API response
33
- */
34
- export declare function queryWebsite(websiteId: string, options?: QueryOptions): Promise<unknown>;
1
+ export { defineSite, isSite } from "./core/define-site.js";
2
+ export type { Site, SiteDef, SiteContext, Endpoint, QueryOptions, ParameterDefinition, PositionalDefinition, FingerprintOption, FingerprintConfig, FormLoginConfig, LoginStrategy, LoginContext, Credentials, HttpCapability, SSEResult, } from "./types.js";
3
+ export { FormLoginStrategy } from "./capabilities/login/login-strategy.js";
4
+ export { sites, loadSites, setSites, getSite, queryWebsite, createUniversalSite, } from "./core/runtime.js";
5
+ export { discoverSites, extensionRoots, BUNDLED_SITES_DIR } from "./core/loader.js";
6
+ export { createContext } from "./core/context.js";
7
+ export type { ContextProviders, ManagedContext } from "./core/context.js";
@@ -1 +1 @@
1
- import{readdirSync as t}from"node:fs";import{dirname as e,join as o}from"node:path";import{fileURLToPath as r,pathToFileURL as n}from"node:url";import{getCookies as i}from"chrome-tools";import{loadEnv as s}from"./env.js";import{createUniversalAdapter as a}from"./universal-adapter.js";s();const c=e(r(import.meta.url)),f=o(c,"website");export let websites=[];export async function loadAdapters(){if(!(websites.length>0))try{const e=t(f,{withFileTypes:!0});for(const r of e){if(!r.isDirectory())continue;const e=o(f,r.name),i=t(e).find(t=>t.endsWith("-adapter.js"));if(!i)continue;const s=n(o(e,i)).href,a=(await import(s)).default;a?.id&&websites.push(a)}}catch(t){}}export function getWebsite(t){if(!t)return null;const e=t.toLowerCase().trim();return websites.find(t=>t.id.toLowerCase()===e||t.id.toLowerCase().replace(".com","")===e)??null}export async function queryWebsite(t,e={}){await loadAdapters();const o=process.env.PROFILE_PATH||process.env.CHROME_PROFILE_PATH||void 0,r=e.profile||process.env.PROFILE_NAME||void 0;let n,s=getWebsite(t);if(!s){if(!function(t){return t.startsWith("http://")||t.startsWith("https://")||t.includes(".")||t.includes("/")||t.includes(":")}(t))throw new Error("command not found");if(s=a(t),!s)throw new Error("command not found")}try{n=i({chromeDir:o,profile:r,domain:s.domain,decrypt:!0})}catch{throw new Error("No login found in browser")}if(!n||0===n.length)throw new Error("No login found in browser");s._lastQueryOptions=e;try{return await s.fetchData(n,e)}finally{delete s._lastQueryOptions}}
1
+ import{loadEnv as e}from"./env.js";e();export{defineSite,isSite}from"./core/define-site.js";export{FormLoginStrategy}from"./capabilities/login/login-strategy.js";export{sites,loadSites,setSites,getSite,queryWebsite,createUniversalSite}from"./core/runtime.js";export{discoverSites,extensionRoots,BUNDLED_SITES_DIR}from"./core/loader.js";export{createContext}from"./core/context.js";
package/package.json CHANGED
@@ -1,9 +1,16 @@
1
1
  {
2
2
  "name": "website-api",
3
- "version": "1.0.5",
3
+ "version": "1.1.1",
4
4
  "description": "CLI and library to fetch website API data",
5
5
  "main": "./dist/src/website-api.js",
6
+ "types": "./dist/src/website-api.d.ts",
6
7
  "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/src/website-api.d.ts",
11
+ "default": "./dist/src/website-api.js"
12
+ }
13
+ },
7
14
  "bin": {
8
15
  "website-api": "./dist/bin/cli.js",
9
16
  "chrome-website-api": "./dist/bin/cli.js"
@@ -21,13 +28,14 @@
21
28
  "license": "MIT",
22
29
  "dependencies": {
23
30
  "chalk": "^5.6.2",
24
- "chrome-tools": "^1.0.6",
31
+ "chrome-tools": "1.0.7",
25
32
  "cli-table3": "^0.6.5",
26
- "commander": "^14.0.3"
33
+ "commander": "^14.0.3",
34
+ "playwright-core": "^1.60.0"
27
35
  },
28
36
  "devDependencies": {
29
37
  "@types/node": "^25.9.1",
30
- "terser": "^5.43.1",
38
+ "terser": "^5.48.0",
31
39
  "typescript": "^6.0.3"
32
40
  },
33
41
  "files": [
@@ -35,6 +43,8 @@
35
43
  ],
36
44
  "scripts": {
37
45
  "build": "node scripts/build.mjs",
46
+ "typecheck": "tsc --noEmit",
47
+ "test": "tsc -p tsconfig.test.json && node --test \"dist/test/**/*.test.js\"",
38
48
  "check:secrets": "node scripts/check-secrets.mjs",
39
49
  "release": "pnpm version patch && pnpm publish --access public",
40
50
  "start": "node dist/bin/cli.js"
@@ -1,19 +0,0 @@
1
- import type { AdapterConfig, WebsiteAdapter } from "./types.js";
2
- /**
3
- * Factory function to create a fully-equipped WebsiteAdapter from a minimal config.
4
- *
5
- * Simple adapters only need `id`, `name`, `domain`, `description`, and `endpoints`.
6
- * Complex adapters can provide a custom `fetchData` that uses `this.buildCookieString()`,
7
- * `this.resolveUserAgent()`, and `this.fetchJson()`.
8
- *
9
- * @example
10
- * // Simple adapter (~5 lines of config):
11
- * export default defineAdapter({
12
- * id: "cursor.com",
13
- * name: "Cursor Usage",
14
- * domain: "cursor.com",
15
- * description: "Fetches Cursor usage summary.",
16
- * endpoints: [{ url: "https://cursor.com/api/usage-summary" }],
17
- * });
18
- */
19
- export declare function defineAdapter(config: AdapterConfig): WebsiteAdapter;
@@ -1 +0,0 @@
1
- const e={buildCookieString:e=>e.map(e=>`${e.name}=${e.value}`).join("; "),resolveUserAgent:e=>e.userAgent||process.env.userAgent||process.env.USER_AGENT||"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",async fetchJson(e,t){const s=this?._lastQueryOptions?.debug;s&&console.log("[debug] Request:",{url:e,init:t});const r=await fetch(e,t),o=await r.text();if(s&&console.log("[debug] Response:",{url:e,status:r.status,statusText:r.statusText,headers:Array.from(r.headers.entries()),body:o}),!r.ok)throw new Error(`HTTP ${r.status}: ${r.statusText}`);try{return JSON.parse(o)}catch{return{response:o}}},async fetchText(e,t){const s=this?._lastQueryOptions?.debug;s&&console.log("[debug] Request:",{url:e,init:t});const r=await fetch(e,t),o=await r.text();if(s&&console.log("[debug] Response:",{url:e,status:r.status,statusText:r.statusText,headers:Array.from(r.headers.entries()),body:o}),!r.ok)throw new Error(`HTTP ${r.status}: ${r.statusText}`);return o},async fetchHtml(e,t){return this.fetchText(e,t)}};async function t(e,t){if(!this.endpoints||0===this.endpoints.length)throw new Error(`Adapter "${this.id}" has no endpoints defined and no fetchData override`);const s=this.endpoints[0],r=this.buildCookieString(e),o=this.resolveUserAgent(t),n=await async function(e,t,s,r){r?.debug&&console.log("[debug] Request:",{url:e.url,method:e.method||"GET",headers:{Cookie:t,"User-Agent":s,...e.headers}});const o=await fetch(e.url,{method:e.method||"GET",headers:{Cookie:t,"User-Agent":s,Accept:"html"===e.responseType?"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8":"text"===e.responseType?"text/plain,*/*;q=0.8":"application/json, text/plain, */*",...e.headers}});if(!o.ok)throw new Error(`HTTP ${o.status}: ${o.statusText}`);const n=await o.text();r?.debug&&console.log("[debug] Response:",{url:e.url,status:o.status,statusText:o.statusText,headers:Array.from(o.headers.entries()),body:n});const a=o.headers.get("content-type")?.toLowerCase()??"";if("json"===e.responseType||"text"!==e.responseType&&"html"!==e.responseType&&(a.includes("application/json")||a.includes("+json")))try{return JSON.parse(n)}catch(t){throw new Error(`Expected JSON from ${e.url}, but received invalid JSON: ${t instanceof Error?t.message:String(t)}`)}return n}(s,r,o,t);return s.transform?s.transform.call(this,n,e,t):n}export function defineAdapter(s){const r={...s,...e,fetchData:s.fetchData??t};return r.fetchData=r.fetchData.bind(r),r}
@@ -1,10 +0,0 @@
1
- import type { WebsiteAdapter } from "./types.js";
2
- /**
3
- * Creates a universal fallback adapter for any domain that doesn't have
4
- * a dedicated adapter. Parses the websiteId as a URL and uses the default
5
- * single-endpoint fetch flow.
6
- *
7
- * @param websiteId - A URL or domain string (e.g. "example.com/api/data")
8
- * @returns A WebsiteAdapter if the URL is valid, or null otherwise.
9
- */
10
- export declare function createUniversalAdapter(websiteId: string): WebsiteAdapter | null;
@@ -1 +0,0 @@
1
- import{defineAdapter as t}from"./base-adapter.js";export function createUniversalAdapter(r){let e=r;e.startsWith("http://")||e.startsWith("https://")||(e="https://"+e);try{const a=new URL(e);return t({id:r,name:r,domain:a.hostname,description:`Universal adapter for ${a.hostname}`,endpoints:[{url:a.href}]})}catch{return null}}
@@ -1,11 +0,0 @@
1
- /**
2
- * ChatGPT adapter — fetches rate limit usage from the wham/usage API.
3
- *
4
- * This is a multi-step flow:
5
- * 1. Exchange Chrome cookies for a Bearer JWT via the Next-Auth session endpoint
6
- * 2. Use the JWT to query the private wham/usage endpoint
7
- *
8
- * See request.md in this directory for full endpoint documentation.
9
- */
10
- declare const _default: import("../../types.js").WebsiteAdapter;
11
- export default _default;
@@ -1 +0,0 @@
1
- import{defineAdapter as e}from"../../base-adapter.js";export default e({id:"codex-usage",name:"ChatGPT / Codex Usage",domain:"chatgpt.com",description:"Fetches ChatGPT rate limit usage and quota details from the private wham/usage API.",async fetchData(e,t){const a=this.buildCookieString(e),s=this.resolveUserAgent(t),o=await this.fetchJson("https://chatgpt.com/api/auth/session",{headers:{Cookie:a,"User-Agent":s,Accept:"application/json"}});if(!o?.accessToken)throw new Error("No ChatGPT login found in browser");return this.fetchJson("https://chatgpt.com/backend-api/wham/usage",{headers:{authorization:`Bearer ${o.accessToken}`}})}});
@@ -1,6 +0,0 @@
1
- /**
2
- * Cursor.com adapter — fetches the active usage summary.
3
- * Simple single-endpoint adapter: just declare the endpoint, BaseAdapter handles the rest.
4
- */
5
- declare const _default: import("../../types.js").WebsiteAdapter;
6
- export default _default;
@@ -1 +0,0 @@
1
- import{defineAdapter as r}from"../../base-adapter.js";export default r({id:"cursor-usage",name:"Cursor Usage",domain:"cursor.com",description:"Fetches the active Cursor usage summary from the private usage-summary API.",endpoints:[{url:"https://cursor.com/api/usage-summary"}]});
@@ -1,2 +0,0 @@
1
- declare const _default: import("../../types.js").WebsiteAdapter;
2
- export default _default;
@@ -1 +0,0 @@
1
- import{defineAdapter as e}from"../../base-adapter.js";export default e({id:"ollama-usage",name:"Ollama Usage",domain:"ollama.com",description:"Fetches Ollama plan and usage details from the authenticated settings page.",endpoints:[{url:"https://ollama.com/settings",responseType:"html",headers:{Accept:"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},transform(e){const t="string"==typeof e?e:String(e),n=a(t,"Session usage"),i=a(t,"Weekly usage");return{time:(new Date).toISOString(),Plan:s(t),"Session Usage":n.usage,"Session Reset":n.reset,"Weekly Usage":i.usage,"Weekly Reset":i.reset}}}]});function s(e){const s=e.match(/Cloud Usage[\s\S]*?<\/span>[\s\S]*?<span[^>]*>([\s\S]*?)<\/span/i);return s?.[1]?.trim()??"unknown"}function a(e,s){const a=s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");const t=new RegExp(`<div[\\s\\S]*?<span[^>]*>\\s*${a}\\s*<\\/span>[\\s\\S]*?aria-label="${a}\\s+([^"]+)"[\\s\\S]*?data-time="([^"]+)"`,"i"),n=e.match(t);return{usage:n?.[1]?.replace(/\s+used$/i,"").trim()??"unknown",reset:n?.[2]?.trim()??"unknown"}}