Live: https://tinyskills.vercel.app
TinySkills generates comprehensive technical SKILL.md guides by combining TinyFish Search (discover URLs by source type), TinyFish Fetch (pull clean page text in batches), and an LLM (OpenAI or OpenRouter) that synthesizes everything into a single markdown skill. It is aimed at learning or documenting a technology from documentation, GitHub, Stack Overflow, and developer blogs in one run.
The user enters a topic and selects which source types to include (docs, GitHub, Stack Overflow, blog). The server calls identifySources() in lib/ai-client.ts, which follows this order:
- TinyFish Search (primary) — If
TINYFISH_API_KEYis set, the app runs one Search query per enabled source type in parallel viaidentifySourcesViaTinyFishSearch()(lib/tinyfish-source-discovery.ts). Each query is tailored so results skew toward the right kind of page (see Search queries below). - LLM fallback — If Search returns no URLs, or Search throws, control falls back to
identifySourcesViaLlm(), which asks the configured model to output a JSON list of URLs with types, titles, and reasons.
Requirements: You need at least TinyFish or an LLM key for this step to succeed; full app flows also need an LLM for synthesis (see Environment variables).
For every identified URL, the app calls the TinyFish Fetch API through the SDK: client.fetch.getContents(). This renders pages in a real browser and returns markdown-oriented text, which is ideal for feeding the synthesis model.
Key behaviors:
- Batching: Up to 10 URLs per
getContentscall (FETCH_BATCH_SIZEinapp/api/scrape-sources/route.ts). Example: 8 sources → usually one batch; 16 sources → two batches. - Parallel batches: Multiple batches run with
Promise.all, so total wall-clock time stays closer to “one heavy round trip” than “N sequential fetches.” - Format: Requests use
format: "markdown"so thetextfield is ready for LLM consumption. - Resilience: The Fetch API returns per-URL errors in an
errorsarray without failing the whole batch; the route maps those tosource_errorSSE events. - SSE: The client still receives a server-sent event stream:
scrape_start,source_start,source_step,source_completeorsource_error, thenscrape_complete. There is no live browser preview URL in this path (unlike the older TinyFish Agent stream).
Collected markdown is passed to synthesizeSkill() in lib/ai-client.ts, which streams a SKILL.md-style document (YAML frontmatter + sections such as Quick Start, Core Concepts, Pitfalls, etc.) using the same LLM provider as the fallback step.
The Search API returns ranked results with title, snippet, URL, and metadata. TinySkills uses the official @tiny-fish/sdk:
import { TinyFish } from "@tiny-fish/sdk";
const client = new TinyFish(); // uses TINYFISH_API_KEY from the environment
const response = await client.search.query({
query: "site:stackoverflow.com react hooks",
});
// response.results[].url, .title, .snippet, .position, ...In lib/tinyfish-source-discovery.ts, each SourceType maps to a dedicated query (topic = user input, trimmed):
| Source type | Query pattern (conceptually) |
|---|---|
| docs | {topic} official documentation programming |
| github | site:github.com {topic} |
| stackoverflow | site:stackoverflow.com {topic} |
| blog | {topic} developer tutorial blog |
These are not arbitrary site lists from an LLM—they are live search results, which tends to surface real, linkable pages.
- All enabled types are queried with
Promise.all, so identification latency is roughly one Search round-trip per “wave”, not the sum of four serial calls. - URLs are normalized (fragment stripped) and deduplicated across types so the same page is not scraped twice.
- Up to
maxPerTyperesults are taken per type (default 2 per type in settings, configurable from the UI / API). - Each
IdentifiedSourcestores title from Search, and reason from the snippet (or a short fallback string referencing the query and hit position).
- LLM-only discovery can run if TinyFish Search is not configured or returns nothing—see
identifySources()inlib/ai-client.ts. - Synthesis always requires
OPENAI_API_KEYorOPENROUTER_API_KEYunless you change the app.
Official Search API overview: TinyFish Search API.
Fetch turns known URLs into clean extracted text (HTML, Markdown, or JSON tree depending on options). TinySkills only needs markdown for the skill body.
import { TinyFish } from "@tiny-fish/sdk";
const client = new TinyFish();
const result = await client.fetch.getContents({
urls: [
"https://example.com/docs/page-a",
"https://example.com/docs/page-b",
// ... up to 10 URLs per request in this app
],
format: "markdown",
});
// result.results[] — per-URL title, description, text, final_url, …
// result.errors[] — per-URL failures without failing siblingsFor each successful row, the API route builds a single string for synthesis:
- Optional
# titlefromtitle descriptionas introductory text when present- Main body from
text(markdown)
If the API returns JSON-shaped content for a URL, the route stringifies it for inclusion so nothing is dropped silently.
- Batched Fetch replaces per-URL browser agents for this step, which keeps latency and cost lower for typical documentation and article pages.
- Heavy or interactive-only content may still be imperfect; the tradeoff is speed and simplicity vs. a full TinyFish Agent run (not used in the current scrape route).
Official Fetch API overview: TinyFish Fetch API.
With all four source types enabled and 2 URLs per type (8 sources total):
- Search runs 4 parallel queries → up to 8 distinct URLs with titles and snippets.
- Fetch runs
getContentsin one batch (≤10 URLs) with parallel batch execution if you ever exceed 10 URLs. - Synthesize streams one SKILL.md from the combined markdown.
┌─────────────────────────────────────────────────────────────┐
│ User (browser) │
│ Next.js UI — topic, source toggles, max per type, Generate │
└────────────────────────────┬────────────────────────────────┘
│
▼
┌──────────────────────────┐
│ POST /api/identify-sources │
└────────────┬─────────────┘
│
┌─────────────────┴─────────────────┐
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ TinyFish Search │ (fallback) │ OpenAI / OpenRouter │
│ client.search.query│ │ identifySourcesViaLlm│
│ per source type │ │ JSON URLs + reasons │
└─────────┬──────────┘ └──────────┬───────────┘
│ │
└──────────────┬────────────────────┘
▼
{ sources: IdentifiedSource[] }
│
▼
┌──────────────────────────┐
│ POST /api/scrape-sources │
│ SSE stream to client │
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ TinyFish Fetch │
│ client.fetch.getContents │
│ batches of ≤10 URLs │
│ format: markdown │
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ POST /api/synthesize │
│ streamText → SKILL.md │
└─────────────────────────────┘
| Variable | Required | Role |
|---|---|---|
TINYFISH_API_KEY |
Strongly recommended | Search + Fetch via @tiny-fish/sdk (API keys) |
OPENAI_API_KEY |
For synthesis / fallback | Preferred LLM when set (gpt-4.1-mini default in code) |
OPENROUTER_API_KEY |
Alternative to OpenAI | Used if OPENAI_API_KEY is unset (default model google/gemini-2.5-flash-lite) |
Copy .env.local.example to .env.local and fill in values. Restart the dev server after changes.
- Node.js 18+
- TinyFish API key (Search + Fetch)
- OpenAI and/or OpenRouter key for LLM steps
git clone https://github.com/tinyfish-io/tinyfish-cookbook.git
cd tinyfish-cookbook/tinyskills
npm install
cp .env.local.example .env.local
# Edit .env.local — at minimum TINYFISH_API_KEY and one LLM key
npm run devOpen http://localhost:3000.
| Feature | Description |
|---|---|
| TinyFish Search | Typed web queries per source; parallel execution; deduplicated URLs |
| TinyFish Fetch | Batched URL → markdown extraction; parallel batches when needed |
| LLM synthesis | Structured SKILL.md with frontmatter and deep sections |
| LLM fallback | URL discovery when Search returns no usable hits |
| SSE scraping | Live progress for identify → scrape → synthesize pipeline |
| Streaming output | Skill guide streams from /api/synthesize |
| Local history | Saved in localStorage (keys use a legacy skillforge_* prefix for compatibility — see types/index.ts) |
| Settings | Default sources, max per type, export/import |
For a topic like “React Server Components”, TinySkills may:
- Search — Find doc pages, GitHub threads, Stack Overflow questions, and blog posts matching the typed queries.
- Fetch — Pull full readable text from each URL.
- Synthesize — Produce a skill that includes overview, patterns, pitfalls, and examples drawn from those sources.
| Path | Purpose |
|---|---|
lib/tinyfish-client.ts |
Singleton-style getTinyFishClient() for SDK access |
lib/tinyfish-source-discovery.ts |
Search queries + identifySourcesViaTinyFishSearch |
lib/ai-client.ts |
LLM provider, identifySources, identifySourcesViaLlm, synthesizeSkill |
lib/utils.ts |
Shared helpers (cn, countWords, etc.) |
app/api/identify-sources/route.ts |
Identify API |
app/api/scrape-sources/route.ts |
Fetch + SSE |
app/api/synthesize/route.ts |
Streaming synthesis |
Package name in package.json: tinyskills.
- TinyFish Search API
- TinyFish Fetch API
- TinyFish Browser API — not used in this app’s default path; documented for comparison
Built with TinyFish. Part of the TinyFish Cookbook.
