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==", "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==",
"license": "Apache-2.0" "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": { "node_modules/@slack/logger": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.1.tgz", "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.1.tgz",
@@ -4262,39 +4256,6 @@
"node": ">= 14" "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": { "node_modules/alien-signals": {
"version": "2.0.8", "version": "2.0.8",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-2.0.8.tgz", "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-2.0.8.tgz",
@@ -5313,12 +5274,6 @@
"@types/yauzl": "^2.9.1" "@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": { "node_modules/fast-glob": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -5336,22 +5291,6 @@
"node": ">=8.6.0" "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": { "node_modules/fast-xml-builder": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz",
@@ -6067,12 +6006,6 @@
"node": ">=16" "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": { "node_modules/jsonschema": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz",
@@ -7295,15 +7228,6 @@
"node": ">=0.10.0" "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": { "node_modules/resolve": {
"version": "1.22.12", "version": "1.22.12",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
@@ -8098,6 +8022,12 @@
"node": "*" "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": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -8597,7 +8527,8 @@
"version": "0.68.1", "version": "0.68.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@mariozechner/pi-ai": "^0.68.1" "@mariozechner/pi-ai": "^0.68.1",
"typebox": "^1.1.24"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.3.0", "@types/node": "^24.3.0",
@@ -8634,13 +8565,11 @@
"@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@aws-sdk/client-bedrock-runtime": "^3.1030.0",
"@google/genai": "^1.40.0", "@google/genai": "^1.40.0",
"@mistralai/mistralai": "^2.2.0", "@mistralai/mistralai": "^2.2.0",
"@sinclair/typebox": "^0.34.41",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"openai": "6.26.0", "openai": "6.26.0",
"partial-json": "^0.1.7", "partial-json": "^0.1.7",
"proxy-agent": "^6.5.0", "proxy-agent": "^6.5.0",
"typebox": "^1.1.24",
"undici": "^7.19.1", "undici": "^7.19.1",
"zod-to-json-schema": "^3.24.6" "zod-to-json-schema": "^3.24.6"
}, },
@@ -8683,8 +8612,6 @@
"@mariozechner/pi-ai": "^0.68.1", "@mariozechner/pi-ai": "^0.68.1",
"@mariozechner/pi-tui": "^0.68.1", "@mariozechner/pi-tui": "^0.68.1",
"@silvia-odwyer/photon-node": "^0.3.4", "@silvia-odwyer/photon-node": "^0.3.4",
"@sinclair/typebox": "^0.34.41",
"ajv": "^8.17.1",
"chalk": "^5.5.0", "chalk": "^5.5.0",
"cli-highlight": "^2.1.11", "cli-highlight": "^2.1.11",
"diff": "^8.0.2", "diff": "^8.0.2",
@@ -8697,6 +8624,7 @@
"minimatch": "^10.2.3", "minimatch": "^10.2.3",
"proper-lockfile": "^4.1.2", "proper-lockfile": "^4.1.2",
"strip-ansi": "^7.1.0", "strip-ansi": "^7.1.0",
"typebox": "^1.1.24",
"undici": "^7.19.1", "undici": "^7.19.1",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"yaml": "^2.8.2" "yaml": "^2.8.2"
@@ -8781,12 +8709,12 @@
"@mariozechner/pi-agent-core": "^0.68.1", "@mariozechner/pi-agent-core": "^0.68.1",
"@mariozechner/pi-ai": "^0.68.1", "@mariozechner/pi-ai": "^0.68.1",
"@mariozechner/pi-coding-agent": "^0.68.1", "@mariozechner/pi-coding-agent": "^0.68.1",
"@sinclair/typebox": "^0.34.0",
"@slack/socket-mode": "^2.0.0", "@slack/socket-mode": "^2.0.0",
"@slack/web-api": "^7.0.0", "@slack/web-api": "^7.0.0",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"croner": "^9.1.0", "croner": "^9.1.0",
"diff": "^8.0.2" "diff": "^8.0.2",
"typebox": "^1.1.24"
}, },
"bin": { "bin": {
"mom": "dist/main.js" "mom": "dist/main.js"
@@ -8893,6 +8821,7 @@
"lucide": "^0.544.0", "lucide": "^0.544.0",
"ollama": "^0.6.0", "ollama": "^0.6.0",
"pdfjs-dist": "5.4.394", "pdfjs-dist": "5.4.394",
"typebox": "^1.1.24",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
}, },
"devDependencies": { "devDependencies": {

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import {
type Model, type Model,
type UserMessage, type UserMessage,
} from "@mariozechner/pi-ai"; } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox"; import { Type } from "typebox";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { agentLoop, agentLoopContinue } from "../src/agent-loop.js"; import { agentLoop, agentLoopContinue } from "../src/agent-loop.js";
import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, AgentTool } from "../src/types.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"; import type { AgentTool, AgentToolResult } from "../../src/types.js";
export interface CalculateResult extends AgentToolResult<undefined> { 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"; import type { AgentTool, AgentToolResult } from "../../src/types.js";
export interface GetCurrentTimeResult extends AgentToolResult<{ utcTimestamp: number }> {} export interface GetCurrentTimeResult extends AgentToolResult<{ utcTimestamp: number }> {}

View File

@@ -202,7 +202,7 @@ for (const block of response.content) {
## Tools ## 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 ### Defining Tools

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ import {
type ToolConfiguration, type ToolConfiguration,
ToolResultStatus, ToolResultStatus,
} from "@aws-sdk/client-bedrock-runtime"; } from "@aws-sdk/client-bedrock-runtime";
import type { DocumentType } from "@smithy/types";
import { calculateCost } from "../models.js"; import { calculateCost } from "../models.js";
import type { import type {
Api, Api,
@@ -760,7 +761,7 @@ function convertToolConfig(
toolSpec: { toolSpec: {
name: tool.name, name: tool.name,
description: tool.description, 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; export type Message = UserMessage | AssistantMessage | ToolResultMessage;
import type { TSchema } from "@sinclair/typebox"; import type { TSchema } from "typebox";
export interface Tool<TParameters extends TSchema = TSchema> { export interface Tool<TParameters extends TSchema = TSchema> {
name: string; 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 * Creates a string enum schema compatible with Google's API and other providers

View File

@@ -1,42 +1,270 @@
import AjvModule from "ajv"; import { Compile } from "typebox/compile";
import addFormatsModule from "ajv-formats"; import type { TLocalizedValidationError } from "typebox/error";
import { Value } from "typebox/value";
// Handle both default and named exports
const Ajv = (AjvModule as any).default || AjvModule;
const addFormats = (addFormatsModule as any).default || addFormatsModule;
import type { Tool, ToolCall } from "../types.js"; import type { Tool, ToolCall } from "../types.js";
// Detect if we're in a browser extension environment with strict CSP const validatorCache = new WeakMap<object, ReturnType<typeof Compile>>();
// Chrome extensions with Manifest V3 don't allow eval/Function constructor const TYPEBOX_KIND = Symbol.for("TypeBox.Kind");
const isBrowserExtension = typeof globalThis !== "undefined" && (globalThis as any).chrome?.runtime?.id !== undefined;
function canUseRuntimeCodegen(): boolean { interface JsonSchemaObject {
if (isBrowserExtension) { type?: string | string[];
return false; 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 { function matchesJsonType(value: unknown, type: string): boolean {
new Function("return true;"); switch (type) {
return true; case "number":
} catch { return typeof value === "number";
return false; 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. function isValidatorSchema(value: unknown): value is Tool["parameters"] {
let ajv: any = null; return isRecord(value);
if (canUseRuntimeCodegen()) { }
try {
ajv = new Ajv({ function getSubSchemaValidator(schema: JsonSchemaObject): ReturnType<typeof Compile> | undefined {
allErrors: true, if (!isValidatorSchema(schema)) {
strict: false, return undefined;
coerceTypes: true,
});
addFormats(ajv);
} catch (_e) {
console.warn("AJV validation disabled due to CSP restrictions");
} }
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 * @throws Error with formatted message if validation fails
*/ */
export function validateToolArguments(tool: Tool, toolCall: ToolCall): any { export function validateToolArguments(tool: Tool, toolCall: ToolCall): any {
// Skip validation in environments where runtime code generation is unavailable. const args = structuredClone(toolCall.arguments);
if (!ajv || !canUseRuntimeCodegen()) { Value.Convert(tool.parameters, args);
return toolCall.arguments;
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. if (validator.Check(args)) {
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)) {
return args; return args;
} }
// Format validation errors nicely
const errors = const errors =
validate.errors validator
?.map((err: any) => { .Errors(args)
const path = err.instancePath ? err.instancePath.substring(1) : err.params.missingProperty || "root"; .map((error) => ` - ${formatValidationPath(error)}: ${error.message}`)
return ` - ${path}: ${err.message}`;
})
.join("\n") || "Unknown validation error"; .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)}`; 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 { describe, expect, it } from "vitest";
import { getModel } from "../src/models.js"; import { getModel } from "../src/models.js";
import { stream } from "../src/stream.js"; import { stream } from "../src/stream.js";

View File

@@ -22,8 +22,8 @@
* Fixtures are generated fresh on each run. * Fixtures are generated fresh on each run.
*/ */
import { Type } from "@sinclair/typebox";
import { writeFileSync } from "fs"; import { writeFileSync } from "fs";
import { Type } from "typebox";
import { beforeAll, describe, expect, it } from "vitest"; import { beforeAll, describe, expect, it } from "vitest";
import { getModel } from "../src/models.js"; import { getModel } from "../src/models.js";
import { completeSimple, getEnvApiKey } from "../src/stream.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 { afterEach, describe, expect, it, vi } from "vitest";
import { streamGoogleGeminiCli } from "../src/providers/google-gemini-cli.js"; import { streamGoogleGeminiCli } from "../src/providers/google-gemini-cli.js";
import type { Context, Model, ToolCall } from "../src/types.js"; import type { Context, Model, ToolCall } from "../src/types.js";

View File

@@ -1,6 +1,6 @@
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { Type } from "@sinclair/typebox"; import { Type } from "typebox";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import type { Api, Context, Model, Tool, ToolResultMessage } from "../src/index.js"; import type { Api, Context, Model, Tool, ToolResultMessage } from "../src/index.js";
import { complete, getModel } 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 { describe, expect, it } from "vitest";
import { getEnvApiKey } from "../src/env-api-keys.js"; import { getEnvApiKey } from "../src/env-api-keys.js";
import { getModel } from "../src/models.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 { describe, expect, it } from "vitest";
import { getModel } from "../src/models.js"; import { getModel } from "../src/models.js";
import { complete } from "../src/stream.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 { beforeEach, describe, expect, it, vi } from "vitest";
import { getModel } from "../src/models.js"; import { getModel } from "../src/models.js";
import { streamOpenAICompletions } from "../src/providers/openai-completions.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 { beforeEach, describe, expect, it, vi } from "vitest";
import { getModel } from "../src/models.js"; import { getModel } from "../src/models.js";
import { streamSimple } from "../src/stream.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 { describe, expect, it } from "vitest";
import { getModel } from "../src/models.js"; import { getModel } from "../src/models.js";
import { complete, getEnvApiKey } from "../src/stream.js"; import { complete, getEnvApiKey } from "../src/stream.js";

View File

@@ -1,8 +1,8 @@
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { Type } from "@sinclair/typebox";
import type { ResponseFunctionCallOutputItemList } from "openai/resources/responses/responses.js"; import type { ResponseFunctionCallOutputItemList } from "openai/resources/responses/responses.js";
import { Type } from "typebox";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import type { Api, Context, Model, StreamOptions, Tool, ToolResultMessage } from "../src/index.js"; import type { Api, Context, Model, StreamOptions, Tool, ToolResultMessage } from "../src/index.js";
import { complete, getModel } 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 { type ChildProcess, execSync, spawn } from "child_process";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { dirname, join } from "path"; import { dirname, join } from "path";
import { Type } from "typebox";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { getModel } from "../src/models.js"; import { getModel } from "../src/models.js";

View File

@@ -10,7 +10,7 @@
* Regression test for: https://github.com/badlogic/pi-mono/issues/1022 * 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 { describe, expect, it } from "vitest";
import { getModel } from "../src/models.js"; import { getModel } from "../src/models.js";
import { completeSimple, getEnvApiKey } from "../src/stream.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 { describe, expect, it } from "vitest";
import { getModel } from "../src/models.js"; import { getModel } from "../src/models.js";
import { complete } from "../src/stream.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 { describe, expect, it } from "vitest";
import { getModel } from "../src/models.js"; import { getModel } from "../src/models.js";
import { complete } from "../src/stream.js"; import { complete } from "../src/stream.js";

View File

@@ -1,17 +1,41 @@
import { Type } from "@sinclair/typebox"; import { Type } from "typebox";
import { afterEach, describe, expect, it, vi } from "vitest"; import { describe, expect, it } from "vitest";
import type { ToolCall } from "../src/types.js"; import type { Tool, ToolCall } from "../src/types.js";
import { validateToolArguments } from "../src/utils/validation.js"; import { validateToolArguments } from "../src/utils/validation.js";
afterEach(() => { function createToolCallWithPlainSchema(
vi.restoreAllMocks(); 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", () => { 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 originalFunction = globalThis.Function;
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const tool: Tool = {
const tool = {
name: "echo", name: "echo",
description: "Echo tool", description: "Echo tool",
parameters: Type.Object({ parameters: Type.Object({
@@ -30,10 +54,63 @@ describe("validateToolArguments", () => {
}) as unknown as FunctionConstructor; }) as unknown as FunctionConstructor;
try { try {
expect(validateToolArguments(tool, toolCall)).toEqual(toolCall.arguments); expect(validateToolArguments(tool, toolCall)).toEqual({ count: 42 });
expect(errorSpy).not.toHaveBeenCalled();
} finally { } finally {
globalThis.Function = originalFunction; 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 { join } from "node:path";
import { StringEnum } from "@mariozechner/pi-ai"; import { StringEnum } from "@mariozechner/pi-ai";
import { type ExtensionAPI, getAgentDir, withFileMutationQueue } from "@mariozechner/pi-coding-agent"; 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"; const PROVIDER = "google-antigravity";

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
*/ */
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox"; import { Type } from "typebox";
export default function (pi: ExtensionAPI) { export default function (pi: ExtensionAPI) {
// Register a /quit command that cleanly exits pi // 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 { StringEnum } from "@mariozechner/pi-ai";
import { type ExtensionAPI, getMarkdownTheme, withFileMutationQueue } from "@mariozechner/pi-coding-agent"; import { type ExtensionAPI, getMarkdownTheme, withFileMutationQueue } from "@mariozechner/pi-coding-agent";
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui"; 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"; import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
const MAX_PARALLEL_TASKS = 8; const MAX_PARALLEL_TASKS = 8;

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,8 +44,7 @@
"@mariozechner/pi-ai": "^0.68.1", "@mariozechner/pi-ai": "^0.68.1",
"@mariozechner/pi-tui": "^0.68.1", "@mariozechner/pi-tui": "^0.68.1",
"@silvia-odwyer/photon-node": "^0.3.4", "@silvia-odwyer/photon-node": "^0.3.4",
"@sinclair/typebox": "^0.34.41", "typebox": "^1.1.24",
"ajv": "^8.17.1",
"chalk": "^5.5.0", "chalk": "^5.5.0",
"cli-highlight": "^2.1.11", "cli-highlight": "^2.1.11",
"diff": "^8.0.2", "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. // Static imports of packages that extensions may use.
// These MUST be static so Bun bundles them into the compiled binary. // These MUST be static so Bun bundles them into the compiled binary.
// The virtualModules option then makes them available to extensions. // 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"; import { getAgentDir, isBunBinary } from "../../config.js";
// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts, // 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. // 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 type { ExecOptions } from "../exec.js";
import { execCommand } from "../exec.js"; import { execCommand } from "../exec.js";
import { createSyntheticSourceInfo } from "../source-info.js"; import { createSyntheticSourceInfo } from "../source-info.js";
import * as _bundledTypeboxCompilerCompat from "./typebox-compiler-compat.js";
import type { import type {
Extension, Extension,
ExtensionAPI, ExtensionAPI,
@@ -41,7 +44,14 @@ import type {
/** Modules available to extensions via virtualModules (for compiled Bun binary) */ /** Modules available to extensions via virtualModules (for compiled Bun binary) */
const VIRTUAL_MODULES: Record<string, unknown> = { const VIRTUAL_MODULES: Record<string, unknown> = {
typebox: _bundledTypebox,
"typebox/compile": _bundledTypeboxCompile,
"typebox/value": _bundledTypeboxValue,
"typebox/compiler": _bundledTypeboxCompilerCompat,
"@sinclair/typebox": _bundledTypebox, "@sinclair/typebox": _bundledTypebox,
"@sinclair/typebox/compile": _bundledTypeboxCompile,
"@sinclair/typebox/value": _bundledTypeboxValue,
"@sinclair/typebox/compiler": _bundledTypeboxCompilerCompat,
"@mariozechner/pi-agent-core": _bundledPiAgentCore, "@mariozechner/pi-agent-core": _bundledPiAgentCore,
"@mariozechner/pi-tui": _bundledPiTui, "@mariozechner/pi-tui": _bundledPiTui,
"@mariozechner/pi-ai": _bundledPiAi, "@mariozechner/pi-ai": _bundledPiAi,
@@ -56,14 +66,25 @@ const require = createRequire(import.meta.url);
* In Bun binary mode, virtualModules is used instead. * In Bun binary mode, virtualModules is used instead.
*/ */
let _aliases: Record<string, string> | null = null; 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> { function getAliases(): Record<string, string> {
if (_aliases) return _aliases; if (_aliases) return _aliases;
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageIndex = path.resolve(__dirname, "../..", "index.js"); const packageIndex = path.resolve(__dirname, "../..", "index.js");
const typeboxEntry = require.resolve("@sinclair/typebox"); const typeboxEntry = require.resolve("typebox");
const typeboxRoot = typeboxEntry.replace(/[\\/]build[\\/]cjs[\\/]index\.js$/, ""); const typeboxCompileEntry = require.resolve("typebox/compile");
const typeboxValueEntry = require.resolve("typebox/value");
const typeboxCompilerCompat = resolveTypeboxCompilerCompatPath(__dirname);
const packagesRoot = path.resolve(__dirname, "../../../../"); const packagesRoot = path.resolve(__dirname, "../../../../");
const resolveWorkspaceOrImport = (workspaceRelativePath: string, specifier: string): string => { 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-tui": resolveWorkspaceOrImport("tui/dist/index.js", "@mariozechner/pi-tui"),
"@mariozechner/pi-ai": resolveWorkspaceOrImport("ai/dist/index.js", "@mariozechner/pi-ai"), "@mariozechner/pi-ai": resolveWorkspaceOrImport("ai/dist/index.js", "@mariozechner/pi-ai"),
"@mariozechner/pi-ai/oauth": resolveWorkspaceOrImport("ai/dist/oauth.js", "@mariozechner/pi-ai/oauth"), "@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; 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, OverlayOptions,
TUI, TUI,
} from "@mariozechner/pi-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 { Theme } from "../../modes/interactive/theme/theme.js";
import type { BashResult } from "../bash-executor.js"; import type { BashResult } from "../bash-executor.js";
import type { CompactionPreparation, CompactionResult } from "../compaction/index.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>( export function defineTool<TParams extends TSchema, TDetails = unknown, TState = any>(
tool: ToolDefinition<TParams, TDetails, TState>, tool: ToolDefinition<TParams, TDetails, TState>,
): ToolDefinition<TParams, TDetails, TState> & AnyToolDefinition { ): ToolDefinition<TParams, TDetails, TState> & AnyToolDefinition {
return tool; return tool as ToolDefinition<TParams, TDetails, TState> & AnyToolDefinition;
} }
// ============================================================================ // ============================================================================

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ import {
type ImageContent, type ImageContent,
type TextContent, type TextContent,
} from "@mariozechner/pi-ai"; } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox"; import { Type } from "typebox";
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AgentSession } from "../src/core/agent-session.js"; import { AgentSession } from "../src/core/agent-session.js";
import { AuthStorage } from "../src/core/auth-storage.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 { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { getModel } from "@mariozechner/pi-ai"; import { getModel } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox"; import { Type } from "typebox";
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { DefaultResourceLoader } from "../src/core/resource-loader.js"; import { DefaultResourceLoader } from "../src/core/resource-loader.js";
import { createAgentSession } from "../src/core/sdk.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 { join } from "node:path";
import { Agent, type AgentEvent, type AgentTool } from "@mariozechner/pi-agent-core"; import { Agent, type AgentEvent, type AgentTool } from "@mariozechner/pi-agent-core";
import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@mariozechner/pi-ai"; 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 { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AgentSession } from "../src/core/agent-session.js"; import { AgentSession } from "../src/core/agent-session.js";
import { AuthStorage } from "../src/core/auth-storage.js"; import { AuthStorage } from "../src/core/auth-storage.js";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import type { AgentTool, ThinkingLevel } from "@mariozechner/pi-agent-core"; import type { AgentTool, ThinkingLevel } from "@mariozechner/pi-agent-core";
import { fauxAssistantMessage, fauxToolCall, type Model } from "@mariozechner/pi-ai"; 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 { afterEach, describe, expect, it } from "vitest";
import type { ExtensionAPI } from "../../src/index.js"; import type { ExtensionAPI } from "../../src/index.js";
import { createHarness, getAssistantTexts, type Harness } from "./harness.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 { join } from "node:path";
import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { AgentTool } from "@mariozechner/pi-agent-core";
import { fauxAssistantMessage, fauxToolCall, type Model } from "@mariozechner/pi-ai"; 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 { afterEach, describe, expect, it } from "vitest";
import type { PromptTemplate } from "../../src/core/prompt-templates.js"; import type { PromptTemplate } from "../../src/core/prompt-templates.js";
import { createSyntheticSourceInfo } from "../../src/core/source-info.js"; import { createSyntheticSourceInfo } from "../../src/core/source-info.js";

View File

@@ -1,7 +1,7 @@
import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { AgentTool } from "@mariozechner/pi-agent-core";
import { fauxAssistantMessage, fauxToolCall } from "@mariozechner/pi-ai"; import { fauxAssistantMessage, fauxToolCall } from "@mariozechner/pi-ai";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; 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 { afterEach, describe, expect, it } from "vitest";
import { createHarness, getAssistantTexts, getMessageText, getUserTexts, type Harness } from "./harness.js"; 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 type { AgentTool } from "@mariozechner/pi-agent-core";
import { fauxAssistantMessage, fauxThinking, fauxToolCall } from "@mariozechner/pi-ai"; 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 { afterEach, describe, expect, it } from "vitest";
import { createHarness, type Harness } from "./harness.js"; import { createHarness, type Harness } from "./harness.js";

View File

@@ -1,7 +1,7 @@
import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { AgentTool } from "@mariozechner/pi-agent-core";
import { fauxAssistantMessage, fauxToolCall } from "@mariozechner/pi-ai"; import { fauxAssistantMessage, fauxToolCall } from "@mariozechner/pi-ai";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; 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 { afterEach, describe, expect, it } from "vitest";
import { createHarness, getAssistantTexts, getUserTexts, type Harness } from "../harness.js"; 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 { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { getModel } from "@mariozechner/pi-ai"; import { getModel } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox"; import { Type } from "typebox";
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { DefaultResourceLoader } from "../../../src/core/resource-loader.js"; import { DefaultResourceLoader } from "../../../src/core/resource-loader.js";
import { createAgentSession } from "../../../src/core/sdk.js"; import { createAgentSession } from "../../../src/core/sdk.js";

View File

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

View File

@@ -1,6 +1,6 @@
import { Text, type TUI } from "@mariozechner/pi-tui"; import { Text, type TUI } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import stripAnsi from "strip-ansi"; import stripAnsi from "strip-ansi";
import { Type } from "typebox";
import { beforeAll, describe, expect, test } from "vitest"; import { beforeAll, describe, expect, test } from "vitest";
import type { ToolDefinition } from "../src/core/extensions/types.js"; import type { ToolDefinition } from "../src/core/extensions/types.js";
import { type BashOperations, createBashToolDefinition } from "../src/core/tools/bash.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-coding-agent/hooks": ["./src/core/hooks/index.ts"],
"@mariozechner/pi-tui": ["../tui/src/index.ts"], "@mariozechner/pi-tui": ["../tui/src/index.ts"],
"@mariozechner/pi-ai": ["../ai/src/index.ts"], "@mariozechner/pi-ai": ["../ai/src/index.ts"],
"@sinclair/typebox": ["../../node_modules/@sinclair/typebox"] "typebox": ["../../node_modules/typebox"]
}, },
"skipLibCheck": true "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({ export default defineConfig({
test: { test: {
globals: true, globals: true,
environment: 'node', environment: "node",
testTimeout: 30000, // 30 seconds for API calls testTimeout: 30000,
server: { server: {
deps: { deps: {
external: [/@silvia-odwyer\/photon-node/], 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-agent-core": "^0.68.1",
"@mariozechner/pi-ai": "^0.68.1", "@mariozechner/pi-ai": "^0.68.1",
"@mariozechner/pi-coding-agent": "^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/socket-mode": "^2.0.0",
"@slack/web-api": "^7.0.0", "@slack/web-api": "^7.0.0",
"chalk": "^5.6.2", "chalk": "^5.6.2",

View File

@@ -1,6 +1,6 @@
import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { AgentTool } from "@mariozechner/pi-agent-core";
import { Type } from "@sinclair/typebox";
import { basename, resolve as resolvePath } from "path"; import { basename, resolve as resolvePath } from "path";
import { Type } from "typebox";
// This will be set by the agent before running // This will be set by the agent before running
let uploadFn: ((filePath: string, title?: string) => Promise<void>) | null = null; 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 { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { AgentTool } from "@mariozechner/pi-agent-core";
import { Type } from "@sinclair/typebox"; import { Type } from "typebox";
import type { Executor } from "../sandbox.js"; import type { Executor } from "../sandbox.js";
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from "./truncate.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 { AgentTool } from "@mariozechner/pi-agent-core";
import { Type } from "@sinclair/typebox";
import * as Diff from "diff"; import * as Diff from "diff";
import { Type } from "typebox";
import type { Executor } from "../sandbox.js"; import type { Executor } from "../sandbox.js";
/** /**

View File

@@ -1,7 +1,7 @@
import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { AgentTool } from "@mariozechner/pi-agent-core";
import type { ImageContent, TextContent } from "@mariozechner/pi-ai"; import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { extname } from "path"; import { extname } from "path";
import { Type } from "typebox";
import type { Executor } from "../sandbox.js"; import type { Executor } from "../sandbox.js";
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.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 { AgentTool } from "@mariozechner/pi-agent-core";
import { Type } from "@sinclair/typebox"; import { Type } from "typebox";
import type { Executor } from "../sandbox.js"; import type { Executor } from "../sandbox.js";
const writeSchema = Type.Object({ const writeSchema = Type.Object({

View File

@@ -20,6 +20,7 @@
"@lmstudio/sdk": "^1.5.0", "@lmstudio/sdk": "^1.5.0",
"@mariozechner/pi-ai": "^0.68.1", "@mariozechner/pi-ai": "^0.68.1",
"@mariozechner/pi-tui": "^0.68.1", "@mariozechner/pi-tui": "^0.68.1",
"typebox": "^1.1.24",
"docx-preview": "^0.3.7", "docx-preview": "^0.3.7",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lucide": "^0.544.0", "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 { Button } from "@mariozechner/mini-lit/dist/Button.js";
import type { Agent, AgentMessage, AgentTool } from "@mariozechner/pi-agent-core"; import type { Agent, AgentMessage, AgentTool } from "@mariozechner/pi-agent-core";
import { StringEnum, type ToolCall } from "@mariozechner/pi-ai"; import { StringEnum, type ToolCall } from "@mariozechner/pi-ai";
import { type Static, Type } from "@sinclair/typebox";
import { html, LitElement, type TemplateResult } from "lit"; import { html, LitElement, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { createRef, type Ref, ref } from "lit/directives/ref.js"; import { createRef, type Ref, ref } from "lit/directives/ref.js";
import { X } from "lucide"; import { X } from "lucide";
import { type Static, Type } from "typebox";
import type { ArtifactMessage } from "../../components/Messages.js"; import type { ArtifactMessage } from "../../components/Messages.js";
import { ArtifactsRuntimeProvider } from "../../components/sandbox/ArtifactsRuntimeProvider.js"; import { ArtifactsRuntimeProvider } from "../../components/sandbox/ArtifactsRuntimeProvider.js";
import { AttachmentsRuntimeProvider } from "../../components/sandbox/AttachmentsRuntimeProvider.js"; import { AttachmentsRuntimeProvider } from "../../components/sandbox/AttachmentsRuntimeProvider.js";

View File

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

View File

@@ -1,10 +1,10 @@
import { i18n } from "@mariozechner/mini-lit"; import { i18n } from "@mariozechner/mini-lit";
import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { AgentTool } from "@mariozechner/pi-agent-core";
import type { ToolResultMessage } from "@mariozechner/pi-ai"; import type { ToolResultMessage } from "@mariozechner/pi-ai";
import { type Static, Type } from "@sinclair/typebox";
import { html } from "lit"; import { html } from "lit";
import { createRef, ref } from "lit/directives/ref.js"; import { createRef, ref } from "lit/directives/ref.js";
import { Code } from "lucide"; import { Code } from "lucide";
import { type Static, Type } from "typebox";
import { type SandboxFile, SandboxIframe, type SandboxResult } from "../components/SandboxedIframe.js"; import { type SandboxFile, SandboxIframe, type SandboxResult } from "../components/SandboxedIframe.js";
import type { SandboxRuntimeProvider } from "../components/sandbox/SandboxRuntimeProvider.js"; import type { SandboxRuntimeProvider } from "../components/sandbox/SandboxRuntimeProvider.js";
import { JAVASCRIPT_REPL_TOOL_DESCRIPTION } from "../prompts/prompts.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": ["./packages/coding-agent/src/index.ts"],
"@mariozechner/pi-coding-agent/hooks": ["./packages/coding-agent/src/core/hooks/index.ts"], "@mariozechner/pi-coding-agent/hooks": ["./packages/coding-agent/src/core/hooks/index.ts"],
"@mariozechner/pi-coding-agent/*": ["./packages/coding-agent/src/*"], "@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/index.ts"],
"@mariozechner/pi-mom/*": ["./packages/mom/src/*"], "@mariozechner/pi-mom/*": ["./packages/mom/src/*"],
"@mariozechner/pi": ["./packages/pods/src/index.ts"], "@mariozechner/pi": ["./packages/pods/src/index.ts"],