Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit b05d3399bfc1d74fb99d821c11aa40002c060f2e
parent f1beea234e970ccf4f858435efd7bfce6cfbf5a5
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri,  2 Oct 2026 10:05:35 -0400

mcp: get_post names the post's archive page (`- archive: <origin>/?v=<channel>%2F<id>&vm=post`) when the source has a viewer origin -- the channel's member site on a hub, the site's own otherwise; viewerPostUrl in common/lib/momentUrl.ts

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mcommon/lib/momentUrl.ts | 22++++++++++++++++++++++
Mmcp/src/momentUrl.test.ts | 21+++++++++++++++++++++
Mmcp/src/search.test.ts | 22++++++++++++++++++++++
Mmcp/src/server.ts | 10+++++++---
4 files changed, 72 insertions(+), 3 deletions(-)

diff --git a/common/lib/momentUrl.ts b/common/lib/momentUrl.ts @@ -101,6 +101,28 @@ export function viewerMomentUrl( return u.toString(); } +// An archived post's page on the viewer: the post modal for `<channel>/<id>` +// (`?v=<slug>&vm=post`), which shows the post and links on to the original. +// Null when the origin or slug is missing or the origin can't be parsed. +export function viewerPostUrl( + siteOrigin: string | null | undefined, + slug: string | null | undefined, +): string | null { + if (!siteOrigin || !slug) return null; + let u: URL; + try { + u = new URL(siteOrigin); + } catch { + return null; + } + u.pathname = "/"; + u.search = ""; + u.hash = ""; + u.searchParams.set("v", slug); + u.searchParams.set("vm", "post"); + return u.toString(); +} + // The preferred moment link: viewer deep link when we have an origin+slug, // else the platform fallback. Null when neither can be built. export function momentUrl(input: MomentUrlInput): string | null { diff --git a/mcp/src/momentUrl.test.ts b/mcp/src/momentUrl.test.ts @@ -7,6 +7,7 @@ import { momentBaseUrl, viewerMomentBaseUrl, platformMomentBaseUrl, + viewerPostUrl, } from "yt-dlp-transcript-common/lib/momentUrl"; // ─── Archilyzer viewer link (preferred) ─── @@ -217,3 +218,23 @@ test("momentBaseUrl: viewer base wins, platform fallback, else null", () => { assert.equal(momentBaseUrl({ siteOrigin: null, slug: "ch/vid" }), null); }); + +// ─── An archived post's page ─── + +test("viewerPostUrl: origin + post slug → ?v=<channel>%2F<id>&vm=post", () => { + assert.equal( + viewerPostUrl("https://jasolyzer.pages.dev", "piratesoftware-bsky/3l6uxj6esfr2z"), + "https://jasolyzer.pages.dev/?v=piratesoftware-bsky%2F3l6uxj6esfr2z&vm=post", + ); + // An origin with its own path is normalized to the viewer's root. + assert.equal( + viewerPostUrl("https://site.example/search?q=x", "ch-x/123"), + "https://site.example/?v=ch-x%2F123&vm=post", + ); +}); + +test("viewerPostUrl: no origin, no slug, or a bad origin → null", () => { + assert.equal(viewerPostUrl(null, "a/b"), null); + assert.equal(viewerPostUrl("https://x.example", ""), null); + assert.equal(viewerPostUrl("not a url", "a/b"), null); +}); diff --git a/mcp/src/search.test.ts b/mcp/src/search.test.ts @@ -1192,6 +1192,28 @@ test("server: get_post returns the post with no timestamps", async () => { await client.close(); }); +test("server: get_post names the post's archive page when the source has a viewer", async () => { + // A stub has no viewer origin: no archive line. + const bare = await connectClient(new StubSource()); + assert.doesNotMatch(firstText(await bare.callTool({ name: "get_post", arguments: { post_id: "p1" } })), /- archive:/); + await bare.close(); + // A site with one: its post modal for <channel>/<id>. + class SiteSource extends StubSource { + publicOrigin(): string | null { + return "https://site.example"; + } + } + const site = await connectClient(new SiteSource()); + const out = firstText(await site.callTool({ name: "get_post", arguments: { post_id: "p1" } })); + const line = /- archive: (\S+)/.exec(out); + assert.ok(line, "archive line present"); + const u = new URL(line![1]); + assert.equal(u.origin, "https://site.example"); + assert.equal(u.searchParams.get("vm"), "post"); + assert.match(u.searchParams.get("v") ?? "", /\/p1$/); + await site.close(); +}); + test("server: get_thread returns parent + replies", async () => { const client = await connectClient(new StubSource()); const res = await client.callTool({ diff --git a/mcp/src/server.ts b/mcp/src/server.ts @@ -8,7 +8,7 @@ import { transcriptToMarkdown } from "yt-dlp-transcript-common/lib/transcriptToM import { formatDate, formatDuration } from "yt-dlp-transcript-common/lib/format"; import { manifestHasDigest } from "yt-dlp-transcript-common/lib/digests"; import type { VideoStat } from "yt-dlp-transcript-common/lib/stats"; -import { momentUrl, momentBaseUrl } from "yt-dlp-transcript-common/lib/momentUrl"; +import { momentUrl, momentBaseUrl, viewerPostUrl } from "yt-dlp-transcript-common/lib/momentUrl"; import type { Platform } from "yt-dlp-transcript-common/lib/platform"; import type { SearchAlias } from "yt-dlp-transcript-common/lib/searchAliases"; import { isTagId, type PublishedTag } from "yt-dlp-transcript-common/lib/curatedTags"; @@ -2136,7 +2136,7 @@ async function handleGetTranscripts( // Render one post as markdown. No timestamps anywhere — a post has no timeline, // and emitting a 0:00 would invite a bogus `@ mm:ss` citation. -function postToMarkdown(post: Post, heading = true): string { +function postToMarkdown(post: Post, heading = true, archive: string | null = null): string { const lines: string[] = []; if (heading) lines.push(`# Post by ${post.authorName || post.author}`); lines.push( @@ -2146,6 +2146,9 @@ function postToMarkdown(post: Post, heading = true): string { lines.push(`- platform: ${post.platform}`); lines.push(`- post_id: ${post.id}`); lines.push(`- source: ${post.url}`); + // The post's page on the archive, when the source has a viewer: a citation + // that survives the post, or the platform, going away. + if (archive) lines.push(`- archive: ${archive}`); if (post.isRepost) lines.push(`- repost: yes`); if (post.isReply) lines.push(`- reply: yes`); if (post.threadId && post.threadId !== post.id) { @@ -2186,7 +2189,8 @@ async function handleGetPost( typeof args.channel === "string" ? args.channel : undefined, ); if (!found) return errorText(`post not found: ${postId}`); - return text(postToMarkdown(found.post)); + const archive = viewerPostUrl(found.ch.siteUrl ?? source.publicOrigin(), found.post.slug); + return text(postToMarkdown(found.post, true, archive)); } async function handleGetThread(