Files
pi-perplexity/src/index.ts
T
Ivan Pereira 63eb22316a Replace brittle error text detection with isError flag
renderPerplexityResult was pattern-matching content text prefixes
to decide whether to render in error style. This was tightly coupled
to exact message strings in index.ts — a wording change would
silently break the error styling.

Set isError: true in details on every error return path in execute,
and check details.isError in the renderer instead.
2026-02-23 10:49:33 +00:00

141 lines
4.8 KiB
TypeScript

import { StringEnum } from "@mariozechner/pi-ai";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { registerPerplexityCommands } from "./commands/login.js";
import { authenticate } from "./auth/login.js";
import { formatForLLM } from "./search/format.js";
import { searchPerplexity } from "./search/client.js";
import { renderPerplexityCall } from "./render/call.js";
import { renderPerplexityResult } from "./render/result.js";
import { AuthError, SearchError } from "./search/types.js";
import { errorMessage } from "./render/util.js";
export default function (pi: ExtensionAPI) {
registerPerplexityCommands(pi);
pi.registerTool({
name: "perplexity_search",
label: "Perplexity Search",
description: "Search the web with your Perplexity subscription.",
parameters: Type.Object({
query: Type.String({ description: "Search query" }),
recency: Type.Optional(
StringEnum(["hour", "day", "week", "month", "year"] as const, {
description: "Filter results by recency",
}),
),
limit: Type.Optional(
Type.Number({ description: "Max sources to return", minimum: 1, maximum: 50 }),
),
}),
renderCall: renderPerplexityCall,
renderResult: renderPerplexityResult,
async execute(toolCallId, params, signal, onUpdate, ctx) {
const start = Date.now();
let sourceCount = 0;
try {
onUpdate?.({
content: [{ type: "text", text: "Authenticating with Perplexity..." }],
details: { toolCallId },
});
const promptInput = async (label: string, placeholder: string): Promise<string | null | undefined> => {
if (!ctx?.ui?.input) {
return undefined;
}
return ctx.ui.input(label, placeholder);
};
const authOptions: Parameters<typeof authenticate>[0] = {
promptForEmail: async () => promptInput("Perplexity email", "you@example.com"),
promptForOtp: async (email) => promptInput(`Enter OTP sent to ${email}`, "123456"),
};
if (signal) authOptions.signal = signal;
const jwt = await authenticate(authOptions);
if (signal?.aborted) {
return {
content: [{ type: "text", text: "Perplexity search was cancelled." }],
details: { sourceCount: 0, queryMs: Date.now() - start },
};
}
onUpdate?.({
content: [{ type: "text", text: "Querying Perplexity..." }],
details: { toolCallId },
});
const searchParams: Parameters<typeof searchPerplexity>[0] = {
query: params.query,
};
if (params.recency) searchParams.recency = params.recency;
if (typeof params.limit === "number") searchParams.limit = params.limit;
const result = await searchPerplexity(searchParams, jwt, signal);
const formatted = formatForLLM(result, params.limit);
sourceCount =
typeof params.limit === "number"
? Math.min(params.limit, result.sources.length)
: result.sources.length;
return {
content: [{ type: "text", text: formatted }],
details: {
model: result.displayModel,
sourceCount,
queryMs: Date.now() - start,
uuid: result.uuid,
},
};
} catch (error) {
const queryMs = Date.now() - start;
if (error instanceof AuthError) {
return {
content: [{ type: "text", text: `Authentication failed: ${error.message}` }],
details: { sourceCount, queryMs, isError: true },
};
}
if (error instanceof SearchError) {
if (error.code === "AUTH") {
// Do NOT clear the token here. The user must re-login explicitly via
// /perplexity-login --force. Clearing automatically would silently discard
// a token that may still be valid (e.g. a transient 401), and removes the
// user's ability to inspect or recover the cached credential themselves.
return {
content: [
{
type: "text",
text: `Perplexity authentication failed. Run /perplexity-login --force to re-authenticate.`,
},
],
details: { sourceCount, queryMs, isError: true },
};
}
return {
content: [{ type: "text", text: `Perplexity search failed: ${error.message}` }],
details: { sourceCount, queryMs, isError: true },
};
}
return {
content: [
{
type: "text",
text: `Perplexity search failed: ${errorMessage(error)}`,
},
],
details: { sourceCount, queryMs, isError: true },
};
}
},
});
}