Skip to content

Commit 445a673

Browse files
committed
add routing for skills
1 parent 95c6058 commit 445a673

12 files changed

Lines changed: 42158 additions & 0 deletions

File tree

packages/skillc/src/buildTopics.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
4+
type SourceDoc = {
5+
id: string;
6+
relPath: string;
7+
ext: "md" | "mdx";
8+
size: number;
9+
sha256: string;
10+
sha256_normalized: string;
11+
frontmatter_keys: string[];
12+
title?: string;
13+
headings: Array<{ depth: number; text: string }>;
14+
text: string;
15+
};
16+
17+
type TopicsIndex = {
18+
schema: 1;
19+
library: string;
20+
version: string; // for now: ref (main) or commit short sha
21+
docs_hash: string;
22+
revision: any;
23+
topics: Array<{
24+
slug: string;
25+
title: string;
26+
kind: "page" | "section";
27+
relPath: string;
28+
anchor?: string;
29+
sourceIds: string[]; // doc ids from sources.jsonl
30+
}>;
31+
};
32+
33+
function slugify(s: string) {
34+
return s
35+
.toLowerCase()
36+
.replace(/['"]/g, "")
37+
.replace(/[^a-z0-9]+/g, "-")
38+
.replace(/^-+|-+$/g, "")
39+
.slice(0, 80);
40+
}
41+
42+
function makeAnchor(text: string) {
43+
// Keep consistent with GitHub-ish anchors (roughly)
44+
return slugify(text);
45+
}
46+
47+
export function buildTopicsIndex(args: {
48+
library: string;
49+
sourceRoot: string; // build/sources/<lib>/<owner>__<repo>/<ref>
50+
indexRoot: string; // build/index/<lib>/<owner>__<repo>/<ref>
51+
version: string; // e.g. "main"
52+
outDir: string; // registry/skills/<lib>/<version>
53+
}) {
54+
const docsIndexPath = path.join(args.indexRoot, "docs.index.json");
55+
const sourcesPath = path.join(args.indexRoot, "docs.sources.jsonl");
56+
const revisionPath = path.join(args.sourceRoot, "REVISION.json");
57+
58+
if (!fs.existsSync(docsIndexPath)) throw new Error(`Missing ${docsIndexPath}`);
59+
if (!fs.existsSync(sourcesPath)) throw new Error(`Missing ${sourcesPath}`);
60+
if (!fs.existsSync(revisionPath)) throw new Error(`Missing ${revisionPath}`);
61+
62+
const docsIndex = JSON.parse(fs.readFileSync(docsIndexPath, "utf8"));
63+
const revision = JSON.parse(fs.readFileSync(revisionPath, "utf8"));
64+
const docs_hash: string = docsIndex.docs_hash;
65+
66+
const lines = fs.readFileSync(sourcesPath, "utf8").split("\n").filter(Boolean);
67+
const docs: SourceDoc[] = lines.map((l) => JSON.parse(l));
68+
69+
const topics: TopicsIndex["topics"] = [];
70+
71+
for (const doc of docs) {
72+
const pageTitle = doc.title ?? doc.relPath.replace(/^docs\//, "");
73+
topics.push({
74+
slug: `page:${slugify(doc.relPath)}`,
75+
title: pageTitle,
76+
kind: "page",
77+
relPath: doc.relPath,
78+
sourceIds: [doc.id],
79+
});
80+
81+
// Add section topics for h2/h3 only (keeps index tight)
82+
for (const h of doc.headings) {
83+
if (h.depth !== 2 && h.depth !== 3) continue;
84+
const anchor = makeAnchor(h.text);
85+
topics.push({
86+
slug: `sec:${slugify(doc.relPath)}#${anchor}`,
87+
title: `${pageTitle}${h.text}`,
88+
kind: "section",
89+
relPath: doc.relPath,
90+
anchor,
91+
sourceIds: [doc.id],
92+
});
93+
}
94+
}
95+
96+
// stable order
97+
topics.sort((a, b) => a.slug.localeCompare(b.slug));
98+
99+
const out: TopicsIndex = {
100+
schema: 1,
101+
library: args.library,
102+
version: args.version,
103+
docs_hash,
104+
revision,
105+
topics,
106+
};
107+
108+
fs.mkdirSync(args.outDir, { recursive: true });
109+
fs.writeFileSync(path.join(args.outDir, "index.json"), JSON.stringify(out, null, 2) + "\n");
110+
}

packages/skillc/src/cli.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import path from "node:path";
33
import process from "node:process";
44
import { fetchDocsFromRepo } from "./fetchDocs.js";
55
import { indexDocsSnapshot } from "./indexDocs.js";
6+
import { buildTopicsIndex } from "./buildTopics.js";
7+
import { renderSkillMd } from "./renderSkill.js";
8+
import { renderLibraryAgent } from "./renderLibraryAgent.js";
9+
import { renderTanstackAgent } from "./renderTanstackAgent.js";
610

711
function arg(name: string) {
812
const idx = process.argv.indexOf(`--${name}`);
@@ -55,6 +59,41 @@ async function main() {
5559
return;
5660
}
5761

62+
if (cmd === "build-topics") {
63+
const lib = arg("lib");
64+
const repo = arg("repo");
65+
const ref = arg("ref") ?? "main";
66+
if (!lib) throw new Error("--lib is required");
67+
if (!repo) throw new Error("--repo is required");
68+
69+
const m = repo.match(/github\.com\/([^/]+)\/([^/]+)(?:\.git)?$/);
70+
if (!m) throw new Error(`bad repo: ${repo}`);
71+
const owner = m[1];
72+
const repoName = m[2].replace(/\.git$/, "");
73+
74+
const sourceRoot = path.join("build", "sources", lib, `${owner}__${repoName}`, ref);
75+
const indexRoot = path.join("build", "index", lib, `${owner}__${repoName}`, ref);
76+
77+
const version = ref; // temporary policy (will change to npm version)
78+
const outDir = path.join("registry", "skills", lib, version);
79+
80+
buildTopicsIndex({ library: lib, sourceRoot, indexRoot, version, outDir });
81+
renderSkillMd({ library: lib, version, outDir });
82+
83+
// agents
84+
renderTanstackAgent({ outDir: path.join("registry", "agents") });
85+
if (lib === "query" || lib === "router") {
86+
renderLibraryAgent({
87+
library: lib,
88+
version,
89+
skillPath: path.join("registry", "skills", lib, version, "SKILL.md").replace(/\\/g, "/"),
90+
outDir: path.join("registry", "agents"),
91+
});
92+
}
93+
94+
return;
95+
}
96+
5897
throw new Error(`Unknown command: ${cmd}`);
5998
}
6099

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
4+
export function renderLibraryAgent(args: {
5+
library: "query" | "router";
6+
version: string;
7+
skillPath: string; // registry/skills/<lib>/<version>/SKILL.md
8+
outDir: string; // registry/agents
9+
}) {
10+
fs.mkdirSync(args.outDir, { recursive: true });
11+
12+
const name = `tanstack-${args.library}`;
13+
const lines: string[] = [];
14+
15+
lines.push(`# ${name}`);
16+
lines.push(``);
17+
lines.push(`Routes requests to TanStack **${args.library}** skills using progressive disclosure.`);
18+
lines.push(``);
19+
lines.push(`## When to use`);
20+
lines.push(`Use when the user asks about TanStack ${args.library} concepts, APIs, patterns, or troubleshooting.`);
21+
lines.push(``);
22+
lines.push(`## Routing algorithm`);
23+
lines.push(`1) If the request is not about TanStack ${args.library}, stop.`);
24+
lines.push(`2) Otherwise, open: \`${args.skillPath}\``);
25+
lines.push(`3) Use its Routing table to pick a topic slug, then open the referenced doc section/file.`);
26+
lines.push(``);
27+
lines.push(`## Boundaries`);
28+
lines.push(`- Do not load unrelated skills.`);
29+
lines.push(`- Do not infer behavior not present in the docs snapshot.`);
30+
lines.push(``);
31+
32+
fs.writeFileSync(path.join(args.outDir, `${name}.md`), lines.join("\n") + "\n");
33+
}

packages/skillc/src/renderSkill.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
4+
export function renderSkillMd(args: {
5+
library: string;
6+
version: string;
7+
outDir: string; // registry/skills/<lib>/<version>
8+
}) {
9+
const indexPath = path.join(args.outDir, "index.json");
10+
if (!fs.existsSync(indexPath)) throw new Error(`Missing ${indexPath}`);
11+
12+
const idx = JSON.parse(fs.readFileSync(indexPath, "utf8"));
13+
const topics: Array<{ slug: string; title: string; kind: string }> = idx.topics;
14+
15+
const lines: string[] = [];
16+
lines.push(`# TanStack ${args.library} skill`);
17+
lines.push(``);
18+
lines.push(`**Version:** \`${args.version}\``);
19+
lines.push(``);
20+
lines.push(`## What this skill is for`);
21+
lines.push(`Use this skill to route TanStack ${args.library} questions to the right docs topic.`);
22+
lines.push(``);
23+
lines.push(`## Boundaries`);
24+
lines.push(`- Do not run builds, tests, or repo automation.`);
25+
lines.push(`- Do not guess behavior that is not in the docs snapshot.`);
26+
lines.push(``);
27+
lines.push(`## Routing`);
28+
lines.push(`Pick the closest topic slug below, then open the referenced doc file/section.`);
29+
lines.push(``);
30+
lines.push(`| Topic | Slug |`);
31+
lines.push(`|---|---|`);
32+
33+
// keep the table compact: show pages + sections
34+
for (const t of topics.slice(0, 250)) {
35+
lines.push(`| ${t.title.replace(/\|/g, "\\|")} | \`${t.slug}\` |`);
36+
}
37+
38+
if (topics.length > 250) {
39+
lines.push(``);
40+
lines.push(`(Index truncated in this view; see \`index.json\` for full list.)`);
41+
}
42+
43+
fs.writeFileSync(path.join(args.outDir, "SKILL.md"), lines.join("\n") + "\n");
44+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
4+
export function renderTanstackAgent(args: {
5+
outDir: string; // registry/agents
6+
}) {
7+
fs.mkdirSync(args.outDir, { recursive: true });
8+
9+
const lines: string[] = [];
10+
lines.push(`# tanstack`);
11+
lines.push(``);
12+
lines.push(`Top-level router for TanStack libraries. Uses progressive disclosure.`);
13+
lines.push(``);
14+
lines.push(`## Routing algorithm`);
15+
lines.push(`1) Determine the library: query vs router/start.`);
16+
lines.push(`2) If query: use \`tanstack-query\`.`);
17+
lines.push(`3) If router or start: use \`tanstack-router\`.`);
18+
lines.push(``);
19+
lines.push(`## Boundaries`);
20+
lines.push(`- Do not open library skills unless the library applies.`);
21+
lines.push(``);
22+
23+
fs.writeFileSync(path.join(args.outDir, `tanstack.md`), lines.join("\n") + "\n");
24+
}

registry/agents/tanstack-query.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# tanstack-query
2+
3+
Routes requests to TanStack **query** skills using progressive disclosure.
4+
5+
## When to use
6+
Use when the user asks about TanStack query concepts, APIs, patterns, or troubleshooting.
7+
8+
## Routing algorithm
9+
1) If the request is not about TanStack query, stop.
10+
2) Otherwise, open: `registry/skills/query/main/SKILL.md`
11+
3) Use its Routing table to pick a topic slug, then open the referenced doc section/file.
12+
13+
## Boundaries
14+
- Do not load unrelated skills.
15+
- Do not infer behavior not present in the docs snapshot.
16+

registry/agents/tanstack-router.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# tanstack-router
2+
3+
Routes requests to TanStack **router** skills using progressive disclosure.
4+
5+
## When to use
6+
Use when the user asks about TanStack router concepts, APIs, patterns, or troubleshooting.
7+
8+
## Routing algorithm
9+
1) If the request is not about TanStack router, stop.
10+
2) Otherwise, open: `registry/skills/router/main/SKILL.md`
11+
3) Use its Routing table to pick a topic slug, then open the referenced doc section/file.
12+
13+
## Boundaries
14+
- Do not load unrelated skills.
15+
- Do not infer behavior not present in the docs snapshot.
16+

registry/agents/tanstack.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# tanstack
2+
3+
Top-level router for TanStack libraries. Uses progressive disclosure.
4+
5+
## Routing algorithm
6+
1) Determine the library: query vs router/start.
7+
2) If query: use `tanstack-query`.
8+
3) If router or start: use `tanstack-router`.
9+
10+
## Boundaries
11+
- Do not open library skills unless the library applies.
12+

0 commit comments

Comments
 (0)