From fd0240bbbb8f9be5d26305b6bdf59088fc68eb5d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 18:12:24 +0000 Subject: [PATCH] feat(skill-generator): per-provider SDK version-tracking via `sdks` field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The version-staleness check in the generator/reviewer was only seeing generic framework deps (Express, FastAPI, vitest, etc.) — provider- specific SDKs were invisible, so heavy-pin drift like Paddle's `@paddle/paddle-node-sdk ^1.4.0` (2 majors behind 3.8.0) didn't get flagged during review. Fixes that by letting each provider declare its SDK packages in `providers.yaml` and threading them through to the version-query batch. Changes: - `providers.yaml` schema gains an optional `sdks: { npm: [...], pip: [...] }` field per provider, documented in the file header. - `scripts/skill-generator/lib/types.ts` — `ProviderConfig.sdks` added. - `scripts/skill-generator/lib/config.ts` — parser reads `sdks` from both the array and object yaml formats. - `scripts/skill-generator/lib/versions.ts`: - Exports `NPM_PACKAGES` / `PIP_PACKAGES` (the generic lists). - `getLatestVersions` takes an optional `extras: { npm, pip }` arg and merges + de-dupes the lists before querying in parallel. - New `formatVersionsTableForProvider(versions, sdks)` filters the global cache down to generic deps + this provider's SDKs, so the prompt doesn't dump every queried SDK across all providers. - New `collectProviderSdks(providers)` unions all SDKs across a config set so `index.ts` can pre-fetch the full superset once. - `scripts/skill-generator/lib/cli.ts` — `buildPromptReplacements` now uses the per-provider table. - `scripts/skill-generator/index.ts` — both generate and review call sites pass `collectProviderSdks(providerConfigs)` to the version query. Seeded `paddle`'s entry as the test case: sdks: npm: - "@paddle/paddle-node-sdk" pip: - paddle-python-sdk Other providers' SDK declarations can be added in a follow-up sweep. Smoke-tested end-to-end: with paddle's `sdks` populated, the version table emitted for the paddle prompt contains `@paddle/paddle-node-sdk@3.8.0` and `paddle-python-sdk@1.14.1` alongside the generic deps. Re-running review on paddle (with the existing `^1.4.0` pins on main) should now flag those as 2 majors behind under the existing severity rubric and propose the bump. https://claude.ai/code/session_01NNTgQRJss1V7gyzzJ9rjnB --- providers.yaml | 13 +++- scripts/skill-generator/index.ts | 14 ++-- scripts/skill-generator/lib/cli.ts | 8 ++- scripts/skill-generator/lib/config.ts | 6 +- scripts/skill-generator/lib/types.ts | 12 ++++ scripts/skill-generator/lib/versions.ts | 85 +++++++++++++++++++++---- 6 files changed, 116 insertions(+), 22 deletions(-) diff --git a/providers.yaml b/providers.yaml index a5b8c5b..c630126 100644 --- a/providers.yaml +++ b/providers.yaml @@ -25,6 +25,12 @@ # events - Array of event types to test with # prompt - Custom prompt template (uses default if not specified) # skillName - Override skill directory name (default: {name}-webhooks) +# sdks - Provider-specific SDK packages to version-track (optional): +# npm - Array of npm package names (e.g., ["@paddle/paddle-node-sdk"]) +# pip - Array of PyPI package names (e.g., ["paddle-python-sdk"]) +# The generator queries these alongside the generic framework deps so the +# AI sees current SDK versions in the {{VERSIONS_TABLE}} and can flag +# stale pins in package.json/requirements.txt during review. # # When adding a new provider skill: # 1. Add provider entry here with documentation URLs and testScenario @@ -186,7 +192,12 @@ providers: notes: > Payment and subscription platform. Uses Paddle-Signature header with HMAC-SHA256. Signature format includes timestamp (ts) and hash (h1). Secret starts with pdl_ntfset_. - Official SDKs available: @paddle/paddle-node-sdk, paddle-billing (Python). + Official SDKs: @paddle/paddle-node-sdk (Node), paddle-python-sdk (Python). + sdks: + npm: + - "@paddle/paddle-node-sdk" + pip: + - paddle-python-sdk testScenario: events: - subscription.created diff --git a/scripts/skill-generator/index.ts b/scripts/skill-generator/index.ts index f3637ce..56fa912 100644 --- a/scripts/skill-generator/index.ts +++ b/scripts/skill-generator/index.ts @@ -28,7 +28,7 @@ import { generateSkill } from './lib/generator'; import { reviewExistingSkill } from './lib/reviewer'; import { setCachedVersions } from './lib/cli'; import { DEFAULT_CLI_TOOL, AVAILABLE_CLI_TOOLS } from './lib/cli-adapters'; -import { getLatestVersions } from './lib/versions'; +import { getLatestVersions, collectProviderSdks } from './lib/versions'; import { createWorktree, removeWorktree, @@ -216,13 +216,15 @@ async function handleGenerate( const { dir: resultsDir } = createResultsDir(); console.log(chalk.gray(`Results directory: ${resultsDir}\n`)); - // Query latest package versions and cache them for prompts (skip in dry-run mode) + // Query latest package versions and cache them for prompts (skip in dry-run mode). + // Include per-provider SDKs declared via `sdks` in providers.yaml so the version + // table seen by the AI covers stale SDK pins, not just generic framework deps. if (!generateOptions.dryRun) { console.log(chalk.blue('Querying package managers for latest stable versions...')); const VERSIONS_TIMEOUT = 30000; // 30 seconds try { const versions = await Promise.race([ - getLatestVersions(), + getLatestVersions(collectProviderSdks(providerConfigs)), new Promise((_, reject) => setTimeout(() => reject(new Error('Network timeout after 30s')), VERSIONS_TIMEOUT) ), @@ -501,13 +503,15 @@ async function handleReview( const { dir: resultsDir } = createResultsDir(); console.log(chalk.gray(`Results directory: ${resultsDir}\n`)); - // Query latest package versions and cache them for prompts (skip in dry-run mode) + // Query latest package versions and cache them for prompts (skip in dry-run mode). + // Include per-provider SDKs declared via `sdks` in providers.yaml so the version + // table seen by the AI covers stale SDK pins, not just generic framework deps. if (!reviewOptions.dryRun) { console.log(chalk.blue('Querying package managers for latest stable versions...')); const VERSIONS_TIMEOUT = 30000; // 30 seconds try { const versions = await Promise.race([ - getLatestVersions(), + getLatestVersions(collectProviderSdks(providerConfigs)), new Promise((_, reject) => setTimeout(() => reject(new Error('Network timeout after 30s')), VERSIONS_TIMEOUT) ), diff --git a/scripts/skill-generator/lib/cli.ts b/scripts/skill-generator/lib/cli.ts index e8f904d..9ef868e 100644 --- a/scripts/skill-generator/lib/cli.ts +++ b/scripts/skill-generator/lib/cli.ts @@ -6,7 +6,7 @@ import { execa, type ExecaError } from 'execa'; import { readFileSync } from 'fs'; import { join } from 'path'; import type { ProviderConfig, Logger, ReviewResult } from './types'; -import { type PackageVersions, formatVersionsTable } from './versions'; +import { type PackageVersions, formatVersionsTableForProvider } from './versions'; import { getCliAdapter, DEFAULT_CLI_TOOL } from './cli-adapters'; const PROMPTS_DIR = join(__dirname, '..', 'prompts'); @@ -95,10 +95,12 @@ export function buildPromptReplacements(provider: ProviderConfig): Record = { diff --git a/scripts/skill-generator/lib/config.ts b/scripts/skill-generator/lib/config.ts index f221ac3..711c500 100644 --- a/scripts/skill-generator/lib/config.ts +++ b/scripts/skill-generator/lib/config.ts @@ -137,6 +137,7 @@ export function loadConfigFile(filePath: string): Map { docs: config.docs as ProviderConfig['docs'], notes: config.notes as string | undefined, testScenario: config.testScenario as TestScenario | undefined, + sdks: config.sdks as ProviderConfig['sdks'], }); } } else { @@ -145,16 +146,17 @@ export function loadConfigFile(filePath: string): Map { if (typeof value !== 'object' || value === null) { continue; } - + const config = value as Record; const normalizedName = normalizeProviderName(key); - + configs.set(normalizedName, { name: normalizedName, displayName: (config.displayName as string) || toDisplayName(key), docs: config.docs as ProviderConfig['docs'], notes: config.notes as string | undefined, testScenario: config.testScenario as TestScenario | undefined, + sdks: config.sdks as ProviderConfig['sdks'], }); } } diff --git a/scripts/skill-generator/lib/types.ts b/scripts/skill-generator/lib/types.ts index faa2f01..3c37e58 100644 --- a/scripts/skill-generator/lib/types.ts +++ b/scripts/skill-generator/lib/types.ts @@ -39,6 +39,18 @@ export interface ProviderConfig { }; notes?: string; // Hints for the agent testScenario?: TestScenario; // Configuration for agent testing + /** + * Provider-specific SDK packages to track for version-staleness review. + * These get queried alongside the generic framework deps and appear in + * {{VERSIONS_TABLE}} for both generate and review prompts, letting the + * reviewer flag stale SDK pins in package.json / requirements.txt. + * Use the canonical package-manager names (e.g., "@paddle/paddle-node-sdk" + * for npm, "paddle-python-sdk" for pip). + */ + sdks?: { + npm?: string[]; + pip?: string[]; + }; } /** diff --git a/scripts/skill-generator/lib/versions.ts b/scripts/skill-generator/lib/versions.ts index eb19761..6cd6921 100644 --- a/scripts/skill-generator/lib/versions.ts +++ b/scripts/skill-generator/lib/versions.ts @@ -10,8 +10,12 @@ export interface PackageVersions { pip: Record; } -const NPM_PACKAGES = ['next', 'express', 'vitest', 'jest', 'typescript']; -const PIP_PACKAGES = ['fastapi', 'pytest', 'httpx']; +/** + * Generic framework deps queried for every run. Per-provider SDK packages + * (declared via `sdks` in providers.yaml) are merged in at query time. + */ +export const NPM_PACKAGES = ['next', 'express', 'vitest', 'jest', 'typescript']; +export const PIP_PACKAGES = ['fastapi', 'pytest', 'httpx']; /** * Get latest stable version from npm @@ -53,17 +57,30 @@ async function getPipVersion(pkg: string): Promise { } /** - * Query all package versions in parallel + * Query all package versions in parallel. + * + * `extras` lets callers add provider-specific SDK packages (declared via the + * `sdks` field in providers.yaml) on top of the generic framework deps. They + * get queried in the same parallel batch and end up in the same + * `PackageVersions` object; per-provider prompts then filter the global map + * down to the relevant subset via `formatVersionsTableForProvider`. */ -export async function getLatestVersions(logger?: Logger): Promise { +export async function getLatestVersions( + extras: { npm?: string[]; pip?: string[] } = {}, + logger?: Logger, +): Promise { logger?.info('Querying package managers for latest versions...'); - - const npmPromises = NPM_PACKAGES.map(async pkg => { + + // De-dupe so generic + per-provider lists don't double-query the same package + const npmList = Array.from(new Set([...NPM_PACKAGES, ...(extras.npm ?? [])])); + const pipList = Array.from(new Set([...PIP_PACKAGES, ...(extras.pip ?? [])])); + + const npmPromises = npmList.map(async pkg => { const version = await getNpmVersion(pkg); return [pkg, version] as const; }); - - const pipPromises = PIP_PACKAGES.map(async pkg => { + + const pipPromises = pipList.map(async pkg => { const version = await getPipVersion(pkg); return [pkg, version] as const; }); @@ -103,20 +120,66 @@ export async function getLatestVersions(logger?: Logger): Promise= for pip table += `| \`${pkg}\` | ${version} | \`>=${version}\` |\n`; } - + return table; } +/** + * Format versions for a single provider — generic framework deps plus the + * provider's own SDKs. Used to keep prompts focused on packages relevant to + * the skill being generated/reviewed, rather than dumping every queried SDK + * across all providers. + */ +export function formatVersionsTableForProvider( + versions: PackageVersions, + sdks?: { npm?: string[]; pip?: string[] }, +): string { + const npmKeys = new Set([...NPM_PACKAGES, ...(sdks?.npm ?? [])]); + const pipKeys = new Set([...PIP_PACKAGES, ...(sdks?.pip ?? [])]); + + let table = '| Package | Latest Stable | Use in package.json/requirements.txt |\n'; + table += '|---------|---------------|--------------------------------------|\n'; + + for (const [pkg, version] of Object.entries(versions.npm)) { + if (!npmKeys.has(pkg)) continue; + table += `| \`${pkg}\` | ${version} | \`^${version}\` |\n`; + } + + for (const [pkg, version] of Object.entries(versions.pip)) { + if (!pipKeys.has(pkg)) continue; + table += `| \`${pkg}\` | ${version} | \`>=${version}\` |\n`; + } + + return table; +} + +/** + * Collect the union of all provider-declared SDKs across a config set, + * de-duplicating across providers. Used at startup to expand the version + * query to cover every SDK that might appear in any prompt. + */ +export function collectProviderSdks( + providers: Array<{ sdks?: { npm?: string[]; pip?: string[] } }>, +): { npm: string[]; pip: string[] } { + const npm = new Set(); + const pip = new Set(); + for (const p of providers) { + for (const pkg of p.sdks?.npm ?? []) npm.add(pkg); + for (const pkg of p.sdks?.pip ?? []) pip.add(pkg); + } + return { npm: Array.from(npm), pip: Array.from(pip) }; +} + /** * Format versions as a simple reference for prompts */