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:
@@ -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
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"tabWidth": 3,
|
||||
"printWidth": 120,
|
||||
"endOfLine": "lf",
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
+3
-3
@@ -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",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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");
|
||||
|
||||
@@ -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[];
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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++;
|
||||
});
|
||||
|
||||
|
||||
@@ -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/);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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> = {}) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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')";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user