Skip to content

Commit 2a71d45

Browse files
committed
add initial configuration and provider setup for skill generation
1 parent 29d7e8d commit 2a71d45

16 files changed

Lines changed: 584 additions & 0 deletions

packages/skillgen/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "@tanstack/skillgen",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "tsc -p tsconfig.json",
8+
"test": "vitest run"
9+
},
10+
"dependencies": {
11+
"@tanstack/ai": "^0.5.0",
12+
"zod": "^4.2.0"
13+
},
14+
"devDependencies": {
15+
"typescript": "^5.5.0"
16+
}
17+
}

packages/skillgen/src/config.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export type SkillgenConfig = {
2+
provider?: string
3+
model?: string
4+
apiKey?: string
5+
baseUrl?: string
6+
temperature?: number
7+
maxTokens?: number
8+
seed?: number
9+
}
10+
11+
const parseNumber = (value?: string): number | undefined => {
12+
if (!value) return undefined
13+
const parsed = Number(value)
14+
return Number.isFinite(parsed) ? parsed : undefined
15+
}
16+
17+
export const readSkillgenConfig = (
18+
env: NodeJS.ProcessEnv = process.env,
19+
): SkillgenConfig => {
20+
return {
21+
provider: env.SKILLGEN_PROVIDER,
22+
model: env.SKILLGEN_MODEL,
23+
apiKey: env.SKILLGEN_API_KEY,
24+
baseUrl: env.SKILLGEN_BASE_URL,
25+
temperature: parseNumber(env.SKILLGEN_TEMPERATURE),
26+
maxTokens: parseNumber(env.SKILLGEN_MAX_TOKENS),
27+
seed: parseNumber(env.SKILLGEN_SEED),
28+
}
29+
}

packages/skillgen/src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export { readSkillgenConfig } from './config'
2+
export { createNoopProvider } from './noop'
3+
export { createTanstackAiProvider } from './tanstack-ai'
4+
export { renderSkillMarkdown } from './render'
5+
export { SkillInputSchema, SkillOutputSchema, SkillTopicSchema } from './schema'
6+
export type {
7+
SkillGenerationInput,
8+
SkillJsonOutput,
9+
SkillTopic,
10+
} from './schema'
11+
export type {
12+
SkillGenerationOptions,
13+
SkillGenerationResult,
14+
SkillGenerationUsage,
15+
SkillProvider,
16+
} from './provider'

packages/skillgen/src/noop.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { createHash } from 'node:crypto'
2+
import type { SkillGenerationOptions, SkillProvider } from './provider'
3+
import {
4+
SkillInputSchema,
5+
SkillOutputSchema,
6+
type SkillGenerationInput,
7+
type SkillJsonOutput,
8+
type SkillTopic,
9+
} from './schema'
10+
11+
const stableStringify = (value: unknown): string => {
12+
if (value === null || typeof value !== 'object') {
13+
return JSON.stringify(value)
14+
}
15+
16+
if (Array.isArray(value)) {
17+
return `[${value.map((item) => stableStringify(item)).join(',')}]`
18+
}
19+
20+
const record = value as Record<string, unknown>
21+
const keys = Object.keys(record).sort()
22+
const entries = keys.map(
23+
(key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`,
24+
)
25+
return `{${entries.join(',')}}`
26+
}
27+
28+
const hashHex = (value: string): string =>
29+
createHash('sha256').update(value).digest('hex')
30+
31+
const sliceHex = (hash: string, index: number, size = 8): string =>
32+
hash.slice(index * size, index * size + size)
33+
34+
const makeTopics = (hash: string, count: number): SkillTopic[] => {
35+
const topics: SkillTopic[] = []
36+
for (let i = 0; i < count; i += 1) {
37+
const token = sliceHex(hash, i + 3, 6)
38+
topics.push({
39+
title: `Topic ${i + 1} (${token.slice(0, 4)})`,
40+
slug: `topic-${token}`,
41+
})
42+
}
43+
return topics
44+
}
45+
46+
const makeUsage = (hash: string) => {
47+
const inputTokens = parseInt(sliceHex(hash, 0, 4), 16) % 1000
48+
const outputTokens = parseInt(sliceHex(hash, 1, 4), 16) % 1000
49+
return {
50+
inputTokens,
51+
outputTokens,
52+
totalTokens: inputTokens + outputTokens,
53+
}
54+
}
55+
56+
const normalizeOptions = (options?: SkillGenerationOptions) => {
57+
if (!options) return undefined
58+
const { model, temperature, maxTokens, seed, metadata } = options
59+
return { model, temperature, maxTokens, seed, metadata }
60+
}
61+
62+
const buildOutput = (
63+
input: SkillGenerationInput,
64+
hash: string,
65+
): SkillJsonOutput => {
66+
const boundaries =
67+
input.boundaries && input.boundaries.length > 0
68+
? input.boundaries
69+
: [
70+
`Avoid assumptions beyond provided inputs (${sliceHex(hash, 2, 6)}).`,
71+
`Keep output deterministic (${sliceHex(hash, 3, 6)}).`,
72+
]
73+
74+
const topics =
75+
input.topics && input.topics.length > 0 ? input.topics : makeTopics(hash, 3)
76+
77+
const output: SkillJsonOutput = {
78+
schema: 1,
79+
library: input.library,
80+
version: input.version,
81+
title: `TanStack ${input.library} skill`,
82+
summary: `Deterministic summary for ${input.library} (${sliceHex(hash, 0, 6)}).`,
83+
boundaries,
84+
routing: `Route requests to ${input.library} topics. (${sliceHex(hash, 1, 6)})`,
85+
topics,
86+
}
87+
88+
return SkillOutputSchema.parse(output)
89+
}
90+
91+
export const createNoopProvider = (): SkillProvider => {
92+
return {
93+
async generateSkillJson(input, options) {
94+
const parsedInput = SkillInputSchema.parse(input)
95+
const seedData = stableStringify({
96+
input: parsedInput,
97+
options: normalizeOptions(options),
98+
})
99+
const hash = hashHex(seedData)
100+
const output = buildOutput(parsedInput, hash)
101+
return {
102+
output,
103+
usage: makeUsage(hash),
104+
rawMeta: {
105+
provider: 'noop',
106+
hash,
107+
},
108+
}
109+
},
110+
}
111+
}

packages/skillgen/src/provider.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { SkillGenerationInput, SkillJsonOutput } from './schema'
2+
3+
export type SkillGenerationOptions = {
4+
model?: string
5+
temperature?: number
6+
maxTokens?: number
7+
seed?: number
8+
metadata?: Record<string, unknown>
9+
signal?: AbortSignal
10+
}
11+
12+
export type SkillGenerationUsage = {
13+
inputTokens?: number
14+
outputTokens?: number
15+
totalTokens?: number
16+
}
17+
18+
export type SkillGenerationResult = {
19+
output: SkillJsonOutput
20+
usage?: SkillGenerationUsage
21+
rawMeta?: unknown
22+
}
23+
24+
export interface SkillProvider {
25+
generateSkillJson(
26+
input: SkillGenerationInput,
27+
options?: SkillGenerationOptions,
28+
): Promise<SkillGenerationResult>
29+
}

packages/skillgen/src/render.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { SkillJsonOutput } from './schema'
2+
3+
export type RenderSkillOptions = {
4+
maxTopics?: number
5+
}
6+
7+
const escapePipes = (value: string): string => value.replaceAll('|', '\\|')
8+
9+
export const renderSkillMarkdown = (
10+
skill: SkillJsonOutput,
11+
options: RenderSkillOptions = {},
12+
): string => {
13+
const maxTopics = options.maxTopics ?? 250
14+
const lines: string[] = []
15+
16+
lines.push(`# ${skill.title}`)
17+
lines.push('')
18+
lines.push(`**Version:** \`${skill.version}\``)
19+
lines.push('')
20+
lines.push('## What this skill is for')
21+
lines.push('')
22+
lines.push(skill.summary)
23+
lines.push('')
24+
lines.push('## Boundaries')
25+
lines.push('')
26+
27+
if (skill.boundaries.length === 0) {
28+
lines.push('- None.')
29+
} else {
30+
for (const boundary of skill.boundaries) {
31+
lines.push(`- ${boundary}`)
32+
}
33+
}
34+
35+
lines.push('')
36+
lines.push('## Routing')
37+
lines.push('')
38+
lines.push(skill.routing)
39+
lines.push('')
40+
lines.push('| Topic | Slug |')
41+
lines.push('| --- | --- |')
42+
43+
const topics = skill.topics.slice(0, maxTopics)
44+
if (topics.length === 0) {
45+
lines.push('| (none) | (none) |')
46+
} else {
47+
for (const topic of topics) {
48+
lines.push(`| ${escapePipes(topic.title)} | ${escapePipes(topic.slug)} |`)
49+
}
50+
}
51+
52+
if (skill.topics.length > maxTopics) {
53+
lines.push('')
54+
lines.push('(Index truncated in this view; see JSON output for full list.)')
55+
}
56+
57+
return `${lines.join('\n')}\n`
58+
}

packages/skillgen/src/schema.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { z } from 'zod'
2+
3+
export const SkillTopicSchema = z.object({
4+
slug: z.string().min(1),
5+
title: z.string().min(1),
6+
kind: z.string().optional(),
7+
relPath: z.string().optional(),
8+
})
9+
10+
export const SkillInputSchema = z.object({
11+
library: z.string().min(1),
12+
version: z.string().min(1),
13+
goal: z.string().min(1),
14+
topics: z.array(SkillTopicSchema).optional(),
15+
boundaries: z.array(z.string()).optional(),
16+
notes: z.string().optional(),
17+
})
18+
19+
export const SkillOutputSchema = z.object({
20+
schema: z.literal(1),
21+
library: z.string().min(1),
22+
version: z.string().min(1),
23+
title: z.string().min(1),
24+
summary: z.string().min(1),
25+
boundaries: z.array(z.string()),
26+
routing: z.string().min(1),
27+
topics: z.array(SkillTopicSchema),
28+
})
29+
30+
export type SkillTopic = z.infer<typeof SkillTopicSchema>
31+
export type SkillGenerationInput = z.infer<typeof SkillInputSchema>
32+
export type SkillJsonOutput = z.infer<typeof SkillOutputSchema>

0 commit comments

Comments
 (0)