continente-mcp 3.1.0
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/.env.example +19 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/check-change.js +157 -0
- package/continente-backup.js +166 -0
- package/continente-cookie-reader.py +164 -0
- package/continente-keepalive.py +135 -0
- package/package.json +55 -0
- package/skills/groceries/SKILL.md +61 -0
- package/src/auth.js +180 -0
- package/src/changedetector.js +158 -0
- package/src/index.js +613 -0
- package/src/utils.js +15 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
continente-cookie-reader — export Continente.pt cookies from your browser.
|
|
4
|
+
|
|
5
|
+
Reads browser cookies for continente.pt and writes them to ~/.continente/cookies.json
|
|
6
|
+
so the MCP server can use them. Auto-detects your browser or let you specify one.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 continente-cookie-reader.py # auto-detect
|
|
10
|
+
python3 continente-cookie-reader.py --browser chrome
|
|
11
|
+
python3 continente-cookie-reader.py --browser firefox
|
|
12
|
+
python3 continente-cookie-reader.py --output /path/to/cookies.json
|
|
13
|
+
python3 continente-cookie-reader.py --list-browsers
|
|
14
|
+
|
|
15
|
+
Requirements:
|
|
16
|
+
pip install browser-cookie3
|
|
17
|
+
|
|
18
|
+
Environment variables:
|
|
19
|
+
CONTINENTE_COOKIES_PATH Override the default output path (~/.continente/cookies.json)
|
|
20
|
+
CONTINENTE_VAULT_COOKIE_PATH If set, also write cookies to this path (useful for syncing
|
|
21
|
+
to a second machine via a shared folder, e.g. Obsidian vault)
|
|
22
|
+
"""
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import stat
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
DOMAIN = "continente.pt"
|
|
31
|
+
DEFAULT_OUTPUT = Path(
|
|
32
|
+
os.environ.get("CONTINENTE_COOKIES_PATH", str(Path.home() / ".continente" / "cookies.json"))
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# If set, cookies are also written here (e.g. for syncing to a remote machine)
|
|
36
|
+
VAULT_PATH = Path(os.environ["CONTINENTE_VAULT_COOKIE_PATH"]) if "CONTINENTE_VAULT_COOKIE_PATH" in os.environ else None
|
|
37
|
+
|
|
38
|
+
# Tried in this order when auto-detecting
|
|
39
|
+
BROWSERS = ["arc", "chrome", "edge", "brave", "firefox", "chromium", "vivaldi", "opera", "safari"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_browser_cookies(browser_name: str) -> list[dict]:
|
|
43
|
+
"""Read continente.pt cookies from the given browser."""
|
|
44
|
+
import browser_cookie3
|
|
45
|
+
|
|
46
|
+
getter = getattr(browser_cookie3, browser_name, None)
|
|
47
|
+
if getter is None:
|
|
48
|
+
raise ValueError(f"Browser '{browser_name}' not supported by browser-cookie3.")
|
|
49
|
+
|
|
50
|
+
jar = getter(domain_name=DOMAIN)
|
|
51
|
+
cookies = []
|
|
52
|
+
for c in jar:
|
|
53
|
+
cookies.append({
|
|
54
|
+
"domain": c.domain,
|
|
55
|
+
"name": c.name,
|
|
56
|
+
"value": c.value,
|
|
57
|
+
"path": getattr(c, "path", "/"),
|
|
58
|
+
"secure": bool(getattr(c, "secure", False)),
|
|
59
|
+
"httpOnly": c.has_nonstandard_attr("HttpOnly") if hasattr(c, "has_nonstandard_attr") else False,
|
|
60
|
+
"sameSite": "Lax",
|
|
61
|
+
"expires": getattr(c, "expires", None),
|
|
62
|
+
})
|
|
63
|
+
return cookies
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def find_cookies(browser: str | None) -> tuple[list[dict], str]:
|
|
67
|
+
"""Return (cookies, browser_name). Tries specified browser or auto-detects."""
|
|
68
|
+
if browser:
|
|
69
|
+
try:
|
|
70
|
+
cookies = get_browser_cookies(browser)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
print(f"Error reading {browser}: {e}", file=sys.stderr)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
if not cookies:
|
|
75
|
+
print(f"No Continente cookies found in {browser}. Are you logged in?", file=sys.stderr)
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
return cookies, browser
|
|
78
|
+
|
|
79
|
+
print("Auto-detecting browser with Continente cookies...", file=sys.stderr)
|
|
80
|
+
for name in BROWSERS:
|
|
81
|
+
try:
|
|
82
|
+
cookies = get_browser_cookies(name)
|
|
83
|
+
if cookies:
|
|
84
|
+
return cookies, name
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f" {name}: {e}", file=sys.stderr)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
print(
|
|
90
|
+
"\nNo Continente cookies found in any supported browser.\n"
|
|
91
|
+
"Make sure you are logged into continente.pt, then try again.\n"
|
|
92
|
+
f"Supported browsers: {', '.join(BROWSERS)}",
|
|
93
|
+
file=sys.stderr,
|
|
94
|
+
)
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def write_cookie_file(cookies: list[dict], path: Path) -> None:
|
|
99
|
+
"""Write cookies JSON and set owner-only permissions."""
|
|
100
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
path.write_text(json.dumps(cookies, indent=2, ensure_ascii=False))
|
|
102
|
+
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) # 600 — cookies = full account access
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main() -> None:
|
|
106
|
+
parser = argparse.ArgumentParser(
|
|
107
|
+
description="Export Continente.pt browser cookies for use with continente-mcp.",
|
|
108
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
109
|
+
)
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--browser", "-b",
|
|
112
|
+
choices=BROWSERS,
|
|
113
|
+
metavar="BROWSER",
|
|
114
|
+
help=f"Browser to read from (default: auto-detect). One of: {', '.join(BROWSERS)}",
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--output", "-o",
|
|
118
|
+
type=Path,
|
|
119
|
+
default=DEFAULT_OUTPUT,
|
|
120
|
+
help=f"Output path (default: {DEFAULT_OUTPUT})",
|
|
121
|
+
)
|
|
122
|
+
parser.add_argument(
|
|
123
|
+
"--list-browsers",
|
|
124
|
+
action="store_true",
|
|
125
|
+
help="List supported browsers and exit",
|
|
126
|
+
)
|
|
127
|
+
args = parser.parse_args()
|
|
128
|
+
|
|
129
|
+
if args.list_browsers:
|
|
130
|
+
print("Supported browsers: " + ", ".join(BROWSERS))
|
|
131
|
+
sys.exit(0)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
import browser_cookie3 # noqa: F401
|
|
135
|
+
except ImportError:
|
|
136
|
+
print(
|
|
137
|
+
"browser-cookie3 is not installed.\n"
|
|
138
|
+
"Run: pip install browser-cookie3",
|
|
139
|
+
file=sys.stderr,
|
|
140
|
+
)
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
143
|
+
cookies, browser_name = find_cookies(args.browser)
|
|
144
|
+
|
|
145
|
+
write_cookie_file(cookies, args.output)
|
|
146
|
+
print(f"Saved {len(cookies)} cookies → {args.output} (browser: {browser_name})")
|
|
147
|
+
|
|
148
|
+
# Optional: also write to a secondary path (e.g. for syncing to a remote machine)
|
|
149
|
+
if VAULT_PATH:
|
|
150
|
+
existing = []
|
|
151
|
+
if VAULT_PATH.exists():
|
|
152
|
+
try:
|
|
153
|
+
existing = json.loads(VAULT_PATH.read_text())
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
if cookies != existing:
|
|
157
|
+
write_cookie_file(cookies, VAULT_PATH)
|
|
158
|
+
print(f"Saved {len(cookies)} cookies → {VAULT_PATH} (vault sync)")
|
|
159
|
+
else:
|
|
160
|
+
print(f"Vault unchanged — skipping write to {VAULT_PATH}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
main()
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Continente Session Keepalive
|
|
4
|
+
|
|
5
|
+
Pings continente.pt to reset the idle session timeout.
|
|
6
|
+
If the session has expired, re-reads cookies from your browser automatically.
|
|
7
|
+
|
|
8
|
+
Run every 20 minutes via cron or launchd:
|
|
9
|
+
*/20 * * * * python3 /path/to/continente-keepalive.py >> ~/.continente/keepalive.log 2>&1
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import stat
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
|
|
20
|
+
LOCAL_COOKIE = Path(
|
|
21
|
+
os.environ.get("CONTINENTE_COOKIES_PATH", str(Path.home() / ".continente" / "cookies.json"))
|
|
22
|
+
)
|
|
23
|
+
PING_URL = "https://www.continente.pt/on/demandware.store/Sites-continente-Site/default/Account-Show"
|
|
24
|
+
CHECK_URL = "https://www.continente.pt/conta/encomendas/"
|
|
25
|
+
LOG_PREFIX = "[continente-keepalive]"
|
|
26
|
+
|
|
27
|
+
USER_AGENT = (
|
|
28
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
29
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def log(msg: str) -> None:
|
|
34
|
+
print(f"{LOG_PREFIX} {msg}", flush=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_cookies(path: Path) -> list[dict] | None:
|
|
38
|
+
if not path.exists():
|
|
39
|
+
return None
|
|
40
|
+
try:
|
|
41
|
+
data = json.loads(path.read_text())
|
|
42
|
+
return data if isinstance(data, list) else data.get("cookies")
|
|
43
|
+
except Exception as e:
|
|
44
|
+
log(f"Failed to read {path}: {e}")
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def save_cookies(cookies: list[dict]) -> None:
|
|
49
|
+
LOCAL_COOKIE.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
LOCAL_COOKIE.write_text(json.dumps(cookies, indent=2, ensure_ascii=False))
|
|
51
|
+
os.chmod(LOCAL_COOKIE, stat.S_IRUSR | stat.S_IWUSR)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_logged_in(session: requests.Session) -> bool:
|
|
55
|
+
try:
|
|
56
|
+
r = session.get(CHECK_URL, timeout=10, allow_redirects=True)
|
|
57
|
+
return "login" not in r.url and r.status_code == 200
|
|
58
|
+
except Exception:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def refresh_from_browser() -> list[dict] | None:
|
|
63
|
+
"""Re-run continente-cookie-reader.py to pull fresh cookies from the browser."""
|
|
64
|
+
script = Path(__file__).parent / "continente-cookie-reader.py"
|
|
65
|
+
if not script.exists():
|
|
66
|
+
log("continente-cookie-reader.py not found next to this script.")
|
|
67
|
+
return None
|
|
68
|
+
try:
|
|
69
|
+
result = subprocess.run(
|
|
70
|
+
[sys.executable, str(script)],
|
|
71
|
+
capture_output=True, text=True, timeout=30
|
|
72
|
+
)
|
|
73
|
+
output = (result.stdout + result.stderr).strip()
|
|
74
|
+
if output:
|
|
75
|
+
log(f"Cookie reader: {output}")
|
|
76
|
+
return load_cookies(LOCAL_COOKIE)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
log(f"Cookie reader failed: {e}")
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def ping(cookies: list[dict]) -> bool:
|
|
83
|
+
session = requests.Session()
|
|
84
|
+
session.headers.update({
|
|
85
|
+
"User-Agent": USER_AGENT,
|
|
86
|
+
"Accept-Language": "pt-PT,pt;q=0.9",
|
|
87
|
+
})
|
|
88
|
+
for c in cookies:
|
|
89
|
+
if c.get("name") and c["name"] != "undefined":
|
|
90
|
+
session.cookies.set(c["name"], c["value"], domain=c.get("domain", ".continente.pt"))
|
|
91
|
+
|
|
92
|
+
logged_in = is_logged_in(session)
|
|
93
|
+
if logged_in:
|
|
94
|
+
try:
|
|
95
|
+
session.get(PING_URL, timeout=10)
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
return logged_in
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main() -> None:
|
|
102
|
+
cookies = load_cookies(LOCAL_COOKIE)
|
|
103
|
+
|
|
104
|
+
if not cookies:
|
|
105
|
+
log("No cookies found — trying to refresh from browser...")
|
|
106
|
+
cookies = refresh_from_browser()
|
|
107
|
+
if not cookies:
|
|
108
|
+
log("ERROR: No cookies found. Run continente-cookie-reader.py first.")
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
if ping(cookies):
|
|
112
|
+
log("Session alive.")
|
|
113
|
+
save_cookies(cookies)
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
log("Session expired — refreshing from browser...")
|
|
117
|
+
cookies = refresh_from_browser()
|
|
118
|
+
if not cookies:
|
|
119
|
+
log("ERROR: Could not refresh cookies. Run continente-cookie-reader.py manually.")
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
if ping(cookies):
|
|
123
|
+
save_cookies(cookies)
|
|
124
|
+
log("Session restored.")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
log(
|
|
128
|
+
"ERROR: Still not logged in after refresh. "
|
|
129
|
+
"Log into Continente in your browser and re-run continente-cookie-reader.py."
|
|
130
|
+
)
|
|
131
|
+
sys.exit(1)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "continente-mcp",
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"description": "MCP server for Continente.pt — search products, manage your cart, and browse order history from your AI assistant.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"continente-mcp": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18.0.0"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node src/index.js",
|
|
15
|
+
"setup": "playwright install chromium --with-deps",
|
|
16
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"model-context-protocol",
|
|
21
|
+
"continente",
|
|
22
|
+
"supermarket",
|
|
23
|
+
"portugal",
|
|
24
|
+
"claude",
|
|
25
|
+
"ai-assistant",
|
|
26
|
+
"shopping",
|
|
27
|
+
"cart"
|
|
28
|
+
],
|
|
29
|
+
"author": "David Marques",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/davidmarqu3s/continente-mcp.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/davidmarqu3s/continente-mcp",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/davidmarqu3s/continente-mcp/issues"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"src/",
|
|
41
|
+
"skills/",
|
|
42
|
+
"continente-cookie-reader.py",
|
|
43
|
+
"continente-keepalive.py",
|
|
44
|
+
"continente-backup.js",
|
|
45
|
+
"check-change.js",
|
|
46
|
+
".env.example",
|
|
47
|
+
"LICENSE",
|
|
48
|
+
"README.md"
|
|
49
|
+
],
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
52
|
+
"cheerio": "^1.2.0",
|
|
53
|
+
"playwright": "^1.59.1"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: groceries
|
|
3
|
+
description: Adds items to the Continente online shopping basket. Use this skill whenever the user says /groceries, wants to add something to their Continente cart, mentions shopping for groceries, or asks to "add X to the basket/cart". Also trigger for multi-item lists like "add milk, cheese and eggs to Continente". Works by matching item names against the user's favourites and order history before searching, so the right product variant gets added.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Groceries Skill
|
|
7
|
+
|
|
8
|
+
Add one or more items to the Continente shopping basket using the `continente-mcp` tools. Match against the user's purchase history to pick the right product variant.
|
|
9
|
+
|
|
10
|
+
## Input parsing
|
|
11
|
+
|
|
12
|
+
Items may come in various formats:
|
|
13
|
+
- Single item: `add queijo flamengo to the cart`
|
|
14
|
+
- Comma-separated: `/groceries leite, ovos, pão de forma`
|
|
15
|
+
- Natural language: `I need some iogurte and manteiga from Continente`
|
|
16
|
+
|
|
17
|
+
Parse all items before starting. If the input is ambiguous, ask to clarify.
|
|
18
|
+
|
|
19
|
+
## Matching strategy (per item)
|
|
20
|
+
|
|
21
|
+
The goal is to pick the product the user actually buys, not just any product with that name.
|
|
22
|
+
|
|
23
|
+
**1. Search with favourites context**
|
|
24
|
+
|
|
25
|
+
Call `search_products` with the item name. Results are automatically ranked by the favourites list — the top result is usually correct. Note the top 3.
|
|
26
|
+
|
|
27
|
+
**2. Cross-check favourites**
|
|
28
|
+
|
|
29
|
+
Call `get_favorites` once (reuse for all items in this request). If any favourite matches the item name, prefer it — even if not the top search result. A favourite is a stronger signal than search rank.
|
|
30
|
+
|
|
31
|
+
**3. Fallback: order history**
|
|
32
|
+
|
|
33
|
+
Only call `get_order_history` if the result is genuinely uncertain (e.g. several near-identical variants with different sizes/brands and no favourites match). Scan recent orders to break the tie.
|
|
34
|
+
|
|
35
|
+
**4. Pick**
|
|
36
|
+
|
|
37
|
+
If confidence is high (clear favourite or obvious top result), add silently and report afterwards. If genuinely ambiguous between two equally plausible products, show the top 2 and ask the user to pick.
|
|
38
|
+
|
|
39
|
+
## Adding to cart
|
|
40
|
+
|
|
41
|
+
Call `add_to_cart` with `product_id`. Default quantity is 1 unless specified (e.g. "2 pacotes de leite").
|
|
42
|
+
|
|
43
|
+
## Output format
|
|
44
|
+
|
|
45
|
+
After all items are processed, print a compact summary:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
✓ Queijo Flamengo Mil Vacas 400g — added (from favourites)
|
|
49
|
+
✓ Leite Mimosa Meio-Gordo 1L — added
|
|
50
|
+
✗ Pão de Forma — unclear match, please check manually
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Error handling
|
|
54
|
+
|
|
55
|
+
- No search results → report as not found, skip, continue with others
|
|
56
|
+
- `add_to_cart` fails → report the error, move on
|
|
57
|
+
- Never stop the whole run because one item failed
|
|
58
|
+
|
|
59
|
+
## Performance
|
|
60
|
+
|
|
61
|
+
`get_favorites` is slow. Call it once and reuse across all items. Only call `get_order_history` when favourites give no clear signal.
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
|
|
3
|
+
const CONTINENTE_LOGIN_URL = 'https://login.continente.pt/u/login?clientId=NLR6WHyO8Iba4eRS&lang=pt-PT';
|
|
4
|
+
const CONTINENTE_MAIN = 'https://www.continente.pt';
|
|
5
|
+
const SESSION_DIR = `${process.env.HOME}/.continente`;
|
|
6
|
+
|
|
7
|
+
export class Authenticator {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.browser = null;
|
|
10
|
+
this.context = null;
|
|
11
|
+
this.page = null;
|
|
12
|
+
this.cookies = [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async ensureBrowser() {
|
|
16
|
+
if (!this.browser) {
|
|
17
|
+
this.browser = await chromium.launch({ headless: true });
|
|
18
|
+
}
|
|
19
|
+
if (!this.context) {
|
|
20
|
+
this.context = await this.browser.newContext({
|
|
21
|
+
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
|
|
22
|
+
locale: 'pt-PT'
|
|
23
|
+
});
|
|
24
|
+
// Load existing cookies if available
|
|
25
|
+
await this.loadCookies();
|
|
26
|
+
}
|
|
27
|
+
if (!this.page) {
|
|
28
|
+
this.page = await this.context.newPage();
|
|
29
|
+
}
|
|
30
|
+
return this.page;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getCookiesPath() {
|
|
34
|
+
return `${SESSION_DIR}/session.json`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async saveCookies() {
|
|
38
|
+
const fs = await import('fs');
|
|
39
|
+
try {
|
|
40
|
+
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
41
|
+
} catch (e) {}
|
|
42
|
+
const cookies = await this.context.cookies();
|
|
43
|
+
fs.writeFileSync(this.getCookiesPath(), JSON.stringify(cookies, null, 2));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async loadCookies() {
|
|
47
|
+
const fs = await import('fs');
|
|
48
|
+
try {
|
|
49
|
+
const data = fs.readFileSync(this.getCookiesPath(), 'utf8');
|
|
50
|
+
const cookies = JSON.parse(data);
|
|
51
|
+
if (cookies.length > 0) {
|
|
52
|
+
await this.context.addCookies(cookies);
|
|
53
|
+
this.cookies = cookies;
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// No cookies yet
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async isLoggedIn() {
|
|
61
|
+
try {
|
|
62
|
+
const page = await this.ensureBrowser();
|
|
63
|
+
await page.goto(CONTINENTE_MAIN, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
64
|
+
// Look for the account/logged-in state
|
|
65
|
+
const loginLink = await page.$('a[href*="logout"], a[href*="sair"], [class*="user"], [class*="account"]');
|
|
66
|
+
const pageContent = await page.content();
|
|
67
|
+
// If we see a logged-in indicator (e.g., user name in header)
|
|
68
|
+
const isLogged = pageContent.includes('data-testid="user"') ||
|
|
69
|
+
pageContent.includes('class="logged-in') ||
|
|
70
|
+
pageContent.includes('bjzIjUWquR'); // some session marker
|
|
71
|
+
return false; // Conservative - always try to re-auth for now
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async login(email, password) {
|
|
78
|
+
const fs = await import('fs');
|
|
79
|
+
try { fs.mkdirSync(SESSION_DIR, { recursive: true }); } catch (e) {}
|
|
80
|
+
|
|
81
|
+
const page = await this.ensureBrowser();
|
|
82
|
+
|
|
83
|
+
console.error(`[Auth] Starting login flow for ${email}`);
|
|
84
|
+
|
|
85
|
+
// Go to login page
|
|
86
|
+
await page.goto(CONTINENTE_LOGIN_URL, { waitUntil: 'networkidle', timeout: 30000 });
|
|
87
|
+
await page.waitForTimeout(2000);
|
|
88
|
+
|
|
89
|
+
// Step 1: Enter email
|
|
90
|
+
console.error('[Auth] Step 1: Entering email...');
|
|
91
|
+
const emailInput = await page.waitForSelector('input[type="email"], input[type="text"]', { timeout: 10000 }).catch(() => null);
|
|
92
|
+
if (!emailInput) {
|
|
93
|
+
throw new Error('Could not find email input field');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await emailInput.fill(email);
|
|
97
|
+
await page.waitForTimeout(500);
|
|
98
|
+
|
|
99
|
+
// Click continue/next
|
|
100
|
+
const continueBtn = await page.waitForSelector('button[type="submit"], button:has-text("Avançar"), button:has-text("Continuar")', { timeout: 5000 }).catch(() => null);
|
|
101
|
+
if (continueBtn) {
|
|
102
|
+
await continueBtn.click();
|
|
103
|
+
} else {
|
|
104
|
+
await page.keyboard.press('Enter');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await page.waitForTimeout(2000);
|
|
108
|
+
|
|
109
|
+
// Step 2: Enter password
|
|
110
|
+
console.error('[Auth] Step 2: Entering password...');
|
|
111
|
+
const passwordInput = await page.waitForSelector('input[type="password"]', { timeout: 10000 }).catch(() => null);
|
|
112
|
+
if (!passwordInput) {
|
|
113
|
+
// Maybe already logged in or different flow
|
|
114
|
+
const currentUrl = page.url();
|
|
115
|
+
console.error(`[Auth] No password field found. Current URL: ${currentUrl}`);
|
|
116
|
+
throw new Error('Password field not found - login flow may have changed');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await passwordInput.fill(password);
|
|
120
|
+
await page.waitForTimeout(500);
|
|
121
|
+
|
|
122
|
+
// Submit
|
|
123
|
+
const submitBtn = await page.waitForSelector('button[type="submit"], button:has-text("Entrar"), button:has-text("Login")', { timeout: 5000 }).catch(() => null);
|
|
124
|
+
if (submitBtn) {
|
|
125
|
+
await submitBtn.click();
|
|
126
|
+
} else {
|
|
127
|
+
await page.keyboard.press('Enter');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Wait for redirect to main site
|
|
131
|
+
await page.waitForTimeout(3000);
|
|
132
|
+
await page.waitForURL(/continente\.pt/, { timeout: 15000 }).catch(() => {});
|
|
133
|
+
|
|
134
|
+
const finalUrl = page.url();
|
|
135
|
+
console.error(`[Auth] Final URL after login: ${finalUrl}`);
|
|
136
|
+
|
|
137
|
+
// Save session cookies
|
|
138
|
+
await this.saveCookies();
|
|
139
|
+
this.cookies = await this.context.cookies();
|
|
140
|
+
|
|
141
|
+
console.error(`[Auth] Login successful, ${this.cookies.length} cookies saved`);
|
|
142
|
+
|
|
143
|
+
return { success: true, url: finalUrl, cookies: this.cookies.length };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async logout() {
|
|
147
|
+
const fs = await import('fs');
|
|
148
|
+
if (this.page) {
|
|
149
|
+
try {
|
|
150
|
+
await this.page.goto(`${CONTINENTE_MAIN}/logout`, { timeout: 5000 }).catch(() => {});
|
|
151
|
+
} catch (e) {}
|
|
152
|
+
await this.page.close();
|
|
153
|
+
this.page = null;
|
|
154
|
+
}
|
|
155
|
+
if (this.context) {
|
|
156
|
+
await this.context.close();
|
|
157
|
+
this.context = null;
|
|
158
|
+
}
|
|
159
|
+
if (this.browser) {
|
|
160
|
+
await this.browser.close();
|
|
161
|
+
this.browser = null;
|
|
162
|
+
}
|
|
163
|
+
// Remove saved cookies
|
|
164
|
+
try { fs.unlinkSync(this.getCookiesPath()); } catch (e) {}
|
|
165
|
+
console.error('[Auth] Logged out, session cleared');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async getSessionCookies() {
|
|
169
|
+
if (!this.context && !this.browser) {
|
|
170
|
+
await this.ensureBrowser();
|
|
171
|
+
}
|
|
172
|
+
return this.context ? await this.context.cookies() : [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async close() {
|
|
176
|
+
if (this.page) { try { await this.page.close(); } catch (e) {} this.page = null; }
|
|
177
|
+
if (this.context) { try { await this.context.close(); } catch (e) {} this.context = null; }
|
|
178
|
+
if (this.browser) { try { await this.browser.close(); } catch (e) {} this.browser = null; }
|
|
179
|
+
}
|
|
180
|
+
}
|