UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

147 lines (146 loc) • 5.67 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. import { isSafeLinkUrl } from "./content-extract.js"; import { coerceString, escapeMarkdownStructure } from "./fetcher-util.js"; import { fetchPinnedJson } from "./host-guard.js"; /** * Bluesky profile fetcher, using the public AT Protocol XRPC endpoints. * * No authentication: `public.api.bsky.app` serves public profiles unauthed. * The fetcher contract takes a URL, so bare-handle input (`@user.bsky.social`) * is out of scope — that is `akm bundle add` sugar, not an addressable source. */ const BSKY_HOSTS = new Set(["bsky.app", "www.bsky.app"]); const PUBLIC_API_BASE = "https://public.api.bsky.app"; /** The XRPC endpoint caps `limit` at 100; asking for more is an error. */ const MAX_FEED_LIMIT = 100; const DEFAULT_FEED_LIMIT = 50; /** Bound the JSON read: fetch timeouts cover only the header phase. */ const BSKY_BYTE_CAP = 4 * 1024 * 1024; const BSKY_BODY_TIMEOUT_MS = 30_000; /** Extract a handle from `https://bsky.app/profile/<handle>`. */ export function extractBlueskyHandle(url) { if (!BSKY_HOSTS.has(url.hostname.toLowerCase())) return null; const segments = url.pathname.split("/").filter(Boolean); if (segments.length < 2 || segments[0] !== "profile") return null; const handle = segments[1]?.trim(); if (!handle) return null; // Only the profile root is a snapshot target; /post/<id> and other // sub-routes are individual records, not a profile feed. if (segments.length > 2) return null; return handle.replace(/^@/, ""); } /** Emit a link only when it is a safe absolute http(s) URL. */ function safeLink(value) { if (!value) return ""; try { return isSafeLinkUrl(new URL(value)) ? value : ""; } catch { return ""; } } function num(value) { return typeof value === "number" && Number.isFinite(value) ? value : 0; } function xrpcJson(url, timeoutMs, signal) { return fetchPinnedJson(url, { headers: { Accept: "application/json", "User-Agent": "akm-cli bluesky fetcher" }, byteCap: BSKY_BYTE_CAP, bodyTimeoutMs: BSKY_BODY_TIMEOUT_MS, timeoutMs, signal, }); } /** Resolve a handle to its DID. Returns null when the handle does not exist. */ async function resolveHandle(handle, timeoutMs, signal) { const url = `${PUBLIC_API_BASE}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`; const json = (await xrpcJson(url, timeoutMs, signal)); const did = coerceString(json?.did); return did || null; } function readPosts(feed, limit) { if (!Array.isArray(feed)) return []; const posts = []; for (const entry of feed.slice(0, limit)) { const post = entry?.post; if (!post) continue; const record = (post.record ?? {}); const embed = (post.embed ?? {}); const external = (embed.external ?? {}); posts.push({ text: coerceString(record.text), createdAt: coerceString(record.createdAt), uri: coerceString(post.uri), likeCount: num(post.likeCount), repostCount: num(post.repostCount), externalUri: safeLink(coerceString(external.uri)), }); } return posts; } /** Convert an `at://did/app.bsky.feed.post/<rkey>` URI to a bsky.app permalink. */ function permalink(handle, uri) { const rkey = uri.split("/").pop(); return rkey ? `https://bsky.app/profile/${handle}/post/${rkey}` : ""; } function renderMarkdown(handle, posts) { const sections = []; for (const post of posts) { if (!post.text && !post.externalUri) continue; const when = post.createdAt ? new Date(post.createdAt) : null; const iso = when && !Number.isNaN(when.getTime()) ? when.toISOString() : ""; sections.push(`## ${iso || "(undated)"}`, ""); if (post.text) sections.push(escapeMarkdownStructure(post.text), ""); const meta = []; const link = permalink(handle, post.uri); if (link) meta.push(link); if (post.externalUri) meta.push(`link: ${post.externalUri}`); meta.push(`${post.likeCount} likes, ${post.repostCount} reposts`); sections.push(meta.join(" — "), ""); } return sections.join("\n").trimEnd(); } const blueskyFetcher = { name: "bluesky-profile", matches(url) { return extractBlueskyHandle(url) !== null; }, async fetch(url, context) { const handle = extractBlueskyHandle(url); if (!handle) return null; const did = await resolveHandle(handle, context.timeoutMs, context.signal); if (!did) return null; const feedUrl = `${PUBLIC_API_BASE}/xrpc/app.bsky.feed.getAuthorFeed` + `?actor=${encodeURIComponent(did)}&limit=${Math.min(DEFAULT_FEED_LIMIT, MAX_FEED_LIMIT)}`; const json = (await xrpcJson(feedUrl, context.timeoutMs, context.signal)); if (!json) return null; const posts = readPosts(json.feed, DEFAULT_FEED_LIMIT); const markdown = renderMarkdown(handle, posts); if (!markdown) return null; return { url: `https://bsky.app/profile/${handle}`, title: `Bluesky — @${handle}`, markdown, preferredName: `bluesky/${handle}`, tags: ["bluesky", "social"], }; }, }; export default blueskyFetcher;