style(deps): migrated from Prettier to Biome and updated formatting rules

- Removed Prettier configuration files (.prettierignore and .prettierrc) and migrated formatting to Biome.
- Updated Biome configuration from version 2.3.11 to 2.3.12 and changed arrowParentheses rule from 'always' to 'asNeeded'.
- Pinned @biomejs/biome dependency to exact version 2.3.12 in package.json and bun.lock.
- Applied consistent arrow function formatting across 489 files by removing unnecessary parentheses around single parameters.
- Removed blank lines after comment blocks and reorganized imports for consistency across the codebase.
This commit is contained in:
can1357
2026-01-24 04:20:19 +01:00
parent f901befd19
commit 779ca4872b
489 changed files with 1900 additions and 2215 deletions
-13
View File
@@ -1,13 +0,0 @@
# Vendor and dependencies
**/vendor/**
**/node_modules/**
# Generated files
**/test-sessions.ts
**/models.generated.ts
**/template.generated.ts
**/gen/agent_pb.ts
# Build outputs
**/dist/**
**/build/**
-11
View File
@@ -1,11 +0,0 @@
{
"useTabs": true,
"tabWidth": 3,
"printWidth": 120,
"endOfLine": "lf",
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "always"
}
+3 -3
View File
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.12/schema.json",
"linter": {
"enabled": true,
"rules": {
@@ -29,14 +29,14 @@
"quoteStyle": "double",
"trailingCommas": "all",
"bracketSpacing": true,
"arrowParentheses": "always"
"arrowParentheses": "asNeeded"
}
},
"files": {
"includes": [
"packages/*/src/**/*.ts",
"packages/*/test/**/*.ts",
"packages/coding-agent/examples/**/*.ts",
"packages/*/examples/**/*.ts",
"!**/vendor/**/*",
"!**/node_modules/**/*",
"!**/test-sessions.ts",
+1 -1
View File
@@ -9,7 +9,7 @@
"get-east-asian-width": "^1.4.0",
},
"devDependencies": {
"@biomejs/biome": "^2.3.12",
"@biomejs/biome": "2.3.12",
"@bufbuild/protoc-gen-es": "^2.10.2",
"@types/bun": "^1.2.17",
"@types/node": "^25.0.10",
+1 -1
View File
@@ -24,7 +24,7 @@
"release": "bun scripts/release.ts"
},
"devDependencies": {
"@biomejs/biome": "^2.3.12",
"@biomejs/biome": "2.3.12",
"@bufbuild/protoc-gen-es": "^2.10.2",
"@types/bun": "^1.2.17",
"@types/node": "^25.0.10",
+7 -8
View File
@@ -2,7 +2,6 @@
* Agent loop that works with AgentMessage throughout.
* Transforms to Message[] only at the LLM call boundary.
*/
import {
type AssistantMessage,
type Context,
@@ -107,12 +106,12 @@ function normalizeMessagesForProvider(
}
let changed = false;
const normalized = messages.map((message) => {
const normalized = messages.map(message => {
if (message.role !== "assistant" || !Array.isArray(message.content)) {
return message;
}
const filtered = message.content.filter((block) => block.type !== "thinking");
const filtered = message.content.filter(block => block.type !== "thinking");
if (filtered.length === message.content.length) {
return message;
}
@@ -186,7 +185,7 @@ async function runLoop(
}
// Check for tool calls
const toolCalls = message.content.filter((c) => c.type === "toolCall");
const toolCalls = message.content.filter(c => c.type === "toolCall");
hasMoreToolCalls = toolCalls.length > 0;
const toolResults: ToolResultMessage[] = [];
@@ -373,16 +372,16 @@ async function executeToolCalls(
getToolContext?: AgentLoopConfig["getToolContext"],
interruptMode: AgentLoopConfig["interruptMode"] = "immediate",
): Promise<{ toolResults: ToolResultMessage[]; steeringMessages?: AgentMessage[] }> {
const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
const toolCalls = assistantMessage.content.filter(c => c.type === "toolCall");
const results: ToolResultMessage[] = [];
let steeringMessages: AgentMessage[] | undefined;
const shouldInterruptImmediately = interruptMode !== "wait";
const toolCallInfos = toolCalls.map((call) => ({ id: call.id, name: call.name }));
const toolCallInfos = toolCalls.map(call => ({ id: call.id, name: call.name }));
const batchId = `${assistantMessage.timestamp ?? Date.now()}_${toolCalls[0]?.id ?? "batch"}`;
for (let index = 0; index < toolCalls.length; index++) {
const toolCall = toolCalls[index];
const tool = tools?.find((t) => t.name === toolCall.name);
const tool = tools?.find(t => t.name === toolCall.name);
stream.push({
type: "tool_execution_start",
@@ -411,7 +410,7 @@ async function executeToolCalls(
toolCall.id,
validatedArgs,
tool.nonAbortable ? undefined : signal,
(partialResult) => {
partialResult => {
stream.push({
type: "tool_execution_update",
toolCallId: toolCall.id,
+5 -6
View File
@@ -2,7 +2,6 @@
* Agent class that uses the agent-loop directly.
* No transport abstraction - calls streamSimple via the loop.
*/
import {
type AssistantMessage,
type CursorExecHandlers,
@@ -34,7 +33,7 @@ import type {
* Default convertToLlm: Keep only LLM-compatible messages, convert attachments.
*/
function defaultConvertToLlm(messages: AgentMessage[]): Message[] {
return messages.filter((m) => m.role === "user" || m.role === "assistant" || m.role === "toolResult");
return messages.filter(m => m.role === "user" || m.role === "assistant" || m.role === "toolResult");
}
export interface AgentOptions {
@@ -420,7 +419,7 @@ export class Agent {
const model = this._state.model;
if (!model) throw new Error("No model configured");
this.runningPrompt = new Promise<void>((resolve) => {
this.runningPrompt = new Promise<void>(resolve => {
this.resolveRunningPrompt = resolve;
});
@@ -569,7 +568,7 @@ export class Agent {
// Handle any remaining partial message
if (partial && partial.role === "assistant" && partial.content.length > 0) {
const onlyEmpty = !partial.content.some(
(c) =>
c =>
(c.type === "thinking" && c.thinking.trim().length > 0) ||
(c.type === "text" && c.text.trim().length > 0) ||
(c.type === "toolCall" && c.name.trim().length > 0),
@@ -655,7 +654,7 @@ export class Agent {
}
// Find the split point: minimum text length at first tool call
const splitPoint = Math.min(...buffer.map((r) => r.textLengthAtCall));
const splitPoint = Math.min(...buffer.map(r => r.textLengthAtCall));
// Extract text content from assistant message
const content = assistantMessage.content;
@@ -687,7 +686,7 @@ export class Agent {
const continuationText = fullText.slice(splitPoint);
// Create preamble message (text before tools)
const preambleContent = content.map((block) => {
const preambleContent = content.map(block => {
if (block.type === "text") {
return { ...block, text: preambleText };
}
+2 -3
View File
@@ -2,7 +2,6 @@
* Proxy stream function for apps that route LLM calls through a server.
* The server manages auth and proxies requests to LLM providers.
*/
import {
type AssistantMessage,
type AssistantMessageEvent,
@@ -20,8 +19,8 @@ import { readSseEvents } from "@oh-my-pi/pi-utils";
class ProxyMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
constructor() {
super(
(event) => event.type === "done" || event.type === "error",
(event) => {
event => event.type === "done" || event.type === "error",
event => {
if (event.type === "done") return event.message;
if (event.type === "error") return event.error;
throw new Error("Unexpected event type");
+19 -19
View File
@@ -23,8 +23,8 @@ import { Type } from "@sinclair/typebox";
class MockAssistantStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
constructor() {
super(
(event) => event.type === "done" || event.type === "error",
(event) => {
event => event.type === "done" || event.type === "error",
event => {
if (event.type === "done") return event.message;
if (event.type === "error") return event.error;
throw new Error("Unexpected event type");
@@ -85,7 +85,7 @@ function createUserMessage(text: string): UserMessage {
// Simple identity converter for tests - just passes through standard messages
function identityConverter(messages: AgentMessage[]): Message[] {
return messages.filter((m) => m.role === "user" || m.role === "assistant" || m.role === "toolResult") as Message[];
return messages.filter(m => m.role === "user" || m.role === "assistant" || m.role === "toolResult") as Message[];
}
describe("agentLoop with AgentMessage", () => {
@@ -127,7 +127,7 @@ describe("agentLoop with AgentMessage", () => {
expect(messages[1].role).toBe("assistant");
// Verify event sequence
const eventTypes = events.map((e) => e.type);
const eventTypes = events.map(e => e.type);
expect(eventTypes).toContain("agent_start");
expect(eventTypes).toContain("turn_start");
expect(eventTypes).toContain("message_start");
@@ -161,11 +161,11 @@ describe("agentLoop with AgentMessage", () => {
let convertedMessages: Message[] = [];
const config: AgentLoopConfig = {
model: createModel(),
convertToLlm: (messages) => {
convertToLlm: messages => {
// Filter out notifications, convert rest
convertedMessages = messages
.filter((m) => (m as { role: string }).role !== "notification")
.filter((m) => m.role === "user" || m.role === "assistant" || m.role === "toolResult") as Message[];
.filter(m => (m as { role: string }).role !== "notification")
.filter(m => m.role === "user" || m.role === "assistant" || m.role === "toolResult") as Message[];
return convertedMessages;
},
};
@@ -210,14 +210,14 @@ describe("agentLoop with AgentMessage", () => {
const config: AgentLoopConfig = {
model: createModel(),
transformContext: async (messages) => {
transformContext: async messages => {
// Keep only last 2 messages (prune old ones)
transformedMessages = messages.slice(-2);
return transformedMessages;
},
convertToLlm: (messages) => {
convertToLlm: messages => {
convertedMessages = messages.filter(
(m) => m.role === "user" || m.role === "assistant" || m.role === "toolResult",
m => m.role === "user" || m.role === "assistant" || m.role === "toolResult",
) as Message[];
return convertedMessages;
},
@@ -275,7 +275,7 @@ describe("agentLoop with AgentMessage", () => {
const config: AgentLoopConfig = {
model: createModel(),
convertToLlm: identityConverter,
getToolContext: (toolCall) => ({ toolCall }) as AgentToolContext,
getToolContext: toolCall => ({ toolCall }) as AgentToolContext,
};
let callIndex = 0;
@@ -379,8 +379,8 @@ describe("agentLoop with AgentMessage", () => {
expect(executed).toEqual(["hello"]);
// Should have tool execution events
const toolStart = events.find((e) => e.type === "tool_execution_start");
const toolEnd = events.find((e) => e.type === "tool_execution_end");
const toolStart = events.find(e => e.type === "tool_execution_start");
const toolEnd = events.find(e => e.type === "tool_execution_end");
expect(toolStart).toBeDefined();
expect(toolEnd).toBeDefined();
if (toolEnd?.type === "tool_execution_end") {
@@ -436,7 +436,7 @@ describe("agentLoop with AgentMessage", () => {
// Check if interrupt message is in context on second call
if (callIndex === 1) {
sawInterruptInContext = ctx.messages.some(
(m) => m.role === "user" && typeof m.content === "string" && m.content === "interrupt",
m => m.role === "user" && typeof m.content === "string" && m.content === "interrupt",
);
}
@@ -482,7 +482,7 @@ describe("agentLoop with AgentMessage", () => {
// Queued message should appear in events
const queuedMessageEvent = events.find(
(e) =>
e =>
e.type === "message_start" &&
e.message.role === "user" &&
typeof e.message.content === "string" &&
@@ -548,7 +548,7 @@ describe("agentLoopContinue with AgentMessage", () => {
expect(messages[0].role).toBe("assistant");
// Should NOT have user message events (that's the key difference from agentLoop)
const messageEndEvents = events.filter((e) => e.type === "message_end");
const messageEndEvents = events.filter(e => e.type === "message_end");
expect(messageEndEvents.length).toBe(1);
expect((messageEndEvents[0] as any).message.role).toBe("assistant");
});
@@ -575,10 +575,10 @@ describe("agentLoopContinue with AgentMessage", () => {
const config: AgentLoopConfig = {
model: createModel(),
convertToLlm: (messages) => {
convertToLlm: messages => {
// Convert hookMessage to user message
return messages
.map((m) => {
.map(m => {
if ((m as any).role === "hookMessage") {
return {
role: "user" as const,
@@ -588,7 +588,7 @@ describe("agentLoopContinue with AgentMessage", () => {
}
return m;
})
.filter((m) => m.role === "user" || m.role === "assistant" || m.role === "toolResult") as Message[];
.filter(m => m.role === "user" || m.role === "assistant" || m.role === "toolResult") as Message[];
},
};
+3 -3
View File
@@ -12,8 +12,8 @@ import {
class MockAssistantStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
constructor() {
super(
(event) => event.type === "done" || event.type === "error",
(event) => {
event => event.type === "done" || event.type === "error",
event => {
if (event.type === "done") return event.message;
if (event.type === "error") return event.error;
throw new Error("Unexpected event type");
@@ -84,7 +84,7 @@ describe("Agent", () => {
const agent = new Agent();
let eventCount = 0;
const unsubscribe = agent.subscribe((_event) => {
const unsubscribe = agent.subscribe(_event => {
eventCount++;
});
+10 -10
View File
@@ -25,7 +25,7 @@ async function basicPrompt(model: Model<any>) {
if (assistantMessage.role !== "assistant") throw new Error("Expected assistant message");
expect(assistantMessage.content.length).toBeGreaterThan(0);
const textContent = assistantMessage.content.find((c) => c.type === "text");
const textContent = assistantMessage.content.find(c => c.type === "text");
expect(textContent).toBeDefined();
if (textContent?.type !== "text") throw new Error("Expected text content");
expect(textContent.text).toContain("4");
@@ -46,12 +46,12 @@ async function toolExecution(model: Model<any>) {
expect(agent.state.isStreaming).toBe(false);
expect(agent.state.messages.length).toBeGreaterThanOrEqual(3);
const toolResultMsg = agent.state.messages.find((m) => m.role === "toolResult");
const toolResultMsg = agent.state.messages.find(m => m.role === "toolResult");
expect(toolResultMsg).toBeDefined();
if (toolResultMsg?.role !== "toolResult") throw new Error("Expected tool result message");
const textContent =
toolResultMsg.content
?.filter((c) => c.type === "text")
?.filter(c => c.type === "text")
.map((c: any) => c.text)
.join("\n") || "";
expect(textContent).toBeDefined();
@@ -61,7 +61,7 @@ async function toolExecution(model: Model<any>) {
const finalMessage = agent.state.messages[agent.state.messages.length - 1];
if (finalMessage.role !== "assistant") throw new Error("Expected final assistant message");
const finalText = finalMessage.content.find((c) => c.type === "text");
const finalText = finalMessage.content.find(c => c.type === "text");
expect(finalText).toBeDefined();
if (finalText?.type !== "text") throw new Error("Expected text content");
// Check for number with or without comma formatting
@@ -113,7 +113,7 @@ async function stateUpdates(model: Model<any>) {
const events: Array<string> = [];
agent.subscribe((event) => {
agent.subscribe(event => {
events.push(event.type);
});
@@ -125,7 +125,7 @@ async function stateUpdates(model: Model<any>) {
expect(events).toContain("message_start");
expect(events).toContain("message_end");
// May have message_update events during streaming
const hasMessageUpdates = events.some((e) => e === "message_update");
const hasMessageUpdates = events.some(e => e === "message_update");
expect(hasMessageUpdates).toBe(true);
// Check final state
@@ -151,7 +151,7 @@ async function multiTurnConversation(model: Model<any>) {
const lastMessage = agent.state.messages[3];
if (lastMessage.role !== "assistant") throw new Error("Expected assistant message");
const lastText = lastMessage.content.find((c) => c.type === "text");
const lastText = lastMessage.content.find(c => c.type === "text");
if (lastText?.type !== "text") throw new Error("Expected text content");
expect(lastText.text.toLowerCase()).toContain("alice");
}
@@ -400,7 +400,7 @@ describe("Agent.continue()", () => {
expect(agent.state.messages[1].role).toBe("assistant");
const assistantMsg = agent.state.messages[1] as AssistantMessage;
const textContent = assistantMsg.content.find((c) => c.type === "text");
const textContent = assistantMsg.content.find(c => c.type === "text");
expect(textContent).toBeDefined();
if (textContent?.type === "text") {
expect(textContent.text.toUpperCase()).toContain("HELLO WORLD");
@@ -473,8 +473,8 @@ describe("Agent.continue()", () => {
if (lastMessage.role === "assistant") {
const textContent = lastMessage.content
.filter((c) => c.type === "text")
.map((c) => (c as { type: "text"; text: string }).text)
.filter(c => c.type === "text")
.map(c => (c as { type: "text"; text: string }).text)
.join(" ");
// Should mention 8 in the response
expect(textContent).toMatch(/8/);
+3 -3
View File
@@ -91,7 +91,7 @@ async function login(provider: OAuthProvider): Promise<void> {
case "cursor":
credentials = await loginCursor(
(url) => {
url => {
console.log(`\nOpen this URL in your browser:\n${url}\n`);
},
() => {
@@ -243,7 +243,7 @@ Examples:
provider = PROVIDERS[index].id;
}
if (!PROVIDERS.some((p) => p.id === provider)) {
if (!PROVIDERS.some(p => p.id === provider)) {
console.error(`Unknown provider: ${provider}`);
console.error(`Use 'bunx @oh-my-pi/pi-ai list' to see available providers`);
process.exit(1);
@@ -259,7 +259,7 @@ Examples:
process.exit(1);
}
main().catch((err) => {
main().catch(err => {
console.error("Error:", err.message);
process.exit(1);
});
+7 -8
View File
@@ -17,7 +17,6 @@ import {
type ToolConfiguration,
ToolResultStatus,
} from "@aws-sdk/client-bedrock-runtime";
import { calculateCost } from "../models";
import type {
Api,
@@ -200,7 +199,7 @@ function handleContentBlockDelta(
): void {
const contentBlockIndex = event.contentBlockIndex!;
const delta = event.delta;
let index = blocks.findIndex((b) => b.index === contentBlockIndex);
let index = blocks.findIndex(b => b.index === contentBlockIndex);
let block = blocks[index];
if (delta?.text !== undefined) {
@@ -271,7 +270,7 @@ function handleContentBlockStop(
output: AssistantMessage,
stream: AssistantMessageEventStream,
): void {
const index = blocks.findIndex((b) => b.index === event.contentBlockIndex);
const index = blocks.findIndex(b => b.index === event.contentBlockIndex);
const block = blocks[index];
if (!block) return;
delete (block as Block).index;
@@ -351,7 +350,7 @@ function convertMessages(context: Context, model: Model<"bedrock-converse-stream
});
} else {
const contentBlocks = m.content
.map((c) => {
.map(c => {
switch (c.type) {
case "text":
return { text: sanitizeSurrogates(c.text) };
@@ -361,7 +360,7 @@ function convertMessages(context: Context, model: Model<"bedrock-converse-stream
throw new Error("Unknown user content type");
}
})
.filter((block) => {
.filter(block => {
// Filter out empty text blocks
if ("text" in block && block.text) {
return block.text.trim().length > 0;
@@ -442,7 +441,7 @@ function convertMessages(context: Context, model: Model<"bedrock-converse-stream
toolResults.push({
toolResult: {
toolUseId: sanitizeToolCallId(m.toolCallId),
content: m.content.map((c) =>
content: m.content.map(c =>
c.type === "image"
? { image: createImageBlock(c.mimeType, c.data) }
: { text: sanitizeSurrogates(c.text) },
@@ -458,7 +457,7 @@ function convertMessages(context: Context, model: Model<"bedrock-converse-stream
toolResults.push({
toolResult: {
toolUseId: sanitizeToolCallId(nextMsg.toolCallId),
content: nextMsg.content.map((c) =>
content: nextMsg.content.map(c =>
c.type === "image"
? { image: createImageBlock(c.mimeType, c.data) }
: { text: sanitizeSurrogates(c.text) },
@@ -500,7 +499,7 @@ function convertToolConfig(
): ToolConfiguration | undefined {
if (!tools?.length || toolChoice === "none") return undefined;
const bedrockTools: BedrockTool[] = tools.map((tool) => ({
const bedrockTools: BedrockTool[] = tools.map(tool => ({
toolSpec: {
name: tool.name,
description: tool.description,
+18 -19
View File
@@ -26,7 +26,6 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
import { parseStreamingJson } from "../utils/json-parse";
import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
import { sanitizeSurrogates } from "../utils/sanitize-unicode";
import { transformMessages } from "./transform-messages";
// Stealth mode: Mimic Claude Code headers and tool prefixing.
@@ -89,13 +88,13 @@ function convertContentBlocks(content: (TextContent | ImageContent)[]):
}
> {
// If only text blocks, return as concatenated string for simplicity
const hasImages = content.some((c) => c.type === "image");
const hasImages = content.some(c => c.type === "image");
if (!hasImages) {
return sanitizeSurrogates(content.map((c) => (c as TextContent).text).join("\n"));
return sanitizeSurrogates(content.map(c => (c as TextContent).text).join("\n"));
}
// If we have images, convert to content block array
const blocks = content.map((block) => {
const blocks = content.map(block => {
if (block.type === "text") {
return {
type: "text" as const,
@@ -113,7 +112,7 @@ function convertContentBlocks(content: (TextContent | ImageContent)[]):
});
// If only images (no text), add placeholder text block
const hasText = blocks.some((b) => b.type === "text");
const hasText = blocks.some(b => b.type === "text");
if (!hasText) {
blocks.unshift({
type: "text" as const,
@@ -218,7 +217,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
}
} else if (event.type === "content_block_delta") {
if (event.delta.type === "text_delta") {
const index = blocks.findIndex((b) => b.index === event.index);
const index = blocks.findIndex(b => b.index === event.index);
const block = blocks[index];
if (block && block.type === "text") {
block.text += event.delta.text;
@@ -230,7 +229,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
});
}
} else if (event.delta.type === "thinking_delta") {
const index = blocks.findIndex((b) => b.index === event.index);
const index = blocks.findIndex(b => b.index === event.index);
const block = blocks[index];
if (block && block.type === "thinking") {
block.thinking += event.delta.thinking;
@@ -242,7 +241,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
});
}
} else if (event.delta.type === "input_json_delta") {
const index = blocks.findIndex((b) => b.index === event.index);
const index = blocks.findIndex(b => b.index === event.index);
const block = blocks[index];
if (block && block.type === "toolCall") {
block.partialJson += event.delta.partial_json;
@@ -255,7 +254,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
});
}
} else if (event.delta.type === "signature_delta") {
const index = blocks.findIndex((b) => b.index === event.index);
const index = blocks.findIndex(b => b.index === event.index);
const block = blocks[index];
if (block && block.type === "thinking") {
block.thinkingSignature = block.thinkingSignature || "";
@@ -263,7 +262,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
}
}
} else if (event.type === "content_block_stop") {
const index = blocks.findIndex((b) => b.index === event.index);
const index = blocks.findIndex(b => b.index === event.index);
const block = blocks[index];
if (block) {
delete (block as any).index;
@@ -360,7 +359,7 @@ function isAnthropicBaseUrl(baseUrl?: string): boolean {
export function normalizeExtraBetas(betas?: string[] | string): string[] {
if (!betas) return [];
const raw = Array.isArray(betas) ? betas : betas.split(",");
return raw.map((beta) => beta.trim()).filter((beta) => beta.length > 0);
return raw.map(beta => beta.trim()).filter(beta => beta.length > 0);
}
// Build deduplicated beta header string
@@ -406,7 +405,7 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
"X-App",
"Authorization",
"X-Api-Key",
].map((key) => key.toLowerCase()),
].map(key => key.toLowerCase()),
);
const modelHeaders = Object.fromEntries(
Object.entries(options.modelHeaders ?? {}).filter(([key]) => !enforcedHeaderKeys.has(key.toLowerCase())),
@@ -687,7 +686,7 @@ function applyPromptCaching(params: MessageCreateParamsStreaming): void {
// 3. Cache penultimate user message for conversation history caching
const userIndexes = params.messages
.map((message, index) => (message.role === "user" ? index : -1))
.filter((index) => index >= 0);
.filter(index => index >= 0);
if (userIndexes.length >= 2) {
const penultimateUserIndex = userIndexes[userIndexes.length - 2];
@@ -748,7 +747,7 @@ function convertMessages(
});
}
} else if (Array.isArray(msg.content)) {
const blocks: Array<ContentBlockParam & CacheControlBlock> = msg.content.map((item) => {
const blocks: Array<ContentBlockParam & CacheControlBlock> = msg.content.map(item => {
if (item.type === "text") {
return {
type: "text",
@@ -764,8 +763,8 @@ function convertMessages(
},
};
});
let filteredBlocks = !model?.input.includes("image") ? blocks.filter((b) => b.type !== "image") : blocks;
filteredBlocks = filteredBlocks.filter((b) => {
let filteredBlocks = !model?.input.includes("image") ? blocks.filter(b => b.type !== "image") : blocks;
filteredBlocks = filteredBlocks.filter(b => {
if (b.type === "text") {
return b.text.trim().length > 0;
}
@@ -786,7 +785,7 @@ function convertMessages(
// block with a missing/invalid signature (e.g., from aborted stream), we must skip
// the entire message to avoid API rejection. Checking the first non-empty block.
const firstContentBlock = msg.content.find(
(b) =>
b =>
(b.type === "text" && b.text.trim().length > 0) ||
(b.type === "thinking" && b.thinking.trim().length > 0) ||
b.type === "toolCall",
@@ -889,7 +888,7 @@ function convertMessages(
}
// Final validation: filter out any messages with invalid content
return params.filter((msg) => {
return params.filter(msg => {
if (!msg.content) return false;
if (typeof msg.content === "string") return msg.content.length > 0;
if (Array.isArray(msg.content)) return msg.content.length > 0;
@@ -900,7 +899,7 @@ function convertMessages(
function convertTools(tools: Tool[], isOAuthToken: boolean): Anthropic.Messages.Tool[] {
if (!tools) return [];
return tools.map((tool) => {
return tools.map(tool => {
const jsonSchema = tool.parameters as any; // TypeBox already generates JSON Schema
return {
+49 -49
View File
@@ -245,7 +245,7 @@ function decodeLogData(value: unknown): unknown {
return value;
}
if (Array.isArray(value)) {
return value.map((entry) => decodeLogData(entry));
return value.map(entry => decodeLogData(entry));
}
const record = value as Record<string, unknown>;
const typeName = record.$typeName;
@@ -375,13 +375,13 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
get firstTokenTime() {
return firstTokenTime;
},
setTextBlock: (b) => {
setTextBlock: b => {
currentTextBlock = b;
},
setThinkingBlock: (b) => {
setThinkingBlock: b => {
currentThinkingBlock = b;
},
setToolCall: (t) => {
setToolCall: t => {
currentToolCall = t;
},
setFirstTokenTime: () => {
@@ -427,7 +427,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
usageState,
requestContextTools,
onConversationCheckpoint,
).catch((error) => {
).catch(error => {
log("error", "handleServerMessage", { error: String(error) });
});
} catch (e) {
@@ -452,7 +452,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
heartbeatTimer = setInterval(sendHeartbeat, 5000);
await new Promise<void>((resolve, reject) => {
h2Request!.on("trailers", (trailers) => {
h2Request!.on("trailers", trailers => {
const status = trailers["grpc-status"];
const msg = trailers["grpc-message"];
if (status && status !== "0") {
@@ -662,9 +662,9 @@ async function handleShellStreamArgs(
args as any,
execHandlers?.shell,
onToolResult,
(toolResult) => buildShellResultFromToolResult(args as any, toolResult),
(reason) => buildShellRejectedResult((args as any).command, (args as any).workingDirectory, reason),
(error) => buildShellFailureResult((args as any).command, (args as any).workingDirectory, error),
toolResult => buildShellResultFromToolResult(args as any, toolResult),
reason => buildShellRejectedResult((args as any).command, (args as any).workingDirectory, reason),
error => buildShellFailureResult((args as any).command, (args as any).workingDirectory, error),
);
sendShellStreamEvent(h2Request, execMsg, { case: "start", value: create(ShellStreamStartSchema, {}) });
@@ -810,9 +810,9 @@ async function handleExecServerMessage(
args,
execHandlers?.read,
onToolResult,
(toolResult) => buildReadResultFromToolResult(args.path, toolResult),
(reason) => buildReadRejectedResult(args.path, reason),
(error) => buildReadErrorResult(args.path, error),
toolResult => buildReadResultFromToolResult(args.path, toolResult),
reason => buildReadRejectedResult(args.path, reason),
error => buildReadErrorResult(args.path, error),
);
sendExecClientMessage(h2Request, execMsg, "readResult", execResult);
return;
@@ -823,9 +823,9 @@ async function handleExecServerMessage(
args,
execHandlers?.ls,
onToolResult,
(toolResult) => buildLsResultFromToolResult(args.path, toolResult),
(reason) => buildLsRejectedResult(args.path, reason),
(error) => buildLsErrorResult(args.path, error),
toolResult => buildLsResultFromToolResult(args.path, toolResult),
reason => buildLsRejectedResult(args.path, reason),
error => buildLsErrorResult(args.path, error),
);
sendExecClientMessage(h2Request, execMsg, "lsResult", execResult);
return;
@@ -836,9 +836,9 @@ async function handleExecServerMessage(
args,
execHandlers?.grep,
onToolResult,
(toolResult) => buildGrepResultFromToolResult(args, toolResult),
(reason) => buildGrepErrorResult(reason),
(error) => buildGrepErrorResult(error),
toolResult => buildGrepResultFromToolResult(args, toolResult),
reason => buildGrepErrorResult(reason),
error => buildGrepErrorResult(error),
);
sendExecClientMessage(h2Request, execMsg, "grepResult", execResult);
return;
@@ -849,7 +849,7 @@ async function handleExecServerMessage(
args,
execHandlers?.write,
onToolResult,
(toolResult) =>
toolResult =>
buildWriteResultFromToolResult(
{
path: args.path,
@@ -859,8 +859,8 @@ async function handleExecServerMessage(
},
toolResult,
),
(reason) => buildWriteRejectedResult(args.path, reason),
(error) => buildWriteErrorResult(args.path, error),
reason => buildWriteRejectedResult(args.path, reason),
error => buildWriteErrorResult(args.path, error),
);
sendExecClientMessage(h2Request, execMsg, "writeResult", execResult);
return;
@@ -871,9 +871,9 @@ async function handleExecServerMessage(
args,
execHandlers?.delete,
onToolResult,
(toolResult) => buildDeleteResultFromToolResult(args.path, toolResult),
(reason) => buildDeleteRejectedResult(args.path, reason),
(error) => buildDeleteErrorResult(args.path, error),
toolResult => buildDeleteResultFromToolResult(args.path, toolResult),
reason => buildDeleteRejectedResult(args.path, reason),
error => buildDeleteErrorResult(args.path, error),
);
sendExecClientMessage(h2Request, execMsg, "deleteResult", execResult);
return;
@@ -884,9 +884,9 @@ async function handleExecServerMessage(
args,
execHandlers?.shell,
onToolResult,
(toolResult) => buildShellResultFromToolResult(args, toolResult),
(reason) => buildShellRejectedResult(args.command, args.workingDirectory, reason),
(error) => buildShellFailureResult(args.command, args.workingDirectory, error),
toolResult => buildShellResultFromToolResult(args, toolResult),
reason => buildShellRejectedResult(args.command, args.workingDirectory, reason),
error => buildShellFailureResult(args.command, args.workingDirectory, error),
);
sendExecClientMessage(h2Request, execMsg, "shellResult", execResult);
return;
@@ -944,9 +944,9 @@ async function handleExecServerMessage(
args,
execHandlers?.diagnostics,
onToolResult,
(toolResult) => buildDiagnosticsResultFromToolResult(args.path, toolResult),
(reason) => buildDiagnosticsRejectedResult(args.path, reason),
(error) => buildDiagnosticsErrorResult(args.path, error),
toolResult => buildDiagnosticsResultFromToolResult(args.path, toolResult),
reason => buildDiagnosticsRejectedResult(args.path, reason),
error => buildDiagnosticsErrorResult(args.path, error),
);
sendExecClientMessage(h2Request, execMsg, "diagnosticsResult", execResult);
return;
@@ -958,9 +958,9 @@ async function handleExecServerMessage(
mcpCall,
execHandlers?.mcp,
onToolResult,
(toolResult) => buildMcpResultFromToolResult(mcpCall, toolResult),
(_reason) => buildMcpToolNotFoundResult(mcpCall),
(error) => buildMcpErrorResult(error),
toolResult => buildMcpResultFromToolResult(mcpCall, toolResult),
_reason => buildMcpToolNotFoundResult(mcpCall),
error => buildMcpErrorResult(error),
);
sendExecClientMessage(h2Request, execMsg, "mcpResult", execResult);
return;
@@ -1075,7 +1075,7 @@ async function applyToolResultHandler(
}
function toolResultToText(toolResult: ToolResultMessage): string {
return toolResult.content.map((item) => (item.type === "text" ? item.text : `[${item.mimeType} image]`)).join("\n");
return toolResult.content.map(item => (item.type === "text" ? item.text : `[${item.mimeType} image]`)).join("\n");
}
function toolResultWasTruncated(toolResult: ToolResultMessage): boolean {
@@ -1274,8 +1274,8 @@ function buildLsResultFromToolResult(path: string, toolResult: ToolResultMessage
const rootPath = path || ".";
const entries = text
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0 && !line.startsWith("["));
.map(line => line.trim())
.filter(line => line.length > 0 && !line.startsWith("["));
const childrenDirs: LsDirectoryTreeNode[] = [];
const childrenFiles: LsDirectoryTreeNode_File[] = [];
@@ -1346,8 +1346,8 @@ function buildGrepResultFromToolResult(
const clientTruncated = toolResultDetailBoolean(toolResult, "truncated");
const lines = text
.split("\n")
.map((line) => line.trimEnd())
.filter((line) => line.length > 0 && !line.startsWith("[") && !line.toLowerCase().startsWith("no matches"));
.map(line => line.trimEnd())
.filter(line => line.length > 0 && !line.startsWith("[") && !line.toLowerCase().startsWith("no matches"));
const workspaceKey = args.path || ".";
let unionResult: GrepUnionResult;
@@ -1367,7 +1367,7 @@ function buildGrepResultFromToolResult(
});
} else if (outputMode === "count") {
const counts = lines
.map((line) => {
.map(line => {
const separatorIndex = line.lastIndexOf(":");
if (separatorIndex === -1) {
return null;
@@ -1417,7 +1417,7 @@ function buildGrepResultFromToolResult(
const matches = Array.from(matchMap.entries()).map(([file, matches]) =>
create(GrepFileMatchSchema, {
file,
matches: matches.map((entry) =>
matches: matches.map(entry =>
create(GrepContentMatchSchema, {
lineNumber: entry.line,
content: entry.content,
@@ -1586,7 +1586,7 @@ function buildTodoWriteArgs(toolCall: CursorUpdateTodosToolCall): {
const todos = toolCall.updateTodosToolCall?.args?.todos;
if (!todos) return null;
return {
todos: todos.map((todo) => ({
todos: todos.map(todo => ({
id: typeof todo.id === "string" && todo.id.length > 0 ? todo.id : undefined,
content: typeof todo.content === "string" ? todo.content : "",
activeForm: typeof todo.content === "string" ? todo.content : "",
@@ -1599,7 +1599,7 @@ function buildMcpResultFromToolResult(_mcpCall: CursorMcpCall, toolResult: ToolR
if (toolResult.isError) {
return buildMcpErrorResult(toolResultToText(toolResult) || "MCP tool failed");
}
const content = toolResult.content.map((item) => {
const content = toolResult.content.map(item => {
if (item.type === "image") {
return create(McpToolResultContentItemSchema, {
content: {
@@ -1810,12 +1810,12 @@ function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[]
return [];
}
const advertisedTools = tools.filter((tool) => !CURSOR_NATIVE_TOOL_NAMES.has(tool.name));
const advertisedTools = tools.filter(tool => !CURSOR_NATIVE_TOOL_NAMES.has(tool.name));
if (advertisedTools.length === 0) {
return [];
}
return advertisedTools.map((tool) => {
return advertisedTools.map(tool => {
const jsonSchema = tool.parameters as Record<string, unknown> | undefined;
const schemaValue: JsonValue =
jsonSchema && typeof jsonSchema === "object"
@@ -1841,7 +1841,7 @@ function extractUserMessageText(msg: Message): string {
if (typeof content === "string") return content.trim();
const text = content
.filter((c): c is TextContent => c.type === "text")
.map((c) => c.text)
.map(c => c.text)
.join("\n");
return text.trim();
}
@@ -1854,7 +1854,7 @@ function extractAssistantMessageText(msg: Message): string {
if (!Array.isArray(msg.content)) return "";
return msg.content
.filter((c): c is TextContent => c.type === "text")
.map((c) => c.text)
.map(c => c.text)
.join("\n");
}
@@ -2007,7 +2007,7 @@ function buildGrpcRequest(
// Build conversation turns from prior messages (excluding the last user message)
const turns = buildConversationTurns(context.messages);
const hasMatchingPrompt = state.conversationState?.rootPromptMessagesJson?.some((entry) =>
const hasMatchingPrompt = state.conversationState?.rootPromptMessagesJson?.some(entry =>
Buffer.from(entry).equals(systemPromptId),
);
@@ -2064,7 +2064,7 @@ function buildGrpcRequest(
const requestBytes = toBinary(AgentClientMessageSchema, clientMessage);
const toolNames = context.tools?.map((tool) => tool.name) ?? [];
const toolNames = context.tools?.map(tool => tool.name) ?? [];
const detail =
process.env.DEBUG_CURSOR === "2"
? ` ${JSON.stringify(clientMessage.message.value, debugReplacer, 2)?.slice(0, 2000)}`
@@ -2082,6 +2082,6 @@ function buildGrpcRequest(
function extractText(content: (TextContent | ImageContent)[]): string {
return content
.filter((c): c is TextContent => c.type === "text")
.map((c) => c.text)
.map(c => c.text)
.join("\n");
}
@@ -3,7 +3,6 @@
* Shared implementation for both google-gemini-cli and google-antigravity providers.
* Uses the Cloud Code Assist API endpoint to access Gemini and Claude models.
*/
import { createHash } from "node:crypto";
import type { Content, ThinkingConfig } from "@google/genai";
import { abortableSleep } from "@oh-my-pi/pi-utils";
@@ -660,8 +659,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
const providedId = part.functionCall.id;
const needsNewId =
!providedId ||
output.content.some((b) => b.type === "toolCall" && b.id === providedId);
!providedId || output.content.some(b => b.type === "toolCall" && b.id === providedId);
const toolCallId = needsNewId
? `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}`
: providedId;
@@ -695,7 +693,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
if (candidate?.finishReason) {
output.stopReason = mapStopReasonString(candidate.finishReason);
if (output.content.some((b) => b.type === "toolCall")) {
if (output.content.some(b => b.type === "toolCall")) {
output.stopReason = "toolUse";
}
}
@@ -840,7 +838,7 @@ function deriveSessionId(context: Context): string | undefined {
} else if (Array.isArray(message.content)) {
text = message.content
.filter((item): item is TextContent => item.type === "text")
.map((item) => item.text)
.map(item => item.text)
.join("\n");
}
+10 -11
View File
@@ -1,7 +1,6 @@
/**
* Shared utilities for Google Generative AI and Google Cloud Code Assist providers.
*/
import { type Content, FinishReason, FunctionCallingConfigMode, type Part, type Schema } from "@google/genai";
import type { Context, ImageContent, Model, StopReason, TextContent, Tool } from "../types";
import { sanitizeSurrogates } from "../utils/sanitize-unicode";
@@ -86,7 +85,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
parts: [{ text: sanitizeSurrogates(msg.content) }],
});
} else {
const parts: Part[] = msg.content.map((item) => {
const parts: Part[] = msg.content.map(item => {
if (item.type === "text") {
return { text: sanitizeSurrogates(item.text) };
} else {
@@ -99,8 +98,8 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
}
});
// Filter out images if model doesn't support them, and empty text blocks
let filteredParts = !model.input.includes("image") ? parts.filter((p) => p.text !== undefined) : parts;
filteredParts = filteredParts.filter((p) => {
let filteredParts = !model.input.includes("image") ? parts.filter(p => p.text !== undefined) : parts;
filteredParts = filteredParts.filter(p => {
if (p.text !== undefined) {
return p.text.trim().length > 0;
}
@@ -180,7 +179,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
} else if (msg.role === "toolResult") {
// Extract text and image content
const textContent = msg.content.filter((c): c is TextContent => c.type === "text");
const textResult = textContent.map((c) => c.text).join("\n");
const textResult = textContent.map(c => c.text).join("\n");
const imageContent = model.input.includes("image")
? msg.content.filter((c): c is ImageContent => c.type === "image")
: [];
@@ -196,7 +195,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
// Use "output" key for success, "error" key for errors as per SDK documentation
const responseValue = hasText ? sanitizeSurrogates(textResult) : hasImages ? "(see attached image)" : "";
const imageParts: Part[] = imageContent.map((imageBlock) => ({
const imageParts: Part[] = imageContent.map(imageBlock => ({
inlineData: {
mimeType: imageBlock.mimeType,
data: imageBlock.data,
@@ -221,7 +220,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
// Cloud Code Assist API requires all function responses to be in a single user turn.
// Check if the last content is already a user turn with function responses and merge.
const lastContent = contents[contents.length - 1];
if (lastContent?.role === "user" && lastContent.parts?.some((p) => p.functionResponse)) {
if (lastContent?.role === "user" && lastContent.parts?.some(p => p.functionResponse)) {
lastContent.parts.push(functionResponsePart);
} else {
contents.push({
@@ -270,7 +269,7 @@ const UNSUPPORTED_SCHEMA_FIELDS = new Set([
function sanitizeSchemaImpl(value: unknown, isInsideProperties: boolean): unknown {
if (Array.isArray(value)) {
return value.map((entry) => sanitizeSchemaImpl(entry, isInsideProperties));
return value.map(entry => sanitizeSchemaImpl(entry, isInsideProperties));
}
if (!value || typeof value !== "object") {
@@ -286,11 +285,11 @@ function sanitizeSchemaImpl(value: unknown, isInsideProperties: boolean): unknow
const variants = obj[combiner] as Record<string, unknown>[];
// Check if ALL variants have a const field
const allHaveConst = variants.every((v) => v && typeof v === "object" && "const" in v);
const allHaveConst = variants.every(v => v && typeof v === "object" && "const" in v);
if (allHaveConst && variants.length > 0) {
// Extract all const values into enum
result.enum = variants.map((v) => v.const);
result.enum = variants.map(v => v.const);
// Inherit type from first variant if present
const firstType = variants[0]?.type;
@@ -327,7 +326,7 @@ function sanitizeSchemaImpl(value: unknown, isInsideProperties: boolean): unknow
if (constValue !== undefined) {
// Convert const to enum, merging with existing enum if present
const existingEnum = Array.isArray(result.enum) ? result.enum : [];
if (!existingEnum.some((item) => Object.is(item, constValue))) {
if (!existingEnum.some(item => Object.is(item, constValue))) {
existingEnum.push(constValue);
}
result.enum = existingEnum;
+2 -2
View File
@@ -183,7 +183,7 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
const providedId = part.functionCall.id;
const needsNewId =
!providedId || output.content.some((b) => b.type === "toolCall" && b.id === providedId);
!providedId || output.content.some(b => b.type === "toolCall" && b.id === providedId);
const toolCallId = needsNewId
? `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}`
: providedId;
@@ -211,7 +211,7 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
if (candidate?.finishReason) {
output.stopReason = mapStopReason(candidate.finishReason);
if (output.content.some((b) => b.type === "toolCall")) {
if (output.content.some(b => b.type === "toolCall")) {
output.stopReason = "toolUse";
}
}
+2 -2
View File
@@ -170,7 +170,7 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
// Generate unique ID if not provided or if it's a duplicate
const providedId = part.functionCall.id;
const needsNewId =
!providedId || output.content.some((b) => b.type === "toolCall" && b.id === providedId);
!providedId || output.content.some(b => b.type === "toolCall" && b.id === providedId);
const toolCallId = needsNewId
? `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}`
: providedId;
@@ -198,7 +198,7 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
if (candidate?.finishReason) {
output.stopReason = mapStopReason(candidate.finishReason);
if (output.content.some((b) => b.type === "toolCall")) {
if (output.content.some(b => b.type === "toolCall")) {
output.stopReason = "toolUse";
}
}
@@ -341,7 +341,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
} else if (eventType === "response.output_item.done") {
const item = rawEvent.item as ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall;
if (item.type === "reasoning" && currentBlock?.type === "thinking") {
currentBlock.thinking = item.summary?.map((s) => s.text).join("\n\n") || "";
currentBlock.thinking = item.summary?.map(s => s.text).join("\n\n") || "";
currentBlock.thinkingSignature = JSON.stringify(item);
stream.push({
type: "thinking_end",
@@ -351,7 +351,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
});
currentBlock = null;
} else if (item.type === "message" && currentBlock?.type === "text") {
currentBlock.text = item.content.map((c) => (c.type === "output_text" ? c.text : c.refusal)).join("");
currentBlock.text = item.content.map(c => (c.type === "output_text" ? c.text : c.refusal)).join("");
currentBlock.textSignature = item.id;
stream.push({
type: "text_end",
@@ -396,7 +396,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
}
calculateCost(model, output.usage);
output.stopReason = mapStopReason(response?.status);
if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") {
if (output.content.some(b => b.type === "toolCall") && output.stopReason === "stop") {
output.stopReason = "toolUse";
}
} else if (eventType === "error") {
@@ -593,9 +593,9 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
});
// Filter out images if model doesn't support them, and empty text blocks
let filteredContent = !model.input.includes("image")
? content.filter((c) => c.type !== "input_image")
? content.filter(c => c.type !== "input_image")
: content;
filteredContent = filteredContent.filter((c) => {
filteredContent = filteredContent.filter(c => {
if (c.type === "input_text") {
return c.text.trim().length > 0;
}
@@ -647,10 +647,10 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
messages.push(...output);
} else if (msg.role === "toolResult") {
const textResult = msg.content
.filter((c) => c.type === "text")
.map((c) => (c as { text: string }).text)
.filter(c => c.type === "text")
.map(c => (c as { text: string }).text)
.join("\n");
const hasImages = msg.content.some((c) => c.type === "image");
const hasImages = msg.content.some(c => c.type === "image");
const normalized = normalizeResponsesToolCallId(msg.toolCallId);
const hasText = textResult.length > 0;
@@ -692,7 +692,7 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
function convertTools(
tools: Tool[],
): Array<{ type: "function"; name: string; description: string; parameters: Record<string, unknown>; strict: null }> {
return tools.map((tool) => ({
return tools.map(tool => ({
type: "function",
name: tool.name,
description: tool.description,
@@ -73,8 +73,8 @@ function filterInput(input: InputItem[] | undefined): InputItem[] | undefined {
if (!Array.isArray(input)) return input;
return input
.filter((item) => item.type !== "item_reference")
.map((item) => {
.filter(item => item.type !== "item_reference")
.map(item => {
if (item.id != null) {
const { id: _id, ...rest } = item;
return rest as InputItem;
@@ -97,11 +97,11 @@ export async function transformRequestBody(
if (body.input) {
const functionCallIds = new Set(
body.input
.filter((item) => item.type === "function_call" && typeof item.call_id === "string")
.map((item) => item.call_id as string),
.filter(item => item.type === "function_call" && typeof item.call_id === "string")
.map(item => item.call_id as string),
);
body.input = body.input.map((item) => {
body.input = body.input.map(item => {
if (item.type === "function_call_output" && typeof item.call_id === "string") {
const callId = item.call_id as string;
if (!functionCallIds.has(callId)) {
@@ -131,7 +131,7 @@ export async function transformRequestBody(
if (prompt?.developerMessages && prompt.developerMessages.length > 0 && Array.isArray(body.input)) {
const developerMessages = prompt.developerMessages.map(
(text) =>
text =>
({
type: "message",
role: "developer",
+22 -22
View File
@@ -61,7 +61,7 @@ function hasToolHistory(messages: Message[]): boolean {
return true;
}
if (msg.role === "assistant") {
if (msg.content.some((block) => block.type === "toolCall")) {
if (msg.content.some(block => block.type === "toolCall")) {
return true;
}
}
@@ -287,7 +287,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
for (const detail of reasoningDetails) {
if (detail.type === "reasoning.encrypted" && detail.id && detail.data) {
const matchingToolCall = output.content.find(
(b) => b.type === "toolCall" && b.id === detail.id,
b => b.type === "toolCall" && b.id === detail.id,
) as ToolCall | undefined;
if (matchingToolCall) {
matchingToolCall.thoughtSignature = JSON.stringify(detail);
@@ -356,12 +356,12 @@ function createClient(
headers["Openai-Intent"] = "conversation-edits";
// Copilot requires this header when sending images
const hasImages = messages.some((msg) => {
const hasImages = messages.some(msg => {
if (msg.role === "user" && Array.isArray(msg.content)) {
return msg.content.some((c) => c.type === "image");
return msg.content.some(c => c.type === "image");
}
if (msg.role === "toolResult" && Array.isArray(msg.content)) {
return msg.content.some((c) => c.type === "image");
return msg.content.some(c => c.type === "image");
}
return false;
});
@@ -516,7 +516,7 @@ export function convertMessages(
}
});
const filteredContent = !model.input.includes("image")
? content.filter((c) => c.type !== "image_url")
? content.filter(c => c.type !== "image_url")
: content;
if (filteredContent.length === 0) continue;
params.push({
@@ -531,29 +531,29 @@ export function convertMessages(
content: compat.requiresAssistantAfterToolResult ? "" : null,
};
const textBlocks = msg.content.filter((b) => b.type === "text") as TextContent[];
const textBlocks = msg.content.filter(b => b.type === "text") as TextContent[];
// Filter out empty text blocks to avoid API validation errors
const nonEmptyTextBlocks = textBlocks.filter((b) => b.text && b.text.trim().length > 0);
const nonEmptyTextBlocks = textBlocks.filter(b => b.text && b.text.trim().length > 0);
if (nonEmptyTextBlocks.length > 0) {
// GitHub Copilot requires assistant content as a string, not an array.
// Sending as array causes Claude models to re-answer all previous prompts.
if (model.provider === "github-copilot") {
assistantMsg.content = nonEmptyTextBlocks.map((b) => sanitizeSurrogates(b.text)).join("");
assistantMsg.content = nonEmptyTextBlocks.map(b => sanitizeSurrogates(b.text)).join("");
} else {
assistantMsg.content = nonEmptyTextBlocks.map((b) => {
assistantMsg.content = nonEmptyTextBlocks.map(b => {
return { type: "text", text: sanitizeSurrogates(b.text) };
});
}
}
// Handle thinking blocks
const thinkingBlocks = msg.content.filter((b) => b.type === "thinking") as ThinkingContent[];
const thinkingBlocks = msg.content.filter(b => b.type === "thinking") as ThinkingContent[];
// Filter out empty thinking blocks to avoid API validation errors
const nonEmptyThinkingBlocks = thinkingBlocks.filter((b) => b.thinking && b.thinking.trim().length > 0);
const nonEmptyThinkingBlocks = thinkingBlocks.filter(b => b.thinking && b.thinking.trim().length > 0);
if (nonEmptyThinkingBlocks.length > 0) {
if (compat.requiresThinkingAsText) {
// Convert thinking blocks to plain text (no tags to avoid model mimicking them)
const thinkingText = nonEmptyThinkingBlocks.map((b) => b.thinking).join("\n\n");
const thinkingText = nonEmptyThinkingBlocks.map(b => b.thinking).join("\n\n");
const textContent = assistantMsg.content as Array<{ type: "text"; text: string }> | null;
if (textContent) {
textContent.unshift({ type: "text", text: thinkingText });
@@ -564,14 +564,14 @@ export function convertMessages(
// Use the signature from the first thinking block if available (for llama.cpp server + gpt-oss)
const signature = nonEmptyThinkingBlocks[0].thinkingSignature;
if (signature && signature.length > 0) {
(assistantMsg as any)[signature] = nonEmptyThinkingBlocks.map((b) => b.thinking).join("\n");
(assistantMsg as any)[signature] = nonEmptyThinkingBlocks.map(b => b.thinking).join("\n");
}
}
}
const toolCalls = msg.content.filter((b) => b.type === "toolCall") as ToolCall[];
const toolCalls = msg.content.filter(b => b.type === "toolCall") as ToolCall[];
if (toolCalls.length > 0) {
assistantMsg.tool_calls = toolCalls.map((tc) => ({
assistantMsg.tool_calls = toolCalls.map(tc => ({
id: normalizeMistralToolId(tc.id, compat.requiresMistralToolIds),
type: "function" as const,
function: {
@@ -580,8 +580,8 @@ export function convertMessages(
},
}));
const reasoningDetails = toolCalls
.filter((tc) => tc.thoughtSignature)
.map((tc) => {
.filter(tc => tc.thoughtSignature)
.map(tc => {
try {
return JSON.parse(tc.thoughtSignature!);
} catch {
@@ -616,10 +616,10 @@ export function convertMessages(
// Extract text and image content
const textResult = toolMsg.content
.filter((c) => c.type === "text")
.map((c) => (c as any).text)
.filter(c => c.type === "text")
.map(c => (c as any).text)
.join("\n");
const hasImages = toolMsg.content.some((c) => c.type === "image");
const hasImages = toolMsg.content.some(c => c.type === "image");
// Always send tool result with text (or placeholder if only images)
const hasText = textResult.length > 0;
@@ -683,7 +683,7 @@ export function convertMessages(
}
function convertTools(tools: Tool[]): OpenAI.Chat.Completions.ChatCompletionTool[] {
return tools.map((tool) => ({
return tools.map(tool => ({
type: "function",
function: {
name: tool.name,
+12 -12
View File
@@ -245,7 +245,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
const item = event.item;
if (item.type === "reasoning" && currentBlock && currentBlock.type === "thinking") {
currentBlock.thinking = item.summary?.map((s) => s.text).join("\n\n") || "";
currentBlock.thinking = item.summary?.map(s => s.text).join("\n\n") || "";
currentBlock.thinkingSignature = JSON.stringify(item);
stream.push({
type: "thinking_end",
@@ -255,7 +255,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
});
currentBlock = null;
} else if (item.type === "message" && currentBlock && currentBlock.type === "text") {
currentBlock.text = item.content.map((c) => (c.type === "output_text" ? c.text : c.refusal)).join("");
currentBlock.text = item.content.map(c => (c.type === "output_text" ? c.text : c.refusal)).join("");
currentBlock.textSignature = item.id;
stream.push({
type: "text_end",
@@ -293,7 +293,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
calculateCost(model, output.usage);
// Map status to stop reason
output.stopReason = mapStopReason(response?.status);
if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") {
if (output.content.some(b => b.type === "toolCall") && output.stopReason === "stop") {
output.stopReason = "toolUse";
}
}
@@ -358,12 +358,12 @@ function createClient(
headers["Openai-Intent"] = "conversation-edits";
// Copilot requires this header when sending images
const hasImages = messages.some((msg) => {
const hasImages = messages.some(msg => {
if (msg.role === "user" && Array.isArray(msg.content)) {
return msg.content.some((c) => c.type === "image");
return msg.content.some(c => c.type === "image");
}
if (msg.role === "toolResult" && Array.isArray(msg.content)) {
return msg.content.some((c) => c.type === "image");
return msg.content.some(c => c.type === "image");
}
return false;
});
@@ -491,9 +491,9 @@ function convertMessages(
});
// Filter out images if model doesn't support them, and empty text blocks
let filteredContent = !model.input.includes("image")
? content.filter((c) => c.type !== "input_image")
? content.filter(c => c.type !== "input_image")
: content;
filteredContent = filteredContent.filter((c) => {
filteredContent = filteredContent.filter(c => {
if (c.type === "input_text") {
return c.text.trim().length > 0;
}
@@ -567,10 +567,10 @@ function convertMessages(
} else if (msg.role === "toolResult") {
// Extract text and image content
const textResult = msg.content
.filter((c) => c.type === "text")
.map((c) => (c as any).text)
.filter(c => c.type === "text")
.map(c => (c as any).text)
.join("\n");
const hasImages = msg.content.some((c) => c.type === "image");
const hasImages = msg.content.some(c => c.type === "image");
const normalized = normalizeResponsesToolCallId(msg.toolCallId);
if (strictResponsesPairing && !knownCallIds.has(normalized.callId)) {
continue;
@@ -618,7 +618,7 @@ function convertMessages(
}
function convertTools(tools: Tool[]): OpenAITool[] {
return tools.map((tool) => ({
return tools.map(tool => ({
type: "function",
name: tool.name,
description: tool.description,
@@ -98,7 +98,7 @@ export function transformMessages<TApi extends Api>(messages: Message[], model:
const needsToolCallIdNormalization = targetRequiresStrictIds && (crossProviderSwitch || copilotCrossApiSwitch);
// Transform message from different provider/model
const transformedContent = assistantMsg.content.flatMap((block) => {
const transformedContent = assistantMsg.content.flatMap(block => {
if (block.type === "thinking") {
// Skip empty thinking blocks, convert others to plain text
if (!block.thinking || block.thinking.trim() === "") return [];
@@ -173,7 +173,7 @@ export function transformMessages<TApi extends Api>(messages: Message[], model:
const assistantMsg = msg as AssistantMessage;
const isErroredAssistant = assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted";
const toolCalls = assistantMsg.content.filter((b) => b.type === "toolCall") as ToolCall[];
const toolCalls = assistantMsg.content.filter(b => b.type === "toolCall") as ToolCall[];
result.push(msg);
+1 -1
View File
@@ -105,7 +105,7 @@ export class CliAuthStorage {
const dir = path.dirname(dbPath);
const dirExists = await fs
.stat(dir)
.then((s) => s.isDirectory())
.then(s => s.isDirectory())
.catch(() => false);
if (!dirExists) {
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
+1 -2
View File
@@ -1,3 +1,4 @@
import type { TSchema } from "@sinclair/typebox";
import type { BedrockOptions } from "./providers/amazon-bedrock";
import type { AnthropicOptions } from "./providers/anthropic";
import type { CursorOptions } from "./providers/cursor";
@@ -237,8 +238,6 @@ export interface CursorExecHandlers {
onToolResult?: CursorToolResultHandler;
}
import type { TSchema } from "@sinclair/typebox";
export interface Tool<TParameters extends TSchema = TSchema> {
name: string;
description: string;
-1
View File
@@ -4,7 +4,6 @@
* Provides a normalized schema to represent multiple limit windows, model tiers,
* and shared quotas across providers.
*/
import type { Provider } from "./types";
export type UsageUnit = "percent" | "tokens" | "requests" | "usd" | "minutes" | "bytes" | "unknown";
+3 -3
View File
@@ -241,10 +241,10 @@ function buildCacheKey(params: UsageFetchParams): string {
function resolveCacheExpiry(now: number, limits: UsageLimit[]): number {
const earliestReset = limits
.map((limit) => limit.window?.resetsAt)
.map(limit => limit.window?.resetsAt)
.filter((value): value is number => typeof value === "number" && Number.isFinite(value))
.reduce((min, value) => (min === undefined ? value : Math.min(min, value)), undefined as number | undefined);
const exhausted = limits.some((limit) => limit.status === "exhausted");
const exhausted = limits.some(limit => limit.status === "exhausted");
if (earliestReset === undefined) return now + DEFAULT_CACHE_TTL_MS;
if (exhausted) return earliestReset;
return Math.min(now + DEFAULT_CACHE_TTL_MS, earliestReset);
@@ -351,5 +351,5 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
export const claudeUsageProvider: UsageProvider = {
id: "anthropic",
fetchUsage: fetchClaudeUsage,
supports: (params) => params.provider === "anthropic" && params.credential.type === "oauth",
supports: params => params.provider === "anthropic" && params.credential.type === "oauth",
};
+2 -3
View File
@@ -3,7 +3,6 @@
*
* Normalizes Copilot quota usage into the shared UsageReport schema.
*/
import type {
UsageAmount,
UsageCacheEntry,
@@ -316,7 +315,7 @@ function normalizeBillingUsage(data: BillingUsageResponse): UsageLimit[] {
};
const premiumItems = data.usageItems.filter(
(item) => item.sku === "Copilot Premium Request" || item.sku.includes("Premium"),
item => item.sku === "Copilot Premium Request" || item.sku.includes("Premium"),
);
const totalUsed = premiumItems.reduce((sum, item) => sum + item.grossQuantity, 0);
const totalLimit = premiumItems.reduce((sum, item) => sum + (item.limit ?? 0), 0) || undefined;
@@ -359,7 +358,7 @@ function normalizeBillingUsage(data: BillingUsageResponse): UsageLimit[] {
function resolveCacheTtl(now: number, report: UsageReport | null): UsageCacheEntry["expiresAt"] {
if (!report) return now + DEFAULT_CACHE_TTL_MS;
const resetInMs = report.limits
.map((limit) => limit.window?.resetInMs)
.map(limit => limit.window?.resetInMs)
.find((value): value is number => typeof value === "number" && Number.isFinite(value));
if (!resetInMs || resetInMs <= 0) return now + DEFAULT_CACHE_TTL_MS;
return now + Math.min(MAX_CACHE_TTL_MS, resetInMs);
+1 -1
View File
@@ -214,5 +214,5 @@ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchCo
export const antigravityUsageProvider: UsageProvider = {
id: "google-antigravity",
fetchUsage: fetchAntigravityUsage,
supports: (params) => params.provider === "google-antigravity",
supports: params => params.provider === "google-antigravity",
};
+2 -2
View File
@@ -264,9 +264,9 @@ function buildUsageLimit(args: {
function resolveCacheExpiry(args: { report: UsageReport | null; nowMs: number }): number {
const { report, nowMs } = args;
if (!report) return nowMs + DEFAULT_CACHE_TTL_MS;
const exhausted = report.limits.some((limit) => limit.status === "exhausted");
const exhausted = report.limits.some(limit => limit.status === "exhausted");
const resetCandidates = report.limits
.map((limit) => limit.window?.resetsAt)
.map(limit => limit.window?.resetsAt)
.filter((value): value is number => typeof value === "number" && Number.isFinite(value));
const earliestReset = resetCandidates.length > 0 ? Math.min(...resetCandidates) : undefined;
if (exhausted && earliestReset) return earliestReset;
+2 -2
View File
@@ -133,7 +133,7 @@ function buildCacheKey(params: UsageFetchParams): string {
function resolveCacheExpiry(now: number, limits: UsageLimit[]): number {
const earliestReset = limits
.map((limit) => limit.window?.resetsAt)
.map(limit => limit.window?.resetsAt)
.filter((value): value is number => typeof value === "number" && Number.isFinite(value))
.reduce((min, value) => (min === undefined ? value : Math.min(min, value)), undefined as number | undefined);
if (!earliestReset) return now + DEFAULT_CACHE_TTL_MS;
@@ -288,5 +288,5 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
export const zaiUsageProvider: UsageProvider = {
id: "zai",
fetchUsage: fetchZaiUsage,
supports: (params) => params.provider === "zai" && params.credential.type === "api_key",
supports: params => params.provider === "zai" && params.credential.type === "api_key",
};
+3 -3
View File
@@ -53,7 +53,7 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
} else if (this.done) {
return;
} else {
const result = await new Promise<IteratorResult<T>>((resolve) => this.waiting.push(resolve));
const result = await new Promise<IteratorResult<T>>(resolve => this.waiting.push(resolve));
if (result.done) return;
yield result.value;
}
@@ -68,8 +68,8 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
export class AssistantMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
constructor() {
super(
(event) => event.type === "done" || event.type === "error",
(event) => {
event => event.type === "done" || event.type === "error",
event => {
if (event.type === "done") {
return event.message;
} else if (event.type === "error") {
-1
View File
@@ -1,7 +1,6 @@
/**
* Anthropic OAuth flow (Claude Pro/Max)
*/
import { OAuthCallbackFlow } from "./callback-server";
import { generatePKCE } from "./pkce";
import type { OAuthController, OAuthCredentials } from "./types";
@@ -10,7 +10,6 @@
* - generateAuthUrl(): Build provider-specific authorization URL
* - exchangeToken(): Exchange authorization code for tokens
*/
import templateHtml from "./oauth.html" with { type: "text" };
import type { OAuthController, OAuthCredentials } from "./types";
@@ -63,7 +62,7 @@ export abstract class OAuthCallbackFlow {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
return Array.from(bytes)
.map((value) => value.toString(16).padStart(2, "0"))
.map(value => value.toString(16).padStart(2, "0"))
.join("");
}
@@ -125,7 +124,7 @@ export abstract class OAuthCallbackFlow {
hostname: DEFAULT_HOSTNAME,
port,
reusePort: false,
fetch: (req) => this.handleCallback(req, expectedState),
fetch: req => this.handleCallback(req, expectedState),
});
}
@@ -1,7 +1,6 @@
/**
* GitHub Copilot OAuth flow
*/
import { abortableSleep } from "@oh-my-pi/pi-utils";
import { getModels } from "../../models";
import type { OAuthCredentials } from "./types";
@@ -279,7 +278,7 @@ async function enableAllGitHubCopilotModels(
): Promise<void> {
const models = getModels("github-copilot");
await Promise.all(
models.map(async (model) => {
models.map(async model => {
const success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain);
onProgress?.(model.id, success);
}),
@@ -2,7 +2,6 @@
* Antigravity OAuth flow (Gemini 3, Claude, GPT-OSS via Google Cloud)
* Uses different OAuth credentials than google-gemini-cli for access to additional models.
*/
import { OAuthCallbackFlow } from "./callback-server";
import { generatePKCE } from "./pkce";
import type { OAuthController, OAuthCredentials } from "./types";
@@ -2,7 +2,6 @@
* Gemini CLI OAuth flow (Google Cloud Code Assist)
* Standard Gemini models only (gemini-2.0-flash, gemini-2.5-*)
*/
import { OAuthCallbackFlow } from "./callback-server";
import { generatePKCE } from "./pkce";
import type { OAuthController, OAuthCredentials } from "./types";
@@ -49,7 +48,7 @@ interface GoogleRpcErrorResponse {
function getDefaultTier(allowedTiers?: Array<{ id?: string; isDefault?: boolean }>): { id?: string } {
if (!allowedTiers || allowedTiers.length === 0) return { id: TIER_LEGACY };
const defaultTier = allowedTiers.find((t) => t.isDefault);
const defaultTier = allowedTiers.find(t => t.isDefault);
return defaultTier ?? { id: TIER_LEGACY };
}
@@ -58,7 +57,7 @@ function isVpcScAffectedUser(payload: unknown): boolean {
if (!("error" in payload)) return false;
const error = (payload as GoogleRpcErrorResponse).error;
if (!error?.details || !Array.isArray(error.details)) return false;
return error.details.some((detail) => detail.reason === "SECURITY_POLICY_VIOLATED");
return error.details.some(detail => detail.reason === "SECURITY_POLICY_VIOLATED");
}
async function pollOperation(
+11 -12
View File
@@ -1,3 +1,14 @@
// ============================================================================
// High-level API
// ============================================================================
import { refreshAnthropicToken } from "./anthropic";
import { refreshCursorToken } from "./cursor";
import { refreshGitHubCopilotToken } from "./github-copilot";
import { refreshAntigravityToken } from "./google-antigravity";
import { refreshGoogleCloudToken } from "./google-gemini-cli";
import { refreshOpenAICodexToken } from "./openai-codex";
import type { OAuthCredentials, OAuthProvider, OAuthProviderInfo } from "./types";
/**
* OAuth credential management for AI providers.
*
@@ -36,18 +47,6 @@ export { loginOpenAICodex, refreshOpenAICodexToken } from "./openai-codex";
export * from "./types";
// ============================================================================
// High-level API
// ============================================================================
import { refreshAnthropicToken } from "./anthropic";
import { refreshCursorToken } from "./cursor";
import { refreshGitHubCopilotToken } from "./github-copilot";
import { refreshAntigravityToken } from "./google-antigravity";
import { refreshGoogleCloudToken } from "./google-gemini-cli";
import { refreshOpenAICodexToken } from "./openai-codex";
import type { OAuthCredentials, OAuthProvider, OAuthProviderInfo } from "./types";
/**
* Refresh token for any OAuth provider.
* Saves the new credentials and returns the new access token.
@@ -1,7 +1,6 @@
/**
* OpenAI Codex (ChatGPT OAuth) flow
*/
import { OAuthCallbackFlow } from "./callback-server";
import { generatePKCE } from "./pkce";
import type { OAuthController, OAuthCredentials } from "./types";
+1 -1
View File
@@ -85,7 +85,7 @@ export function isContextOverflow(message: AssistantMessage, contextWindow?: num
// Case 1: Check error message patterns
if (message.stopReason === "error" && message.errorMessage) {
// Check known patterns
if (OVERFLOW_PATTERNS.some((p) => p.test(message.errorMessage!))) {
if (OVERFLOW_PATTERNS.some(p => p.test(message.errorMessage!))) {
return true;
}
+4 -5
View File
@@ -1,12 +1,11 @@
import AjvModule from "ajv";
import addFormatsModule from "ajv-formats";
import type { Tool, ToolCall } from "../types";
// Handle both default and named exports (ESM/CJS interop)
const Ajv = (AjvModule as any).default || AjvModule;
const addFormats = (addFormatsModule as any).default || addFormatsModule;
import type { Tool, ToolCall } from "../types";
// ============================================================================
// Type Coercion Utilities
// ============================================================================
@@ -50,7 +49,7 @@ function normalizeExpectedTypes(typeParam: unknown): string[] {
* Used to verify that a parsed JSON value is actually what the schema wants.
*/
function matchesExpectedType(value: unknown, expectedTypes: string[]): boolean {
return expectedTypes.some((type) => {
return expectedTypes.some(type => {
switch (type) {
case "string":
return typeof value === "string";
@@ -157,7 +156,7 @@ function decodeJsonPointer(pointer: string): string[] {
return pointer
.split("/")
.slice(1) // Remove leading empty segment from initial "/"
.map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
.map(segment => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
}
/**
@@ -310,7 +309,7 @@ if (!isBrowserExtension) {
* @throws Error if tool is not found or validation fails
*/
export function validateToolCall(tools: Tool[], toolCall: ToolCall): any {
const tool = tools.find((t) => t.name === toolCall.name);
const tool = tools.find(t => t.name === toolCall.name);
if (!tool) {
throw new Error(`Tool "${toolCall.name}" not found`);
}
+1 -1
View File
@@ -481,7 +481,7 @@ describe("Context overflow error handling", () => {
});
// Wait for server to be ready
await new Promise<void>((resolve) => {
await new Promise<void>(resolve => {
const checkServer = async () => {
try {
const response = await fetch("http://localhost:11434/api/tags");
@@ -80,7 +80,7 @@ describe("Duplicate Tool Results Regression", () => {
// Count tool results with the same ID
const toolResults = transformed.filter(
(m) => m.role === "toolResult" && (m as ToolResultMessage).toolCallId === toolCallId,
m => m.role === "toolResult" && (m as ToolResultMessage).toolCallId === toolCallId,
);
// Should have exactly ONE tool result, not two
@@ -138,7 +138,7 @@ describe("Duplicate Tool Results Regression", () => {
const transformed = transformMessages(messages, model);
const toolResults = transformed.filter(
(m) => m.role === "toolResult" && (m as ToolResultMessage).toolCallId === toolCallId,
m => m.role === "toolResult" && (m as ToolResultMessage).toolCallId === toolCallId,
);
expect(toolResults.length).toBe(1);
@@ -187,7 +187,7 @@ describe("Duplicate Tool Results Regression", () => {
const transformed = transformMessages(messages, model);
const toolResults = transformed.filter(
(m) => m.role === "toolResult" && (m as ToolResultMessage).toolCallId === toolCallId,
m => m.role === "toolResult" && (m as ToolResultMessage).toolCallId === toolCallId,
);
// Should have exactly ONE synthetic tool result added
@@ -241,13 +241,13 @@ describe("Duplicate Tool Results Regression", () => {
const transformed = transformMessages(messages, model);
// Should have exactly 3 tool results total
const allToolResults = transformed.filter((m) => m.role === "toolResult");
const allToolResults = transformed.filter(m => m.role === "toolResult");
expect(allToolResults.length).toBe(3);
// Each tool call should have exactly one result
const result1 = allToolResults.filter((m) => (m as ToolResultMessage).toolCallId === toolCallId1);
const result2 = allToolResults.filter((m) => (m as ToolResultMessage).toolCallId === toolCallId2);
const result3 = allToolResults.filter((m) => (m as ToolResultMessage).toolCallId === toolCallId3);
const result1 = allToolResults.filter(m => (m as ToolResultMessage).toolCallId === toolCallId1);
const result2 = allToolResults.filter(m => (m as ToolResultMessage).toolCallId === toolCallId2);
const result3 = allToolResults.filter(m => (m as ToolResultMessage).toolCallId === toolCallId3);
expect(result1.length).toBe(1);
expect(result2.length).toBe(1);
+5 -5
View File
@@ -319,16 +319,16 @@ async function testProviderHandoff<TApi extends Api>(
// Extract text from response
const responseText = response.content
.filter((b) => b.type === "text")
.map((b) => b.text)
.filter(b => b.type === "text")
.map(b => b.text)
.join(" ")
.toLowerCase();
// For aborted messages, we don't expect to find the facts
if (sourceContext.message.stopReason === "error") {
const hasToolCalls = response.content.some((b) => b.type === "toolCall");
const hasThinking = response.content.some((b) => b.type === "thinking");
const hasText = response.content.some((b) => b.type === "text");
const hasToolCalls = response.content.some(b => b.type === "toolCall");
const hasThinking = response.content.some(b => b.type === "thinking");
const hasText = response.content.some(b => b.type === "text");
expect(response.stopReason === "stop" || response.stopReason === "toolUse").toBe(true);
expect(hasThinking || hasText || hasToolCalls).toBe(true);
+8 -13
View File
@@ -290,7 +290,7 @@ describe("Image Limits E2E Tests", () => {
async () => {
// Known limit: 100 images
const { limit, lastError } = await findLimit(
(count) => testImageCount(model, count, smallImage),
count => testImageCount(model, count, smallImage),
20,
120,
20,
@@ -380,7 +380,7 @@ describe("Image Limits E2E Tests", () => {
async () => {
// Known limit: 500 images
const { limit, lastError } = await findLimit(
(count) => testImageCount(model, count, smallImage),
count => testImageCount(model, count, smallImage),
100,
600,
100,
@@ -468,7 +468,7 @@ describe("Image Limits E2E Tests", () => {
async () => {
// Known to work up to ~2500, hits errors around 3000
const { limit, lastError } = await findLimit(
(count) => testImageCount(model, count, smallImage),
count => testImageCount(model, count, smallImage),
500,
3000,
500,
@@ -554,7 +554,7 @@ describe("Image Limits E2E Tests", () => {
"should find maximum image count limit",
async () => {
// Known to fail around 9 images
const { limit, lastError } = await findLimit((count) => testImageCount(model, count, smallImage), 5, 15, 1);
const { limit, lastError } = await findLimit(count => testImageCount(model, count, smallImage), 5, 15, 1);
console.log(`\n Mistral max images: ~${limit} (last error: ${lastError})`);
expect(limit).toBeGreaterThanOrEqual(5);
},
@@ -635,12 +635,7 @@ describe("Image Limits E2E Tests", () => {
"should find maximum image count limit",
async () => {
// Limited by context window, not explicit image limit
const { limit, lastError } = await findLimit(
(count) => testImageCount(model, count, smallImage),
10,
60,
10,
);
const { limit, lastError } = await findLimit(count => testImageCount(model, count, smallImage), 10, 60, 10);
console.log(`\n OpenRouter max images: ~${limit} (last error: ${lastError})`);
expect(limit).toBeGreaterThanOrEqual(10);
},
@@ -720,7 +715,7 @@ describe("Image Limits E2E Tests", () => {
"should find maximum image count limit",
async () => {
const { limit, lastError } = await findLimit(
(count) => testImageCount(model, count, smallImage),
count => testImageCount(model, count, smallImage),
10,
100,
10,
@@ -803,7 +798,7 @@ describe("Image Limits E2E Tests", () => {
it(
"should find maximum image count limit",
async () => {
const { limit, lastError } = await findLimit((count) => testImageCount(model, count, smallImage), 5, 50, 5);
const { limit, lastError } = await findLimit(count => testImageCount(model, count, smallImage), 5, 50, 5);
console.log(`\n Groq max images: ~${limit} (last error: ${lastError})`);
expect(limit).toBeGreaterThanOrEqual(5);
},
@@ -883,7 +878,7 @@ describe("Image Limits E2E Tests", () => {
"should find maximum image count limit",
async () => {
const { limit, lastError } = await findLimit(
(count) => testImageCount(model, count, smallImage),
count => testImageCount(model, count, smallImage),
10,
100,
10,
+4 -4
View File
@@ -61,7 +61,7 @@ async function handleToolWithImageResult<TApi extends Api>(model: Model<TApi>, o
expect(firstResponse.stopReason).toBe("toolUse");
// Find the tool call
const toolCall = firstResponse.content.find((b) => b.type === "toolCall");
const toolCall = firstResponse.content.find(b => b.type === "toolCall");
expect(toolCall).toBeTruthy();
if (!toolCall || toolCall.type !== "toolCall") {
throw new Error("Expected tool call");
@@ -95,7 +95,7 @@ async function handleToolWithImageResult<TApi extends Api>(model: Model<TApi>, o
expect(secondResponse.errorMessage).toBeFalsy();
// Verify the LLM can see and describe the image
const textContent = secondResponse.content.find((b) => b.type === "text");
const textContent = secondResponse.content.find(b => b.type === "text");
expect(textContent).toBeTruthy();
if (textContent && textContent.type === "text") {
const lowerContent = textContent.text.toLowerCase();
@@ -150,7 +150,7 @@ async function handleToolWithTextAndImageResult<TApi extends Api>(model: Model<T
expect(firstResponse.stopReason).toBe("toolUse");
// Find the tool call
const toolCall = firstResponse.content.find((b) => b.type === "toolCall");
const toolCall = firstResponse.content.find(b => b.type === "toolCall");
expect(toolCall).toBeTruthy();
if (!toolCall || toolCall.type !== "toolCall") {
throw new Error("Expected tool call");
@@ -188,7 +188,7 @@ async function handleToolWithTextAndImageResult<TApi extends Api>(model: Model<T
expect(secondResponse.errorMessage).toBeFalsy();
// Verify the LLM can see both text and image
const textContent = secondResponse.content.find((b) => b.type === "text");
const textContent = secondResponse.content.find(b => b.type === "text");
expect(textContent).toBeTruthy();
if (textContent && textContent.type === "text") {
const lowerContent = textContent.text.toLowerCase();
+4 -6
View File
@@ -6,8 +6,11 @@
*
* E2E tests are disabled by default. Set E2E=1 environment variable to enable.
*/
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { getOAuthApiKey } from "@oh-my-pi/pi-ai/utils/oauth";
import type { OAuthCredentials, OAuthProvider } from "@oh-my-pi/pi-ai/utils/oauth/types";
import { isEnoent } from "@oh-my-pi/pi-utils";
/**
@@ -25,11 +28,6 @@ export function e2eApiKey(envVar: string): string | undefined {
return process.env[envVar];
}
import * as os from "node:os";
import * as path from "node:path";
import { getOAuthApiKey } from "@oh-my-pi/pi-ai/utils/oauth";
import type { OAuthCredentials, OAuthProvider } from "@oh-my-pi/pi-ai/utils/oauth/types";
const AUTH_PATH = path.join(os.homedir(), ".pi", "agent", "auth.json");
type ApiKeyCredential = {
+1 -1
View File
@@ -120,7 +120,7 @@ describe("openai-codex streaming", () => {
}
if (event.type === "done") {
sawDone = true;
expect(event.message.content.find((c) => c.type === "text")?.text).toBe("Hello");
expect(event.message.content.find(c => c.type === "text")?.text).toBe("Hello");
}
}
+3 -3
View File
@@ -35,14 +35,14 @@ describe("openai-codex request transformer", () => {
expect(transformed.include).toEqual(["reasoning.encrypted_content"]);
const input = transformed.input || [];
expect(input.some((item) => item.type === "item_reference")).toBe(false);
expect(input.some((item) => "id" in item)).toBe(false);
expect(input.some(item => item.type === "item_reference")).toBe(false);
expect(input.some(item => "id" in item)).toBe(false);
const first = input[0];
expect(first?.type).toBe("message");
expect(first?.role).toBe("developer");
expect(first?.content).toEqual([{ type: "input_text", text: `${DEFAULT_PROMPT_PREFIX}...` }]);
const orphaned = input.find((item) => item.type === "message" && item.role === "assistant");
const orphaned = input.find(item => item.type === "message" && item.role === "assistant");
expect(orphaned?.content).toMatch(/Previous tool result/);
});
});
@@ -73,16 +73,14 @@ describe("openai-completions convertMessages", () => {
};
const messages = convertMessages(model, context, compat);
const roles = messages.map((message) => message.role);
const roles = messages.map(message => message.role);
expect(roles).toEqual(["user", "assistant", "tool", "tool", "user"]);
const imageMessage = messages[messages.length - 1];
expect(imageMessage.role).toBe("user");
expect(Array.isArray(imageMessage.content)).toBe(true);
const imageParts = (imageMessage.content as Array<{ type?: string }>).filter(
(part) => part?.type === "image_url",
);
const imageParts = (imageMessage.content as Array<{ type?: string }>).filter(part => part?.type === "image_url");
expect(imageParts.length).toBe(2);
});
});
+8 -8
View File
@@ -48,7 +48,7 @@ async function basicTextGeneration<TApi extends Api>(model: Model<TApi>, options
expect(response.usage.input + response.usage.cacheRead).toBeGreaterThan(0);
expect(response.usage.output).toBeGreaterThan(0);
expect(response.errorMessage).toBeFalsy();
expect(response.content.map((b) => (b.type === "text" ? b.text : "")).join("")).toContain("Hello test successful");
expect(response.content.map(b => (b.type === "text" ? b.text : "")).join("")).toContain("Hello test successful");
context.messages.push(response);
context.messages.push({ role: "user", content: "Now say 'Goodbye test successful'", timestamp: Date.now() });
@@ -60,7 +60,7 @@ async function basicTextGeneration<TApi extends Api>(model: Model<TApi>, options
expect(secondResponse.usage.input + secondResponse.usage.cacheRead).toBeGreaterThan(0);
expect(secondResponse.usage.output).toBeGreaterThan(0);
expect(secondResponse.errorMessage).toBeFalsy();
expect(secondResponse.content.map((b) => (b.type === "text" ? b.text : "")).join("")).toContain(
expect(secondResponse.content.map(b => (b.type === "text" ? b.text : "")).join("")).toContain(
"Goodbye test successful",
);
}
@@ -133,8 +133,8 @@ async function handleToolCall<TApi extends Api>(model: Model<TApi>, options?: Op
const response = await s.result();
expect(response.stopReason).toBe("toolUse");
expect(response.content.some((b) => b.type === "toolCall")).toBeTruthy();
const toolCall = response.content.find((b) => b.type === "toolCall");
expect(response.content.some(b => b.type === "toolCall")).toBeTruthy();
const toolCall = response.content.find(b => b.type === "toolCall");
if (toolCall && toolCall.type === "toolCall") {
expect(toolCall.name).toBe("calculator");
expect(toolCall.id).toBeTruthy();
@@ -169,7 +169,7 @@ async function handleStreaming<TApi extends Api>(model: Model<TApi>, options?: O
expect(textStarted).toBe(true);
expect(textChunks.length).toBeGreaterThan(0);
expect(textCompleted).toBe(true);
expect(response.content.some((b) => b.type === "text")).toBeTruthy();
expect(response.content.some(b => b.type === "text")).toBeTruthy();
}
async function handleThinking<TApi extends Api>(model: Model<TApi>, options?: OptionsForApi<TApi>) {
@@ -207,7 +207,7 @@ async function handleThinking<TApi extends Api>(model: Model<TApi>, options?: Op
expect(thinkingStarted).toBe(true);
expect(thinkingChunks.length).toBeGreaterThan(0);
expect(thinkingCompleted).toBe(true);
expect(response.content.some((b) => b.type === "thinking")).toBeTruthy();
expect(response.content.some(b => b.type === "thinking")).toBeTruthy();
}
async function handleImage<TApi extends Api>(model: Model<TApi>, options?: OptionsForApi<TApi>) {
@@ -248,7 +248,7 @@ async function handleImage<TApi extends Api>(model: Model<TApi>, options?: Optio
// Check the response mentions red and circle
expect(response.content.length > 0).toBeTruthy();
const textContent = response.content.find((b) => b.type === "text");
const textContent = response.content.find(b => b.type === "text");
if (textContent && textContent.type === "text") {
const lowerContent = textContent.text.toLowerCase();
expect(lowerContent).toContain("red");
@@ -1384,7 +1384,7 @@ describe("Generate E2E Tests", () => {
});
// Wait for server to be ready
await new Promise<void>((resolve) => {
await new Promise<void>(resolve => {
const checkServer = async () => {
try {
const response = await fetch("http://localhost:11434/api/tags");
@@ -51,7 +51,7 @@ async function testToolCallWithoutResult<TApi extends Api>(
console.log("First response:", JSON.stringify(firstResponse, null, 2));
// Verify the response contains a tool call
const hasToolCall = firstResponse.content.some((block) => block.type === "toolCall");
const hasToolCall = firstResponse.content.some(block => block.type === "toolCall");
expect(hasToolCall).toBe(true);
if (!hasToolCall) {
@@ -79,10 +79,10 @@ async function testToolCallWithoutResult<TApi extends Api>(
// The LLM may choose to answer directly or make a new tool call - either is fine
// The important thing is it didn't fail with the orphaned tool call error
const textContent = secondResponse.content
.filter((block) => block.type === "text")
.map((block) => (block.type === "text" ? block.text : ""))
.filter(block => block.type === "text")
.map(block => (block.type === "text" ? block.text : ""))
.join(" ");
const toolCalls = secondResponse.content.filter((block) => block.type === "toolCall").length;
const toolCalls = secondResponse.content.filter(block => block.type === "toolCall").length;
expect(toolCalls || textContent.length).toBeGreaterThan(0);
console.log("Answer:", textContent);
+1 -1
View File
@@ -200,7 +200,7 @@ Unanswered Comments: 2
expect(response.stopReason).not.toBe("error");
expect(response.errorMessage).toBeFalsy();
expect(response.content.some((b) => b.type === "text")).toBe(true);
expect(response.content.some(b => b.type === "text")).toBe(true);
}
async function testUnpairedHighSurrogate<TApi extends Api>(llm: Model<TApi>, options: OptionsForApi<TApi> = {}) {
+2 -2
View File
@@ -32,8 +32,8 @@ describe.skipIf(!e2eApiKey("OPENAI_API_KEY"))("xhigh reasoning", () => {
const response = await s.result();
expect(response.stopReason, `Error: ${response.errorMessage}`).toBe("stop");
expect(response.content.some((b) => b.type === "text")).toBe(true);
expect(hasThinking || response.content.some((b) => b.type === "thinking")).toBe(true);
expect(response.content.some(b => b.type === "text")).toBe(true);
expect(hasThinking || response.content.some(b => b.type === "thinking")).toBe(true);
});
});
+1 -1
View File
@@ -7,7 +7,7 @@ import { e2eApiKey } from "./oauth";
describe.skipIf(!e2eApiKey("OPENCODE_API_KEY"))("OpenCode Zen Models Smoke Test", () => {
const zenModels = Object.values(MODELS.opencode);
zenModels.forEach((model) => {
zenModels.forEach(model => {
it(`${model.id}`, async () => {
const response = await complete(model as Model<any>, {
messages: [{ role: "user", content: "Say hello.", timestamp: Date.now() }],
@@ -1,6 +1,6 @@
import type { CustomToolFactory } from "@oh-my-pi/pi-coding-agent";
const factory: CustomToolFactory = (pi) => ({
const factory: CustomToolFactory = pi => ({
name: "hello",
label: "Hello",
description: "A simple greeting tool",
@@ -7,7 +7,6 @@
*
* The onSession callback reconstructs state by scanning past tool results.
*/
import type {
CustomTool,
CustomToolContext,
@@ -29,7 +28,7 @@ interface TodoDetails {
error?: string;
}
const factory: CustomToolFactory = (pi) => {
const factory: CustomToolFactory = pi => {
const { Type } = pi.typebox;
const { StringEnum, Text } = pi.pi;
@@ -85,7 +84,7 @@ const factory: CustomToolFactory = (pi) => {
{
type: "text",
text: todos.length
? todos.map((t) => `[${t.done ? "x" : " "}] #${t.id}: ${t.text}`).join("\n")
? todos.map(t => `[${t.done ? "x" : " "}] #${t.id}: ${t.text}`).join("\n")
: "No todos",
},
],
@@ -114,7 +113,7 @@ const factory: CustomToolFactory = (pi) => {
details: { action: "toggle", todos: [...todos], nextId, error: "id required" },
};
}
const todo = todos.find((t) => t.id === params.id);
const todo = todos.find(t => t.id === params.id);
if (!todo) {
return {
content: [{ type: "text", text: `Todo #${params.id} not found` }],
@@ -5,7 +5,6 @@
* These features are now exposed directly on the ExtensionAPI, matching
* the CustomToolAPI interface.
*/
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
export default function (pi: ExtensionAPI) {
@@ -2,7 +2,6 @@
* Example extension that uses a 3rd party dependency (chalk).
* Tests that jiti can resolve npm modules correctly.
*/
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
import chalk from "chalk";
@@ -14,12 +13,12 @@ export default function (pi: ExtensionAPI) {
console.log(`${chalk.blue("[chalk-logger]")} Agent starting`);
});
pi.on("tool_call", async (event) => {
pi.on("tool_call", async event => {
console.log(`${chalk.yellow("[chalk-logger]")} Tool: ${chalk.cyan(event.toolName)}`);
return undefined;
});
pi.on("agent_end", async (event) => {
pi.on("agent_end", async event => {
const count = event.messages.length;
console.log(`${chalk.green("[chalk-logger]")} Done with ${chalk.bold(String(count))} messages`);
});
@@ -3,7 +3,6 @@
*
* Demonstrates using ExtensionAPI's logger, typebox, and pi module access.
*/
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
export default function (pi: ExtensionAPI) {
@@ -9,7 +9,6 @@
* 2. Use /pirate to toggle pirate mode
* 3. When enabled, the agent will respond like a pirate
*/
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
export default function pirateExtension(pi: ExtensionAPI) {
@@ -18,7 +18,6 @@
* 2. Use /plan to toggle plan mode on/off
* 3. Or start in plan mode with --plan flag
*/
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
import { Key } from "@oh-my-pi/pi-tui";
@@ -120,12 +119,12 @@ const SAFE_COMMANDS = [
];
function isSafeCommand(command: string): boolean {
if (SAFE_COMMANDS.some((pattern) => pattern.test(command))) {
if (!DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(command))) {
if (SAFE_COMMANDS.some(pattern => pattern.test(command))) {
if (!DESTRUCTIVE_PATTERNS.some(pattern => pattern.test(command))) {
return true;
}
}
if (DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(command))) {
if (DESTRUCTIVE_PATTERNS.some(pattern => pattern.test(command))) {
return false;
}
return true;
@@ -223,7 +222,7 @@ export default function planModeExtension(pi: ExtensionAPI) {
// Helper to update status displays
function updateStatus(ctx: ExtensionContext) {
if (executionMode && todoItems.length > 0) {
const completed = todoItems.filter((t) => t.completed).length;
const completed = todoItems.filter(t => t.completed).length;
ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("accent", `📋 ${completed}/${todoItems.length}`));
} else if (planModeEnabled) {
ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("warning", "⏸ plan"));
@@ -293,13 +292,13 @@ export default function planModeExtension(pi: ExtensionAPI) {
// Register Shift+P shortcut
pi.registerShortcut(Key.shift("p"), {
description: "Toggle plan mode",
handler: async (ctx) => {
handler: async ctx => {
await togglePlanMode(ctx);
},
});
// Block destructive bash in plan mode
pi.on("tool_call", async (event) => {
pi.on("tool_call", async event => {
if (!planModeEnabled) return;
if (event.toolName !== "bash") return;
@@ -319,7 +318,7 @@ export default function planModeExtension(pi: ExtensionAPI) {
if (!executionMode || todoItems.length === 0) return;
// Mark the first uncompleted step as done when any tool succeeds
const nextStep = todoItems.find((t) => !t.completed);
const nextStep = todoItems.find(t => !t.completed);
if (nextStep) {
nextStep.completed = true;
updateStatus(ctx);
@@ -328,7 +327,7 @@ export default function planModeExtension(pi: ExtensionAPI) {
// Filter out stale plan mode context messages from LLM context
// This ensures the agent only sees the CURRENT state (plan mode on/off)
pi.on("context", async (event) => {
pi.on("context", async event => {
// Only filter when NOT in plan mode (i.e., when executing)
if (planModeEnabled) {
return;
@@ -336,7 +335,7 @@ export default function planModeExtension(pi: ExtensionAPI) {
// Remove any previous plan-mode-context messages
const _beforeCount = event.messages.length;
const filtered = event.messages.filter((m) => {
const filtered = event.messages.filter(m => {
if (m.role === "user" && Array.isArray(m.content)) {
const hasOldContext = m.content.some(
(c: { type: string; text?: string }) => c.type === "text" && c.text?.includes("[PLAN MODE ACTIVE]"),
@@ -381,8 +380,8 @@ Do NOT attempt to make changes - just describe what you would do.`,
}
if (executionMode && todoItems.length > 0) {
const remaining = todoItems.filter((t) => !t.completed);
const todoList = remaining.map((t) => `${t.step}. ${t.text}`).join("\n");
const remaining = todoItems.filter(t => !t.completed);
const todoList = remaining.map(t => `${t.step}. ${t.text}`).join("\n");
return {
message: {
customType: "plan-execution-context",
@@ -402,10 +401,10 @@ Execute each step in order.`,
pi.on("agent_end", async (event, ctx) => {
// In execution mode, check if all steps complete
if (executionMode && todoItems.length > 0) {
const allComplete = todoItems.every((t) => t.completed);
const allComplete = todoItems.every(t => t.completed);
if (allComplete) {
// Show final completed list in chat
const completedList = todoItems.map((t) => `~~${t.text}~~`).join("\n");
const completedList = todoItems.map(t => `~~${t.text}~~`).join("\n");
pi.sendMessage(
{
customType: "plan-complete",
@@ -428,7 +427,7 @@ Execute each step in order.`,
// Extract todos from last message
const messages = event.messages;
const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
const lastAssistant = [...messages].reverse().find(m => m.role === "assistant");
if (lastAssistant && Array.isArray(lastAssistant.content)) {
const textContent = lastAssistant.content
.filter(
@@ -541,7 +540,7 @@ Execute each step in order.`,
// If no tools were called this turn, the agent was doing analysis/explanation
// Mark the next uncompleted step as done
if (!toolsCalledThisTurn) {
const nextStep = todoItems.find((t) => !t.completed);
const nextStep = todoItems.find(t => !t.completed);
if (nextStep) {
nextStep.completed = true;
updateStatus(ctx);
@@ -9,7 +9,6 @@
* proper branching - when you branch, the todo state is automatically
* correct for that point in history.
*/
import { StringEnum } from "@oh-my-pi/pi-ai";
import type { ExtensionAPI, ExtensionContext, Theme } from "@oh-my-pi/pi-coding-agent";
import { matchesKey, Text, truncateToWidth } from "@oh-my-pi/pi-tui";
@@ -74,7 +73,7 @@ class TodoListComponent {
if (this.todos.length === 0) {
lines.push(truncateToWidth(` ${th.fg("dim", "No todos yet. Ask the agent to add some!")}`, width));
} else {
const done = this.todos.filter((t) => t.done).length;
const done = this.todos.filter(t => t.done).length;
const total = this.todos.length;
lines.push(truncateToWidth(` ${th.fg("muted", `${done}/${total} completed`)}`, width));
lines.push("");
@@ -149,7 +148,7 @@ export default function (pi: ExtensionAPI) {
{
type: "text",
text: todos.length
? todos.map((t) => `[${t.done ? "x" : " "}] #${t.id}: ${t.text}`).join("\n")
? todos.map(t => `[${t.done ? "x" : " "}] #${t.id}: ${t.text}`).join("\n")
: "No todos",
},
],
@@ -178,7 +177,7 @@ export default function (pi: ExtensionAPI) {
details: { action: "toggle", todos: [...todos], nextId, error: "id required" } as TodoDetails,
};
}
const todo = todos.find((t) => t.id === params.id);
const todo = todos.find(t => t.id === params.id);
if (!todo) {
return {
content: [{ type: "text", text: `Todo #${params.id} not found` }],
@@ -8,7 +8,6 @@
* 1. Copy this file to ~/.omp/agent/extensions/ (legacy: ~/.pi/agent/extensions/) or your project's .omp/extensions/
* 2. Use /tools to open the tool selector
*/
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
import { getSettingsListTheme } from "@oh-my-pi/pi-coding-agent";
import { Container, type SettingItem, SettingsList } from "@oh-my-pi/pi-tui";
@@ -71,7 +70,7 @@ export default function toolsExtension(pi: ExtensionAPI) {
await ctx.ui.custom((tui, theme, done) => {
// Build settings items for each tool
const items: SettingItem[] = allTools.map((tool) => ({
const items: SettingItem[] = allTools.map(tool => ({
id: tool,
label: tool,
currentValue: enabledTools.has(tool) ? "enabled" : "disabled",
@@ -4,7 +4,6 @@
*
* Requires: npm install in this directory
*/
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import ms from "ms";
@@ -4,7 +4,6 @@
* Automatically commits changes when the agent exits.
* Uses the last assistant message to generate a commit message.
*/
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
export default function (pi: HookAPI) {
@@ -27,7 +26,7 @@ export default function (pi: HookAPI) {
if (Array.isArray(content)) {
lastAssistantText = content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.map(c => c.text)
.join("\n");
}
break;
@@ -4,7 +4,6 @@
* Prompts for confirmation before destructive session actions (clear, switch, branch).
* Demonstrates how to cancel session events using the before_* events.
*/
import type { HookAPI, SessionBeforeSwitchEvent, SessionMessageEntry } from "@oh-my-pi/pi-coding-agent";
export default function (pi: HookAPI) {
@@ -12,7 +12,6 @@
* Usage:
* omp --hook examples/hooks/custom-compaction.ts
*/
import { complete, getModel } from "@oh-my-pi/pi-ai";
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
import { convertToLlm, serializeConversation } from "@oh-my-pi/pi-coding-agent";
@@ -89,7 +88,7 @@ ${conversationText}
const summary = response.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.map(c => c.text)
.join("\n");
if (!summary.trim()) {
@@ -4,7 +4,6 @@
* Prevents session changes when there are uncommitted git changes.
* Useful to ensure work is committed before switching context.
*/
import type { HookAPI, HookContext } from "@oh-my-pi/pi-coding-agent";
async function checkDirtyRepo(pi: HookAPI, ctx: HookContext, action: string): Promise<{ cancel: boolean } | undefined> {
@@ -7,7 +7,6 @@
* Usage:
* echo "Run the tests" > /tmp/agent-trigger.txt
*/
import * as fs from "node:fs";
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
@@ -4,7 +4,6 @@
* Creates git stash checkpoints at each turn so /branch can restore code state.
* When branching, offers to restore code to that point in history.
*/
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
export default function (pi: HookAPI) {
@@ -11,7 +11,6 @@
*
* The generated prompt appears as a draft in the editor for review/editing.
*/
import { complete, type Message } from "@oh-my-pi/pi-ai";
import type { HookAPI, SessionEntry } from "@oh-my-pi/pi-coding-agent";
import { BorderedLoader, convertToLlm, serializeConversation } from "@oh-my-pi/pi-coding-agent";
@@ -62,7 +61,7 @@ export default function (pi: HookAPI) {
const branch = ctx.sessionManager.getBranch();
const messages = branch
.filter((entry): entry is SessionEntry & { type: "message" } => entry.type === "message")
.map((entry) => entry.message);
.map(entry => entry.message);
if (messages.length === 0) {
ctx.ui.notify("No conversation to hand off", "error");
@@ -105,13 +104,13 @@ export default function (pi: HookAPI) {
return response.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.map(c => c.text)
.join("\n");
};
doGenerate()
.then(done)
.catch((err) => {
.catch(err => {
console.error("Handoff generation failed:", err);
done(null);
});
@@ -4,7 +4,6 @@
* Prompts for confirmation before running potentially dangerous bash commands.
* Patterns checked: rm -rf, sudo, chmod/chown 777
*/
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
export default function (pi: HookAPI) {
@@ -14,7 +13,7 @@ export default function (pi: HookAPI) {
if (event.toolName !== "bash") return undefined;
const command = event.input.command as string;
const isDangerous = dangerousPatterns.some((p) => p.test(command));
const isDangerous = dangerousPatterns.some(p => p.test(command));
if (isDangerous) {
if (!ctx.hasUI) {
@@ -4,7 +4,6 @@
* Blocks write and edit operations to protected paths.
* Useful for preventing accidental modifications to sensitive files.
*/
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
export default function (pi: HookAPI) {
@@ -16,7 +15,7 @@ export default function (pi: HookAPI) {
}
const path = event.input.path as string;
const isProtected = protectedPaths.some((p) => path.includes(p));
const isProtected = protectedPaths.some(p => path.includes(p));
if (isProtected) {
if (ctx.hasUI) {
+2 -3
View File
@@ -6,7 +6,6 @@
* 2. Shows a spinner while extracting (hides editor)
* 3. Loads the result into the editor for user to fill in answers
*/
import { complete, type UserMessage } from "@oh-my-pi/pi-ai";
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
import { BorderedLoader } from "@oh-my-pi/pi-coding-agent";
@@ -56,7 +55,7 @@ export default function (pi: HookAPI) {
}
const textParts = msg.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text);
.map(c => c.text);
if (textParts.length > 0) {
lastAssistantText = textParts.join("\n");
break;
@@ -96,7 +95,7 @@ export default function (pi: HookAPI) {
return response.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.map(c => c.text)
.join("\n");
};
@@ -1,7 +1,6 @@
/**
* Snake game hook - play snake with /snake command
*/
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
import { matchesKey, visibleWidth } from "@oh-my-pi/pi-tui";
@@ -47,7 +46,7 @@ function spawnFood(snake: Point[]): Point {
x: Math.floor(Math.random() * GAME_WIDTH),
y: Math.floor(Math.random() * GAME_HEIGHT),
};
} while (snake.some((s) => s.x === food.x && s.y === food.y));
} while (snake.some(s => s.x === food.x && s.y === food.y));
return food;
}
@@ -127,7 +126,7 @@ class SnakeComponent {
}
// Check self collision
if (this.state.snake.some((s) => s.x === newHead.x && s.y === newHead.y)) {
if (this.state.snake.some(s => s.x === newHead.x && s.y === newHead.y)) {
this.state.gameOver = true;
return;
}
@@ -249,7 +248,7 @@ class SnakeComponent {
let row = "";
for (let x = 0; x < effectiveWidth; x++) {
const isHead = this.state.snake[0].x === x && this.state.snake[0].y === y;
const isBody = this.state.snake.slice(1).some((s) => s.x === x && s.y === y);
const isBody = this.state.snake.slice(1).some(s => s.x === x && s.y === y);
const isFood = this.state.food.x === x && this.state.food.y === y;
if (isHead) {
@@ -331,7 +330,7 @@ export default function (pi: HookAPI) {
return new SnakeComponent(
tui,
() => done(undefined),
(state) => {
state => {
// Save or clear state
pi.appendEntry(SNAKE_SAVE_TYPE, state);
},
@@ -4,7 +4,6 @@
* Demonstrates ctx.ui.setStatus() for displaying persistent status text in the footer.
* Shows turn progress with themed colors.
*/
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
export default function (pi: HookAPI) {
@@ -4,19 +4,18 @@
* Uses all defaults: discovers skills, hooks, tools, context files
* from cwd and ~/.omp/agent. Model chosen from settings or first available.
*/
import { createAgentSession } from "@oh-my-pi/pi-coding-agent";
const { session } = await createAgentSession();
session.subscribe((event) => {
session.subscribe(event => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
});
await session.prompt("What files are in the current directory?");
session.state.messages.forEach((msg) => {
session.state.messages.forEach(msg => {
console.log(msg);
});
console.log();
@@ -3,7 +3,6 @@
*
* Shows how to select a specific model and thinking level.
*/
import { getModel } from "@oh-my-pi/pi-ai";
import { createAgentSession, discoverAuthStorage, discoverModels } from "@oh-my-pi/pi-coding-agent";
@@ -27,7 +26,7 @@ if (customModel) {
const available = modelRegistry.getAvailable();
console.log(
"Available models:",
available.map((m) => `${m.provider}/${m.id}`),
available.map(m => `${m.provider}/${m.id}`),
);
if (available.length > 0) {
@@ -38,7 +37,7 @@ if (available.length > 0) {
modelRegistry,
});
session.subscribe((event) => {
session.subscribe(event => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
@@ -3,7 +3,6 @@
*
* Shows how to replace or modify the default system prompt.
*/
import { createAgentSession, SessionManager } from "@oh-my-pi/pi-coding-agent";
// Option 1: Replace prompt entirely
@@ -13,7 +12,7 @@ Always end responses with "Arrr!"`,
sessionManager: SessionManager.inMemory(),
});
session1.subscribe((event) => {
session1.subscribe(event => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
@@ -25,7 +24,7 @@ console.log("\n");
// Option 2: Modify default prompt (receives default, returns modified)
const { session: session2 } = await createAgentSession({
systemPrompt: (defaultPrompt) => `${defaultPrompt}
systemPrompt: defaultPrompt => `${defaultPrompt}
## Additional Instructions
- Always be concise
@@ -33,7 +32,7 @@ const { session: session2 } = await createAgentSession({
sessionManager: SessionManager.inMemory(),
});
session2.subscribe((event) => {
session2.subscribe(event => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
@@ -4,18 +4,17 @@
* Skills provide specialized instructions loaded into the system prompt.
* Discover, filter, merge, or replace them.
*/
import { createAgentSession, discoverSkills, SessionManager, type Skill } from "@oh-my-pi/pi-coding-agent";
// Discover all skills from cwd/.omp/skills, ~/.omp/agent/skills, etc.
const { skills: allSkills } = await discoverSkills();
console.log(
"Discovered skills:",
allSkills.map((s) => s.name),
allSkills.map(s => s.name),
);
// Filter to specific skills
const filteredSkills = allSkills.filter((s) => s.name.includes("browser") || s.name.includes("search"));
const filteredSkills = allSkills.filter(s => s.name.includes("browser") || s.name.includes("search"));
// Or define custom skills inline
const customSkill: Skill = {
@@ -13,7 +13,6 @@
* An extension is a TypeScript file that exports a default function:
* export default function (pi: ExtensionAPI) { ... }
*/
import { createAgentSession, SessionManager } from "@oh-my-pi/pi-coding-agent";
// Extensions are loaded from disk, not passed inline to createAgentSession.
@@ -28,7 +27,7 @@ const { session } = await createAgentSession({
sessionManager: SessionManager.inMemory(),
});
session.subscribe((event) => {
session.subscribe(event => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
@@ -4,28 +4,27 @@
* Hooks intercept agent events for logging, blocking, or modification.
* Note: "hooks" is now called "extensions" in the API.
*/
import { createAgentSession, type ExtensionFactory, SessionManager } from "@oh-my-pi/pi-coding-agent";
// Logging hook (now called extension)
const loggingHook: ExtensionFactory = (api) => {
const loggingHook: ExtensionFactory = api => {
api.on("agent_start", async () => {
console.log("[Hook] Agent starting");
});
api.on("tool_call", async (event) => {
api.on("tool_call", async event => {
console.log(`[Hook] Tool: ${event.toolName}`);
return undefined; // Don't block
});
api.on("agent_end", async (event) => {
api.on("agent_end", async event => {
console.log(`[Hook] Done, ${event.messages.length} messages`);
});
};
// Blocking extension (returns { block: true, reason: "..." })
const safetyHook: ExtensionFactory = (api) => {
api.on("tool_call", async (event) => {
const safetyHook: ExtensionFactory = api => {
api.on("tool_call", async event => {
if (event.toolName === "bash") {
const cmd = (event.input as { command?: string }).command ?? "";
if (cmd.includes("rm -rf")) {
@@ -42,7 +41,7 @@ const { session } = await createAgentSession({
sessionManager: SessionManager.inMemory(),
});
session.subscribe((event) => {
session.subscribe(event => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
@@ -3,7 +3,6 @@
*
* Context files provide project-specific instructions loaded into the system prompt.
*/
import { createAgentSession, discoverContextFiles, SessionManager } from "@oh-my-pi/pi-coding-agent";
// Discover AGENTS.md files walking up from cwd
@@ -3,7 +3,6 @@
*
* File-based templates that inject content when invoked with /templatename.
*/
import {
createAgentSession,
discoverPromptTemplates,
@@ -4,7 +4,6 @@
* File-based commands that inject content when invoked with /commandname.
* Note: File-based slash commands are now called "prompt templates".
*/
import {
createAgentSession,
discoverPromptTemplates,
@@ -3,7 +3,6 @@
*
* Configure API key resolution via AuthStorage and ModelRegistry.
*/
import {
AuthStorage,
createAgentSession,
@@ -3,7 +3,6 @@
*
* Override settings using SettingsManager.
*/
import { createAgentSession, loadSettings, SessionManager, SettingsManager } from "@oh-my-pi/pi-coding-agent";
// Load current settings (merged global + project)
@@ -3,7 +3,6 @@
*
* Control session persistence: in-memory, new file, continue, or open specific.
*/
import { createAgentSession, SessionManager } from "@oh-my-pi/pi-coding-agent";
// In-memory (no persistence)
@@ -10,7 +10,6 @@
* 5. Trim trailing whitespace (preserve indentation)
* 6. Ensure single newline at EOF
*/
import { Glob } from "bun";
const PROMPTS_DIR = new URL("../src/prompts/", import.meta.url).pathname;
@@ -4,7 +4,6 @@
* System instruction files (CLAUDE.md, AGENTS.md, GEMINI.md, etc.) that provide
* persistent guidance to the agent.
*/
import { defineCapability } from ".";
import type { SourceMeta } from "./types";
@@ -30,8 +29,8 @@ export const contextFileCapability = defineCapability<ContextFile>({
description: "Persistent instruction files (CLAUDE.md, AGENTS.md, etc.) that guide agent behavior",
// Deduplicate by level: one user-level file, one project-level file
// Higher-priority providers shadow lower-priority ones at the same scope
key: (file) => file.level,
validate: (file) => {
key: file => file.level,
validate: file => {
if (!file.path) return "Missing path";
if (file.content === undefined) return "Missing content";
if (file.level !== "user" && file.level !== "project") return "Invalid level: must be 'user' or 'project'";
@@ -3,7 +3,6 @@
*
* TypeScript/JavaScript extension modules loaded by the extension system.
*/
import { defineCapability } from ".";
import type { SourceMeta } from "./types";
@@ -25,8 +24,8 @@ export const extensionModuleCapability = defineCapability<ExtensionModule>({
id: "extension-modules",
displayName: "Extension Modules",
description: "TypeScript/JavaScript extension modules loaded by the extension system",
key: (ext) => ext.name,
validate: (ext) => {
key: ext => ext.name,
validate: ext => {
if (!ext.name) return "Missing name";
if (!ext.path) return "Missing path";
return undefined;
@@ -3,7 +3,6 @@
*
* Gemini-style extensions that provide MCP servers, tools, and context.
*/
import { defineCapability } from ".";
import type { MCPServer } from "./mcp";
import type { SourceMeta } from "./types";
@@ -39,8 +38,8 @@ export const extensionCapability = defineCapability<Extension>({
id: "extensions",
displayName: "Extensions",
description: "Gemini-style extensions providing MCP servers, tools, and context",
key: (ext) => ext.name,
validate: (ext) => {
key: ext => ext.name,
validate: ext => {
if (!ext.name) return "Missing extension name";
if (!ext.path) return "Missing extension path";
return undefined;
+2 -2
View File
@@ -42,7 +42,7 @@ export async function readDirEntries(dirPath: string): Promise<fs.Dirent[]> {
export async function readDir(dirPath: string): Promise<string[]> {
const entries = await readDirEntries(dirPath);
return entries.map((entry) => entry.name);
return entries.map(entry => entry.name);
}
export async function walkUp(
@@ -55,7 +55,7 @@ export async function walkUp(
while (true) {
const entries = await readDirEntries(current);
const entry = entries.find((e) => e.name === name);
const entry = entries.find(e => e.name === name);
if (entry) {
if (file && entry.isFile()) return path.join(current, name);
if (dir && entry.isDirectory()) return path.join(current, name);
+2 -3
View File
@@ -3,7 +3,6 @@
*
* Pre/post tool execution hooks defined as shell scripts.
*/
import { defineCapability } from ".";
import type { SourceMeta } from "./types";
@@ -29,8 +28,8 @@ export const hookCapability = defineCapability<Hook>({
id: "hooks",
displayName: "Hooks",
description: "Pre/post tool execution hooks",
key: (hook) => `${hook.type}:${hook.tool}:${hook.name}`,
validate: (hook) => {
key: hook => `${hook.type}:${hook.tool}:${hook.name}`,
validate: hook => {
if (!hook.name) return "Missing name";
if (!hook.path) return "Missing path";
if (hook.type !== "pre" && hook.type !== "post") return "Invalid type (must be 'pre' or 'post')";
+9 -10
View File
@@ -6,7 +6,6 @@
* - Registering providers (where to find it)
* - Loading items for a capability across all providers
*/
import * as os from "node:os";
import * as path from "node:path";
import { clearCache as clearFsCache, cacheStats as fsCacheStats, invalidate as invalidateFs } from "./fs";
@@ -81,7 +80,7 @@ export function registerProvider<T>(capabilityId: string, provider: Provider<T>)
// Insert in priority order (highest first)
const providers = capability.providers as Provider<T>[];
const idx = providers.findIndex((p) => p.priority < provider.priority);
const idx = providers.findIndex(p => p.priority < provider.priority);
if (idx === -1) {
providers.push(provider);
} else {
@@ -107,7 +106,7 @@ async function loadImpl<T>(
const contributingProviders: string[] = [];
const results = await Promise.all(
providers.map(async (provider) => {
providers.map(async provider => {
try {
const result = await provider.load(ctx);
return { provider, result };
@@ -128,7 +127,7 @@ async function loadImpl<T>(
if (!result) continue;
if (result.warnings) {
allWarnings.push(...result.warnings.map((w) => `[${provider.displayName}] ${w}`));
allWarnings.push(...result.warnings.map(w => `[${provider.displayName}] ${w}`));
}
if (result.items.length > 0) {
@@ -190,15 +189,15 @@ async function loadImpl<T>(
* Filter providers based on options and disabled state.
*/
function filterProviders<T>(capability: Capability<T>, options: LoadOptions): Provider<T>[] {
let providers = (capability.providers as Provider<T>[]).filter((p) => !disabledProviders.has(p.id));
let providers = (capability.providers as Provider<T>[]).filter(p => !disabledProviders.has(p.id));
if (options.providers) {
const allowed = new Set(options.providers);
providers = providers.filter((p) => allowed.has(p.id));
providers = providers.filter(p => allowed.has(p.id));
}
if (options.excludeProviders) {
const excluded = new Set(options.excludeProviders);
providers = providers.filter((p) => !excluded.has(p.id));
providers = providers.filter(p => !excluded.has(p.id));
}
return providers;
@@ -321,7 +320,7 @@ export function getCapabilityInfo(capabilityId: string): CapabilityInfo | undefi
id: capability.id,
displayName: capability.displayName,
description: capability.description,
providers: capability.providers.map((p) => ({
providers: capability.providers.map(p => ({
id: p.id,
displayName: p.displayName,
description: p.description,
@@ -335,7 +334,7 @@ export function getCapabilityInfo(capabilityId: string): CapabilityInfo | undefi
* Get all capabilities info for UI display.
*/
export function getAllCapabilitiesInfo(): CapabilityInfo[] {
return listCapabilities().map((id) => getCapabilityInfo(id)!);
return listCapabilities().map(id => getCapabilityInfo(id)!);
}
/**
@@ -350,7 +349,7 @@ export function getProviderInfo(providerId: string): ProviderInfo | undefined {
let priority = 0;
for (const capId of caps) {
const cap = capabilities.get(capId);
const provider = cap?.providers.find((p) => p.id === providerId);
const provider = cap?.providers.find(p => p.id === providerId);
if (provider) {
priority = provider.priority;
break;
@@ -3,7 +3,6 @@
*
* GitHub Copilot-style instructions with optional file pattern matching.
*/
import { defineCapability } from ".";
import type { SourceMeta } from "./types";
@@ -27,8 +26,8 @@ export const instructionCapability = defineCapability<Instruction>({
id: "instructions",
displayName: "Instructions",
description: "File-specific instructions with glob pattern matching (GitHub Copilot format)",
key: (inst) => inst.name,
validate: (inst) => {
key: inst => inst.name,
validate: inst => {
if (!inst.name) return "Missing name";
if (!inst.path) return "Missing path";
if (inst.content === undefined) return "Missing content";
+2 -3
View File
@@ -4,7 +4,6 @@
* Canonical shape for MCP server configurations, regardless of source format.
* All providers translate their native format to this shape.
*/
import { defineCapability } from ".";
import type { SourceMeta } from "./types";
@@ -34,8 +33,8 @@ export const mcpCapability = defineCapability<MCPServer>({
id: "mcps",
displayName: "MCP Servers",
description: "Model Context Protocol server configurations for external tool integrations",
key: (server) => server.name,
validate: (server) => {
key: server => server.name,
validate: server => {
if (!server.name) return "Missing server name";
if (!server.command && !server.url) return "Must have command or url";
@@ -3,7 +3,6 @@
*
* Reusable prompt templates (Codex format) available via /prompts: menu.
*/
import { defineCapability } from ".";
import type { SourceMeta } from "./types";
@@ -25,8 +24,8 @@ export const promptCapability = defineCapability<Prompt>({
id: "prompts",
displayName: "Prompts",
description: "Reusable prompt templates available via /prompts: menu",
key: (prompt) => prompt.name,
validate: (prompt) => {
key: prompt => prompt.name,
validate: prompt => {
if (!prompt.name) return "Missing name";
if (!prompt.path) return "Missing path";
if (prompt.content === undefined) return "Missing content";

Some files were not shown because too many files have changed in this diff Show More