activity-frames 0.1.0__py3-none-any.whl
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.
- activity_frames/__init__.py +129 -0
- activity_frames/_time.py +86 -0
- activity_frames/capture.py +251 -0
- activity_frames/cli.py +155 -0
- activity_frames/db.py +86 -0
- activity_frames/emit.py +118 -0
- activity_frames/enrich.py +342 -0
- activity_frames/entities.py +416 -0
- activity_frames/frames.py +358 -0
- activity_frames/mcp_server.py +262 -0
- activity_frames/patterns.py +253 -0
- activity_frames/sessionize.py +356 -0
- activity_frames-0.1.0.dist-info/METADATA +157 -0
- activity_frames-0.1.0.dist-info/RECORD +17 -0
- activity_frames-0.1.0.dist-info/WHEEL +4 -0
- activity_frames-0.1.0.dist-info/entry_points.txt +2 -0
- activity_frames-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""Deterministic URL -> page reference parsing.
|
|
2
|
+
|
|
3
|
+
Turns raw browser URLs into typed page references an agent can read at
|
|
4
|
+
a glance: "profile: najmuzzaman" instead of a 120-char URL. Pure string
|
|
5
|
+
parsing, no network, no AI, no guessing about intent.
|
|
6
|
+
|
|
7
|
+
Resolution order (all deterministic):
|
|
8
|
+
1. a site-specific parser (exact host, then apex domain),
|
|
9
|
+
2. a generic search-parameter detector (?q=/?query=),
|
|
10
|
+
3. subdomain/path heuristics (dashboard, sign-in, email, calendar,
|
|
11
|
+
meeting) that type common infrastructure pages without a bespoke
|
|
12
|
+
parser,
|
|
13
|
+
4. a total generic fallback ({kind: "page", domain, path}).
|
|
14
|
+
|
|
15
|
+
Every URL maps to something; typing is never lossy.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from urllib.parse import parse_qs, unquote, urlsplit
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class PageRef:
|
|
25
|
+
kind: str # e.g. "profile", "search", "repo", "video", "page"
|
|
26
|
+
domain: str # normalized host, no "www."
|
|
27
|
+
entity: str | None = None # the human-relevant identifier, if any
|
|
28
|
+
extra: dict = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
def __post_init__(self):
|
|
31
|
+
if self.entity is not None:
|
|
32
|
+
# collapse whitespace runs and trim; drop empties
|
|
33
|
+
self.entity = " ".join(str(self.entity).split()) or None
|
|
34
|
+
|
|
35
|
+
def label(self) -> str:
|
|
36
|
+
if self.entity:
|
|
37
|
+
return f"{self.kind}: {self.entity}"
|
|
38
|
+
return self.kind
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _host(url: str) -> str:
|
|
42
|
+
try:
|
|
43
|
+
host = urlsplit(url).hostname or ""
|
|
44
|
+
except ValueError:
|
|
45
|
+
return ""
|
|
46
|
+
return host[4:] if host.startswith("www.") else host
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parts(url: str) -> list[str]:
|
|
50
|
+
try:
|
|
51
|
+
return [p for p in urlsplit(url).path.split("/") if p]
|
|
52
|
+
except ValueError:
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _query(url: str) -> dict[str, list[str]]:
|
|
57
|
+
try:
|
|
58
|
+
return parse_qs(urlsplit(url).query)
|
|
59
|
+
except ValueError:
|
|
60
|
+
return {}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _apex(domain: str) -> str:
|
|
64
|
+
bits = domain.split(".")
|
|
65
|
+
return ".".join(bits[-2:]) if len(bits) > 2 else domain
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _slug(s: str) -> str:
|
|
69
|
+
"""Turn a URL slug into readable text: 'my-page-x9f' -> 'my page x9f'."""
|
|
70
|
+
return unquote(s).replace("-", " ").replace("_", " ").strip()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def parse_url(url: str) -> PageRef:
|
|
74
|
+
"""Parse a URL into a PageRef. Never raises; never returns None."""
|
|
75
|
+
domain = _host(url)
|
|
76
|
+
if not domain:
|
|
77
|
+
return PageRef(kind="page", domain="", entity=None)
|
|
78
|
+
parts = _parts(url)
|
|
79
|
+
q = _query(url)
|
|
80
|
+
|
|
81
|
+
parser = _SITE_PARSERS.get(domain) or _SITE_PARSERS.get(_apex(domain))
|
|
82
|
+
if parser:
|
|
83
|
+
ref = parser(domain, parts, q)
|
|
84
|
+
if ref:
|
|
85
|
+
return ref
|
|
86
|
+
|
|
87
|
+
# Generic search detection (?q= / ?query=): meaningful on any site.
|
|
88
|
+
for key in ("q", "query", "search_query"):
|
|
89
|
+
if key in q and q[key] and q[key][0].strip():
|
|
90
|
+
return PageRef(kind="search", domain=domain, entity=unquote(q[key][0]).strip())
|
|
91
|
+
|
|
92
|
+
# Subdomain / path heuristics: type common infrastructure pages.
|
|
93
|
+
heur = _heuristic(domain, parts)
|
|
94
|
+
if heur:
|
|
95
|
+
return heur
|
|
96
|
+
|
|
97
|
+
# Generic fallback: first path segment as context.
|
|
98
|
+
entity = parts[0] if parts else None
|
|
99
|
+
return PageRef(kind="page", domain=domain, entity=entity)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ---- Heuristic layer (applies to any site without a bespoke parser) ----
|
|
103
|
+
|
|
104
|
+
_SIGNIN_HINTS = frozenset(
|
|
105
|
+
{"login", "signin", "sign-in", "sign_in", "auth", "oauth", "sso", "logout"}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _heuristic(domain: str, parts: list[str]) -> PageRef | None:
|
|
110
|
+
first = parts[0].lower() if parts else ""
|
|
111
|
+
# Whole path segments only: "auth" must not match "/authors",
|
|
112
|
+
# "sso" must not match "/lessons".
|
|
113
|
+
segs = {p.lower() for p in parts[:3]}
|
|
114
|
+
|
|
115
|
+
# Authentication pages, by host or by path segment.
|
|
116
|
+
if (
|
|
117
|
+
domain.startswith(("accounts.", "login.", "auth.", "signin."))
|
|
118
|
+
or segs & _SIGNIN_HINTS
|
|
119
|
+
):
|
|
120
|
+
return PageRef(kind="sign_in", domain=domain)
|
|
121
|
+
# Operational surfaces exposed on conventional subdomains.
|
|
122
|
+
if domain.startswith(("dashboard.", "app.", "console.", "admin.", "portal.")):
|
|
123
|
+
return PageRef(kind="dashboard", domain=domain,
|
|
124
|
+
entity=parts[0] if parts else None)
|
|
125
|
+
if first in ("dashboard", "admin", "console", "settings"):
|
|
126
|
+
return PageRef(kind="dashboard", domain=domain, entity=first)
|
|
127
|
+
if domain.startswith("mail.") or first in ("mail", "inbox"):
|
|
128
|
+
return PageRef(kind="email", domain=domain)
|
|
129
|
+
if domain.startswith("calendar.") or first == "calendar":
|
|
130
|
+
return PageRef(kind="calendar", domain=domain)
|
|
131
|
+
if domain.startswith(("meet.", "zoom.")) or "zoom.us" in domain:
|
|
132
|
+
return PageRef(kind="meeting", domain=domain)
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---- Site parsers (each returns PageRef or None to fall through) ----
|
|
137
|
+
|
|
138
|
+
def _linkedin(domain, parts, q):
|
|
139
|
+
if not parts:
|
|
140
|
+
return PageRef(kind="feed", domain=domain)
|
|
141
|
+
head = parts[0]
|
|
142
|
+
if head == "in" and len(parts) > 1:
|
|
143
|
+
return PageRef(kind="profile", domain=domain, entity=unquote(parts[1]))
|
|
144
|
+
if head == "company" and len(parts) > 1:
|
|
145
|
+
return PageRef(kind="company", domain=domain, entity=unquote(parts[1]))
|
|
146
|
+
if head == "search":
|
|
147
|
+
kw = q.get("keywords", [""])[0]
|
|
148
|
+
what = parts[2] if len(parts) > 2 else "results"
|
|
149
|
+
return PageRef(kind=f"{what}_search", domain=domain, entity=unquote(kw) or None)
|
|
150
|
+
if head == "jobs":
|
|
151
|
+
return PageRef(kind="jobs", domain=domain)
|
|
152
|
+
if head == "feed":
|
|
153
|
+
return PageRef(kind="feed", domain=domain)
|
|
154
|
+
if head == "messaging":
|
|
155
|
+
return PageRef(kind="messaging", domain=domain)
|
|
156
|
+
if head == "mynetwork":
|
|
157
|
+
return PageRef(kind="network", domain=domain)
|
|
158
|
+
if head == "notifications":
|
|
159
|
+
return PageRef(kind="notifications", domain=domain)
|
|
160
|
+
if head == "posts" and len(parts) > 1:
|
|
161
|
+
return PageRef(kind="post", domain=domain, entity=_slug(parts[1])[:50] or None)
|
|
162
|
+
if head in ("uas", "login", "checkpoint"):
|
|
163
|
+
return PageRef(kind="sign_in", domain=domain)
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _github(domain, parts, q):
|
|
168
|
+
if not parts:
|
|
169
|
+
return PageRef(kind="home", domain=domain)
|
|
170
|
+
if len(parts) == 1:
|
|
171
|
+
return PageRef(kind="user", domain=domain, entity=parts[0])
|
|
172
|
+
repo = f"{parts[0]}/{parts[1]}"
|
|
173
|
+
if len(parts) >= 4 and parts[2] == "pull":
|
|
174
|
+
return PageRef(kind="pull_request", domain=domain, entity=f"{repo}#{parts[3]}")
|
|
175
|
+
if len(parts) >= 4 and parts[2] == "issues":
|
|
176
|
+
return PageRef(kind="issue", domain=domain, entity=f"{repo}#{parts[3]}")
|
|
177
|
+
if len(parts) >= 3 and parts[2] in ("commits", "commit"):
|
|
178
|
+
return PageRef(kind="commits", domain=domain, entity=repo)
|
|
179
|
+
if len(parts) >= 3 and parts[2] in ("blob", "tree"):
|
|
180
|
+
return PageRef(kind="code", domain=domain, entity=repo)
|
|
181
|
+
return PageRef(kind="repo", domain=domain, entity=repo)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _google(domain, parts, q):
|
|
185
|
+
if "q" in q and q["q"] and q["q"][0].strip():
|
|
186
|
+
return PageRef(kind="search", domain=domain, entity=unquote(q["q"][0]).strip())
|
|
187
|
+
if parts and parts[0] == "maps":
|
|
188
|
+
if len(parts) >= 2 and parts[1] == "place":
|
|
189
|
+
return PageRef(kind="map_place", domain=domain,
|
|
190
|
+
entity=_slug(parts[2]) if len(parts) > 2 else None)
|
|
191
|
+
if len(parts) >= 2 and parts[1] == "dir":
|
|
192
|
+
return PageRef(kind="map_directions", domain=domain)
|
|
193
|
+
return PageRef(kind="map", domain=domain)
|
|
194
|
+
if parts and parts[0] == "search":
|
|
195
|
+
return None # let search-param detector handle
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _google_docs(domain, parts, q):
|
|
200
|
+
kinds = {"document": "doc", "spreadsheets": "sheet", "presentation": "slides", "forms": "form"}
|
|
201
|
+
if parts and parts[0] in kinds:
|
|
202
|
+
return PageRef(kind=kinds[parts[0]], domain=domain)
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _gmail(domain, parts, q):
|
|
207
|
+
if parts and parts[0] == "mail":
|
|
208
|
+
return PageRef(kind="email", domain=domain)
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _youtube(domain, parts, q):
|
|
213
|
+
if parts and parts[0] == "watch":
|
|
214
|
+
return PageRef(kind="video", domain=domain, entity=q.get("v", [None])[0])
|
|
215
|
+
if parts and parts[0] == "results":
|
|
216
|
+
return PageRef(kind="search", domain=domain,
|
|
217
|
+
entity=unquote(q.get("search_query", [""])[0]) or None)
|
|
218
|
+
if parts and parts[0].startswith("@"):
|
|
219
|
+
return PageRef(kind="channel", domain=domain, entity=parts[0])
|
|
220
|
+
if parts and parts[0] == "shorts" and len(parts) > 1:
|
|
221
|
+
return PageRef(kind="video", domain=domain, entity=parts[1])
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _x(domain, parts, q):
|
|
226
|
+
if not parts:
|
|
227
|
+
return PageRef(kind="feed", domain=domain)
|
|
228
|
+
if parts[0] in ("home", "explore"):
|
|
229
|
+
return PageRef(kind="feed", domain=domain)
|
|
230
|
+
if parts[0] == "notifications":
|
|
231
|
+
return PageRef(kind="notifications", domain=domain)
|
|
232
|
+
if parts[0] == "messages":
|
|
233
|
+
return PageRef(kind="messages", domain=domain)
|
|
234
|
+
if parts[0] == "search":
|
|
235
|
+
return PageRef(kind="search", domain=domain,
|
|
236
|
+
entity=unquote(q.get("q", [""])[0]) or None)
|
|
237
|
+
if parts[0] == "i":
|
|
238
|
+
sub = parts[1] if len(parts) > 1 else ""
|
|
239
|
+
if sub == "chat":
|
|
240
|
+
return PageRef(kind="messages", domain=domain)
|
|
241
|
+
if sub == "flow":
|
|
242
|
+
return PageRef(kind="sign_in", domain=domain)
|
|
243
|
+
return PageRef(kind="page", domain=domain, entity=sub or "i")
|
|
244
|
+
if parts[0] in ("settings", "compose", "jobs", "logout"):
|
|
245
|
+
return PageRef(kind="page", domain=domain, entity=parts[0])
|
|
246
|
+
if len(parts) >= 3 and parts[1] == "status":
|
|
247
|
+
return PageRef(kind="post", domain=domain, entity=parts[0])
|
|
248
|
+
return PageRef(kind="profile", domain=domain, entity=parts[0])
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _instagram(domain, parts, q):
|
|
252
|
+
if not parts:
|
|
253
|
+
return PageRef(kind="feed", domain=domain)
|
|
254
|
+
head = parts[0]
|
|
255
|
+
if head == "p" and len(parts) > 1:
|
|
256
|
+
return PageRef(kind="post", domain=domain, entity=parts[1])
|
|
257
|
+
if head == "reel" and len(parts) > 1:
|
|
258
|
+
return PageRef(kind="reel", domain=domain, entity=parts[1])
|
|
259
|
+
if head == "reels":
|
|
260
|
+
return PageRef(kind="reels", domain=domain)
|
|
261
|
+
if head == "stories" and len(parts) > 1:
|
|
262
|
+
return PageRef(kind="story", domain=domain, entity=parts[1])
|
|
263
|
+
if head == "explore":
|
|
264
|
+
return PageRef(kind="explore", domain=domain)
|
|
265
|
+
if head == "direct":
|
|
266
|
+
return PageRef(kind="messages", domain=domain)
|
|
267
|
+
if head in ("accounts", "emails"):
|
|
268
|
+
return PageRef(kind="sign_in", domain=domain)
|
|
269
|
+
return PageRef(kind="profile", domain=domain, entity=head)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _reddit(domain, parts, q):
|
|
273
|
+
if not parts:
|
|
274
|
+
return PageRef(kind="feed", domain=domain)
|
|
275
|
+
head = parts[0]
|
|
276
|
+
if head == "r" and len(parts) >= 2:
|
|
277
|
+
if len(parts) >= 5 and parts[2] == "comments":
|
|
278
|
+
title = _slug(parts[4])[:50] if len(parts) > 4 else None
|
|
279
|
+
return PageRef(kind="post", domain=domain, entity=title or f"r/{parts[1]}")
|
|
280
|
+
return PageRef(kind="subreddit", domain=domain, entity=f"r/{parts[1]}")
|
|
281
|
+
if head in ("user", "u") and len(parts) >= 2:
|
|
282
|
+
return PageRef(kind="profile", domain=domain, entity=f"u/{parts[1]}")
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _stackoverflow(domain, parts, q):
|
|
287
|
+
if len(parts) >= 2 and parts[0] == "questions":
|
|
288
|
+
return PageRef(kind="question", domain=domain,
|
|
289
|
+
entity=parts[2].replace("-", " ") if len(parts) > 2 else parts[1])
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _calendly(domain, parts, q):
|
|
294
|
+
if parts:
|
|
295
|
+
return PageRef(kind="booking", domain=domain, entity="/".join(parts[:2]))
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _luma(domain, parts, q):
|
|
300
|
+
if not parts:
|
|
301
|
+
return PageRef(kind="home", domain=domain)
|
|
302
|
+
if parts[0] in ("home", "discover", "explore", "signin", "user", "settings"):
|
|
303
|
+
return PageRef(kind=parts[0] if parts[0] != "signin" else "sign_in", domain=domain)
|
|
304
|
+
# lu.ma/<eventid> is a short event slug
|
|
305
|
+
return PageRef(kind="event", domain=domain, entity=parts[0])
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _partiful(domain, parts, q):
|
|
309
|
+
if parts and parts[0] == "e" and len(parts) > 1:
|
|
310
|
+
return PageRef(kind="event", domain=domain, entity=parts[1])
|
|
311
|
+
if parts:
|
|
312
|
+
return PageRef(kind="event", domain=domain, entity=parts[0])
|
|
313
|
+
return PageRef(kind="home", domain=domain)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _producthunt(domain, parts, q):
|
|
317
|
+
if len(parts) >= 2 and parts[0] in ("posts", "products"):
|
|
318
|
+
return PageRef(kind="product", domain=domain, entity=_slug(parts[1]))
|
|
319
|
+
if len(parts) >= 2 and parts[0] == "@":
|
|
320
|
+
return PageRef(kind="profile", domain=domain, entity=parts[1])
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _google_meet(domain, parts, q):
|
|
325
|
+
return PageRef(kind="meeting", domain=domain, entity=parts[0] if parts else None)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _google_calendar(domain, parts, q):
|
|
329
|
+
return PageRef(kind="calendar", domain=domain)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _vercel(domain, parts, q):
|
|
333
|
+
if not parts:
|
|
334
|
+
return PageRef(kind="dashboard", domain=domain)
|
|
335
|
+
if len(parts) >= 2:
|
|
336
|
+
return PageRef(kind="project", domain=domain, entity=f"{parts[0]}/{parts[1]}")
|
|
337
|
+
return PageRef(kind="dashboard", domain=domain, entity=parts[0])
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _supabase(domain, parts, q):
|
|
341
|
+
if len(parts) >= 3 and parts[0] == "dashboard" and parts[1] == "project":
|
|
342
|
+
return PageRef(kind="project", domain=domain, entity=parts[2])
|
|
343
|
+
if parts and parts[0] == "dashboard":
|
|
344
|
+
return PageRef(kind="dashboard", domain=domain)
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _stripe(domain, parts, q):
|
|
349
|
+
return PageRef(kind="dashboard", domain=domain,
|
|
350
|
+
entity=parts[0] if parts else None)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _discord(domain, parts, q):
|
|
354
|
+
if len(parts) >= 2 and parts[0] == "channels":
|
|
355
|
+
if parts[1] == "@me":
|
|
356
|
+
return PageRef(kind="messages", domain=domain)
|
|
357
|
+
return PageRef(kind="channel", domain=domain, entity=parts[1])
|
|
358
|
+
return PageRef(kind="app", domain=domain)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _notion(domain, parts, q):
|
|
362
|
+
if parts:
|
|
363
|
+
# Notion slugs end with the page id; the readable part is the prefix.
|
|
364
|
+
last = parts[-1]
|
|
365
|
+
slug = last.rsplit("-", 1)[0] if "-" in last else last
|
|
366
|
+
return PageRef(kind="doc", domain=domain, entity=_slug(slug) or None)
|
|
367
|
+
return PageRef(kind="home", domain=domain)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _figma(domain, parts, q):
|
|
371
|
+
if len(parts) >= 3 and parts[0] in ("file", "design", "board"):
|
|
372
|
+
return PageRef(kind="design", domain=domain, entity=_slug(parts[2]))
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _chat_ai(domain, parts, q):
|
|
377
|
+
return PageRef(kind="ai_chat", domain=domain)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _localhost(domain, parts, q):
|
|
381
|
+
return PageRef(kind="local_dev", domain=domain, entity="/".join(parts[:2]) or None)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
_SITE_PARSERS = {
|
|
385
|
+
"linkedin.com": _linkedin,
|
|
386
|
+
"github.com": _github,
|
|
387
|
+
"google.com": _google,
|
|
388
|
+
"docs.google.com": _google_docs,
|
|
389
|
+
"mail.google.com": _gmail,
|
|
390
|
+
"meet.google.com": _google_meet,
|
|
391
|
+
"calendar.google.com": _google_calendar,
|
|
392
|
+
"youtube.com": _youtube,
|
|
393
|
+
"x.com": _x,
|
|
394
|
+
"twitter.com": _x,
|
|
395
|
+
"instagram.com": _instagram,
|
|
396
|
+
"reddit.com": _reddit,
|
|
397
|
+
"stackoverflow.com": _stackoverflow,
|
|
398
|
+
"calendly.com": _calendly,
|
|
399
|
+
"luma.com": _luma,
|
|
400
|
+
"lu.ma": _luma,
|
|
401
|
+
"partiful.com": _partiful,
|
|
402
|
+
"producthunt.com": _producthunt,
|
|
403
|
+
"vercel.com": _vercel,
|
|
404
|
+
"supabase.com": _supabase,
|
|
405
|
+
"dashboard.stripe.com": _stripe,
|
|
406
|
+
"discord.com": _discord,
|
|
407
|
+
"notion.so": _notion,
|
|
408
|
+
"notion.com": _notion,
|
|
409
|
+
"app.notion.com": _notion,
|
|
410
|
+
"figma.com": _figma,
|
|
411
|
+
"chatgpt.com": _chat_ai,
|
|
412
|
+
"chat.openai.com": _chat_ai,
|
|
413
|
+
"claude.ai": _chat_ai,
|
|
414
|
+
"localhost": _localhost,
|
|
415
|
+
"127.0.0.1": _localhost,
|
|
416
|
+
}
|