fix(typebox): migrate to v1 with extension compat (#3474)

* fix(typebox): migrate to v1 with extension compat

Replace AJV-based validation with TypeBox-native validation, keep legacy extension imports working (including @sinclair/typebox/compiler), and restore coercion for serialized/plain JSON schemas.

This change closes #3112.

* fix(typebox): use canonical imports and harden coercion

Switch first-party code to canonical typebox imports while retaining legacy extension aliases in the loader.

Remove obsolete runtime codegen guards, expand serialized JSON-schema coercion coverage, and update related tests and fixtures.

Fixes #3112.

---------

Co-authored-by: Mario Zechner <badlogicgames@gmail.com>
This commit is contained in:
Armin Ronacher
2026-04-22 19:59:33 +02:00
committed by GitHub
parent a23fab4693
commit 35ff2689ee
82 changed files with 580 additions and 264 deletions

97
package-lock.json generated
View File

@@ -2868,12 +2868,6 @@
"integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==",
"license": "Apache-2.0"
},
"node_modules/@sinclair/typebox": {
"version": "0.34.49",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz",
"integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==",
"license": "MIT"
},
"node_modules/@slack/logger": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.1.tgz",
@@ -4262,39 +4256,6 @@
"node": ">= 14"
}
},
"node_modules/ajv": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/alien-signals": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-2.0.8.tgz",
@@ -5313,12 +5274,6 @@
"@types/yauzl": "^2.9.1"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -5336,22 +5291,6 @@
"node": ">=8.6.0"
}
},
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/fast-xml-builder": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz",
@@ -6067,12 +6006,6 @@
"node": ">=16"
}
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/jsonschema": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz",
@@ -7295,15 +7228,6 @@
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve": {
"version": "1.22.12",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
@@ -8098,6 +8022,12 @@
"node": "*"
}
},
"node_modules/typebox": {
"version": "1.1.29",
"resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.29.tgz",
"integrity": "sha512-zPwgxCka1v976Zn2Hg0THUHY6JFPzbBkz12ZOZeyc/vcoeqLyeFEsXw5zGg/nMhbNE/rOEALVKRTlHIEDR3Jhw==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -8597,7 +8527,8 @@
"version": "0.68.1",
"license": "MIT",
"dependencies": {
"@mariozechner/pi-ai": "^0.68.1"
"@mariozechner/pi-ai": "^0.68.1",
"typebox": "^1.1.24"
},
"devDependencies": {
"@types/node": "^24.3.0",
@@ -8634,13 +8565,11 @@
"@aws-sdk/client-bedrock-runtime": "^3.1030.0",
"@google/genai": "^1.40.0",
"@mistralai/mistralai": "^2.2.0",
"@sinclair/typebox": "^0.34.41",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"chalk": "^5.6.2",
"openai": "6.26.0",
"partial-json": "^0.1.7",
"proxy-agent": "^6.5.0",
"typebox": "^1.1.24",
"undici": "^7.19.1",
"zod-to-json-schema": "^3.24.6"
},
@@ -8683,8 +8612,6 @@
"@mariozechner/pi-ai": "^0.68.1",
"@mariozechner/pi-tui": "^0.68.1",
"@silvia-odwyer/photon-node": "^0.3.4",
"@sinclair/typebox": "^0.34.41",
"ajv": "^8.17.1",
"chalk": "^5.5.0",
"cli-highlight": "^2.1.11",
"diff": "^8.0.2",
@@ -8697,6 +8624,7 @@
"minimatch": "^10.2.3",
"proper-lockfile": "^4.1.2",
"strip-ansi": "^7.1.0",
"typebox": "^1.1.24",
"undici": "^7.19.1",
"uuid": "^11.1.0",
"yaml": "^2.8.2"
@@ -8781,12 +8709,12 @@
"@mariozechner/pi-agent-core": "^0.68.1",
"@mariozechner/pi-ai": "^0.68.1",
"@mariozechner/pi-coding-agent": "^0.68.1",
"@sinclair/typebox": "^0.34.0",
"@slack/socket-mode": "^2.0.0",
"@slack/web-api": "^7.0.0",
"chalk": "^5.6.2",
"croner": "^9.1.0",
"diff": "^8.0.2"
"diff": "^8.0.2",
"typebox": "^1.1.24"
},
"bin": {
"mom": "dist/main.js"
@@ -8893,6 +8821,7 @@
"lucide": "^0.544.0",
"ollama": "^0.6.0",
"pdfjs-dist": "5.4.394",
"typebox": "^1.1.24",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
},
"devDependencies": {

View File

@@ -17,7 +17,8 @@
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"@mariozechner/pi-ai": "^0.68.1"
"@mariozechner/pi-ai": "^0.68.1",
"typebox": "^1.1.24"
},
"keywords": [
"ai",

View File

@@ -10,7 +10,7 @@ import type {
Tool,
ToolResultMessage,
} from "@mariozechner/pi-ai";
import type { Static, TSchema } from "@sinclair/typebox";
import type { Static, TSchema } from "typebox";
/**
* Stream function used by the agent loop.

View File

@@ -6,7 +6,7 @@ import {
type Model,
type UserMessage,
} from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import { agentLoop, agentLoopContinue } from "../src/agent-loop.js";
import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, AgentTool } from "../src/types.js";

View File

@@ -1,4 +1,4 @@
import { type Static, Type } from "@sinclair/typebox";
import { type Static, Type } from "typebox";
import type { AgentTool, AgentToolResult } from "../../src/types.js";
export interface CalculateResult extends AgentToolResult<undefined> {

View File

@@ -1,4 +1,4 @@
import { type Static, Type } from "@sinclair/typebox";
import { type Static, Type } from "typebox";
import type { AgentTool, AgentToolResult } from "../../src/types.js";
export interface GetCurrentTimeResult extends AgentToolResult<{ utcTimestamp: number }> {}

View File

@@ -202,7 +202,7 @@ for (const block of response.content) {
## Tools
Tools enable LLMs to interact with external systems. This library uses TypeBox schemas for type-safe tool definitions with automatic validation using AJV. TypeBox schemas can be serialized and deserialized as plain JSON, making them ideal for distributed systems.
Tools enable LLMs to interact with external systems. This library uses TypeBox schemas for type-safe tool definitions with automatic validation using TypeBox's built-in validator and value conversion utilities. TypeBox schemas can be serialized and deserialized as plain JSON, making them ideal for distributed systems.
### Defining Tools

View File

@@ -76,9 +76,7 @@
"@aws-sdk/client-bedrock-runtime": "^3.1030.0",
"@google/genai": "^1.40.0",
"@mistralai/mistralai": "^2.2.0",
"@sinclair/typebox": "^0.34.41",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"typebox": "^1.1.24",
"chalk": "^5.6.2",
"openai": "6.26.0",
"partial-json": "^0.1.7",

View File

@@ -1,5 +1,5 @@
export type { Static, TSchema } from "@sinclair/typebox";
export { Type } from "@sinclair/typebox";
export type { Static, TSchema } from "typebox";
export { Type } from "typebox";
export * from "./api-registry.js";
export * from "./env-api-keys.js";

View File

@@ -20,6 +20,7 @@ import {
type ToolConfiguration,
ToolResultStatus,
} from "@aws-sdk/client-bedrock-runtime";
import type { DocumentType } from "@smithy/types";
import { calculateCost } from "../models.js";
import type {
Api,
@@ -760,7 +761,7 @@ function convertToolConfig(
toolSpec: {
name: tool.name,
description: tool.description,
inputSchema: { json: tool.parameters },
inputSchema: { json: tool.parameters as unknown as DocumentType },
},
}));

View File

@@ -223,7 +223,7 @@ export interface ToolResultMessage<TDetails = any> {
export type Message = UserMessage | AssistantMessage | ToolResultMessage;
import type { TSchema } from "@sinclair/typebox";
import type { TSchema } from "typebox";
export interface Tool<TParameters extends TSchema = TSchema> {
name: string;

View File

@@ -1,4 +1,4 @@
import { type TUnsafe, Type } from "@sinclair/typebox";
import { type TUnsafe, Type } from "typebox";
/**
* Creates a string enum schema compatible with Google's API and other providers

View File

@@ -1,42 +1,270 @@
import AjvModule from "ajv";
import addFormatsModule from "ajv-formats";
// Handle both default and named exports
const Ajv = (AjvModule as any).default || AjvModule;
const addFormats = (addFormatsModule as any).default || addFormatsModule;
import { Compile } from "typebox/compile";
import type { TLocalizedValidationError } from "typebox/error";
import { Value } from "typebox/value";
import type { Tool, ToolCall } from "../types.js";
// Detect if we're in a browser extension environment with strict CSP
// Chrome extensions with Manifest V3 don't allow eval/Function constructor
const isBrowserExtension = typeof globalThis !== "undefined" && (globalThis as any).chrome?.runtime?.id !== undefined;
const validatorCache = new WeakMap<object, ReturnType<typeof Compile>>();
const TYPEBOX_KIND = Symbol.for("TypeBox.Kind");
function canUseRuntimeCodegen(): boolean {
if (isBrowserExtension) {
return false;
interface JsonSchemaObject {
type?: string | string[];
properties?: Record<string, JsonSchemaObject>;
items?: JsonSchemaObject | JsonSchemaObject[];
additionalProperties?: boolean | JsonSchemaObject;
allOf?: JsonSchemaObject[];
anyOf?: JsonSchemaObject[];
oneOf?: JsonSchemaObject[];
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function isJsonSchemaObject(value: unknown): value is JsonSchemaObject {
return isRecord(value);
}
function hasTypeBoxMetadata(schema: unknown): boolean {
return isRecord(schema) && Object.getOwnPropertySymbols(schema).includes(TYPEBOX_KIND);
}
function getSchemaTypes(schema: JsonSchemaObject): string[] {
if (typeof schema.type === "string") {
return [schema.type];
}
if (Array.isArray(schema.type)) {
return schema.type.filter((type): type is string => typeof type === "string");
}
return [];
}
try {
new Function("return true;");
return true;
} catch {
return false;
function matchesJsonType(value: unknown, type: string): boolean {
switch (type) {
case "number":
return typeof value === "number";
case "integer":
return typeof value === "number" && Number.isInteger(value);
case "boolean":
return typeof value === "boolean";
case "string":
return typeof value === "string";
case "null":
return value === null;
case "array":
return Array.isArray(value);
case "object":
return isRecord(value) && !Array.isArray(value);
default:
return false;
}
}
// Create a singleton AJV instance with formats only when runtime code generation is available.
let ajv: any = null;
if (canUseRuntimeCodegen()) {
try {
ajv = new Ajv({
allErrors: true,
strict: false,
coerceTypes: true,
});
addFormats(ajv);
} catch (_e) {
console.warn("AJV validation disabled due to CSP restrictions");
function isValidatorSchema(value: unknown): value is Tool["parameters"] {
return isRecord(value);
}
function getSubSchemaValidator(schema: JsonSchemaObject): ReturnType<typeof Compile> | undefined {
if (!isValidatorSchema(schema)) {
return undefined;
}
try {
return getValidator(schema);
} catch {
return undefined;
}
}
function coercePrimitiveByType(value: unknown, type: string): unknown {
switch (type) {
case "number": {
if (value === null) {
return 0;
}
if (typeof value === "string" && value.trim() !== "") {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
if (typeof value === "boolean") {
return value ? 1 : 0;
}
return value;
}
case "integer": {
if (value === null) {
return 0;
}
if (typeof value === "string" && value.trim() !== "") {
const parsed = Number(value);
if (Number.isInteger(parsed)) {
return parsed;
}
}
if (typeof value === "boolean") {
return value ? 1 : 0;
}
return value;
}
case "boolean": {
if (value === null) {
return false;
}
if (typeof value === "string") {
if (value === "true") {
return true;
}
if (value === "false") {
return false;
}
}
if (typeof value === "number") {
if (value === 1) {
return true;
}
if (value === 0) {
return false;
}
}
return value;
}
case "string": {
if (value === null) {
return "";
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return value;
}
case "null": {
if (value === "" || value === 0 || value === false) {
return null;
}
return value;
}
default:
return value;
}
}
function applySchemaObjectCoercion(value: Record<string, unknown>, schema: JsonSchemaObject): void {
const properties = schema.properties;
const definedKeys = new Set<string>(properties ? Object.keys(properties) : []);
if (properties) {
for (const [key, propertySchema] of Object.entries(properties)) {
if (!(key in value)) {
continue;
}
value[key] = coerceWithJsonSchema(value[key], propertySchema);
}
}
if (schema.additionalProperties && isJsonSchemaObject(schema.additionalProperties)) {
for (const [key, propertyValue] of Object.entries(value)) {
if (definedKeys.has(key)) {
continue;
}
value[key] = coerceWithJsonSchema(propertyValue, schema.additionalProperties);
}
}
}
function applySchemaArrayCoercion(value: unknown[], schema: JsonSchemaObject): void {
if (Array.isArray(schema.items)) {
for (let index = 0; index < value.length; index++) {
const itemSchema = schema.items[index];
if (!itemSchema) {
continue;
}
value[index] = coerceWithJsonSchema(value[index], itemSchema);
}
return;
}
if (isJsonSchemaObject(schema.items)) {
for (let index = 0; index < value.length; index++) {
value[index] = coerceWithJsonSchema(value[index], schema.items);
}
}
}
function coerceWithUnionSchema(value: unknown, schemas: JsonSchemaObject[]): unknown {
for (const schema of schemas) {
const candidate = structuredClone(value);
const coerced = coerceWithJsonSchema(candidate, schema);
const validator = getSubSchemaValidator(schema);
if (validator?.Check(coerced)) {
return coerced;
}
}
return value;
}
function coerceWithJsonSchema(value: unknown, schema: JsonSchemaObject): unknown {
let nextValue = value;
if (Array.isArray(schema.allOf)) {
for (const nested of schema.allOf) {
nextValue = coerceWithJsonSchema(nextValue, nested);
}
}
if (Array.isArray(schema.anyOf)) {
nextValue = coerceWithUnionSchema(nextValue, schema.anyOf);
}
if (Array.isArray(schema.oneOf)) {
nextValue = coerceWithUnionSchema(nextValue, schema.oneOf);
}
const schemaTypes = getSchemaTypes(schema);
const matchesUnionMember =
schemaTypes.length > 1 && schemaTypes.some((schemaType) => matchesJsonType(nextValue, schemaType));
if (schemaTypes.length > 0 && !matchesUnionMember) {
for (const schemaType of schemaTypes) {
const candidate = coercePrimitiveByType(nextValue, schemaType);
if (candidate !== nextValue) {
nextValue = candidate;
break;
}
}
}
if (schemaTypes.includes("object") && isRecord(nextValue) && !Array.isArray(nextValue)) {
applySchemaObjectCoercion(nextValue, schema);
}
if (schemaTypes.includes("array") && Array.isArray(nextValue)) {
applySchemaArrayCoercion(nextValue, schema);
}
return nextValue;
}
function getValidator(schema: Tool["parameters"]): ReturnType<typeof Compile> {
const key = schema as object;
const cached = validatorCache.get(key);
if (cached) {
return cached;
}
const validator = Compile(schema);
validatorCache.set(key, validator);
return validator;
}
function formatValidationPath(error: TLocalizedValidationError): string {
if (error.keyword === "required") {
const requiredProperties = (error.params as { requiredProperties?: string[] }).requiredProperties;
const requiredProperty = requiredProperties?.[0];
if (requiredProperty) {
const basePath = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
return basePath ? `${basePath}.${requiredProperty}` : requiredProperty;
}
}
const path = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
return path || "root";
}
/**
@@ -62,29 +290,32 @@ export function validateToolCall(tools: Tool[], toolCall: ToolCall): any {
* @throws Error with formatted message if validation fails
*/
export function validateToolArguments(tool: Tool, toolCall: ToolCall): any {
// Skip validation in environments where runtime code generation is unavailable.
if (!ajv || !canUseRuntimeCodegen()) {
return toolCall.arguments;
const args = structuredClone(toolCall.arguments);
Value.Convert(tool.parameters, args);
const validator = getValidator(tool.parameters);
if (!hasTypeBoxMetadata(tool.parameters) && isJsonSchemaObject(tool.parameters)) {
const coerced = coerceWithJsonSchema(args, tool.parameters);
if (coerced !== args) {
if (isRecord(args) && isRecord(coerced)) {
for (const key of Object.keys(args)) {
delete args[key];
}
Object.assign(args, coerced);
} else {
return validator.Check(coerced) ? coerced : args;
}
}
}
// Compile the schema.
const validate = ajv.compile(tool.parameters);
// Clone arguments so AJV can safely mutate for type coercion
const args = structuredClone(toolCall.arguments);
// Validate the arguments (AJV mutates args in-place for type coercion)
if (validate(args)) {
if (validator.Check(args)) {
return args;
}
// Format validation errors nicely
const errors =
validate.errors
?.map((err: any) => {
const path = err.instancePath ? err.instancePath.substring(1) : err.params.missingProperty || "root";
return ` - ${path}: ${err.message}`;
})
validator
.Errors(args)
.map((error) => ` - ${formatValidationPath(error)}: ${error.message}`)
.join("\n") || "Unknown validation error";
const errorMessage = `Validation failed for tool "${toolCall.name}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(toolCall.arguments, null, 2)}`;

View File

@@ -1,4 +1,4 @@
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import { getModel } from "../src/models.js";
import { stream } from "../src/stream.js";

View File

@@ -22,8 +22,8 @@
* Fixtures are generated fresh on each run.
*/
import { Type } from "@sinclair/typebox";
import { writeFileSync } from "fs";
import { Type } from "typebox";
import { beforeAll, describe, expect, it } from "vitest";
import { getModel } from "../src/models.js";
import { completeSimple, getEnvApiKey } from "../src/stream.js";

View File

@@ -1,4 +1,4 @@
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { afterEach, describe, expect, it, vi } from "vitest";
import { streamGoogleGeminiCli } from "../src/providers/google-gemini-cli.js";
import type { Context, Model, ToolCall } from "../src/types.js";

View File

@@ -1,6 +1,6 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import type { Api, Context, Model, Tool, ToolResultMessage } from "../src/index.js";
import { complete, getModel } from "../src/index.js";

View File

@@ -1,4 +1,4 @@
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import { getEnvApiKey } from "../src/env-api-keys.js";
import { getModel } from "../src/models.js";

View File

@@ -1,4 +1,4 @@
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import { getModel } from "../src/models.js";
import { complete } from "../src/stream.js";

View File

@@ -1,4 +1,4 @@
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getModel } from "../src/models.js";
import { streamOpenAICompletions } from "../src/providers/openai-completions.js";

View File

@@ -1,4 +1,4 @@
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getModel } from "../src/models.js";
import { streamSimple } from "../src/stream.js";

View File

@@ -1,4 +1,4 @@
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import { getModel } from "../src/models.js";
import { complete, getEnvApiKey } from "../src/stream.js";

View File

@@ -1,8 +1,8 @@
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { Type } from "@sinclair/typebox";
import type { ResponseFunctionCallOutputItemList } from "openai/resources/responses/responses.js";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import type { Api, Context, Model, StreamOptions, Tool, ToolResultMessage } from "../src/index.js";
import { complete, getModel } from "../src/index.js";

View File

@@ -1,7 +1,7 @@
import { Type } from "@sinclair/typebox";
import { type ChildProcess, execSync, spawn } from "child_process";
import { readFileSync } from "fs";
import { dirname, join } from "path";
import { Type } from "typebox";
import { fileURLToPath } from "url";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { getModel } from "../src/models.js";

View File

@@ -10,7 +10,7 @@
* Regression test for: https://github.com/badlogic/pi-mono/issues/1022
*/
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import { getModel } from "../src/models.js";
import { completeSimple, getEnvApiKey } from "../src/stream.js";

View File

@@ -1,4 +1,4 @@
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import { getModel } from "../src/models.js";
import { complete } from "../src/stream.js";

View File

@@ -1,4 +1,4 @@
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import { getModel } from "../src/models.js";
import { complete } from "../src/stream.js";

View File

@@ -1,17 +1,41 @@
import { Type } from "@sinclair/typebox";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { ToolCall } from "../src/types.js";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import type { Tool, ToolCall } from "../src/types.js";
import { validateToolArguments } from "../src/utils/validation.js";
afterEach(() => {
vi.restoreAllMocks();
});
function createToolCallWithPlainSchema(
schema: Tool["parameters"],
value: unknown,
): {
tool: Tool;
toolCall: ToolCall;
} {
const tool: Tool = {
name: "echo",
description: "Echo tool",
parameters: {
type: "object",
properties: {
value: schema,
},
required: ["value"],
} as Tool["parameters"],
};
const toolCall: ToolCall = {
type: "toolCall",
id: "tool-1",
name: "echo",
arguments: { value },
};
return { tool, toolCall };
}
describe("validateToolArguments", () => {
it("falls back to raw arguments without writing to stderr when runtime code generation is blocked", () => {
it("still validates when Function constructor is unavailable", () => {
const originalFunction = globalThis.Function;
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const tool = {
const tool: Tool = {
name: "echo",
description: "Echo tool",
parameters: Type.Object({
@@ -30,10 +54,63 @@ describe("validateToolArguments", () => {
}) as unknown as FunctionConstructor;
try {
expect(validateToolArguments(tool, toolCall)).toEqual(toolCall.arguments);
expect(errorSpy).not.toHaveBeenCalled();
expect(validateToolArguments(tool, toolCall)).toEqual({ count: 42 });
} finally {
globalThis.Function = originalFunction;
}
});
it("coerces serialized plain JSON schemas with AJV-compatible primitive rules", () => {
const passingCases: Array<{
schema: Tool["parameters"];
input: unknown;
expected: unknown;
}> = [
{ schema: { type: "number" } as Tool["parameters"], input: "42", expected: 42 },
{ schema: { type: "number" } as Tool["parameters"], input: true, expected: 1 },
{ schema: { type: "number" } as Tool["parameters"], input: null, expected: 0 },
{ schema: { type: "integer" } as Tool["parameters"], input: "42", expected: 42 },
{ schema: { type: "boolean" } as Tool["parameters"], input: "true", expected: true },
{ schema: { type: "boolean" } as Tool["parameters"], input: "false", expected: false },
{ schema: { type: "boolean" } as Tool["parameters"], input: 1, expected: true },
{ schema: { type: "boolean" } as Tool["parameters"], input: 0, expected: false },
{ schema: { type: "string" } as Tool["parameters"], input: null, expected: "" },
{ schema: { type: "string" } as Tool["parameters"], input: true, expected: "true" },
{ schema: { type: "null" } as Tool["parameters"], input: "", expected: null },
{ schema: { type: "null" } as Tool["parameters"], input: 0, expected: null },
{ schema: { type: "null" } as Tool["parameters"], input: false, expected: null },
{
schema: { type: ["number", "string"] } as Tool["parameters"],
input: "1",
expected: "1",
},
{
schema: { type: ["boolean", "number"] } as Tool["parameters"],
input: "1",
expected: 1,
},
];
for (const testCase of passingCases) {
const { tool, toolCall } = createToolCallWithPlainSchema(testCase.schema, testCase.input);
expect(validateToolArguments(tool, toolCall)).toEqual({ value: testCase.expected });
}
});
it("rejects invalid coercions for serialized plain JSON schemas", () => {
const failingCases: Array<{
schema: Tool["parameters"];
input: unknown;
}> = [
{ schema: { type: "boolean" } as Tool["parameters"], input: "1" },
{ schema: { type: "boolean" } as Tool["parameters"], input: "0" },
{ schema: { type: "null" } as Tool["parameters"], input: "null" },
{ schema: { type: "integer" } as Tool["parameters"], input: "42.1" },
];
for (const testCase of failingCases) {
const { tool, toolCall } = createToolCallWithPlainSchema(testCase.schema, testCase.input);
expect(() => validateToolArguments(tool, toolCall)).toThrow("Validation failed");
}
});
});

View File

@@ -31,7 +31,7 @@ import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { StringEnum } from "@mariozechner/pi-ai";
import { type ExtensionAPI, getAgentDir, withFileMutationQueue } from "@mariozechner/pi-coding-agent";
import { type Static, Type } from "@sinclair/typebox";
import { type Static, Type } from "typebox";
const PROVIDER = "google-antigravity";

View File

@@ -8,7 +8,7 @@
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
const ECHO_PARAMS = Type.Object({
message: Type.String({ description: "Message to echo" }),

View File

@@ -6,7 +6,7 @@
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
interface OptionWithDesc {
label: string;

View File

@@ -7,7 +7,7 @@
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
// Types
interface QuestionOption {

View File

@@ -6,7 +6,7 @@
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// Command entrypoint for reload.

View File

@@ -6,7 +6,7 @@
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// Register a /quit command that cleanly exits pi

View File

@@ -21,7 +21,7 @@ import type { Message } from "@mariozechner/pi-ai";
import { StringEnum } from "@mariozechner/pi-ai";
import { type ExtensionAPI, getMarkdownTheme, withFileMutationQueue } from "@mariozechner/pi-coding-agent";
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
const MAX_PARALLEL_TASKS = 8;

View File

@@ -20,7 +20,7 @@
import { StringEnum } from "@mariozechner/pi-ai";
import type { ExtensionAPI, ExtensionContext, Theme, ToolExecutionMode } from "@mariozechner/pi-coding-agent";
import { type Component, matchesKey, Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
// Thrown from the tool on illegal actions. The agent runtime surfaces thrown
// errors as tool errors (isError=true) without resetting any of our state.

View File

@@ -13,7 +13,7 @@
import { StringEnum } from "@mariozechner/pi-ai";
import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
import { matchesKey, Text, truncateToWidth } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
interface Todo {
id: number;

View File

@@ -22,10 +22,10 @@
import type { TextContent } from "@mariozechner/pi-ai";
import { type ExtensionAPI, getAgentDir, withFileMutationQueue } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { constants, readFileSync } from "fs";
import { access, appendFile, readFile } from "fs/promises";
import { join, resolve } from "path";
import { Type } from "typebox";
const LOG_FILE = join(getAgentDir(), "read-access.log");

View File

@@ -25,10 +25,10 @@ import {
withFileMutationQueue,
} from "@mariozechner/pi-coding-agent";
import { Text } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import { execSync } from "child_process";
import { tmpdir } from "os";
import { join } from "path";
import { Type } from "typebox";
const RgParams = Type.Object({
pattern: Type.String({ description: "Search pattern (regex)" }),

View File

@@ -6,8 +6,8 @@
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import ms from "ms";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// Register a tool that uses ms

View File

@@ -44,8 +44,7 @@
"@mariozechner/pi-ai": "^0.68.1",
"@mariozechner/pi-tui": "^0.68.1",
"@silvia-odwyer/photon-node": "^0.3.4",
"@sinclair/typebox": "^0.34.41",
"ajv": "^8.17.1",
"typebox": "^1.1.24",
"chalk": "^5.5.0",
"cli-highlight": "^2.1.11",
"diff": "^8.0.2",

View File

@@ -18,7 +18,9 @@ import * as _bundledPiTui from "@mariozechner/pi-tui";
// Static imports of packages that extensions may use.
// These MUST be static so Bun bundles them into the compiled binary.
// The virtualModules option then makes them available to extensions.
import * as _bundledTypebox from "@sinclair/typebox";
import * as _bundledTypebox from "typebox";
import * as _bundledTypeboxCompile from "typebox/compile";
import * as _bundledTypeboxValue from "typebox/value";
import { getAgentDir, isBunBinary } from "../../config.js";
// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
// avoiding a circular dependency. Extensions can import from @mariozechner/pi-coding-agent.
@@ -27,6 +29,7 @@ import { createEventBus, type EventBus } from "../event-bus.js";
import type { ExecOptions } from "../exec.js";
import { execCommand } from "../exec.js";
import { createSyntheticSourceInfo } from "../source-info.js";
import * as _bundledTypeboxCompilerCompat from "./typebox-compiler-compat.js";
import type {
Extension,
ExtensionAPI,
@@ -41,7 +44,14 @@ import type {
/** Modules available to extensions via virtualModules (for compiled Bun binary) */
const VIRTUAL_MODULES: Record<string, unknown> = {
typebox: _bundledTypebox,
"typebox/compile": _bundledTypeboxCompile,
"typebox/value": _bundledTypeboxValue,
"typebox/compiler": _bundledTypeboxCompilerCompat,
"@sinclair/typebox": _bundledTypebox,
"@sinclair/typebox/compile": _bundledTypeboxCompile,
"@sinclair/typebox/value": _bundledTypeboxValue,
"@sinclair/typebox/compiler": _bundledTypeboxCompilerCompat,
"@mariozechner/pi-agent-core": _bundledPiAgentCore,
"@mariozechner/pi-tui": _bundledPiTui,
"@mariozechner/pi-ai": _bundledPiAi,
@@ -56,14 +66,25 @@ const require = createRequire(import.meta.url);
* In Bun binary mode, virtualModules is used instead.
*/
let _aliases: Record<string, string> | null = null;
function resolveTypeboxCompilerCompatPath(baseDir: string): string {
const jsPath = path.resolve(baseDir, "typebox-compiler-compat.js");
if (fs.existsSync(jsPath)) {
return jsPath;
}
return path.resolve(baseDir, "typebox-compiler-compat.ts");
}
function getAliases(): Record<string, string> {
if (_aliases) return _aliases;
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageIndex = path.resolve(__dirname, "../..", "index.js");
const typeboxEntry = require.resolve("@sinclair/typebox");
const typeboxRoot = typeboxEntry.replace(/[\\/]build[\\/]cjs[\\/]index\.js$/, "");
const typeboxEntry = require.resolve("typebox");
const typeboxCompileEntry = require.resolve("typebox/compile");
const typeboxValueEntry = require.resolve("typebox/value");
const typeboxCompilerCompat = resolveTypeboxCompilerCompatPath(__dirname);
const packagesRoot = path.resolve(__dirname, "../../../../");
const resolveWorkspaceOrImport = (workspaceRelativePath: string, specifier: string): string => {
@@ -80,7 +101,14 @@ function getAliases(): Record<string, string> {
"@mariozechner/pi-tui": resolveWorkspaceOrImport("tui/dist/index.js", "@mariozechner/pi-tui"),
"@mariozechner/pi-ai": resolveWorkspaceOrImport("ai/dist/index.js", "@mariozechner/pi-ai"),
"@mariozechner/pi-ai/oauth": resolveWorkspaceOrImport("ai/dist/oauth.js", "@mariozechner/pi-ai/oauth"),
"@sinclair/typebox": typeboxRoot,
typebox: typeboxEntry,
"typebox/compile": typeboxCompileEntry,
"typebox/value": typeboxValueEntry,
"typebox/compiler": typeboxCompilerCompat,
"@sinclair/typebox": typeboxEntry,
"@sinclair/typebox/compile": typeboxCompileEntry,
"@sinclair/typebox/value": typeboxValueEntry,
"@sinclair/typebox/compiler": typeboxCompilerCompat,
};
return _aliases;

View File

@@ -0,0 +1,19 @@
import { Code, Compile, Validator } from "typebox/compile";
export { Code, Compile, Validator };
// Legacy @sinclair/typebox/compiler compatibility for extensions.
export const TypeCompiler = {
Compile,
};
// In TypeBox 0.x this was a named export. Map it to the v1 Validator class.
export const TypeCheck = Validator;
export default {
Code,
Compile,
Validator,
TypeCompiler,
TypeCheck,
};

View File

@@ -39,7 +39,7 @@ import type {
OverlayOptions,
TUI,
} from "@mariozechner/pi-tui";
import type { Static, TSchema } from "@sinclair/typebox";
import type { Static, TSchema } from "typebox";
import type { Theme } from "../../modes/interactive/theme/theme.js";
import type { BashResult } from "../bash-executor.js";
import type { CompactionPreparation, CompactionResult } from "../compaction/index.js";
@@ -479,7 +479,7 @@ type AnyToolDefinition = ToolDefinition<any, any, any>;
export function defineTool<TParams extends TSchema, TDetails = unknown, TState = any>(
tool: ToolDefinition<TParams, TDetails, TState>,
): ToolDefinition<TParams, TDetails, TState> & AnyToolDefinition {
return tool;
return tool as ToolDefinition<TParams, TDetails, TState> & AnyToolDefinition;
}
// ============================================================================

View File

@@ -18,10 +18,11 @@ import {
type SimpleStreamOptions,
} from "@mariozechner/pi-ai";
import { registerOAuthProvider, resetOAuthProviders } from "@mariozechner/pi-ai/oauth";
import { type Static, Type } from "@sinclair/typebox";
import AjvModule from "ajv";
import { existsSync, readFileSync } from "fs";
import { join } from "path";
import { type Static, Type } from "typebox";
import { Compile } from "typebox/compile";
import type { TLocalizedValidationError } from "typebox/error";
import { getAgentDir } from "../config.js";
import type { AuthStorage } from "./auth-storage.js";
import {
@@ -31,9 +32,6 @@ import {
resolveHeadersOrThrow,
} from "./resolve-config-value.js";
const Ajv = (AjvModule as any).default || AjvModule;
const ajv = new Ajv();
// Schema for OpenRouter routing preferences
const PercentileCutoffsSchema = Type.Object({
p50: Type.Optional(Type.Number()),
@@ -179,10 +177,23 @@ const ModelsConfigSchema = Type.Object({
providers: Type.Record(Type.String(), ProviderConfigSchema),
});
ajv.addSchema(ModelsConfigSchema, "ModelsConfig");
const validateModelsConfig = Compile(ModelsConfigSchema);
type ModelsConfig = Static<typeof ModelsConfigSchema>;
function formatValidationPath(error: TLocalizedValidationError): string {
if (error.keyword === "required") {
const requiredProperties = (error.params as { requiredProperties?: string[] }).requiredProperties;
const requiredProperty = requiredProperties?.[0];
if (requiredProperty) {
const basePath = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
return basePath ? `${basePath}.${requiredProperty}` : requiredProperty;
}
}
const path = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
return path || "root";
}
/** Provider override config (baseUrl, compat) without request auth/headers */
interface ProviderOverride {
baseUrl?: string;
@@ -417,17 +428,19 @@ export class ModelRegistry {
try {
const content = readFileSync(modelsJsonPath, "utf-8");
const config: ModelsConfig = JSON.parse(content);
const parsed = JSON.parse(content) as unknown;
// Validate schema
const validate = ajv.getSchema("ModelsConfig")!;
if (!validate(config)) {
if (!validateModelsConfig.Check(parsed)) {
const errors =
validate.errors?.map((e: any) => ` - ${e.instancePath || "root"}: ${e.message}`).join("\n") ||
"Unknown schema error";
validateModelsConfig
.Errors(parsed)
.map((error) => ` - ${formatValidationPath(error)}: ${error.message}`)
.join("\n") || "Unknown schema error";
return emptyCustomModelsResult(`Invalid models.json schema:\n${errors}\n\nFile: ${modelsJsonPath}`);
}
const config = parsed as ModelsConfig;
// Additional validation
this.validateConfig(config);

View File

@@ -4,8 +4,8 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { Container, Text, truncateToWidth } from "@mariozechner/pi-tui";
import { type Static, Type } from "@sinclair/typebox";
import { spawn } from "child_process";
import { type Static, Type } from "typebox";
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.js";
import { theme } from "../../modes/interactive/theme/theme.js";

View File

@@ -1,8 +1,8 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { Box, Container, Spacer, Text } from "@mariozechner/pi-tui";
import { type Static, Type } from "@sinclair/typebox";
import { constants } from "fs";
import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
import { type Static, Type } from "typebox";
import { renderDiff } from "../../modes/interactive/components/diff.js";
import type { ToolDefinition } from "../extensions/types.js";
import {

View File

@@ -1,10 +1,10 @@
import { createInterface } from "node:readline";
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { Text } from "@mariozechner/pi-tui";
import { type Static, Type } from "@sinclair/typebox";
import { spawn } from "child_process";
import { existsSync } from "fs";
import path from "path";
import { type Static, Type } from "typebox";
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
import { ensureTool } from "../../utils/tools-manager.js";
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";

View File

@@ -1,10 +1,10 @@
import { createInterface } from "node:readline";
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { Text } from "@mariozechner/pi-tui";
import { type Static, Type } from "@sinclair/typebox";
import { spawn } from "child_process";
import { readFileSync, statSync } from "fs";
import path from "path";
import { type Static, Type } from "typebox";
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
import { ensureTool } from "../../utils/tools-manager.js";
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";

View File

@@ -1,8 +1,8 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { Text } from "@mariozechner/pi-tui";
import { type Static, Type } from "@sinclair/typebox";
import { existsSync, readdirSync, statSync } from "fs";
import nodePath from "path";
import { type Static, Type } from "typebox";
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
import { resolveToCwd } from "./path-utils.js";

View File

@@ -1,9 +1,9 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
import type { Api, ImageContent, Model, TextContent } from "@mariozechner/pi-ai";
import { Text } from "@mariozechner/pi-tui";
import { type Static, Type } from "@sinclair/typebox";
import { constants } from "fs";
import { access as fsAccess, readFile as fsReadFile } from "fs/promises";
import { type Static, Type } from "typebox";
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js";
import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";

View File

@@ -1,8 +1,8 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { Container, Text } from "@mariozechner/pi-tui";
import { type Static, Type } from "@sinclair/typebox";
import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises";
import { dirname } from "path";
import { type Static, Type } from "typebox";
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js";
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";

View File

@@ -1,10 +1,10 @@
import * as fs from "node:fs";
import * as path from "node:path";
import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@mariozechner/pi-tui";
import { type Static, Type } from "@sinclair/typebox";
import { TypeCompiler } from "@sinclair/typebox/compiler";
import chalk from "chalk";
import { highlight, supportsLanguage } from "cli-highlight";
import { type Static, Type } from "typebox";
import { Compile } from "typebox/compile";
import { getCustomThemesDir, getThemesDir } from "../../../config.js";
import type { SourceInfo } from "../../../core/source-info.js";
@@ -94,7 +94,7 @@ const ThemeJsonSchema = Type.Object({
type ThemeJson = Static<typeof ThemeJsonSchema>;
const validateThemeJson = TypeCompiler.Compile(ThemeJsonSchema);
const validateThemeJson = Compile(ThemeJsonSchema);
export type ThemeColor =
| "accent"
@@ -515,23 +515,29 @@ export function getAvailableThemesWithPaths(): ThemeInfo[] {
function parseThemeJson(label: string, json: unknown): ThemeJson {
if (!validateThemeJson.Check(json)) {
const errors = Array.from(validateThemeJson.Errors(json));
const missingColors: string[] = [];
const missingColors = new Set<string>();
const otherErrors: string[] = [];
for (const e of errors) {
// Check for missing required color properties
const match = e.path.match(/^\/colors\/(\w+)$/);
if (match && e.message.includes("Required")) {
missingColors.push(match[1]);
} else {
otherErrors.push(` - ${e.path}: ${e.message}`);
for (const error of errors) {
if (error.keyword === "required" && error.instancePath === "/colors") {
const requiredProperties = (error.params as { requiredProperties?: string[] }).requiredProperties;
for (const requiredProperty of requiredProperties ?? []) {
missingColors.add(requiredProperty);
}
continue;
}
const path = error.instancePath || "/";
otherErrors.push(` - ${path}: ${error.message}`);
}
let errorMessage = `Invalid theme "${label}":\n`;
if (missingColors.length > 0) {
if (missingColors.size > 0) {
errorMessage += "\nMissing required color tokens:\n";
errorMessage += missingColors.map((c) => ` - ${c}`).join("\n");
errorMessage += Array.from(missingColors)
.sort()
.map((color) => ` - ${color}`)
.join("\n");
errorMessage += '\n\nPlease add these colors to your theme\'s "colors" object.';
errorMessage += "\nSee the built-in themes (dark.json, light.json) for reference values.";
}

View File

@@ -14,7 +14,7 @@ import {
type ImageContent,
type TextContent,
} from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AgentSession } from "../src/core/agent-session.js";
import { AuthStorage } from "../src/core/auth-storage.js";

View File

@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getModel } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { DefaultResourceLoader } from "../src/core/resource-loader.js";
import { createAgentSession } from "../src/core/sdk.js";

View File

@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { Agent, type AgentEvent, type AgentTool } from "@mariozechner/pi-agent-core";
import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AgentSession } from "../src/core/agent-session.js";
import { AuthStorage } from "../src/core/auth-storage.js";

View File

@@ -28,7 +28,7 @@ describe("extensions discovery", () => {
`;
const extensionCodeWithTool = (toolName: string) => `
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
export default function(pi) {
pi.registerTool({
name: "${toolName}",

View File

@@ -291,7 +291,7 @@ describe("ExtensionRunner", () => {
describe("tool collection", () => {
it("collects tools from multiple extensions", async () => {
const toolCode = (name: string) => `
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
export default function(pi) {
pi.registerTool({
name: "${name}",
@@ -315,7 +315,7 @@ describe("ExtensionRunner", () => {
it("keeps first tool when two extensions register the same name", async () => {
const first = `
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
export default function(pi) {
pi.registerTool({
name: "shared",
@@ -327,7 +327,7 @@ describe("ExtensionRunner", () => {
}
`;
const second = `
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
export default function(pi) {
pi.registerTool({
name: "shared",

View File

@@ -471,7 +471,7 @@ Content`,
join(ext1Dir, "index.ts"),
`
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
export default function(pi: ExtensionAPI) {
pi.registerTool({
name: "duplicate-tool",
@@ -486,7 +486,7 @@ export default function(pi: ExtensionAPI) {
join(ext2Dir, "index.ts"),
`
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
export default function(pi: ExtensionAPI) {
pi.registerTool({
name: "duplicate-tool",
@@ -513,7 +513,7 @@ export default function(pi: ExtensionAPI) {
join(globalExtDir, "global.ts"),
`
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
export default function(pi: ExtensionAPI) {
pi.registerTool({
name: "duplicate-tool",
@@ -532,7 +532,7 @@ export default function(pi: ExtensionAPI) {
explicitExtPath,
`
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
export default function(pi: ExtensionAPI) {
pi.registerTool({
name: "duplicate-tool",

View File

@@ -60,6 +60,7 @@ async function runCli(args: string[]): Promise<{ stdout: string; stderr: string;
env: {
...process.env,
[ENV_AGENT_DIR]: agentDir,
TSX_TSCONFIG_PATH: resolve(__dirname, "../../../tsconfig.json"),
},
stdio: ["ignore", "pipe", "pipe"],
});

View File

@@ -1,7 +1,7 @@
import { Buffer } from "node:buffer";
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { fauxAssistantMessage, fauxToolCall } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { afterEach, describe, expect, it } from "vitest";
import type { BashOperations } from "../../src/core/tools/bash.js";
import { createHarness, type Harness } from "./harness.js";

View File

@@ -1,6 +1,6 @@
import type { AgentTool, ThinkingLevel } from "@mariozechner/pi-agent-core";
import { fauxAssistantMessage, fauxToolCall, type Model } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { afterEach, describe, expect, it } from "vitest";
import type { ExtensionAPI } from "../../src/index.js";
import { createHarness, getAssistantTexts, type Harness } from "./harness.js";

View File

@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { fauxAssistantMessage, fauxToolCall, type Model } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { afterEach, describe, expect, it } from "vitest";
import type { PromptTemplate } from "../../src/core/prompt-templates.js";
import { createSyntheticSourceInfo } from "../../src/core/source-info.js";

View File

@@ -1,7 +1,7 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { fauxAssistantMessage, fauxToolCall } from "@mariozechner/pi-ai";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { afterEach, describe, expect, it } from "vitest";
import { createHarness, getAssistantTexts, getMessageText, getUserTexts, type Harness } from "./harness.js";

View File

@@ -1,6 +1,6 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { fauxAssistantMessage, fauxThinking, fauxToolCall } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { afterEach, describe, expect, it } from "vitest";
import { createHarness, type Harness } from "./harness.js";

View File

@@ -1,7 +1,7 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { fauxAssistantMessage, fauxToolCall } from "@mariozechner/pi-ai";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { afterEach, describe, expect, it } from "vitest";
import { createHarness, getAssistantTexts, getUserTexts, type Harness } from "../harness.js";

View File

@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getModel } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { DefaultResourceLoader } from "../../../src/core/resource-loader.js";
import { createAgentSession } from "../../../src/core/sdk.js";

View File

@@ -5,7 +5,7 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
import type { AssistantMessage } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import { afterEach, describe, expect, it } from "vitest";
import { createHarness, createHarnessWithExtensions, type Harness } from "./test-harness.js";

View File

@@ -1,6 +1,6 @@
import { Text, type TUI } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import stripAnsi from "strip-ansi";
import { Type } from "typebox";
import { beforeAll, describe, expect, test } from "vitest";
import type { ToolDefinition } from "../src/core/extensions/types.js";
import { type BashOperations, createBashToolDefinition } from "../src/core/tools/bash.js";

View File

@@ -7,7 +7,7 @@
"@mariozechner/pi-coding-agent/hooks": ["./src/core/hooks/index.ts"],
"@mariozechner/pi-tui": ["../tui/src/index.ts"],
"@mariozechner/pi-ai": ["../ai/src/index.ts"],
"@sinclair/typebox": ["../../node_modules/@sinclair/typebox"]
"typebox": ["../../node_modules/typebox"]
},
"skipLibCheck": true
},

View File

@@ -1,14 +1,26 @@
import { defineConfig } from 'vitest/config';
import { fileURLToPath } from "node:url";
import { defineConfig } from "vitest/config";
const aiSrcIndex = fileURLToPath(new URL("../ai/src/index.ts", import.meta.url));
const aiSrcOAuth = fileURLToPath(new URL("../ai/src/oauth.ts", import.meta.url));
const agentSrcIndex = fileURLToPath(new URL("../agent/src/index.ts", import.meta.url));
export default defineConfig({
test: {
globals: true,
environment: 'node',
testTimeout: 30000, // 30 seconds for API calls
server: {
deps: {
external: [/@silvia-odwyer\/photon-node/],
},
},
},
test: {
globals: true,
environment: "node",
testTimeout: 30000,
server: {
deps: {
external: [/@silvia-odwyer\/photon-node/],
},
},
},
resolve: {
alias: [
{ find: /^@mariozechner\/pi-ai$/, replacement: aiSrcIndex },
{ find: /^@mariozechner\/pi-ai\/oauth$/, replacement: aiSrcOAuth },
{ find: /^@mariozechner\/pi-agent-core$/, replacement: agentSrcIndex },
],
},
});

View File

@@ -23,7 +23,7 @@
"@mariozechner/pi-agent-core": "^0.68.1",
"@mariozechner/pi-ai": "^0.68.1",
"@mariozechner/pi-coding-agent": "^0.68.1",
"@sinclair/typebox": "^0.34.0",
"typebox": "^1.1.24",
"@slack/socket-mode": "^2.0.0",
"@slack/web-api": "^7.0.0",
"chalk": "^5.6.2",

View File

@@ -1,6 +1,6 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { Type } from "@sinclair/typebox";
import { basename, resolve as resolvePath } from "path";
import { Type } from "typebox";
// This will be set by the agent before running
let uploadFn: ((filePath: string, title?: string) => Promise<void>) | null = null;

View File

@@ -3,7 +3,7 @@ import { createWriteStream } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import type { Executor } from "../sandbox.js";
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from "./truncate.js";

View File

@@ -1,6 +1,6 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { Type } from "@sinclair/typebox";
import * as Diff from "diff";
import { Type } from "typebox";
import type { Executor } from "../sandbox.js";
/**

View File

@@ -1,7 +1,7 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { extname } from "path";
import { Type } from "typebox";
import type { Executor } from "../sandbox.js";
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";

View File

@@ -1,5 +1,5 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { Type } from "@sinclair/typebox";
import { Type } from "typebox";
import type { Executor } from "../sandbox.js";
const writeSchema = Type.Object({

View File

@@ -20,6 +20,7 @@
"@lmstudio/sdk": "^1.5.0",
"@mariozechner/pi-ai": "^0.68.1",
"@mariozechner/pi-tui": "^0.68.1",
"typebox": "^1.1.24",
"docx-preview": "^0.3.7",
"jszip": "^3.10.1",
"lucide": "^0.544.0",

View File

@@ -3,11 +3,11 @@ import "@mariozechner/mini-lit/dist/MarkdownBlock.js";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import type { Agent, AgentMessage, AgentTool } from "@mariozechner/pi-agent-core";
import { StringEnum, type ToolCall } from "@mariozechner/pi-ai";
import { type Static, Type } from "@sinclair/typebox";
import { html, LitElement, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { createRef, type Ref, ref } from "lit/directives/ref.js";
import { X } from "lucide";
import { type Static, Type } from "typebox";
import type { ArtifactMessage } from "../../components/Messages.js";
import { ArtifactsRuntimeProvider } from "../../components/sandbox/ArtifactsRuntimeProvider.js";
import { AttachmentsRuntimeProvider } from "../../components/sandbox/AttachmentsRuntimeProvider.js";

View File

@@ -1,9 +1,9 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
import type { ToolResultMessage } from "@mariozechner/pi-ai";
import { type Static, Type } from "@sinclair/typebox";
import { html } from "lit";
import { createRef, ref } from "lit/directives/ref.js";
import { FileText } from "lucide";
import { type Static, Type } from "typebox";
import { EXTRACT_DOCUMENT_DESCRIPTION } from "../prompts/prompts.js";
import { loadAttachment } from "../utils/attachment-utils.js";
import { isCorsError } from "../utils/proxy-utils.js";

View File

@@ -1,10 +1,10 @@
import { i18n } from "@mariozechner/mini-lit";
import type { AgentTool } from "@mariozechner/pi-agent-core";
import type { ToolResultMessage } from "@mariozechner/pi-ai";
import { type Static, Type } from "@sinclair/typebox";
import { html } from "lit";
import { createRef, ref } from "lit/directives/ref.js";
import { Code } from "lucide";
import { type Static, Type } from "typebox";
import { type SandboxFile, SandboxIframe, type SandboxResult } from "../components/SandboxedIframe.js";
import type { SandboxRuntimeProvider } from "../components/sandbox/SandboxRuntimeProvider.js";
import { JAVASCRIPT_REPL_TOOL_DESCRIPTION } from "../prompts/prompts.js";

View File

@@ -13,7 +13,7 @@
"@mariozechner/pi-coding-agent": ["./packages/coding-agent/src/index.ts"],
"@mariozechner/pi-coding-agent/hooks": ["./packages/coding-agent/src/core/hooks/index.ts"],
"@mariozechner/pi-coding-agent/*": ["./packages/coding-agent/src/*"],
"@sinclair/typebox": ["./node_modules/@sinclair/typebox"],
"typebox": ["./node_modules/typebox"],
"@mariozechner/pi-mom": ["./packages/mom/src/index.ts"],
"@mariozechner/pi-mom/*": ["./packages/mom/src/*"],
"@mariozechner/pi": ["./packages/pods/src/index.ts"],