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.
- package/README.md +141 -1
- package/dist/bin/cli.js +204 -1
- package/dist/src/capabilities/browser.d.ts +8 -2
- package/dist/src/capabilities/browser.js +106 -1
- package/dist/src/capabilities/cookies.d.ts +7 -1
- package/dist/src/capabilities/cookies.js +68 -1
- package/dist/src/capabilities/download.js +32 -1
- package/dist/src/capabilities/fingerprint.js +62 -1
- package/dist/src/capabilities/http.js +101 -1
- package/dist/src/capabilities/login/login-helper.js +185 -1
- package/dist/src/capabilities/login/login-strategy.js +36 -1
- package/dist/src/challenges/perimeterx.d.ts +62 -0
- package/dist/src/challenges/perimeterx.js +112 -0
- package/dist/src/cli/ext.js +338 -1
- package/dist/src/core/context.d.ts +2 -2
- package/dist/src/core/context.js +137 -1
- package/dist/src/core/define-site.js +74 -1
- package/dist/src/core/loader.js +142 -1
- package/dist/src/core/registry.js +332 -1
- package/dist/src/core/runtime.d.ts +12 -4
- package/dist/src/core/runtime.js +98 -1
- package/dist/src/env.js +34 -1
- package/dist/src/sites/bloomberg.com/index.d.ts +11 -0
- package/dist/src/sites/bloomberg.com/index.js +49 -0
- package/dist/src/sites/bloomberg.com/openapi.yaml +38 -0
- package/dist/src/sites/chase.com/download-helper.js +266 -1
- package/dist/src/sites/chase.com/index.js +87 -1
- package/dist/src/sites/chase.com/openapi.yaml +76 -0
- package/dist/src/sites/chatgpt.com/index.js +24 -1
- package/dist/src/sites/chatgpt.com/openapi.yaml +29 -0
- package/dist/src/sites/claude.ai/claude-helpers.js +26 -1
- package/dist/src/sites/claude.ai/index.js +42 -1
- package/dist/src/sites/claude.ai/openapi.yaml +54 -0
- package/dist/src/sites/cursor.com/index.js +12 -1
- package/dist/src/sites/cursor.com/openapi.yaml +39 -0
- package/dist/src/sites/e-zpassny.com/index.d.ts +2 -0
- package/dist/src/sites/e-zpassny.com/index.js +344 -0
- package/dist/src/sites/e-zpassny.com/openapi.yaml +68 -0
- package/dist/src/sites/gemini.google.com/index.js +80 -1
- package/dist/src/sites/gemini.google.com/openapi.yaml +39 -0
- package/dist/src/sites/google.com/google-helpers.js +255 -1
- package/dist/src/sites/google.com/index.js +253 -1
- package/dist/src/sites/google.com/openapi.yaml +59 -0
- package/dist/src/sites/ollama.com/index.js +43 -1
- package/dist/src/sites/ollama.com/openapi.yaml +39 -0
- package/dist/src/sites/perplexity.ai/index.js +253 -1
- package/dist/src/sites/perplexity.ai/openapi.yaml +51 -0
- package/dist/src/sites/pseg.com/index.js +243 -1
- package/dist/src/sites/pseg.com/openapi.yaml +42 -0
- package/dist/src/sites/pseg.com/pseg-helpers.js +53 -1
- package/dist/src/sites/voice.google.com/index.d.ts +2 -0
- package/dist/src/sites/voice.google.com/index.js +122 -0
- package/dist/src/sites/voice.google.com/openapi.yaml +67 -0
- package/dist/src/sites/voice.google.com/voice-helpers.d.ts +105 -0
- package/dist/src/sites/voice.google.com/voice-helpers.js +181 -0
- package/dist/src/sites/zillow.com/index.d.ts +2 -0
- package/dist/src/sites/zillow.com/index.js +303 -0
- package/dist/src/sites/zillow.com/openapi.yaml +55 -0
- package/dist/src/types.d.ts +7 -0
- package/dist/src/types.js +1 -1
- package/dist/src/util/args-parser.js +145 -1
- package/dist/src/util/google-json.js +74 -1
- package/dist/src/website-api.d.ts +7 -7
- package/dist/src/website-api.js +13 -1
- 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
|
|
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
|
|
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
|