website-api 1.1.3 → 1.1.4

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 (65) hide show
  1. package/README.md +141 -1
  2. package/dist/bin/cli.js +204 -1
  3. package/dist/src/capabilities/browser.d.ts +8 -2
  4. package/dist/src/capabilities/browser.js +106 -1
  5. package/dist/src/capabilities/cookies.d.ts +7 -1
  6. package/dist/src/capabilities/cookies.js +68 -1
  7. package/dist/src/capabilities/download.js +32 -1
  8. package/dist/src/capabilities/fingerprint.js +62 -1
  9. package/dist/src/capabilities/http.js +101 -1
  10. package/dist/src/capabilities/login/login-helper.js +185 -1
  11. package/dist/src/capabilities/login/login-strategy.js +36 -1
  12. package/dist/src/challenges/perimeterx.d.ts +62 -0
  13. package/dist/src/challenges/perimeterx.js +112 -0
  14. package/dist/src/cli/ext.js +338 -1
  15. package/dist/src/core/context.d.ts +2 -2
  16. package/dist/src/core/context.js +137 -1
  17. package/dist/src/core/define-site.js +74 -1
  18. package/dist/src/core/loader.js +142 -1
  19. package/dist/src/core/registry.js +332 -1
  20. package/dist/src/core/runtime.d.ts +12 -4
  21. package/dist/src/core/runtime.js +98 -1
  22. package/dist/src/env.js +34 -1
  23. package/dist/src/sites/bloomberg.com/index.d.ts +11 -0
  24. package/dist/src/sites/bloomberg.com/index.js +49 -0
  25. package/dist/src/sites/bloomberg.com/openapi.yaml +38 -0
  26. package/dist/src/sites/chase.com/download-helper.js +266 -1
  27. package/dist/src/sites/chase.com/index.js +87 -1
  28. package/dist/src/sites/chase.com/openapi.yaml +76 -0
  29. package/dist/src/sites/chatgpt.com/index.js +24 -1
  30. package/dist/src/sites/chatgpt.com/openapi.yaml +29 -0
  31. package/dist/src/sites/claude.ai/claude-helpers.js +26 -1
  32. package/dist/src/sites/claude.ai/index.js +42 -1
  33. package/dist/src/sites/claude.ai/openapi.yaml +54 -0
  34. package/dist/src/sites/cursor.com/index.js +12 -1
  35. package/dist/src/sites/cursor.com/openapi.yaml +39 -0
  36. package/dist/src/sites/e-zpassny.com/index.d.ts +2 -0
  37. package/dist/src/sites/e-zpassny.com/index.js +344 -0
  38. package/dist/src/sites/e-zpassny.com/openapi.yaml +68 -0
  39. package/dist/src/sites/gemini.google.com/index.js +80 -1
  40. package/dist/src/sites/gemini.google.com/openapi.yaml +39 -0
  41. package/dist/src/sites/google.com/google-helpers.js +255 -1
  42. package/dist/src/sites/google.com/index.js +253 -1
  43. package/dist/src/sites/google.com/openapi.yaml +59 -0
  44. package/dist/src/sites/ollama.com/index.js +43 -1
  45. package/dist/src/sites/ollama.com/openapi.yaml +39 -0
  46. package/dist/src/sites/perplexity.ai/index.js +253 -1
  47. package/dist/src/sites/perplexity.ai/openapi.yaml +51 -0
  48. package/dist/src/sites/pseg.com/index.js +243 -1
  49. package/dist/src/sites/pseg.com/openapi.yaml +42 -0
  50. package/dist/src/sites/pseg.com/pseg-helpers.js +53 -1
  51. package/dist/src/sites/voice.google.com/index.d.ts +2 -0
  52. package/dist/src/sites/voice.google.com/index.js +122 -0
  53. package/dist/src/sites/voice.google.com/openapi.yaml +67 -0
  54. package/dist/src/sites/voice.google.com/voice-helpers.d.ts +105 -0
  55. package/dist/src/sites/voice.google.com/voice-helpers.js +181 -0
  56. package/dist/src/sites/zillow.com/index.d.ts +2 -0
  57. package/dist/src/sites/zillow.com/index.js +303 -0
  58. package/dist/src/sites/zillow.com/openapi.yaml +55 -0
  59. package/dist/src/types.d.ts +7 -0
  60. package/dist/src/types.js +1 -1
  61. package/dist/src/util/args-parser.js +145 -1
  62. package/dist/src/util/google-json.js +74 -1
  63. package/dist/src/website-api.d.ts +7 -7
  64. package/dist/src/website-api.js +13 -1
  65. package/package.json +37 -10
@@ -0,0 +1,39 @@
1
+ # Generated by `pnpm generate:openapi` — do not edit by hand.
2
+ openapi: 3.1.0
3
+ info:
4
+ title: Ollama Usage
5
+ description: Fetches Ollama plan and usage details from the authenticated settings page.
6
+ version: 1.1.3
7
+ servers:
8
+ - url: https://ollama.com
9
+ paths:
10
+ /settings:
11
+ get:
12
+ summary: "Ollama Usage: GET /settings"
13
+ description: Fetches Ollama plan and usage details from the authenticated settings page.
14
+ operationId: ollama_usage_get__settings
15
+ responses:
16
+ "200":
17
+ description: Raw text/HTML body
18
+ security:
19
+ - chromeSession: []
20
+ components:
21
+ securitySchemes:
22
+ chromeSession:
23
+ type: apiKey
24
+ in: cookie
25
+ name: session
26
+ description: "Authenticated via the user's real Chrome session: website-api injects decrypted Chrome
27
+ cookies for ollama.com into every request."
28
+ x-website-api:
29
+ id: ollama-usage
30
+ domain: ollama.com
31
+ cookieDomain: ollama.com
32
+ transport: http
33
+ cookies: required
34
+ requiresLogin: true
35
+ imperative: false
36
+ cli:
37
+ command: website-api ollama-usage
38
+ positionals: []
39
+ parameters: []
@@ -1 +1,253 @@
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}}});
1
+ import { randomUUID } from "node:crypto";
2
+ import { defineSite } from "../../core/define-site.js";
3
+ const ENDPOINT = "https://www.perplexity.ai/rest/sse/perplexity_ask";
4
+ // ───────────────────────── SSE frame reducers (pure, testable) ─────────────────────────
5
+ function decodePointerPart(s) {
6
+ return s.replace(/~1/g, "/").replace(/~0/g, "~");
7
+ }
8
+ export function applyPatchOne(doc, patch) {
9
+ const op = patch.op;
10
+ const path = patch.path ?? "";
11
+ if (path === "") {
12
+ if (op === "replace" || op === "add")
13
+ return patch.value;
14
+ if (op === "remove")
15
+ return null;
16
+ }
17
+ const parts = path.split("/").slice(1).map(decodePointerPart);
18
+ if (doc == null)
19
+ doc = /^\d+$/.test(parts[0] ?? "") ? [] : {};
20
+ let parent = doc;
21
+ for (let i = 0; i < parts.length - 1; i++) {
22
+ const key = parts[i];
23
+ const nextIsArray = /^\d+$/.test(parts[i + 1] ?? "");
24
+ if (Array.isArray(parent)) {
25
+ const idx = Number(key);
26
+ while (parent.length <= idx)
27
+ parent.push(nextIsArray ? [] : {});
28
+ parent = parent[idx];
29
+ }
30
+ else {
31
+ parent[key] ??= nextIsArray ? [] : {};
32
+ parent = parent[key];
33
+ }
34
+ }
35
+ const last = parts.at(-1);
36
+ if (last == null)
37
+ return doc;
38
+ if (Array.isArray(parent)) {
39
+ const idx = last === "-" ? parent.length : Number(last);
40
+ if (op === "add")
41
+ parent.splice(idx, 0, patch.value);
42
+ else if (op === "replace")
43
+ parent[idx] = patch.value;
44
+ else if (op === "remove")
45
+ parent.splice(idx, 1);
46
+ }
47
+ else if (op === "remove") {
48
+ delete parent[last];
49
+ }
50
+ else if (op === "add" || op === "replace") {
51
+ parent[last] = patch.value;
52
+ }
53
+ return doc;
54
+ }
55
+ export function applyBlockState(state, data) {
56
+ for (const block of data?.blocks ?? []) {
57
+ const usage = block.intended_usage;
58
+ for (const [key, value] of Object.entries(block)) {
59
+ if (key === "intended_usage" || key === "diff_block")
60
+ continue;
61
+ if (usage)
62
+ state[usage] = value;
63
+ }
64
+ const diff = block.diff_block;
65
+ if (!diff?.field)
66
+ continue;
67
+ state[diff.field] ??= null;
68
+ for (const patch of diff.patches ?? []) {
69
+ state[diff.field] = applyPatchOne(state[diff.field], patch);
70
+ }
71
+ }
72
+ }
73
+ export function extractAnswer(final, state) {
74
+ for (const block of final?.blocks ?? []) {
75
+ const mb = block.markdown_block;
76
+ if (mb?.answer != null)
77
+ return mb.answer;
78
+ if (Array.isArray(mb?.chunks))
79
+ return mb.chunks.join("");
80
+ }
81
+ for (const key of ["ask_text", "ask_text_0_markdown", "markdown_block"]) {
82
+ const v = state[key];
83
+ if (v?.answer != null)
84
+ return v.answer;
85
+ if (Array.isArray(v?.chunks))
86
+ return v.chunks.join("");
87
+ }
88
+ return null;
89
+ }
90
+ /** Reduces a list of parsed SSE frames into the final answer + state. */
91
+ export function reduceFrames(frames) {
92
+ const state = {};
93
+ let final = null;
94
+ for (const data of frames) {
95
+ applyBlockState(state, data);
96
+ if (data.final || data.status === "COMPLETED" || data.text_completed)
97
+ final = data;
98
+ }
99
+ if (!final && frames.length)
100
+ final = frames[frames.length - 1];
101
+ return { state, final, answer: extractAnswer(final, state), chunks: frames };
102
+ }
103
+ export function makeDefaultBody() {
104
+ return {
105
+ params: {
106
+ attachments: [],
107
+ language: "en-US",
108
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "America/New_York",
109
+ search_focus: "internet",
110
+ sources: ["web"],
111
+ frontend_uuid: randomUUID(),
112
+ mode: "copilot",
113
+ model_preference: "claude46sonnet",
114
+ is_related_query: false,
115
+ is_sponsored: false,
116
+ frontend_context_uuid: randomUUID(),
117
+ prompt_source: "user",
118
+ query_source: "home",
119
+ is_incognito: false,
120
+ time_from_first_type: 1,
121
+ local_search_enabled: false,
122
+ use_schematized_api: true,
123
+ send_back_text_in_streaming_api: false,
124
+ supported_block_use_cases: [
125
+ "answer_modes",
126
+ "media_items",
127
+ "knowledge_cards",
128
+ "inline_entity_cards",
129
+ "place_widgets",
130
+ "finance_widgets",
131
+ "prediction_market_widgets",
132
+ "sports_widgets",
133
+ "flight_status_widgets",
134
+ "news_widgets",
135
+ "shopping_widgets",
136
+ "jobs_widgets",
137
+ "search_result_widgets",
138
+ "inline_images",
139
+ "inline_assets",
140
+ "placeholder_cards",
141
+ "diff_blocks",
142
+ "inline_knowledge_cards",
143
+ "entity_group_v2",
144
+ "refinement_filters",
145
+ "canvas_mode",
146
+ "maps_preview",
147
+ "answer_tabs",
148
+ "price_comparison_widgets",
149
+ "preserve_latex",
150
+ "generic_onboarding_widgets",
151
+ "in_context_suggestions",
152
+ "pending_followups",
153
+ "inline_claims",
154
+ "unified_assets",
155
+ "workflow_steps",
156
+ "background_agents",
157
+ ],
158
+ client_coordinates: null,
159
+ mentions: [],
160
+ dsl_query: "",
161
+ skip_search_enabled: true,
162
+ is_nav_suggestions_disabled: false,
163
+ source: "default",
164
+ always_search_override: false,
165
+ override_no_search: false,
166
+ should_ask_for_mcp_tool_confirmation: true,
167
+ browser_agent_allow_once_from_toggle: false,
168
+ force_enable_browser_agent: false,
169
+ supported_features: ["browser_agent_permission_banner_v1.1"],
170
+ extended_context: false,
171
+ version: "2.18",
172
+ },
173
+ query_str: "",
174
+ };
175
+ }
176
+ export default defineSite({
177
+ id: "perplexity",
178
+ name: "Perplexity AI Ask",
179
+ domain: "perplexity.ai",
180
+ description: "Fetches live streaming answers from Perplexity AI using its private REST/SSE API.",
181
+ positionals: [
182
+ {
183
+ name: "question",
184
+ description: "The query or question to ask Perplexity AI",
185
+ required: true,
186
+ variadic: true,
187
+ },
188
+ ],
189
+ parameters: [
190
+ {
191
+ name: "model",
192
+ type: "string",
193
+ description: "Model preference (e.g. 'claude46sonnet')",
194
+ default: "claude46sonnet",
195
+ short: "m",
196
+ },
197
+ { name: "out", type: "string", description: "Write decoded response JSON to file instead of stdout" },
198
+ { name: "timeout", type: "number", description: "Request timeout in milliseconds", default: 75000 },
199
+ { name: "text", type: "boolean", description: "Print only the extracted text answer", short: "t" },
200
+ ],
201
+ run: async (ctx) => {
202
+ const question = ctx.options.question;
203
+ const model = ctx.options.model || "claude46sonnet";
204
+ const timeout = ctx.options.timeout !== undefined ? Number(ctx.options.timeout) : 75000;
205
+ if (ctx.cookies().length === 0) {
206
+ throw new Error("No login found in browser. Please log in to perplexity.ai in Google Chrome.");
207
+ }
208
+ const body = makeDefaultBody();
209
+ body.params.frontend_uuid = randomUUID();
210
+ body.params.frontend_context_uuid = randomUUID();
211
+ body.params.model_preference = model;
212
+ body.params.dsl_query = question;
213
+ body.params.time_from_first_type = 1;
214
+ body.query_str = question;
215
+ let sse;
216
+ try {
217
+ sse = await ctx.http.sse(ENDPOINT, {
218
+ method: "POST",
219
+ headers: {
220
+ "content-type": "application/json",
221
+ origin: "https://www.perplexity.ai",
222
+ referer: "https://www.perplexity.ai/",
223
+ "x-perplexity-request-endpoint": ENDPOINT,
224
+ "x-request-id": body.params.frontend_uuid,
225
+ },
226
+ body: JSON.stringify(body),
227
+ signal: AbortSignal.timeout(timeout),
228
+ });
229
+ }
230
+ catch (err) {
231
+ if (err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
232
+ throw new Error(`Perplexity request timed out after ${timeout}ms.`);
233
+ }
234
+ throw err;
235
+ }
236
+ const decoded = reduceFrames(sse.frames);
237
+ return {
238
+ endpoint: ENDPOINT,
239
+ http_code: sse.status,
240
+ content_type: sse.contentType,
241
+ request: {
242
+ query: question,
243
+ model_preference: model,
244
+ frontend_uuid: body.params.frontend_uuid,
245
+ frontend_context_uuid: body.params.frontend_context_uuid,
246
+ },
247
+ answer: decoded.answer,
248
+ state: decoded.state,
249
+ final: decoded.final,
250
+ chunks: decoded.chunks,
251
+ };
252
+ },
253
+ });
@@ -0,0 +1,51 @@
1
+ # Generated by `pnpm generate:openapi` — do not edit by hand.
2
+ openapi: 3.1.0
3
+ info:
4
+ title: Perplexity AI Ask
5
+ description: Fetches live streaming answers from Perplexity AI using its private REST/SSE API.
6
+ version: 1.1.3
7
+ servers:
8
+ - url: https://perplexity.ai
9
+ paths: {}
10
+ components:
11
+ securitySchemes:
12
+ chromeSession:
13
+ type: apiKey
14
+ in: cookie
15
+ name: session
16
+ description: "Authenticated via the user's real Chrome session: website-api injects decrypted Chrome
17
+ cookies for perplexity.ai into every request."
18
+ x-website-api:
19
+ id: perplexity
20
+ domain: perplexity.ai
21
+ cookieDomain: perplexity.ai
22
+ transport: http
23
+ cookies: required
24
+ requiresLogin: true
25
+ imperative: true
26
+ cli:
27
+ command: website-api perplexity
28
+ positionals:
29
+ - name: question
30
+ description: The query or question to ask Perplexity AI
31
+ required: true
32
+ variadic: true
33
+ parameters:
34
+ - flag: --model
35
+ type: string
36
+ description: Model preference (e.g. 'claude46sonnet')
37
+ default: claude46sonnet
38
+ required: false
39
+ - flag: --out
40
+ type: string
41
+ description: Write decoded response JSON to file instead of stdout
42
+ required: false
43
+ - flag: --timeout
44
+ type: number
45
+ description: Request timeout in milliseconds
46
+ default: 75000
47
+ required: false
48
+ - flag: --text
49
+ type: boolean
50
+ description: Print only the extracted text answer
51
+ required: false
@@ -1 +1,243 @@
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)}});
1
+ import { defineSite } from "../../core/define-site.js";
2
+ import { formatPropertyLabel, inferServiceTypeFromPropertyTitle, intervalToValue, normalizeInterval, } from "./pseg-helpers.js";
3
+ const DASHBOARD_URL = "https://mysmartenergy.nj.pseg.com/Dashboard";
4
+ async function gotoDashboardIfNeeded(ctx, page) {
5
+ const url = page.url().toLowerCase();
6
+ if (!url.startsWith("https://mysmartenergy.nj.pseg.com/dashboard")) {
7
+ if (ctx.debug)
8
+ console.log(`Navigating to Dashboard from '${page.url()}'...`);
9
+ await page.goto(DASHBOARD_URL, { waitUntil: "domcontentloaded" });
10
+ const isLoginVisible = await page
11
+ .locator("#LoginEmail")
12
+ .isVisible()
13
+ .catch(() => false);
14
+ if (isLoginVisible) {
15
+ if (ctx.debug)
16
+ console.log("Session expired or redirected to login. Re-authenticating...");
17
+ if (ctx.site.auth) {
18
+ await ctx.site.auth.ensureLoggedIn({ page, debug: ctx.debug, getCredentials: ctx.credentials });
19
+ }
20
+ }
21
+ }
22
+ }
23
+ async function ensurePropertyOverlayOpen(ctx, page) {
24
+ await gotoDashboardIfNeeded(ctx, page);
25
+ const searchVisible = await page
26
+ .locator('.selectPropertyContainer input[placeholder="Search"]')
27
+ .isVisible()
28
+ .catch(() => false);
29
+ if (!searchVisible) {
30
+ const link = page.getByRole("link", { name: "Select Property" });
31
+ await link.waitFor({ state: "visible", timeout: 5000 }).catch(() => { });
32
+ try {
33
+ await link.click();
34
+ }
35
+ catch {
36
+ await page.locator('a:has-text("Select Property")').first().click();
37
+ }
38
+ await page.waitForSelector(".selectPropertyContainer", { state: "visible", timeout: 5000 });
39
+ }
40
+ }
41
+ async function getProperties(ctx, page) {
42
+ await ensurePropertyOverlayOpen(ctx, page);
43
+ return page.evaluate(() => {
44
+ const lis = Array.from(document.querySelectorAll(".selectPropertyContainer li"));
45
+ return lis.map((li) => {
46
+ const h4s = Array.from(li.querySelectorAll("h4"));
47
+ return {
48
+ propertyId: li.getAttribute("data-property-id") || "",
49
+ propertyType: li.getAttribute("data-property-type") || "",
50
+ title: li.querySelector("h2")?.textContent?.trim() || "",
51
+ owner: h4s[0]?.textContent?.trim() || "",
52
+ address: h4s[1]?.textContent?.trim() || "",
53
+ isCurrent: li.classList.contains("current"),
54
+ };
55
+ });
56
+ });
57
+ }
58
+ async function selectProperty(_ctx, page, property) {
59
+ await page.goto(`https://mysmartenergy.nj.pseg.com/Dashboard/SetMeterGroup?meterGroupId=${property.propertyId}`, { waitUntil: "domcontentloaded" });
60
+ await page.waitForTimeout(1000);
61
+ return {
62
+ label: formatPropertyLabel(property),
63
+ title: property.title,
64
+ serviceType: inferServiceTypeFromPropertyTitle(property.title),
65
+ };
66
+ }
67
+ async function openDownloadPanelIfNeeded(page, debug) {
68
+ const sel = "#downloadOptions";
69
+ if (await page
70
+ .locator(sel)
71
+ .isVisible()
72
+ .catch(() => false))
73
+ return;
74
+ if (debug)
75
+ console.log("Clicking 'Data' link...");
76
+ try {
77
+ await page.getByRole("link", { name: "Data" }).click();
78
+ }
79
+ catch {
80
+ await page.locator('a:has-text("Data")').first().click();
81
+ }
82
+ try {
83
+ await page.waitForSelector(sel, { state: "visible", timeout: 5000 });
84
+ return;
85
+ }
86
+ catch { }
87
+ try {
88
+ await page.getByRole("link", { name: "download" }).click();
89
+ }
90
+ catch {
91
+ await page.locator('a:has-text("download")').first().click();
92
+ }
93
+ await page.waitForSelector(sel, { state: "visible", timeout: 10000 });
94
+ }
95
+ async function setNativeSelectValue(page, selectName, value) {
96
+ return page.evaluate(({ selector, value }) => {
97
+ const select = document.querySelector(selector);
98
+ if (!select)
99
+ throw new Error(`${selector} select not found`);
100
+ select.value = value;
101
+ select.dispatchEvent(new Event("change", { bubbles: true }));
102
+ return { value: select.value };
103
+ }, { selector: `select[name="${selectName}"]`, value });
104
+ }
105
+ async function getSelectOptions(page, selectName) {
106
+ return page.evaluate((selector) => {
107
+ const select = document.querySelector(selector);
108
+ if (!select)
109
+ throw new Error(`${selector} select not found`);
110
+ return {
111
+ value: select.value,
112
+ options: Array.from(select.options).map((o) => ({ value: o.value, text: o.textContent?.trim() || "" })),
113
+ };
114
+ }, `select[name="${selectName}"]`);
115
+ }
116
+ async function setDownloadOptions(page, serviceType, interval) {
117
+ const serviceValue = serviceType === "Gas" ? "4" : "1";
118
+ if (serviceType === "Gas" && interval !== "Billing") {
119
+ throw new Error("Gas usage only supports the Billing interval.");
120
+ }
121
+ const intervalValue = intervalToValue(interval);
122
+ if (!intervalValue)
123
+ throw new Error(`Unsupported interval mapping: ${interval}`);
124
+ await page.waitForSelector('select[name="SelectedServiceType"]', { state: "attached", timeout: 5000 });
125
+ const serviceState = await setNativeSelectValue(page, "SelectedServiceType", serviceValue);
126
+ if (serviceState.value !== serviceValue) {
127
+ throw new Error(`Failed to set service type: ${JSON.stringify(serviceState)}`);
128
+ }
129
+ await page.waitForTimeout(500);
130
+ await page.waitForSelector('select[name="SelectedInterval"]', { state: "attached", timeout: 5000 });
131
+ const before = await getSelectOptions(page, "SelectedInterval");
132
+ if (!new Set(before.options.map((o) => o.value)).has(intervalValue)) {
133
+ const available = before.options.map((o) => o.text).join(", ");
134
+ throw new Error(`Interval ${interval} is not available for ${serviceType}. Available: ${available}`);
135
+ }
136
+ const state = before.value === intervalValue
137
+ ? before
138
+ : await setNativeSelectValue(page, "SelectedInterval", intervalValue);
139
+ if (state.value !== intervalValue)
140
+ throw new Error(`Failed to set interval: ${JSON.stringify(state)}`);
141
+ return interval;
142
+ }
143
+ async function fetchDownloadCsvText(page) {
144
+ return page.evaluate(async () => {
145
+ const form = document.querySelector("#downloadOptions");
146
+ if (!form)
147
+ throw new Error("Download form not found");
148
+ const response = await fetch(form.action, {
149
+ method: "POST",
150
+ body: new FormData(form),
151
+ credentials: "same-origin",
152
+ });
153
+ if (!response.ok)
154
+ throw new Error(`Download request failed with status ${response.status} ${response.statusText}`);
155
+ return response.text();
156
+ });
157
+ }
158
+ export default defineSite({
159
+ id: "pseg-usage",
160
+ name: "PSEG Usage",
161
+ domain: "mysmartenergy.nj.pseg.com",
162
+ description: "Downloads PSEG Smart Energy usage data (CSV) or lists available properties.",
163
+ transport: "browser",
164
+ cookies: "optional",
165
+ // Keep the authenticated tab open so the PSEG session survives for the next command.
166
+ keepBrowserOpen: true,
167
+ // Declarative login — passed as a plain config; the framework wraps it.
168
+ auth: {
169
+ intendedUrl: DASHBOARD_URL,
170
+ emailSelector: "#LoginEmail",
171
+ passwordSelector: "#LoginPassword",
172
+ submitButtonSelector: "button.loginBtn",
173
+ delayMs: 1000,
174
+ },
175
+ positionals: [
176
+ {
177
+ name: "property",
178
+ description: "Property name (e.g. '100 Electric') or 1-based index.",
179
+ required: false,
180
+ },
181
+ ],
182
+ parameters: [
183
+ {
184
+ name: "interval",
185
+ type: "string",
186
+ description: "15, 30, hourly, daily, weekly, monthly, billing",
187
+ default: "billing",
188
+ short: "i",
189
+ },
190
+ {
191
+ name: "list",
192
+ type: "boolean",
193
+ description: "List all downloadable properties instead of downloading",
194
+ short: "l",
195
+ },
196
+ ],
197
+ run: async (ctx) => {
198
+ const page = await ctx.browser();
199
+ if (ctx.options.list) {
200
+ const properties = await getProperties(ctx, page);
201
+ const out = ["Downloadable properties:"];
202
+ for (const p of properties) {
203
+ const tag = p.isCurrent ? " [current]" : "";
204
+ out.push(`${formatPropertyLabel(p)}${tag} | ${p.address} | ${p.owner}`);
205
+ }
206
+ return out.join("\n");
207
+ }
208
+ const propertyOption = ctx.options.property;
209
+ if (!propertyOption)
210
+ throw new Error("Missing required argument: <property> or --list");
211
+ const interval = normalizeInterval(ctx.options.interval);
212
+ const properties = await getProperties(ctx, page);
213
+ const clean = String(propertyOption).trim();
214
+ let property;
215
+ if (/^\d+$/.test(clean)) {
216
+ const idx = Number(clean);
217
+ if (idx < 1 || idx > properties.length) {
218
+ throw new Error(`Property index ${idx} is out of range. Use 1-${properties.length}.`);
219
+ }
220
+ property = properties[idx - 1];
221
+ }
222
+ else {
223
+ const target = clean.toLowerCase();
224
+ property =
225
+ properties.find((p) => p.title.trim().toLowerCase() === target) ??
226
+ properties.find((p) => formatPropertyLabel(p).toLowerCase() === target);
227
+ if (!property) {
228
+ const available = properties
229
+ .map((p) => formatPropertyLabel(p))
230
+ .filter(Boolean)
231
+ .join(", ");
232
+ throw new Error(`Property not found: ${propertyOption}. Available properties: ${available}`);
233
+ }
234
+ }
235
+ const selected = await selectProperty(ctx, page, property);
236
+ if (ctx.debug)
237
+ console.log(`Downloading ${selected.label} usage CSV for ${interval}...`);
238
+ await gotoDashboardIfNeeded(ctx, page);
239
+ await openDownloadPanelIfNeeded(page, ctx.debug);
240
+ await setDownloadOptions(page, selected.serviceType, interval);
241
+ return fetchDownloadCsvText(page);
242
+ },
243
+ });
@@ -0,0 +1,42 @@
1
+ # Generated by `pnpm generate:openapi` — do not edit by hand.
2
+ openapi: 3.1.0
3
+ info:
4
+ title: PSEG Usage
5
+ description: Downloads PSEG Smart Energy usage data (CSV) or lists available properties.
6
+ version: 1.1.3
7
+ servers:
8
+ - url: https://mysmartenergy.nj.pseg.com
9
+ paths: {}
10
+ components:
11
+ securitySchemes:
12
+ chromeSession:
13
+ type: apiKey
14
+ in: cookie
15
+ name: session
16
+ description: "Authenticated via the user's real Chrome session: website-api injects decrypted Chrome
17
+ cookies for mysmartenergy.nj.pseg.com into every request."
18
+ x-website-api:
19
+ id: pseg-usage
20
+ domain: mysmartenergy.nj.pseg.com
21
+ cookieDomain: mysmartenergy.nj.pseg.com
22
+ transport: browser
23
+ cookies: optional
24
+ requiresLogin: true
25
+ imperative: true
26
+ cli:
27
+ command: website-api pseg-usage
28
+ positionals:
29
+ - name: property
30
+ description: Property name (e.g. '100 Electric') or 1-based index.
31
+ required: false
32
+ variadic: false
33
+ parameters:
34
+ - flag: --interval
35
+ type: string
36
+ description: 15, 30, hourly, daily, weekly, monthly, billing
37
+ default: billing
38
+ required: false
39
+ - flag: --list
40
+ type: boolean
41
+ description: List all downloadable properties instead of downloading
42
+ required: false