Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion providers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 9 additions & 5 deletions scripts/skill-generator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<never>((_, reject) =>
setTimeout(() => reject(new Error('Network timeout after 30s')), VERSIONS_TIMEOUT)
),
Expand Down Expand Up @@ -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<never>((_, reject) =>
setTimeout(() => reject(new Error('Network timeout after 30s')), VERSIONS_TIMEOUT)
),
Expand Down
8 changes: 5 additions & 3 deletions scripts/skill-generator/lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -95,10 +95,12 @@ export function buildPromptReplacements(provider: ProviderConfig): Record<string
}
}

// Build versions table from cached versions (or empty if not available)
// Build versions table from cached versions (or empty if not available).
// Scoped to generic framework deps + this provider's declared `sdks`,
// so the prompt doesn't dump every queried SDK across all providers.
let versionsTable = '*Version lookup not available - use latest stable versions from npm/pip*';
if (cachedVersions) {
versionsTable = formatVersionsTable(cachedVersions);
versionsTable = formatVersionsTableForProvider(cachedVersions, provider.sdks);
}

const replacements: Record<string, string> = {
Expand Down
6 changes: 4 additions & 2 deletions scripts/skill-generator/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export function loadConfigFile(filePath: string): Map<string, ProviderConfig> {
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 {
Expand All @@ -145,16 +146,17 @@ export function loadConfigFile(filePath: string): Map<string, ProviderConfig> {
if (typeof value !== 'object' || value === null) {
continue;
}

const config = value as Record<string, unknown>;
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'],
});
}
}
Expand Down
12 changes: 12 additions & 0 deletions scripts/skill-generator/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
};
}

/**
Expand Down
85 changes: 74 additions & 11 deletions scripts/skill-generator/lib/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ export interface PackageVersions {
pip: Record<string, string>;
}

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
Expand Down Expand Up @@ -53,17 +57,30 @@ async function getPipVersion(pkg: string): Promise<string | null> {
}

/**
* 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<PackageVersions> {
export async function getLatestVersions(
extras: { npm?: string[]; pip?: string[] } = {},
logger?: Logger,
): Promise<PackageVersions> {
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;
});
Expand Down Expand Up @@ -103,20 +120,66 @@ export async function getLatestVersions(logger?: Logger): Promise<PackageVersion
export function formatVersionsTable(versions: PackageVersions): string {
let table = '| Package | Latest Stable | Use in package.json/requirements.txt |\n';
table += '|---------|---------------|--------------------------------------|\n';

for (const [pkg, version] of Object.entries(versions.npm)) {
// Use ^ for npm to allow minor/patch updates
table += `| \`${pkg}\` | ${version} | \`^${version}\` |\n`;
}

for (const [pkg, version] of Object.entries(versions.pip)) {
// Use >= 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<string>([...NPM_PACKAGES, ...(sdks?.npm ?? [])]);
const pipKeys = new Set<string>([...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<string>();
const pip = new Set<string>();
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
*/
Expand Down
Loading