feat(deps): added pi.zod exports and removed TypeBox package exports

- Added canonical `pi.zod` schema API exports and removed TypeBox package exports/imports.
- Migrated Tool schema typing from TypeBox to shared `TSchema`/Zod flow with legacy TypeBox compatibility.
- Updated AI provider adapters and MCP/agent builders to convert tool params through `toolWireSchema()`.
- Reworked schema validation from AJV to Zod-safe parsing with `fromTypeBox`, `toolWireSchema`, and meta schema checks.
This commit is contained in:
can1357
2026-05-15 14:46:54 +02:00
parent 80103f383d
commit 2867e1f4e3
173 changed files with 3066 additions and 2284 deletions
@@ -1,5 +1,5 @@
import { prompt } from "@oh-my-pi/pi-utils";
import { Type } from "@sinclair/typebox";
import * as z from "zod/v4";
import analyzeFilePrompt from "../../../commit/agentic/prompts/analyze-file.md" with { type: "text" };
import type { CommitAgentState } from "../../../commit/agentic/state";
import type { NumstatEntry } from "../../../commit/types";
@@ -12,9 +12,9 @@ import type { TaskParams } from "../../../task/types";
import type { ToolSession } from "../../../tools";
import { getFilePriority } from "./git-file-diff";
const analyzeFileSchema = Type.Object({
files: Type.Array(Type.String({ description: "File path" }), { minItems: 1 }),
goal: Type.Optional(Type.String({ description: "Optional analysis focus" })),
const analyzeFileSchema = z.object({
files: z.array(z.string().describe("File path")).min(1),
goal: z.string().describe("Optional analysis focus").optional(),
});
const analyzeFileOutputSchema = {
@@ -1,4 +1,4 @@
import { Type } from "@sinclair/typebox";
import * as z from "zod/v4";
import type { CommitAgentState } from "../../../commit/agentic/state";
import type { CustomTool } from "../../../extensibility/custom-tools/types";
import * as git from "../../../utils/git";
@@ -131,9 +131,9 @@ function processDiffs(files: string[], diffs: Map<string, string>): { result: st
return { result: parts.join("\n\n"), truncatedFiles };
}
const gitFileDiffSchema = Type.Object({
files: Type.Array(Type.String({ description: "Files to diff" }), { minItems: 1, maxItems: 10 }),
staged: Type.Optional(Type.Boolean({ description: "Use staged changes (default: true)" })),
const gitFileDiffSchema = z.object({
files: z.array(z.string().describe("Files to diff")).min(1).max(10),
staged: z.boolean().describe("Use staged changes (default: true)").optional(),
});
export function createGitFileDiffTool(cwd: string, state: CommitAgentState): CustomTool<typeof gitFileDiffSchema> {
@@ -1,12 +1,12 @@
import { Type } from "@sinclair/typebox";
import * as z from "zod/v4";
import type { DiffHunk, FileHunks } from "../../../commit/types";
import type { CustomTool } from "../../../extensibility/custom-tools/types";
import * as git from "../../../utils/git";
const gitHunkSchema = Type.Object({
file: Type.String({ description: "File path" }),
hunks: Type.Optional(Type.Array(Type.Number({ description: "1-based hunk indices" }), { minItems: 1 })),
staged: Type.Optional(Type.Boolean({ description: "Use staged changes (default: true)" })),
const gitHunkSchema = z.object({
file: z.string().describe("File path"),
hunks: z.array(z.number().describe("1-based hunk indices")).min(1).optional(),
staged: z.boolean().describe("Use staged changes (default: true)").optional(),
});
function selectHunks(fileHunks: FileHunks, requested?: number[]): DiffHunk[] {
@@ -1,4 +1,4 @@
import { Type } from "@sinclair/typebox";
import * as z from "zod/v4";
import type { CommitAgentState, GitOverviewSnapshot } from "../../../commit/agentic/state";
import { extractScopeCandidates } from "../../../commit/analysis/scope";
import type { CustomTool } from "../../../extensibility/custom-tools/types";
@@ -42,9 +42,9 @@ function filterExcludedFiles(files: string[]): { filtered: string[]; excluded: s
return { filtered, excluded };
}
const gitOverviewSchema = Type.Object({
staged: Type.Optional(Type.Boolean({ description: "Use staged changes (default: true)" })),
include_untracked: Type.Optional(Type.Boolean({ description: "Include untracked files when staged=false" })),
const gitOverviewSchema = z.object({
staged: z.boolean().describe("Use staged changes (default: true)").optional(),
include_untracked: z.boolean().describe("Include untracked files when staged=false").optional(),
});
export function createGitOverviewTool(cwd: string, state: CommitAgentState): CustomTool<typeof gitOverviewSchema> {
@@ -1,29 +1,29 @@
import { type TSchema, Type } from "@sinclair/typebox";
import * as z from "zod/v4";
import type { CommitAgentState } from "../../../commit/agentic/state";
import { CHANGELOG_CATEGORIES, type ChangelogCategory } from "../../../commit/types";
import type { CustomTool } from "../../../extensibility/custom-tools/types";
const changelogEntryProperties = CHANGELOG_CATEGORIES.reduce<Record<ChangelogCategory, TSchema>>(
const changelogEntryProperties = CHANGELOG_CATEGORIES.reduce<Record<ChangelogCategory, z.ZodType>>(
(acc, category) => {
acc[category] = Type.Optional(Type.Array(Type.String()));
acc[category] = z.array(z.string()).optional();
return acc;
},
{} as Record<ChangelogCategory, TSchema>,
{} as Record<ChangelogCategory, z.ZodType>,
);
const changelogEntriesSchema = Type.Object(changelogEntryProperties);
const changelogDeletionsSchema = Type.Object(changelogEntryProperties, {
description: "Entries to remove from existing changelog sections (case-insensitive match)",
});
const changelogEntriesSchema = z.object(changelogEntryProperties);
const changelogDeletionsSchema = z
.object(changelogEntryProperties)
.describe("Entries to remove from existing changelog sections (case-insensitive match)");
const changelogEntrySchema = Type.Object({
path: Type.String(),
const changelogEntrySchema = z.object({
path: z.string(),
entries: changelogEntriesSchema,
deletions: Type.Optional(changelogDeletionsSchema),
deletions: changelogDeletionsSchema.optional(),
});
const proposeChangelogSchema = Type.Object({
entries: Type.Array(changelogEntrySchema),
const proposeChangelogSchema = z.object({
entries: z.array(changelogEntrySchema),
});
interface ChangelogResponse {
@@ -1,4 +1,4 @@
import { Type } from "@sinclair/typebox";
import * as z from "zod/v4";
import type { CommitAgentState } from "../../../commit/agentic/state";
import {
capDetails,
@@ -15,12 +15,12 @@ import type { CustomTool } from "../../../extensibility/custom-tools/types";
import * as git from "../../../utils/git";
import { commitTypeSchema, detailSchema } from "./schemas.js";
const proposeCommitSchema = Type.Object({
const proposeCommitSchema = z.object({
type: commitTypeSchema,
scope: Type.Union([Type.String(), Type.Null()]),
summary: Type.String(),
details: Type.Array(detailSchema),
issue_refs: Type.Array(Type.String()),
scope: z.union([z.string(), z.null()]),
summary: z.string(),
details: z.array(detailSchema),
issue_refs: z.array(z.string()),
});
interface ProposalResponse {
@@ -1,9 +1,9 @@
import { Type } from "@sinclair/typebox";
import * as z from "zod/v4";
import type { CustomTool } from "../../../extensibility/custom-tools/types";
import * as git from "../../../utils/git";
const recentCommitsSchema = Type.Object({
count: Type.Optional(Type.Number({ description: "Number of commits to fetch", minimum: 1, maximum: 50 })),
const recentCommitsSchema = z.object({
count: z.number().min(1).max(50).describe("Number of commits to fetch").optional(),
});
interface RecentCommitStats {
@@ -1,31 +1,31 @@
import { Type } from "@sinclair/typebox";
import * as z from "zod/v4";
export const commitTypeSchema = Type.Union([
Type.Literal("feat"),
Type.Literal("fix"),
Type.Literal("refactor"),
Type.Literal("perf"),
Type.Literal("docs"),
Type.Literal("test"),
Type.Literal("build"),
Type.Literal("ci"),
Type.Literal("chore"),
Type.Literal("style"),
Type.Literal("revert"),
]);
export const commitTypeSchema = z.enum([
"feat",
"fix",
"refactor",
"perf",
"docs",
"test",
"build",
"ci",
"chore",
"style",
"revert",
] as const);
export const detailSchema = Type.Object({
text: Type.String(),
changelog_category: Type.Optional(
Type.Union([
Type.Literal("Added"),
Type.Literal("Changed"),
Type.Literal("Fixed"),
Type.Literal("Deprecated"),
Type.Literal("Removed"),
Type.Literal("Security"),
Type.Literal("Breaking Changes"),
]),
),
user_visible: Type.Optional(Type.Boolean()),
export const detailSchema = z.object({
text: z.string(),
changelog_category: z
.union([
z.literal("Added"),
z.literal("Changed"),
z.literal("Fixed"),
z.literal("Deprecated"),
z.literal("Removed"),
z.literal("Security"),
z.literal("Breaking Changes"),
])
.optional(),
user_visible: z.boolean().optional(),
});
@@ -1,4 +1,4 @@
import { Type } from "@sinclair/typebox";
import * as z from "zod/v4";
import type { CommitAgentState, SplitCommitGroup, SplitCommitPlan } from "../../../commit/agentic/state";
import { computeDependencyOrder } from "../../../commit/agentic/topo-sort";
import {
@@ -15,31 +15,32 @@ import type { CustomTool } from "../../../extensibility/custom-tools/types";
import * as git from "../../../utils/git";
import { commitTypeSchema, detailSchema } from "./schemas.js";
const hunkSelectorSchema = Type.Union([
Type.Object({ type: Type.Literal("all") }),
Type.Object({ type: Type.Literal("indices"), indices: Type.Array(Type.Number(), { minItems: 1 }) }),
Type.Object({ type: Type.Literal("lines"), start: Type.Number(), end: Type.Number() }),
const hunkSelectorSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("all") }),
z.object({ type: z.literal("indices"), indices: z.array(z.number()).min(1) }),
z.object({ type: z.literal("lines"), start: z.number(), end: z.number() }),
]);
const fileChangeSchema = Type.Object({
path: Type.String(),
const fileChangeSchema = z.object({
path: z.string(),
hunks: hunkSelectorSchema,
});
const splitCommitSchema = Type.Object({
commits: Type.Array(
Type.Object({
changes: Type.Array(fileChangeSchema, { minItems: 1 }),
type: commitTypeSchema,
scope: Type.Union([Type.String(), Type.Null()]),
summary: Type.String(),
details: Type.Optional(Type.Array(detailSchema)),
issue_refs: Type.Optional(Type.Array(Type.String())),
rationale: Type.Optional(Type.String()),
dependencies: Type.Optional(Type.Array(Type.Number())),
}),
{ minItems: 2 },
),
const splitCommitSchema = z.object({
commits: z
.array(
z.object({
changes: z.array(fileChangeSchema).min(1),
type: commitTypeSchema,
scope: z.union([z.string(), z.null()]),
summary: z.string(),
details: z.array(detailSchema).optional(),
issue_refs: z.array(z.string()).optional(),
rationale: z.string().optional(),
dependencies: z.array(z.number()).optional(),
}),
)
.min(2),
});
interface SplitCommitResponse {
@@ -2,7 +2,7 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
import { prompt } from "@oh-my-pi/pi-utils";
import { Type } from "@sinclair/typebox";
import * as z from "zod/v4";
import summarySystemPrompt from "../../commit/prompts/summary-system.md" with { type: "text" };
import summaryUserPrompt from "../../commit/prompts/summary-user.md" with { type: "text" };
import type { CommitSummary } from "../../commit/types";
@@ -12,8 +12,8 @@ import { extractTextContent, extractToolCall } from "../utils";
const SummaryTool = {
name: "create_commit_summary",
description: "Generate the summary line for a conventional commit message.",
parameters: Type.Object({
summary: Type.String(),
parameters: z.object({
summary: z.string(),
}),
};
@@ -83,7 +83,7 @@ function renderSummaryPrompt({
function parseSummaryFromResponse(message: AssistantMessage, commitType: string, scope: string | null): CommitSummary {
const toolCall = extractToolCall(message, "create_commit_summary");
if (toolCall) {
const parsed = validateToolCall([SummaryTool], toolCall) as { summary: string };
const parsed = validateToolCall([SummaryTool], toolCall) as z.infer<(typeof SummaryTool)["parameters"]>;
return { summary: stripTypePrefix(parsed.summary, commitType, scope) };
}
const text = extractTextContent(message);
@@ -2,27 +2,23 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
import { prompt } from "@oh-my-pi/pi-utils";
import { type TSchema, Type } from "@sinclair/typebox";
import * as z from "zod/v4";
import changelogSystemPrompt from "../../commit/prompts/changelog-system.md" with { type: "text" };
import changelogUserPrompt from "../../commit/prompts/changelog-user.md" with { type: "text" };
import { CHANGELOG_CATEGORIES, type ChangelogCategory, type ChangelogGenerationResult } from "../../commit/types";
import { toReasoningEffort } from "../../thinking";
import { extractTextContent, extractToolCall, parseJsonPayload } from "../utils";
const changelogEntryProperties = CHANGELOG_CATEGORIES.reduce<Record<ChangelogCategory, TSchema>>(
(acc, category) => {
acc[category] = Type.Optional(Type.Array(Type.String()));
return acc;
},
{} as Record<ChangelogCategory, TSchema>,
);
const changelogEntryShape = Object.fromEntries(
CHANGELOG_CATEGORIES.map(c => [c, z.array(z.string()).optional()] as const),
) as Record<ChangelogCategory, z.ZodOptional<z.ZodArray<z.ZodString>>>;
const changelogEntriesSchema = Type.Object(changelogEntryProperties);
const changelogEntriesSchema = z.object(changelogEntryShape);
export const changelogTool = {
name: "create_changelog_entries",
description: "Generate changelog entries grouped by Keep a Changelog categories.",
parameters: Type.Object({
parameters: z.object({
entries: changelogEntriesSchema,
}),
};
@@ -72,7 +68,7 @@ export async function generateChangelogEntries({
function parseChangelogResponse(message: AssistantMessage): ChangelogGenerationResult {
const toolCall = extractToolCall(message, "create_changelog_entries");
if (toolCall) {
const parsed = validateToolCall([changelogTool], toolCall) as ChangelogGenerationResult;
const parsed = validateToolCall([changelogTool], toolCall) as z.infer<(typeof changelogTool)["parameters"]>;
return { entries: parsed.entries ?? {} };
}
+27 -37
View File
@@ -1,49 +1,39 @@
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
import { validateToolCall } from "@oh-my-pi/pi-ai";
import { Type } from "@sinclair/typebox";
import * as z from "zod/v4";
import type { ChangelogCategory, ConventionalAnalysis } from "./types";
import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "./utils";
const changelogCategoryLiteral = z.enum([
"Added",
"Changed",
"Fixed",
"Deprecated",
"Removed",
"Security",
"Breaking Changes",
]);
/**
* Shared TypeBox schema for the `create_conventional_analysis` tool used by
* Shared Zod schema for the `create_conventional_analysis` tool used by
* both the single-pass analysis call and the map-reduce reduce phase. Schemas
* are identical across phases — only the surrounding tool `description`
* differs to reflect the input the phase is summarizing.
*/
export const conventionalAnalysisParameters = Type.Object({
type: Type.Union([
Type.Literal("feat"),
Type.Literal("fix"),
Type.Literal("refactor"),
Type.Literal("docs"),
Type.Literal("test"),
Type.Literal("chore"),
Type.Literal("style"),
Type.Literal("perf"),
Type.Literal("build"),
Type.Literal("ci"),
Type.Literal("revert"),
]),
scope: Type.Union([Type.String(), Type.Null()]),
details: Type.Array(
Type.Object({
text: Type.String(),
changelog_category: Type.Optional(
Type.Union([
Type.Literal("Added"),
Type.Literal("Changed"),
Type.Literal("Fixed"),
Type.Literal("Deprecated"),
Type.Literal("Removed"),
Type.Literal("Security"),
Type.Literal("Breaking Changes"),
]),
),
user_visible: Type.Optional(Type.Boolean()),
}),
),
issue_refs: Type.Array(Type.String()),
});
export const conventionalAnalysisParameters = z
.object({
type: z.enum(["feat", "fix", "refactor", "docs", "test", "chore", "style", "perf", "build", "ci", "revert"]),
scope: z.union([z.string(), z.null()]),
details: z.array(
z.object({
text: z.string(),
changelog_category: changelogCategoryLiteral.optional(),
user_visible: z.boolean().optional(),
}),
),
issue_refs: z.array(z.string()),
})
.strict();
export interface ConventionalAnalysisTool {
name: "create_conventional_analysis";
@@ -80,7 +70,7 @@ export function parseConventionalAnalysisResponse(
): ConventionalAnalysis {
const toolCall = extractToolCall(message, tool.name);
if (toolCall) {
const parsed = validateToolCall([tool], toolCall) as ParsedConventionalAnalysis;
const parsed = validateToolCall([tool], toolCall) as z.infer<typeof conventionalAnalysisParameters>;
return normalizeAnalysis(parsed);
}
const text = extractTextContent(message);