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

142 lines (111 loc) • 5.1 kB
# Website Sources `akm import <url>` and every other URL-based knowledge read path pass through a pluggable fetcher seam before the built-in website scraper. akm ships five built-in fetchers, probed in this order (host-specific fetchers before the deliberately loose path-shaped RSS matcher; any fetcher returning `null` falls through to the generic website crawler): | Fetcher | Handles | What it extracts | | --- | --- | --- | | `github-repository` | `github.com/<owner>/<repo>` URLs | The repository README, rendered to markdown | | `youtube-transcript` | YouTube watch/short URLs | The video description and transcript when captions are available | | `bluesky-profile` | `bsky.app` profile URLs | Public profile and recent posts via the unauthenticated AT Protocol API | | `x` | `x.com`/`twitter.com` profiles, posts, and articles | Post/article content; profile timelines need a bearer token (below) | | `rss-feed` | Feed-shaped paths (`.rss`/`.atom`/`.xml`, `?feed=`) | Feed items as markdown sections | ## X credentials The `x` fetcher reads public post/article pages without credentials. Profile timelines require an API bearer token, resolved in order from the `X_BEARER_TOKEN` environment variable, then the `secrets/x-bearer-token` akm secret (`akm secret set secrets/x-bearer-token`). Alternatively, set `X_RSS_TEMPLATE` to an RSS-bridge URL containing `{username}` to fetch profiles through the `rss-feed` path instead. ## Crawl options When no fetcher claims a URL, the generic crawler applies the `website` source descriptor's knobs (see the config schema): `maxPages` (default 50), `maxDepth` (default 3), `respectRobots` (default `true`; set `false` to bypass robots.txt for that source), and `crawlTimeoutMs` per request. ## Why Some sites need custom extraction logic that the generic HTML-to-markdown path cannot provide well. Examples: - YouTube: fetch the transcript instead of the video description page chrome - GitHub issues: fetch the issue body plus comments instead of the repository shell - PDF-heavy sites: extract the underlying text instead of scraping an embed page ## Discovery Drop fetcher modules into: ```text <stashDir>/scripts/wiki-fetchers/ ``` `<stashDir>` is the active bundle for the current operation. When a command has a resolved write target (for example `akm import --target ...`), akm loads fetchers from that target bundle before falling back to the built-in website scraper. Files ending in `.ts`, `.js`, or `.mjs` are loaded in alphabetical order. The first fetcher whose `matches()` returns `true` gets a chance to handle the URL. If a fetcher: - returns `null`, akm falls through to the next fetcher or the built-in website scraper - throws, akm logs a warning and falls through to the next fetcher or the built-in website scraper ## Interface Fetchers should export a default object with this shape: ```ts export interface WikiSnapshotResult { url: string; title: string; markdown: string; preferredName?: string; tags?: string[]; } export interface FetcherContext { stashDir: string; timeoutMs: number; signal?: AbortSignal; /** * Resolve a secret by ref (e.g. `secrets/x-bearer-token`) from akm's * secret store, or null when absent. Injected rather than imported so * fetchers stay leaves in the import graph; never log the returned value. */ resolveSecret?: (ref: string) => string | null; } export interface WikiSnapshotFetcher { name: string; matches(url: URL, context: FetcherContext): boolean; fetch(url: URL, context: FetcherContext): Promise<WikiSnapshotResult | null>; } ``` `markdown` should contain only the body content. akm still wraps the result in the standard raw snapshot frontmatter (`name`, `description`, `sourceUrl`, `title`, `updated`, a `lint_skip` entry for `stale-path`, and `tags`) so downstream wiki tooling continues to work normally. Custom `tags` are appended to the default snapshot tags (`website` and the resolved hostname). They do not replace those defaults. ## Example ```ts export default { name: "youtube-transcript", matches(url: URL, _context: FetcherContext) { return ( (url.hostname === "www.youtube.com" && url.pathname === "/watch") || url.hostname === "youtu.be" ); }, async fetch(url: URL, _context: FetcherContext) { const videoId = url.hostname === "youtu.be" ? url.pathname.slice(1) : url.searchParams.get("v"); if (!videoId) return null; const transcript = await getTranscript(videoId); return { url: url.toString(), title: `Video ${videoId}`, markdown: `## Transcript\n\n${transcript}`, preferredName: `videos/${videoId}`, tags: ["video", "transcript"], }; }, }; ``` ## Notes - The fetcher seam is shared by `akm import <url>` and other URL-based knowledge reads that use `fetchWebsiteMarkdownSnapshot()`. - The built-in website scraper remains the default path when no custom fetcher matches. - Bundle-local fetchers are loaded before built-ins, so you can override any built-in fetcher's behavior for your own workflow when needed.