+37
README.md
+37
README.md
···
10
10
11
11
## scripts
12
12
13
+
- [`analyze-github-followers`](#analyze-github-followers)
13
14
- [`check-files-for-bad-links`](#check-files-for-bad-links)
14
15
- [`dm-me-when-a-flight-passes-over`](#dm-me-when-a-flight-passes-over)
15
16
- [`find-longest-bsky-thread`](#find-longest-bsky-thread)
17
+
- [`find-stale-bsky-follows`](#find-stale-bsky-follows)
16
18
- [`kill-processes`](#kill-processes)
17
19
- [`predict-github-stars`](#predict-github-stars)
18
20
- [`update-lights`](#update-lights)
19
21
- [`update-readme`](#update-readme)
22
+
23
+
---
24
+
25
+
### `analyze-github-followers`
26
+
27
+
analyze your github followers and following.
28
+
29
+
usage:
30
+
./analyze-github-followers
31
+
./analyze-github-followers --summary-only # skip detailed analysis
32
+
33
+
details:
34
+
- uses github rest api to fetch followers/following
35
+
- shows rich tables with follower stats
36
+
- identifies mutual follows, notable followers, etc.
37
+
- requires GITHUB_TOKEN in .env file
20
38
21
39
---
22
40
···
169
187
Details:
170
188
- uses [`atproto`](https://github.com/MarshalX/atproto) to fetch the thread
171
189
- uses [`jinja2`](https://github.com/pallets/jinja) to render the thread
190
+
191
+
---
192
+
193
+
### `find-stale-bsky-follows`
194
+
195
+
Find stale/inactive accounts among those you follow on Bluesky.
196
+
197
+
Usage:
198
+
199
+
```bash
200
+
./find-stale-bsky-follows
201
+
# or with custom inactivity threshold (days)
202
+
./find-stale-bsky-follows --days 180
203
+
```
204
+
205
+
Details:
206
+
- uses [`atproto`](https://github.com/MarshalX/atproto) to fetch following list
207
+
- uses [`rich`](https://github.com/Textualize/rich) for pretty output
208
+
- identifies accounts with no recent posts
172
209
173
210
---
174
211
+366
analyze-github-followers
+366
analyze-github-followers
···
1
+
#!/usr/bin/env -S uv run --script --quiet
2
+
# /// script
3
+
# requires-python = ">=3.12"
4
+
# dependencies = ["httpx", "rich", "pydantic-settings"]
5
+
# ///
6
+
"""
7
+
analyze your github followers and following.
8
+
9
+
usage:
10
+
./analyze-github-followers
11
+
./analyze-github-followers --summary-only # skip detailed analysis
12
+
13
+
details:
14
+
- uses github rest api to fetch followers/following
15
+
- shows rich tables with follower stats
16
+
- identifies mutual follows, notable followers, etc.
17
+
- requires GITHUB_TOKEN in .env file
18
+
"""
19
+
20
+
from __future__ import annotations
21
+
22
+
import argparse
23
+
import os
24
+
from datetime import datetime, timezone
25
+
from typing import NamedTuple
26
+
27
+
import httpx
28
+
from pydantic import Field
29
+
from pydantic_settings import BaseSettings, SettingsConfigDict
30
+
from rich.console import Console
31
+
from rich.panel import Panel
32
+
from rich.progress import Progress, SpinnerColumn, TextColumn
33
+
from rich.table import Table
34
+
35
+
console = Console()
36
+
37
+
38
+
class Settings(BaseSettings):
39
+
"""load settings from environment"""
40
+
41
+
model_config = SettingsConfigDict(
42
+
env_file=os.environ.get("ENV_FILE", ".env"), extra="ignore"
43
+
)
44
+
github_token: str = Field(description="github api token")
45
+
46
+
47
+
class GitHubUser(NamedTuple):
48
+
"""github user information"""
49
+
50
+
login: str
51
+
name: str | None
52
+
bio: str | None
53
+
followers: int
54
+
following: int
55
+
public_repos: int
56
+
created_at: datetime
57
+
url: str
58
+
company: str | None
59
+
location: str | None
60
+
blog: str | None
61
+
62
+
@property
63
+
def follower_ratio(self) -> float:
64
+
"""ratio of followers to following (higher = more influential)"""
65
+
if self.following == 0:
66
+
return float(self.followers) if self.followers > 0 else 0.0
67
+
return self.followers / self.following
68
+
69
+
70
+
def _headers(token: str) -> dict[str, str]:
71
+
return {
72
+
"Accept": "application/vnd.github.v3+json",
73
+
"Authorization": f"token {token}",
74
+
}
75
+
76
+
77
+
def get_authenticated_user(token: str) -> str:
78
+
"""get the authenticated user's login"""
79
+
with httpx.Client() as client:
80
+
r = client.get("https://api.github.com/user", headers=_headers(token))
81
+
r.raise_for_status()
82
+
return r.json()["login"]
83
+
84
+
85
+
def get_user_details(username: str, token: str) -> GitHubUser:
86
+
"""fetch detailed user information"""
87
+
with httpx.Client() as client:
88
+
r = client.get(
89
+
f"https://api.github.com/users/{username}", headers=_headers(token)
90
+
)
91
+
r.raise_for_status()
92
+
data = r.json()
93
+
return GitHubUser(
94
+
login=data["login"],
95
+
name=data.get("name"),
96
+
bio=data.get("bio"),
97
+
followers=data["followers"],
98
+
following=data["following"],
99
+
public_repos=data["public_repos"],
100
+
created_at=datetime.fromisoformat(data["created_at"].replace("Z", "+00:00")),
101
+
url=data["html_url"],
102
+
company=data.get("company"),
103
+
location=data.get("location"),
104
+
blog=data.get("blog"),
105
+
)
106
+
107
+
108
+
def get_all_followers(username: str, token: str) -> list[str]:
109
+
"""fetch all followers (just logins for now)"""
110
+
followers = []
111
+
page = 1
112
+
with httpx.Client() as client:
113
+
while True:
114
+
r = client.get(
115
+
f"https://api.github.com/users/{username}/followers?page={page}&per_page=100",
116
+
headers=_headers(token),
117
+
)
118
+
r.raise_for_status()
119
+
data = r.json()
120
+
if not data:
121
+
break
122
+
followers.extend([user["login"] for user in data])
123
+
page += 1
124
+
return followers
125
+
126
+
127
+
def get_all_following(username: str, token: str) -> list[str]:
128
+
"""fetch all users being followed"""
129
+
following = []
130
+
page = 1
131
+
with httpx.Client() as client:
132
+
while True:
133
+
r = client.get(
134
+
f"https://api.github.com/users/{username}/following?page={page}&per_page=100",
135
+
headers=_headers(token),
136
+
)
137
+
r.raise_for_status()
138
+
data = r.json()
139
+
if not data:
140
+
break
141
+
following.extend([user["login"] for user in data])
142
+
page += 1
143
+
return following
144
+
145
+
146
+
def main():
147
+
"""main function to analyze github followers"""
148
+
parser = argparse.ArgumentParser(description="analyze your github followers")
149
+
parser.add_argument(
150
+
"--summary-only",
151
+
action="store_true",
152
+
help="show summary only, skip detailed follower analysis",
153
+
)
154
+
args = parser.parse_args()
155
+
156
+
try:
157
+
settings = Settings() # type: ignore
158
+
except Exception as e:
159
+
console.print(f"[red]error loading settings: {e}[/red]")
160
+
console.print("[dim]ensure .env file exists with GITHUB_TOKEN[/dim]")
161
+
return
162
+
163
+
token = settings.github_token.strip()
164
+
165
+
try:
166
+
# get authenticated user
167
+
username = get_authenticated_user(token)
168
+
console.print(f"[blue]analyzing followers for @{username}[/blue]\n")
169
+
170
+
# fetch user details
171
+
with Progress(
172
+
SpinnerColumn(),
173
+
TextColumn("[progress.description]{task.description}"),
174
+
console=console,
175
+
) as progress:
176
+
task = progress.add_task("fetching your profile...", total=None)
177
+
user = get_user_details(username, token)
178
+
progress.update(task, completed=True)
179
+
180
+
# show profile info
181
+
profile_text = f"[bold cyan]@{user.login}[/bold cyan]"
182
+
if user.name:
183
+
profile_text += f" ({user.name})"
184
+
profile_text += f"\n[dim]joined {user.created_at:%Y-%m-%d}[/dim]"
185
+
if user.bio:
186
+
profile_text += f"\n{user.bio}"
187
+
if user.location:
188
+
profile_text += f"\n๐ {user.location}"
189
+
if user.company:
190
+
profile_text += f"\n๐ข {user.company}"
191
+
192
+
console.print(Panel.fit(profile_text, border_style="blue"))
193
+
console.print()
194
+
195
+
# fetch followers and following
196
+
with Progress(
197
+
SpinnerColumn(),
198
+
TextColumn("[progress.description]{task.description}"),
199
+
console=console,
200
+
) as progress:
201
+
task1 = progress.add_task("fetching followers...", total=None)
202
+
followers = get_all_followers(username, token)
203
+
progress.update(task1, completed=True)
204
+
205
+
task2 = progress.add_task("fetching following...", total=None)
206
+
following = get_all_following(username, token)
207
+
progress.update(task2, completed=True)
208
+
209
+
# analyze relationships
210
+
followers_set = set(followers)
211
+
following_set = set(following)
212
+
213
+
mutual = followers_set & following_set
214
+
followers_only = followers_set - following_set
215
+
following_only = following_set - followers_set
216
+
217
+
# summary table
218
+
summary_table = Table(show_header=True, header_style="bold magenta")
219
+
summary_table.add_column("metric", style="cyan")
220
+
summary_table.add_column("count", justify="right", style="white")
221
+
222
+
summary_table.add_row("total followers", str(len(followers)))
223
+
summary_table.add_row("total following", str(len(following)))
224
+
summary_table.add_row("mutual follows", f"[green]{len(mutual)}[/green]")
225
+
summary_table.add_row(
226
+
"followers not following back", f"[yellow]{len(followers_only)}[/yellow]"
227
+
)
228
+
summary_table.add_row(
229
+
"following but not following back", f"[red]{len(following_only)}[/red]"
230
+
)
231
+
summary_table.add_row("public repos", str(user.public_repos))
232
+
233
+
console.print(summary_table)
234
+
console.print()
235
+
236
+
# fetch details for all followers
237
+
if followers and not args.summary_only:
238
+
console.print(
239
+
f"\n[bold yellow]analyzing all {len(followers)} followers...[/bold yellow]"
240
+
)
241
+
follower_details = []
242
+
243
+
with Progress(
244
+
SpinnerColumn(),
245
+
TextColumn("[progress.description]{task.description}"),
246
+
console=console,
247
+
) as progress:
248
+
task = progress.add_task(
249
+
f"fetching follower details...", total=len(followers)
250
+
)
251
+
252
+
for follower_login in followers:
253
+
try:
254
+
details = get_user_details(follower_login, token)
255
+
follower_details.append(details)
256
+
except Exception:
257
+
pass # skip if we can't fetch details
258
+
progress.advance(task)
259
+
260
+
if follower_details:
261
+
# most influential followers by follower ratio
262
+
# filter out accounts with very few followers to avoid noise
263
+
influential = [f for f in follower_details if f.followers >= 100]
264
+
influential = sorted(influential, key=lambda u: u.follower_ratio, reverse=True)[:10]
265
+
266
+
console.print()
267
+
console.print("[bold magenta]most influential followers:[/bold magenta]")
268
+
console.print("[dim]ranked by followers-to-following ratio[/dim]\n")
269
+
followers_table = Table(show_header=True, header_style="bold magenta")
270
+
followers_table.add_column("username", style="cyan")
271
+
followers_table.add_column("name", style="white")
272
+
followers_table.add_column("followers", justify="right", style="blue")
273
+
followers_table.add_column("following", justify="right", style="yellow")
274
+
followers_table.add_column("ratio", justify="right", style="green")
275
+
followers_table.add_column("mutual", justify="center", style="magenta")
276
+
277
+
for follower in influential:
278
+
is_mutual = "โ" if follower.login in mutual else ""
279
+
followers_table.add_row(
280
+
f"@{follower.login}",
281
+
follower.name or "[dim]no name[/dim]",
282
+
f"{follower.followers:,}",
283
+
f"{follower.following:,}",
284
+
f"{follower.follower_ratio:.1f}x",
285
+
is_mutual,
286
+
)
287
+
288
+
console.print(followers_table)
289
+
290
+
# location analysis
291
+
locations = [
292
+
f.location for f in follower_details if f.location
293
+
]
294
+
if locations:
295
+
from collections import Counter
296
+
297
+
location_counts = Counter(locations).most_common(5)
298
+
console.print("\n[bold magenta]top follower locations:[/bold magenta]")
299
+
location_table = Table(show_header=False)
300
+
location_table.add_column("location", style="cyan")
301
+
location_table.add_column("count", justify="right", style="white")
302
+
for loc, count in location_counts:
303
+
location_table.add_row(loc, str(count))
304
+
console.print(location_table)
305
+
306
+
# company analysis
307
+
companies = [
308
+
f.company.lstrip("@").strip()
309
+
for f in follower_details
310
+
if f.company
311
+
]
312
+
if companies:
313
+
from collections import Counter
314
+
315
+
company_counts = Counter(companies).most_common(5)
316
+
console.print("\n[bold magenta]top follower companies:[/bold magenta]")
317
+
company_table = Table(show_header=False)
318
+
company_table.add_column("company", style="cyan")
319
+
company_table.add_column("count", justify="right", style="white")
320
+
for comp, count in company_counts:
321
+
company_table.add_row(comp, str(count))
322
+
console.print(company_table)
323
+
324
+
# account age analysis
325
+
now = datetime.now(timezone.utc)
326
+
ages_years = [(now - f.created_at).days / 365.25 for f in follower_details]
327
+
avg_age = sum(ages_years) / len(ages_years)
328
+
oldest = min(follower_details, key=lambda f: f.created_at)
329
+
newest = max(follower_details, key=lambda f: f.created_at)
330
+
331
+
console.print("\n[bold magenta]account age stats:[/bold magenta]")
332
+
age_table = Table(show_header=False)
333
+
age_table.add_column("metric", style="cyan")
334
+
age_table.add_column("value", style="white")
335
+
age_table.add_row("average follower account age", f"{avg_age:.1f} years")
336
+
age_table.add_row("oldest follower account", f"@{oldest.login} ({oldest.created_at:%Y-%m-%d})")
337
+
age_table.add_row("newest follower account", f"@{newest.login} ({newest.created_at:%Y-%m-%d})")
338
+
console.print(age_table)
339
+
340
+
# repo stats
341
+
repo_counts = [f.public_repos for f in follower_details]
342
+
avg_repos = sum(repo_counts) / len(repo_counts)
343
+
most_repos = max(follower_details, key=lambda f: f.public_repos)
344
+
345
+
console.print("\n[bold magenta]repository stats:[/bold magenta]")
346
+
repo_table = Table(show_header=False)
347
+
repo_table.add_column("metric", style="cyan")
348
+
repo_table.add_column("value", style="white")
349
+
repo_table.add_row("average repos per follower", f"{avg_repos:.1f}")
350
+
repo_table.add_row("follower with most repos", f"@{most_repos.login} ({most_repos.public_repos:,} repos)")
351
+
repo_table.add_row("followers with 0 repos", str(sum(1 for f in follower_details if f.public_repos == 0)))
352
+
console.print(repo_table)
353
+
354
+
except httpx.HTTPStatusError as e:
355
+
if e.response.status_code == 401:
356
+
console.print("[red]error: invalid github token[/red]")
357
+
elif e.response.status_code == 403:
358
+
console.print("[red]error: rate limit exceeded[/red]")
359
+
else:
360
+
console.print(f"[red]github api error: {e.response.status_code}[/red]")
361
+
except Exception as e:
362
+
console.print(f"[red]error: {e}[/red]")
363
+
364
+
365
+
if __name__ == "__main__":
366
+
main()
+2
-1
find-longest-bsky-thread
+2
-1
find-longest-bsky-thread
···
38
38
39
39
bsky_handle: str
40
40
bsky_password: str
41
+
bsky_pds_url: str = "https://bsky.social"
41
42
42
43
43
44
def extract_post_uri(bluesky_url: str) -> str:
···
171
172
)
172
173
return
173
174
174
-
client = Client()
175
+
client = Client(base_url=settings.bsky_pds_url)
175
176
try:
176
177
client.login(settings.bsky_handle, settings.bsky_password)
177
178
except Exception as e:
+268
find-stale-bsky-follows
+268
find-stale-bsky-follows
···
1
+
#!/usr/bin/env -S uv run --script --quiet
2
+
# /// script
3
+
# requires-python = ">=3.12"
4
+
# dependencies = ["atproto", "pydantic-settings", "rich"]
5
+
# ///
6
+
"""
7
+
Find stale/inactive accounts among those you follow on Bluesky.
8
+
9
+
Usage:
10
+
11
+
```bash
12
+
./find-stale-bsky-follows
13
+
# or with custom inactivity threshold (days)
14
+
./find-stale-bsky-follows --days 180
15
+
```
16
+
17
+
Details:
18
+
- uses [`atproto`](https://github.com/MarshalX/atproto) to fetch following list
19
+
- uses [`rich`](https://github.com/Textualize/rich) for pretty output
20
+
- identifies accounts with no recent posts
21
+
"""
22
+
23
+
import argparse
24
+
import os
25
+
from datetime import datetime, timedelta, timezone
26
+
from typing import NamedTuple
27
+
28
+
from atproto import Client
29
+
from pydantic_settings import BaseSettings, SettingsConfigDict
30
+
from rich.console import Console
31
+
from rich.progress import Progress, SpinnerColumn, TextColumn
32
+
from rich.table import Table
33
+
34
+
35
+
class Settings(BaseSettings):
36
+
"""App settings loaded from environment variables"""
37
+
38
+
model_config = SettingsConfigDict(
39
+
env_file=os.environ.get("ENV_FILE", ".env"), extra="ignore"
40
+
)
41
+
42
+
bsky_handle: str
43
+
bsky_password: str
44
+
bsky_pds_url: str = "https://bsky.social"
45
+
46
+
47
+
class AccountActivity(NamedTuple):
48
+
"""Activity information for a Bluesky account"""
49
+
50
+
handle: str
51
+
display_name: str | None
52
+
did: str
53
+
posts_count: int
54
+
last_post_date: datetime | None
55
+
days_inactive: int | None
56
+
is_stale: bool
57
+
58
+
59
+
def get_following_list(client: Client) -> list[dict]:
60
+
"""Fetch all accounts the authenticated user follows"""
61
+
following = []
62
+
cursor = None
63
+
64
+
while True:
65
+
assert client.me, "client.me should be set"
66
+
response = client.get_follows(client.me.did, cursor=cursor)
67
+
following.extend(response.follows)
68
+
69
+
if not response.cursor:
70
+
break
71
+
cursor = response.cursor
72
+
73
+
return following
74
+
75
+
76
+
def check_account_activity(
77
+
client: Client, actor: dict, inactivity_threshold_days: int
78
+
) -> AccountActivity:
79
+
"""
80
+
Check the activity of a single account.
81
+
82
+
Returns AccountActivity with stale status based on:
83
+
- No posts at all
84
+
- Last post older than threshold
85
+
"""
86
+
handle = actor.handle
87
+
did = actor.did
88
+
display_name = getattr(actor, "display_name", None)
89
+
90
+
try:
91
+
# Get the user's profile to check post count
92
+
profile = client.get_profile(handle)
93
+
posts_count = profile.posts_count or 0
94
+
95
+
# If no posts, immediately mark as stale
96
+
if posts_count == 0:
97
+
return AccountActivity(
98
+
handle=handle,
99
+
display_name=display_name,
100
+
did=did,
101
+
posts_count=0,
102
+
last_post_date=None,
103
+
days_inactive=None,
104
+
is_stale=True,
105
+
)
106
+
107
+
# Get author feed to find last post
108
+
feed_response = client.get_author_feed(actor=handle, limit=1)
109
+
110
+
last_post_date = None
111
+
if feed_response.feed:
112
+
last_post = feed_response.feed[0].post
113
+
if hasattr(last_post.record, "created_at"):
114
+
created_at_str = last_post.record.created_at
115
+
# Parse ISO 8601 timestamp
116
+
last_post_date = datetime.fromisoformat(
117
+
created_at_str.replace("Z", "+00:00")
118
+
)
119
+
120
+
# Calculate days inactive
121
+
if last_post_date:
122
+
days_inactive = (datetime.now(timezone.utc) - last_post_date).days
123
+
is_stale = days_inactive > inactivity_threshold_days
124
+
else:
125
+
# Has posts but couldn't determine date - consider stale
126
+
days_inactive = None
127
+
is_stale = True
128
+
129
+
return AccountActivity(
130
+
handle=handle,
131
+
display_name=display_name,
132
+
did=did,
133
+
posts_count=posts_count,
134
+
last_post_date=last_post_date,
135
+
days_inactive=days_inactive,
136
+
is_stale=is_stale,
137
+
)
138
+
139
+
except Exception as e:
140
+
# If we can't check activity, mark as potentially problematic
141
+
# (could be deleted, suspended, or private)
142
+
return AccountActivity(
143
+
handle=handle,
144
+
display_name=display_name,
145
+
did=did,
146
+
posts_count=0,
147
+
last_post_date=None,
148
+
days_inactive=None,
149
+
is_stale=True,
150
+
)
151
+
152
+
153
+
def format_account_link(handle: str) -> str:
154
+
"""Format a clickable Bluesky profile link"""
155
+
return f"https://bsky.app/profile/{handle}"
156
+
157
+
158
+
def main(inactivity_threshold_days: int):
159
+
"""Main function to find stale accounts"""
160
+
console = Console()
161
+
162
+
try:
163
+
settings = Settings() # type: ignore
164
+
except Exception as e:
165
+
console.print(
166
+
f"[red]Error loading settings (ensure .env file exists with BSKY_HANDLE and BSKY_PASSWORD): {e}[/red]"
167
+
)
168
+
return
169
+
170
+
client = Client(base_url=settings.bsky_pds_url)
171
+
try:
172
+
client.login(settings.bsky_handle, settings.bsky_password)
173
+
except Exception as e:
174
+
console.print(f"[red]Error logging into Bluesky: {e}[/red]")
175
+
return
176
+
177
+
console.print(f"[blue]Logged in as {client.me.handle}[/blue]")
178
+
console.print(
179
+
f"[blue]Checking for accounts inactive for more than {inactivity_threshold_days} days...[/blue]\n"
180
+
)
181
+
182
+
# Fetch following list
183
+
with Progress(
184
+
SpinnerColumn(),
185
+
TextColumn("[progress.description]{task.description}"),
186
+
console=console,
187
+
) as progress:
188
+
task = progress.add_task("Fetching following list...", total=None)
189
+
following = get_following_list(client)
190
+
progress.update(task, completed=True)
191
+
192
+
console.print(f"[green]Found {len(following)} accounts you follow[/green]\n")
193
+
194
+
# Check activity for each account
195
+
stale_accounts = []
196
+
with Progress(
197
+
SpinnerColumn(),
198
+
TextColumn("[progress.description]{task.description}"),
199
+
console=console,
200
+
) as progress:
201
+
task = progress.add_task("Analyzing account activity...", total=len(following))
202
+
203
+
for actor in following:
204
+
activity = check_account_activity(
205
+
client, actor, inactivity_threshold_days
206
+
)
207
+
if activity.is_stale:
208
+
stale_accounts.append(activity)
209
+
progress.advance(task)
210
+
211
+
# Display results
212
+
console.print(f"\n[yellow]Found {len(stale_accounts)} stale accounts:[/yellow]\n")
213
+
214
+
if stale_accounts:
215
+
table = Table(show_header=True, header_style="bold magenta")
216
+
table.add_column("Handle", style="cyan")
217
+
table.add_column("Display Name", style="white")
218
+
table.add_column("Posts", justify="right", style="blue")
219
+
table.add_column("Last Post", style="yellow")
220
+
table.add_column("Days Inactive", justify="right", style="red")
221
+
222
+
# Sort by days inactive (None values last)
223
+
stale_accounts.sort(
224
+
key=lambda x: (x.days_inactive is None, x.days_inactive or 0),
225
+
reverse=True,
226
+
)
227
+
228
+
for account in stale_accounts:
229
+
last_post = (
230
+
account.last_post_date.strftime("%Y-%m-%d")
231
+
if account.last_post_date
232
+
else "Never"
233
+
)
234
+
days = str(account.days_inactive) if account.days_inactive else "Unknown"
235
+
236
+
table.add_row(
237
+
f"@{account.handle}",
238
+
account.display_name or "[dim]No name[/dim]",
239
+
str(account.posts_count),
240
+
last_post,
241
+
days,
242
+
)
243
+
244
+
console.print(table)
245
+
246
+
# Print links for easy access
247
+
console.print("\n[dim]Profile links:[/dim]")
248
+
for account in stale_accounts[:10]: # Limit to first 10
249
+
console.print(f" {format_account_link(account.handle)}")
250
+
if len(stale_accounts) > 10:
251
+
console.print(f" [dim]... and {len(stale_accounts) - 10} more[/dim]")
252
+
else:
253
+
console.print("[green]All accounts you follow are active![/green]")
254
+
255
+
256
+
if __name__ == "__main__":
257
+
parser = argparse.ArgumentParser(
258
+
description="Find stale/inactive accounts you follow on Bluesky."
259
+
)
260
+
parser.add_argument(
261
+
"--days",
262
+
type=int,
263
+
default=180,
264
+
help="Number of days of inactivity to consider an account stale (default: 180)",
265
+
)
266
+
args = parser.parse_args()
267
+
268
+
main(args.days)
+1
-1
update-lights
+1
-1
update-lights
···
37
37
hue_bridge_username: str = Field(default=...)
38
38
anthropic_api_key: str | None = Field(default=None)
39
39
40
-
ai_model: KnownModelName = Field(default="anthropic:claude-sonnet-4-5")
40
+
ai_model: KnownModelName = Field(default="anthropic:claude-opus-4-5")
41
41
42
42
43
43
settings = Settings()