tpmjs/packages/npm-client/src/package.ts
Ajax Davis a92c2c250c feat: upgrade to AI SDK v6 beta and Zod v4 to fix tool schema errors
OpenAI was rejecting tool definitions with error "schema must be a JSON Schema of 'type: "object"'". This was caused by AI SDK v5 not properly converting Zod schemas to JSON Schema format.

**Changes:**
- Upgrade AI SDK from v5.0.104 to v6.0.0-beta.124
- Upgrade @ai-sdk/openai from v2.0.74 to v3.0.0-beta.22
- Upgrade Zod from v3.25.76 to v4.1.13 across all packages

**AI SDK v6 breaking changes:**
- Tool definition API: `parameters` renamed to `inputSchema`
- Removed `aiTool()` wrapper - use plain object with description, inputSchema, execute
- Streaming API: Use `textStream` async iterator instead of onChunk callback
- Zod schemas now properly converted to JSON Schema for OpenAI

**Zod v4 breaking changes:**
- `z.record()` now requires two arguments: `z.record(keySchema, valueSchema)`
- `z.enum()` params changed: `errorMap` removed, use `message` instead
- Type system improvements require explicit type parameters
- Fixed type errors in @tpmjs/env, @tpmjs/npm-client, @tpmjs/types

**Files changed:**
- apps/web/src/lib/ai-agent/tool-executor-agent.ts
  - Updated tool definition to use `inputSchema` instead of `parameters`
  - Removed `aiTool()` wrapper
  - Fixed streaming to use `textStream` iterator
- packages/env/src/index.ts
  - Updated type constraint from `z.ZodRawShape` to `Record<string, z.ZodTypeAny>`
- packages/npm-client/src/package.ts
  - Fixed `z.record()` calls to include both key and value schemas
  - Added type assertions for record indexing
- packages/types/src/tpmjs.ts
  - Changed `errorMap` to `message` in z.enum() calls

**Testing:**
- ✅ Type-check passes
- ✅ Production build succeeds
- ✅ All routes compile correctly

This fixes the tool execution error where OpenAI rejected tool schemas with invalid format.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 19:14:01 +10:00

218 lines
5.1 KiB
TypeScript

/**
* NPM Package Metadata Client
* Fetches detailed package information from NPM registry
*/
import { z } from 'zod';
const NPM_REGISTRY_URL = 'https://registry.npmjs.org';
/**
* Schema for package version metadata
*/
const PackageVersionSchema = z.object({
name: z.string(),
version: z.string(),
description: z.string().optional(),
keywords: z.array(z.string()).optional(),
homepage: z.string().optional(),
repository: z
.union([
z.string(),
z.object({
type: z.string(),
url: z.string(),
}),
])
.optional(),
license: z.string().optional(),
author: z
.union([
z.string(),
z.object({
name: z.string(),
email: z.string().optional(),
url: z.string().optional(),
}),
])
.optional(),
maintainers: z
.array(
z.object({
name: z.string(),
email: z.string().optional(),
})
)
.optional(),
// The tpmjs field - this is what we're looking for!
tpmjs: z.unknown().optional(),
// Dist info
dist: z
.object({
shasum: z.string(),
tarball: z.string(),
})
.optional(),
// Publishing metadata
publishedAt: z.string().optional(),
});
/**
* Schema for full package metadata
*/
const PackageMetadataSchema = z.object({
name: z.string(),
'dist-tags': z.record(z.string(), z.string()),
versions: z.record(z.string(), PackageVersionSchema),
time: z.record(z.string(), z.string()),
maintainers: z
.array(
z.object({
name: z.string(),
email: z.string(),
})
)
.optional(),
description: z.string().optional(),
homepage: z.string().optional(),
keywords: z.array(z.string()).optional(),
repository: z
.union([
z.string(),
z.object({
type: z.string(),
url: z.string(),
}),
])
.optional(),
license: z.string().optional(),
readme: z.string().optional(),
});
export type PackageVersion = z.infer<typeof PackageVersionSchema>;
export type PackageMetadata = z.infer<typeof PackageMetadataSchema>;
/**
* Fetches package metadata from NPM registry
* @param packageName - The name of the package
* @returns The package metadata, or null if not found
*/
export async function fetchPackageMetadata(packageName: string): Promise<PackageMetadata | null> {
// Encode the package name to handle scoped packages (@scope/name)
const encodedName = packageName.replace('/', '%2F');
const url = `${NPM_REGISTRY_URL}/${encodedName}`;
try {
const response = await fetch(url, {
headers: {
Accept: 'application/json',
},
});
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error(`NPM registry error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return PackageMetadataSchema.parse(data);
} catch (error) {
if (error instanceof Error && error.message.includes('404')) {
return null;
}
throw error;
}
}
/**
* Gets the latest version of a package
* @param packageName - The name of the package
* @returns The latest package version metadata, or null if not found
*/
export async function fetchLatestPackageVersion(
packageName: string
): Promise<PackageVersion | null> {
const metadata = await fetchPackageMetadata(packageName);
if (!metadata) {
return null;
}
const latestTag = metadata['dist-tags'].latest;
if (!latestTag) {
return null;
}
const version = metadata.versions[latestTag as string];
if (!version) {
return null;
}
// Add publishedAt from metadata.time
const publishedAt = metadata.time?.[latestTag as string];
return {
...version,
publishedAt,
};
}
export type PackageVersionWithReadme = PackageVersion & {
readme?: string;
topLevelKeywords?: string[];
topLevelDescription?: string;
};
/**
* Gets the latest version of a package with additional top-level metadata
* @param packageName - The name of the package
* @returns The latest package version with README and top-level metadata, or null if not found
*/
export async function fetchLatestPackageWithMetadata(
packageName: string
): Promise<PackageVersionWithReadme | null> {
const metadata = await fetchPackageMetadata(packageName);
if (!metadata) {
return null;
}
const latestTag = metadata['dist-tags'].latest;
if (!latestTag) {
return null;
}
const version = metadata.versions[latestTag as string];
if (!version) {
return null;
}
// Add publishedAt from metadata.time
const publishedAt = metadata.time?.[latestTag as string];
return {
...version,
publishedAt,
readme: metadata.readme,
topLevelKeywords: metadata.keywords,
topLevelDescription: metadata.description,
};
}
/**
* Checks if a package has a tpmjs field in its latest version
* @param packageName - The name of the package
* @returns The tpmjs field if present, null otherwise
*/
export async function fetchPackageTpmjsField(packageName: string): Promise<unknown | null> {
const latestVersion = await fetchLatestPackageVersion(packageName);
if (!latestVersion) {
return null;
}
return latestVersion.tpmjs || null;
}