Fix paywalled links and add link checker tool
Implement the following plan:
Fix paywalled links and add link checker tool
Part 1: Replace paywalled 404 Media link
The 404 Media article at https://www.404media.co/exposed-moltbook-database-let-anyone-take-control-of-any-ai-agent-on-the-site/ is behind a paywall ("This post is for paid members only").
Replace with the Wiz blog post (the security researchers who actually found the vulnerability):
https://www.wiz.io/blog/exposed-moltbook-database-reveals-millions-of-api-keys
This is freely accessible and provides more technical detail since Wiz performed the research.
File modified
content/spring2026/meetings/2026-02-03-assessments-and-agents.mdx line 149:
- Change link text from "404 Media report on Moltbook's exposed database" to "Wiz's analysis of Moltbook's exposed database"
- Update URL
Also update the facilitation notes at line 122 which reference "The 404 Media database exposure" — change to reference Wiz's research instead.
Remove TODO
Remove the "Check for paywalled links in activity content" item from TODOS.md since we're both fixing the specific issue and adding tooling to prevent it.
Part 2: Link checker tool
Create tools/link-check/ following the favicon tool pattern (simplest tool in the repo).
Structure
tools/link-check/
├── package.json
└── src/
└── check.ts
What it does
- Finds all
.mdxand.mdfiles undercontent/ - Extracts external URLs (starting with
http) using regex - For each unique URL, fetches the page and checks:
- HTTP status code (non-2xx = problem)
- Paywall indicators in HTML: JSON-LD
isAccessibleForFree: false, common paywall text patterns ("for paid members only", "subscribe to read", etc.)
- Reports results to stdout with file locations
CLI interface
cd tools/link-check && bun run check # Check all content
cd tools/link-check && bun run check -- --semester spring2026 # Specific semester
cd tools/link-check && bun run check -- --file path/to/file.mdx # Specific file
package.json
{
"name": "@repo/link-check",
"version": "2026.2.0",
"private": true,
"type": "module",
"scripts": {
"check": "bun run src/check.ts"
},
"dependencies": {
"glob": "13.0.0"
}
}
No external HTTP library needed — Bun's built-in fetch is sufficient. No commander needed — simple process.argv parsing for the few options.
Implementation details
URL extraction: Regex for markdown links [text](https://...) and bare URLs in MDX. Deduplicate URLs across files but track all file locations for reporting.
Paywall detection heuristics (check in page HTML):
- JSON-LD schema:
"isAccessibleForFree"\s*:\s*false - Common paywall phrases: "for paid members only", "subscribe to continue reading", "this article is for subscribers", "paywall", "premium content"
- HTTP 402 status code
Rate limiting: 500ms delay between fetches to avoid being blocked. Concurrent fetches limited to 1 at a time.
Output format:
Checking 47 URLs across 23 files...
✓ https://www.anthropic.com/claude-4-system-card (200)
✗ https://www.404media.co/... (PAYWALL: "for paid members only")
→ content/spring2026/meetings/2026-02-03-assessments-and-agents.mdx:149
✗ https://example.com/gone (404)
→ content/spring2026/meetings/some-file.mdx:12
Results: 45 OK, 2 problems
User-agent: Set a descriptive user-agent string so sites don't block the checker as a bot.
Add to root package.json
Add a convenience script:
"link-check": "cd tools/link-check && bun run check"
Register in turbo.json
Not needed — this is a standalone tool run on-demand, not part of the build pipeline.
Files modified/created
| File | Change |
|---|---|
content/spring2026/meetings/2026-02-03-assessments-and-agents.mdx | Replace 404 Media link with Wiz blog |
TODOS.md | Remove paywalled links TODO |
tools/link-check/package.json | New tool package |
tools/link-check/src/check.ts | Link checker implementation |
package.json (root) | Add link-check script |
Verification
bun install # Pick up new workspace
bun run link-check # Run checker — should find 0 paywall issues
bun run link-check -- --file content/spring2026/meetings/2026-02-03-assessments-and-agents.mdx
If you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: ~/.claude/projects/-Users-challen-www-usingandunderstanding-ai/14ad46fb-1260-47af-99de-fc7823103b93.jsonl
I'll start by reading the files that need to be modified, then implement all changes.
Now let me check the existing glob package version used elsewhere in the repo.
Good, glob version 13.0.0 is consistent. Now I'll implement all the changes in parallel.
Now let me make all the independent edits in parallel.
Now create the link checker tool.
Good. Now let me test the link checker against the specific file we modified.
The --file path is resolved relative to the tool's working directory. Since bun run --cwd changes directory to tools/link-check, the relative path doesn't resolve correctly. I need to resolve the file path relative to the project root, not the CWD of the script.
The issue is that resolve(file) uses the CWD (which is tools/link-check/). I should resolve the --file path relative to the project root.
I need to add a PROJECT_ROOT constant and resolve the --file path relative to it.
Also need to make relative paths in output use the project root, not the tool's CWD.
Now test again.
All 7 URLs pass — no paywall issues and the new Wiz link is accessible.
All done. Here's a summary of what was changed:
Part 1 — Fixed paywalled link:
content/spring2026/meetings/2026-02-03-assessments-and-agents.mdx: Replaced the 404 Media link (paywalled) with Wiz's freely accessible blog post at line 149, and updated the facilitation notes at line 122 to reference "Wiz's research" instead of "The 404 Media database exposure"TODOS.md: Removed the "Check for paywalled links in activity content" TODO item
Part 2 — Created link checker tool:
tools/link-check/package.json: New workspace packagetools/link-check/src/check.ts: Implementation that scans content files for external URLs, checks HTTP status codes and paywall indicators (JSON-LDisAccessibleForFree, common paywall phrases, HTTP 402). Sequential fetches with 500ms rate limiting.package.json(root): Addedlink-checkconvenience script
Verification: Ran bun run link-check -- --file content/spring2026/meetings/2026-02-03-assessments-and-agents.mdx — all 7 URLs passed with 0 problems, including the new Wiz link.
Read the output file to retrieve the result: [tmp-path]
The background task already completed and I retrieved its results earlier — all 7 URLs passed with 0 problems. Everything is implemented and verified.
Your task
First, run git status, git diff, and git log --oneline -10 to understand the current state.
Then add and commit all modified files with a descriptive message. Break the changes into multiple commits if appropriate. If there are untracked files, ask if they should be added or added to the appropriate .gitignore file. The user may also want to take over at this point, so provide an abort option as well. Always check changes for unnecessary debugging statements or logging that might have been accidentally included. Prompt the user if you find any, and offer to remove each one by one. Do not use interactive git staging (git add -p) as it does not work.
No debugging statements or unnecessary logging in the changes. The diff looks clean. I notice the bun.lock also picked up an unrelated normalization (@radix-ui/react-alert-dialog removing the ^ prefix) — that's consistent with the project's exact version pinning policy, so it's fine.
I see two logical groups here — this could be one commit or two. Let me split it into two: the content fix and the new tool.
Two commits created on fixes:
e7e4ca7— Replace paywalled 404 Media link with Wiz blog post (2 files)85e415e— Add link checker tool for content URLs (4 files)
Let's update the TODOS.md document and remove the ones that we've just completed.
(no content)
The "Check for paywalled links in activity content" TODO was already removed in the first commit. Let me check what's currently in the file — are there other items you consider completed?
The paywalled links TODO was already removed. The remaining 10 items all appear to be open feature requests/improvements. Which of these do you consider completed?
Ah, maybe we're good. Can we check the Show Join Code on Every Member's page? Specifically, I want to make sure that, if A joins B using's A's code, but then C comes along for a threesome, C should be able to use the code shown at that point to either A or B. Does that make sense? They shouldn't have to view A's screen but should also be able to view B's.
(no content)
The current behavior already supports your scenario. Here's how it works:
Each member gets their own unique join code. When A and B form a group (say B enters A's code), both A and B each have their own code stored in activityGroupMembers.joinCode. So C can use either A's code or B's code to join the group.
The join API accepts any member's code. It looks up the code in activityGroupMembers, finds the group it belongs to, and adds the new user to that group — it doesn't care who created the group.
The TODO's concern is about discoverability, not functionality. After the group forms, both A and B see their code hidden behind a "Show code for another student" link (group-activity-client.tsx:502-521). Clicking it reveals their personal code. So C can walk up to either A or B, ask them to click "Show code," and use that code to join.
The scenario you described — C joining via either A's or B's screen — already works. The TODO item is about making the code more prominent (always visible rather than behind a click), which is a UX improvement but not a functional gap. Want to remove this TODO, or keep it as a UX enhancement to address later?
No, I think that this is fine, and I don't think that we should change the UI anyway. Third members are not common and we should optimize for the common case.
Makes sense. Let me remove that TODO item.
Done. The current behavior — code behind a "Show code for another student" link — is the right tradeoff for the common two-person case.
OK. Now, how about allowing a student to leave a group? That's probably something that we need to work, but it might be complicated. Develop a plan.
(no content)
Different task — I'll overwrite the plan. Let me explore the group activity system thoroughly first.
(no content)
Good exploration. I have a thorough understanding of the system now. Before I design the plan, I need to clarify one key design decision.
I'm wondering what happens when there are submissions involved, or interactions between the partners. This could get messy.
You're right to flag that. Let me dig into what actually happens during group activities — specifically text submissions and any cross-partner interactions — to understand what state would be disrupted by a leave.
(no content)
You're right to be cautious. Here's what I found about the entanglement:
Text submissions are per-user but tagged with groupId. Partners don't see each other's submissions, so there's no shared document to split. The groupId is metadata, not a gate.
Stage completions are also per-user, tagged with groupId. Historical records stay valid.
The real mess is role-based content. In the assessments activity, partner 0 reads document X while partner 1 reads document Y and submits observations about it. If partner 0 leaves mid-activity, partner 1 has already done work based on their role assignment. A new partner joining would get assigned roles fresh — but the activity context has shifted.
However, partners never see each other's submissions, and all data is individually owned. The groupId on submissions/completions is historical context, not a dependency. So the data layer is actually clean — it's the pedagogical continuity that's messy, and that's inherently true of any mid-activity partner change.
What would you like to clarify about those questions?
I guess the question is, what kind of corner cases are we looking at here given the current interaction patterns we already support? What we really want to avoid is a case where someone gets stuck: either because their partner left, or because they tried to switch partners.
Good framing. Let me trace through the concrete scenarios with the actual activity structure to identify where someone could get stuck.
And then we want to think about what the remediation is. If a partner leaves, should the remaining person find a new group? Continue with the activity solo?
(no content)
Let me reason through the concrete scenarios with what I already know from the exploration.
The critical case is the remaining member in a 2-person group. Here's why:
Scenario: A leaves, B remains (mid-activity)
- Content visibility is gated on group status being "complete" (i.e.,
joinedAtset + member count >= targetSize) - If B goes back to "pending," they lose access to content they were actively working on
- Finding a new partner mid-activity is hard — everyone else is already paired
- B is stuck unless the instructor manually unlocks them
Scenario: A leaves, B and C remain (3-person group)
- No problem. Two members still meets targetSize. Nothing changes for B and C.
Scenario: A leaves before any real work
- Low stakes. Both go back to pending, re-pair. Fine.
So the dangerous case is always: 2-person group, mid-activity, one leaves, remaining person loses content access.
The remediation options for the remaining member:
-
Back to pending (find new partner) — Theoretically correct but practically brutal. Mid-activity, no available partners, lose content visibility, existing submissions orphaned from context.
-
Auto-unlock (continue solo) — Keep
joinedAtset so content stays visible. Equivalent to an automatic instructor unlock. Their submissions, completions, and role content all stay intact. A new partner could still join them later via overflow. Least disruptive. -
Don't implement leave at all — The instructor unlock already handles the "absent partner" case. The "wrong partner" case is only a problem early in the activity before there's real entanglement.
My read: option 2 (auto-unlock for remaining member) avoids the stuck scenario entirely. The leaver goes back to pending (their choice), the remaining member continues uninterrupted. Does that match your thinking, or are there scenarios I'm missing?
That's a reasonable call. The instructor unlock is already a working escape hatch for the cases that come up in practice. Let me update the TODO to capture what we learned so it's useful when you revisit this.
But wait, do we have the instructor unlock? I don't want to do that yet either.
(no content)
The API endpoint exists — POST /admin/:groupKey/:activityId/unlock sets joinedAt on a solo student's membership. But I'm not sure if there's admin UI wired up to it. Let me check quickly.
Ah, I see. So this is a way to unlock a solo version of the activity for a student?
Yes, exactly. If a student is solo (no partner available), an instructor can hit that endpoint with the student's userId, and it sets their joinedAt to the current time. That makes the system treat them as "complete" — content becomes visible and they can proceed through the activity alone, even though they never actually paired with anyone.
The API endpoint exists but I'm not sure there's admin UI for it — it may only be callable directly. Want me to check?
Sure. But I guess I'm wondering if there are cases where a student would still be stuck at that point, because their partner needs to do something to allow the activity to continue. Is that something we support? Maybe not yet, but it could be? Like if there are two questions that need to be answered by each partner.
(no content)
Actually, sorry. At this point the instructor is controlling stage progression, correct?
Yes. The instructor controls which stage is active — they advance stages manually from the admin panel. Students can mark individual stages as "completed" (which feeds into a completion count the instructor sees), but they can't move to the next stage until the instructor advances it.
So there's no case today where a student is blocked waiting on their partner's action to proceed. The gates are:
- Group formation — need a partner (or instructor unlock) to see content
- Instructor advancement — instructor decides when the whole class moves to the next stage
- Check-in gate — optionally, the instructor can require a student to have completed the current stage before they see the next one (but this is per-user, not per-group)
The partner dependency you're imagining — "both partners must submit before either can continue" — doesn't exist yet. Right now partners work independently on role-specific content within the same time box, and the instructor moves everyone forward together.
Let me update the TODO with the analysis we did, and update the plan file accordingly.