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
JavaScript
// 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;