refactor(search): always send searches as incognito (#14)
Hardcode is_incognito: true in the Perplexity request and remove the toggle surface: the incognito tool parameter, PI_PERPLEXITY_INCOGNITO env var, config file field, /perplexity-config prompt, and TUI status indicators. Config and resolveDefaultModel are now model-only. Supersedes the incognito portion of PR #4. Co-authored-by: Ivan Pereira <183991+ivanrvpereira@users.noreply.github.com>
This commit is contained in:
co-authored by
Ivan Pereira
parent
a9f252715e
commit
97aab3e9b9
@@ -45,7 +45,7 @@ node --no-deprecation --import ./node_modules/@earendil-works/pi-coding-agent/no
|
|||||||
|
|
||||||
- Endpoint: `POST https://www.perplexity.ai/rest/sse/perplexity_ask`.
|
- Endpoint: `POST https://www.perplexity.ai/rest/sse/perplexity_ask`.
|
||||||
- Required Perplexity constants live in `src/constants.ts`.
|
- Required Perplexity constants live in `src/constants.ts`.
|
||||||
- Searches default to `is_incognito: true` to avoid polluting user history.
|
- Searches always send `is_incognito: true` to avoid polluting user history; there is no toggle.
|
||||||
- The response is `data:` JSON lines with `[DONE]`, not a fully standard SSE stream.
|
- The response is `data:` JSON lines with `[DONE]`, not a fully standard SSE stream.
|
||||||
- Events are incremental snapshots: shallow-merge top level, merge blocks by `intended_usage`, splice markdown via `chunk_starting_offset`, accumulate sources.
|
- Events are incremental snapshots: shallow-merge top level, merge blocks by `intended_usage`, splice markdown via `chunk_starting_offset`, accumulate sources.
|
||||||
- Completion is `event.final === true` or `event.status === "COMPLETED"`.
|
- Completion is `event.final === true` or `event.status === "COMPLETED"`.
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Searches now always run with `is_incognito: true`. The `incognito` tool parameter, `PI_PERPLEXITY_INCOGNITO` env var, config file field, and `/perplexity-config` incognito prompt were removed; `/perplexity-config` now sets the default model only.
|
||||||
|
|
||||||
## [0.3.0] - 2026-07-14
|
## [0.3.0] - 2026-07-14
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ Once installed, the agent automatically calls `perplexity_search` whenever it ne
|
|||||||
| `query` | string | ✅ | The search query |
|
| `query` | string | ✅ | The search query |
|
||||||
| `recency` | string | — | Filter by age: `hour` · `day` · `week` · `month` · `year` |
|
| `recency` | string | — | Filter by age: `hour` · `day` · `week` · `month` · `year` |
|
||||||
| `limit` | number | — | Max sources to include (1–50) |
|
| `limit` | number | — | Max sources to include (1–50) |
|
||||||
| `incognito` | boolean | — | Whether to hide the search from Perplexity history; defaults to `true` |
|
|
||||||
|
|
||||||
Model selection is configured globally with `/perplexity-config` or `PI_PERPLEXITY_MODEL`; it is not exposed as a tool parameter, so agent-generated tool calls cannot accidentally override your configured model.
|
Model selection is configured globally with `/perplexity-config` or `PI_PERPLEXITY_MODEL`; it is not exposed as a tool parameter, so agent-generated tool calls cannot accidentally override your configured model.
|
||||||
|
|
||||||
@@ -102,7 +101,7 @@ Provider: perplexity (oauth)
|
|||||||
Model: pplx_pro_upgraded
|
Model: pplx_pro_upgraded
|
||||||
```
|
```
|
||||||
|
|
||||||
Queries default to `is_incognito: true`, but you can override that per call or via config.
|
Queries always send `is_incognito: true`, so searches never appear in your Perplexity web history.
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
|
|||||||
+3
-14
@@ -8,12 +8,11 @@ import {
|
|||||||
} from "../config.js";
|
} from "../config.js";
|
||||||
import { KNOWN_MODELS } from "../search/models.js";
|
import { KNOWN_MODELS } from "../search/models.js";
|
||||||
|
|
||||||
function formatCurrentConfig(config: { model?: string; incognito?: boolean }): string {
|
function formatCurrentConfig(config: { model?: string }): string {
|
||||||
const model = config.model ?? "pplx_pro_upgraded (default)";
|
const model = config.model ?? "pplx_pro_upgraded (default)";
|
||||||
const modelLabel = KNOWN_MODELS.find((m) => m.value === config.model)?.label;
|
const modelLabel = KNOWN_MODELS.find((m) => m.value === config.model)?.label;
|
||||||
const modelDisplay = modelLabel ? `${model} (${modelLabel})` : model;
|
const modelDisplay = modelLabel ? `${model} (${modelLabel})` : model;
|
||||||
const incognito = config.incognito ?? true;
|
return `Model: ${modelDisplay}`;
|
||||||
return `Model: ${modelDisplay}\nIncognito: ${incognito}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatModelOption(model: { value: string; label: string }, currentModel?: string): string {
|
function formatModelOption(model: { value: string; label: string }, currentModel?: string): string {
|
||||||
@@ -43,7 +42,7 @@ export function registerPerplexityConfigCommand(
|
|||||||
handler: async (args, ctx) => {
|
handler: async (args, ctx) => {
|
||||||
if (args.trim() === "--help" || args.trim() === "-h") {
|
if (args.trim() === "--help" || args.trim() === "-h") {
|
||||||
ctx.ui.notify(
|
ctx.ui.notify(
|
||||||
`Usage: /perplexity-config [--show]\n\nInteractively set default model and incognito mode.\nConfig stored at: ${deps.getConfigPath()}`,
|
`Usage: /perplexity-config [--show]\n\nInteractively set the default model.\nConfig stored at: ${deps.getConfigPath()}`,
|
||||||
"info",
|
"info",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -67,17 +66,7 @@ export function registerPerplexityConfigCommand(
|
|||||||
const selectedModel = KNOWN_MODELS.find((model) => model.label === normalizedSelection)?.value
|
const selectedModel = KNOWN_MODELS.find((model) => model.label === normalizedSelection)?.value
|
||||||
?? normalizedSelection;
|
?? normalizedSelection;
|
||||||
|
|
||||||
const incognito = await ctx.ui.confirm(
|
|
||||||
"Incognito mode",
|
|
||||||
"Hide searches from Perplexity web history? (recommended)",
|
|
||||||
);
|
|
||||||
if (incognito === undefined || incognito === null) {
|
|
||||||
ctx.ui.notify("Perplexity config unchanged.", "info");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.model = selectedModel;
|
config.model = selectedModel;
|
||||||
config.incognito = incognito;
|
|
||||||
|
|
||||||
await deps.saveConfig(config);
|
await deps.saveConfig(config);
|
||||||
ctx.ui.notify(`Perplexity config saved:\n${formatCurrentConfig(config)}`, "info");
|
ctx.ui.notify(`Perplexity config saved:\n${formatCurrentConfig(config)}`, "info");
|
||||||
|
|||||||
+3
-16
@@ -4,7 +4,6 @@ import { dirname, join } from "node:path";
|
|||||||
|
|
||||||
export interface PerplexityConfig {
|
export interface PerplexityConfig {
|
||||||
model?: string;
|
model?: string;
|
||||||
incognito?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_PATH = join(homedir(), ".config", "pi-perplexity", "config.json");
|
const CONFIG_PATH = join(homedir(), ".config", "pi-perplexity", "config.json");
|
||||||
@@ -25,7 +24,6 @@ function parseConfig(raw: string): PerplexityConfig {
|
|||||||
const obj = parsed as Record<string, unknown>;
|
const obj = parsed as Record<string, unknown>;
|
||||||
const config: PerplexityConfig = {};
|
const config: PerplexityConfig = {};
|
||||||
if (typeof obj.model === "string" && obj.model.length > 0) config.model = obj.model;
|
if (typeof obj.model === "string" && obj.model.length > 0) config.model = obj.model;
|
||||||
if (typeof obj.incognito === "boolean") config.incognito = obj.incognito;
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,19 +46,8 @@ export async function saveConfig(config: PerplexityConfig, configPath: string =
|
|||||||
await chmod(configPath, 0o600);
|
await chmod(configPath, 0o600);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve effective search defaults from env vars, config file, and per-call incognito override. */
|
/** Resolve the default model from env var, config file, then hardcoded fallback. */
|
||||||
export function resolveSearchDefaults(
|
export function resolveDefaultModel(config: PerplexityConfig): string {
|
||||||
params: { incognito?: boolean },
|
|
||||||
config: PerplexityConfig,
|
|
||||||
): { model: string; incognito: boolean } {
|
|
||||||
const envModel = process.env.PI_PERPLEXITY_MODEL?.trim() || undefined;
|
const envModel = process.env.PI_PERPLEXITY_MODEL?.trim() || undefined;
|
||||||
const envIncognito = process.env.PI_PERPLEXITY_INCOGNITO || undefined;
|
return envModel ?? config.model ?? "pplx_pro_upgraded";
|
||||||
|
|
||||||
const model = envModel ?? config.model ?? "pplx_pro_upgraded";
|
|
||||||
const incognito = params.incognito
|
|
||||||
?? (envIncognito !== undefined ? envIncognito !== "false" && envIncognito !== "0" : undefined)
|
|
||||||
?? config.incognito
|
|
||||||
?? true;
|
|
||||||
|
|
||||||
return { model, incognito };
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-8
@@ -5,7 +5,7 @@ import { registerPerplexityConfigCommand } from "./commands/config.js";
|
|||||||
import { registerPerplexityCommands } from "./commands/login.js";
|
import { registerPerplexityCommands } from "./commands/login.js";
|
||||||
|
|
||||||
import { authenticate } from "./auth/login.js";
|
import { authenticate } from "./auth/login.js";
|
||||||
import { loadConfig, resolveSearchDefaults } from "./config.js";
|
import { loadConfig, resolveDefaultModel } from "./config.js";
|
||||||
import { effectiveSourceCount, formatForLLM } from "./search/format.js";
|
import { effectiveSourceCount, formatForLLM } from "./search/format.js";
|
||||||
import { searchPerplexity } from "./search/client.js";
|
import { searchPerplexity } from "./search/client.js";
|
||||||
import { renderPerplexityCall } from "./render/call.js";
|
import { renderPerplexityCall } from "./render/call.js";
|
||||||
@@ -30,7 +30,6 @@ export default function (pi: ExtensionAPI) {
|
|||||||
limit: Type.Optional(
|
limit: Type.Optional(
|
||||||
Type.Number({ description: "Max sources to return", minimum: 1, maximum: 50 }),
|
Type.Number({ description: "Max sources to return", minimum: 1, maximum: 50 }),
|
||||||
),
|
),
|
||||||
incognito: Type.Optional(Type.Boolean({ description: "Hide search from Perplexity history" })),
|
|
||||||
}),
|
}),
|
||||||
renderCall: renderPerplexityCall,
|
renderCall: renderPerplexityCall,
|
||||||
renderResult: renderPerplexityResult,
|
renderResult: renderPerplexityResult,
|
||||||
@@ -71,16 +70,12 @@ export default function (pi: ExtensionAPI) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const config = await loadConfig();
|
const config = await loadConfig();
|
||||||
const { model, incognito } = resolveSearchDefaults(
|
const model = resolveDefaultModel(config);
|
||||||
params.incognito !== undefined ? { incognito: params.incognito } : {},
|
|
||||||
config,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await searchPerplexity(
|
const result = await searchPerplexity(
|
||||||
{
|
{
|
||||||
query: params.query,
|
query: params.query,
|
||||||
model,
|
model,
|
||||||
incognito,
|
|
||||||
...(params.recency !== undefined ? { recency: params.recency } : {}),
|
...(params.recency !== undefined ? { recency: params.recency } : {}),
|
||||||
},
|
},
|
||||||
auth,
|
auth,
|
||||||
@@ -94,7 +89,6 @@ export default function (pi: ExtensionAPI) {
|
|||||||
content: [{ type: "text", text: formatted }],
|
content: [{ type: "text", text: formatted }],
|
||||||
details: {
|
details: {
|
||||||
model: result.displayModel,
|
model: result.displayModel,
|
||||||
incognito,
|
|
||||||
sourceCount,
|
sourceCount,
|
||||||
queryMs: Date.now() - start,
|
queryMs: Date.now() - start,
|
||||||
uuid: result.uuid,
|
uuid: result.uuid,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ interface PerplexityCallArgs {
|
|||||||
query?: unknown;
|
query?: unknown;
|
||||||
recency?: unknown;
|
recency?: unknown;
|
||||||
limit?: unknown;
|
limit?: unknown;
|
||||||
incognito?: unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const RECENCY_VALUES: readonly string[] = ["hour", "day", "week", "month", "year"];
|
const RECENCY_VALUES: readonly string[] = ["hour", "day", "week", "month", "year"];
|
||||||
@@ -15,15 +14,10 @@ export function renderPerplexityCall(args: PerplexityCallArgs, theme: Theme): Te
|
|||||||
const recencyRaw = asString(args?.recency)?.trim().toLowerCase();
|
const recencyRaw = asString(args?.recency)?.trim().toLowerCase();
|
||||||
const recency = recencyRaw && RECENCY_VALUES.includes(recencyRaw) ? recencyRaw : undefined;
|
const recency = recencyRaw && RECENCY_VALUES.includes(recencyRaw) ? recencyRaw : undefined;
|
||||||
const limit = asPositiveInteger(args?.limit);
|
const limit = asPositiveInteger(args?.limit);
|
||||||
const incognito = typeof args?.incognito === "boolean" ? args.incognito : undefined;
|
|
||||||
|
|
||||||
let text = theme.fg("toolTitle", theme.bold("perplexity_search "));
|
let text = theme.fg("toolTitle", theme.bold("perplexity_search "));
|
||||||
text += query ? theme.fg("muted", truncate(query, 90)) : theme.fg("warning", "(missing query)");
|
text += query ? theme.fg("muted", truncate(query, 90)) : theme.fg("warning", "(missing query)");
|
||||||
|
|
||||||
if (typeof incognito === "boolean") {
|
|
||||||
text += theme.fg("dim", ` • incognito ${incognito ? "on" : "off"}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recency) {
|
if (recency) {
|
||||||
text += theme.fg("dim", ` • ${recency}`);
|
text += theme.fg("dim", ` • ${recency}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { asString, asNumber, truncate } from "./util.js";
|
|||||||
|
|
||||||
interface PerplexityResultDetails {
|
interface PerplexityResultDetails {
|
||||||
model?: unknown;
|
model?: unknown;
|
||||||
incognito?: unknown;
|
|
||||||
sourceCount?: unknown;
|
sourceCount?: unknown;
|
||||||
queryMs?: unknown;
|
queryMs?: unknown;
|
||||||
uuid?: unknown;
|
uuid?: unknown;
|
||||||
@@ -70,16 +69,12 @@ export function renderPerplexityResult(
|
|||||||
const sourceCount = asNumber(details.sourceCount);
|
const sourceCount = asNumber(details.sourceCount);
|
||||||
const queryMs = asNumber(details.queryMs);
|
const queryMs = asNumber(details.queryMs);
|
||||||
const model = asString(details.model)?.trim();
|
const model = asString(details.model)?.trim();
|
||||||
const incognito = typeof details.incognito === "boolean" ? details.incognito : undefined;
|
|
||||||
const uuid = asString(details.uuid)?.trim();
|
const uuid = asString(details.uuid)?.trim();
|
||||||
|
|
||||||
let text = theme.fg("success", "✓ Perplexity");
|
let text = theme.fg("success", "✓ Perplexity");
|
||||||
if (model) {
|
if (model) {
|
||||||
text += theme.fg("dim", ` • ${model}`);
|
text += theme.fg("dim", ` • ${model}`);
|
||||||
}
|
}
|
||||||
if (typeof incognito === "boolean") {
|
|
||||||
text += theme.fg("dim", ` • incognito ${incognito ? "on" : "off"}`);
|
|
||||||
}
|
|
||||||
if (typeof sourceCount === "number") {
|
if (typeof sourceCount === "number") {
|
||||||
text += theme.fg("muted", ` • ${sourceCount} source${sourceCount === 1 ? "" : "s"}`);
|
text += theme.fg("muted", ` • ${sourceCount} source${sourceCount === 1 ? "" : "s"}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export interface SearchParams {
|
|||||||
query: string;
|
query: string;
|
||||||
recency?: "hour" | "day" | "week" | "month" | "year";
|
recency?: "hour" | "day" | "week" | "month" | "year";
|
||||||
model: string;
|
model: string;
|
||||||
incognito: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeUrl(url: string): string {
|
function normalizeUrl(url: string): string {
|
||||||
@@ -132,7 +131,7 @@ function buildRequestBody(params: SearchParams): Record<string, unknown> {
|
|||||||
language: "en-US",
|
language: "en-US",
|
||||||
timezone,
|
timezone,
|
||||||
search_recency_filter: params.recency ?? null,
|
search_recency_filter: params.recency ?? null,
|
||||||
is_incognito: params.incognito,
|
is_incognito: true,
|
||||||
use_schematized_api: true,
|
use_schematized_api: true,
|
||||||
skip_search_enabled: true,
|
skip_search_enabled: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ describe("perplexity-config command", () => {
|
|||||||
test("marks the configured model as current in the select options", async () => {
|
test("marks the configured model as current in the select options", async () => {
|
||||||
let handler: ((args: string, ctx: any) => Promise<void>) | undefined;
|
let handler: ((args: string, ctx: any) => Promise<void>) | undefined;
|
||||||
|
|
||||||
await saveConfig({ model: "gpt54", incognito: false }, configPath);
|
await saveConfig({ model: "gpt54" }, configPath);
|
||||||
|
|
||||||
registerPerplexityConfigCommand(
|
registerPerplexityConfigCommand(
|
||||||
{
|
{
|
||||||
@@ -54,7 +54,6 @@ describe("perplexity-config command", () => {
|
|||||||
options = receivedOptions;
|
options = receivedOptions;
|
||||||
return "GPT-5.4 [current]";
|
return "GPT-5.4 [current]";
|
||||||
},
|
},
|
||||||
confirm: async () => false,
|
|
||||||
notify: () => undefined,
|
notify: () => undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -85,15 +84,14 @@ describe("perplexity-config command", () => {
|
|||||||
await handler!("", {
|
await handler!("", {
|
||||||
ui: {
|
ui: {
|
||||||
select: async () => "GPT-5.4",
|
select: async () => "GPT-5.4",
|
||||||
confirm: async () => false,
|
|
||||||
notify: (message: string, level: string) => notifications.push({ message, level }),
|
notify: (message: string, level: string) => notifications.push({ message, level }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const raw = await readFile(configPath, "utf8");
|
const raw = await readFile(configPath, "utf8");
|
||||||
expect(JSON.parse(raw)).toEqual({ model: "gpt54", incognito: false });
|
expect(JSON.parse(raw)).toEqual({ model: "gpt54" });
|
||||||
expect(notifications).toContainEqual({
|
expect(notifications).toContainEqual({
|
||||||
message: "Perplexity config saved:\nModel: gpt54 (GPT-5.4)\nIncognito: false",
|
message: "Perplexity config saved:\nModel: gpt54 (GPT-5.4)",
|
||||||
level: "info",
|
level: "info",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+15
-59
@@ -5,10 +5,9 @@ import { join } from "node:path";
|
|||||||
|
|
||||||
let loadConfig: (configPath?: string) => Promise<import("../src/config.js").PerplexityConfig>;
|
let loadConfig: (configPath?: string) => Promise<import("../src/config.js").PerplexityConfig>;
|
||||||
let saveConfig: (config: import("../src/config.js").PerplexityConfig, configPath?: string) => Promise<void>;
|
let saveConfig: (config: import("../src/config.js").PerplexityConfig, configPath?: string) => Promise<void>;
|
||||||
let resolveSearchDefaults: (
|
let resolveDefaultModel: (
|
||||||
params: { incognito?: boolean },
|
|
||||||
config: import("../src/config.js").PerplexityConfig,
|
config: import("../src/config.js").PerplexityConfig,
|
||||||
) => { model: string; incognito: boolean };
|
) => string;
|
||||||
|
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
let configPath: string;
|
let configPath: string;
|
||||||
@@ -20,7 +19,7 @@ beforeEach(async () => {
|
|||||||
const mod = await import(`../src/config.js?t=${Date.now()}`);
|
const mod = await import(`../src/config.js?t=${Date.now()}`);
|
||||||
loadConfig = mod.loadConfig;
|
loadConfig = mod.loadConfig;
|
||||||
saveConfig = mod.saveConfig;
|
saveConfig = mod.saveConfig;
|
||||||
resolveSearchDefaults = mod.resolveSearchDefaults;
|
resolveDefaultModel = mod.resolveDefaultModel;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -34,10 +33,9 @@ describe("loadConfig", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("returns parsed config from file", async () => {
|
test("returns parsed config from file", async () => {
|
||||||
await writeFile(configPath, JSON.stringify({ model: "gpt54", incognito: false }));
|
await writeFile(configPath, JSON.stringify({ model: "gpt54" }));
|
||||||
const config = await loadConfig(configPath);
|
const config = await loadConfig(configPath);
|
||||||
expect(config.model).toBe("gpt54");
|
expect(config.model).toBe("gpt54");
|
||||||
expect(config.incognito).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws on invalid JSON", async () => {
|
test("throws on invalid JSON", async () => {
|
||||||
@@ -51,10 +49,11 @@ describe("loadConfig", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("ignores unknown fields", async () => {
|
test("ignores unknown fields", async () => {
|
||||||
await writeFile(configPath, JSON.stringify({ model: "gpt54", unknown: true }));
|
await writeFile(configPath, JSON.stringify({ model: "gpt54", unknown: true, incognito: false }));
|
||||||
const config = await loadConfig(configPath);
|
const config = await loadConfig(configPath);
|
||||||
expect(config.model).toBe("gpt54");
|
expect(config.model).toBe("gpt54");
|
||||||
expect(config).not.toHaveProperty("unknown");
|
expect(config).not.toHaveProperty("unknown");
|
||||||
|
expect(config).not.toHaveProperty("incognito");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ignores empty model string", async () => {
|
test("ignores empty model string", async () => {
|
||||||
@@ -66,12 +65,11 @@ describe("loadConfig", () => {
|
|||||||
|
|
||||||
describe("saveConfig", () => {
|
describe("saveConfig", () => {
|
||||||
test("writes file with 0600 permissions", async () => {
|
test("writes file with 0600 permissions", async () => {
|
||||||
await saveConfig({ model: "claude46sonnetthinking", incognito: true }, configPath);
|
await saveConfig({ model: "claude46sonnetthinking" }, configPath);
|
||||||
|
|
||||||
const raw = await readFile(configPath, "utf8");
|
const raw = await readFile(configPath, "utf8");
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
expect(parsed.model).toBe("claude46sonnetthinking");
|
expect(parsed.model).toBe("claude46sonnetthinking");
|
||||||
expect(parsed.incognito).toBe(true);
|
|
||||||
|
|
||||||
const stats = await stat(configPath);
|
const stats = await stat(configPath);
|
||||||
expect(stats.mode & 0o777).toBe(0o600);
|
expect(stats.mode & 0o777).toBe(0o600);
|
||||||
@@ -86,46 +84,23 @@ describe("saveConfig", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveSearchDefaults", () => {
|
describe("resolveDefaultModel", () => {
|
||||||
test("returns hardcoded defaults when no config, env, or params", () => {
|
test("returns hardcoded default when no config or env", () => {
|
||||||
const result = resolveSearchDefaults({}, {});
|
expect(resolveDefaultModel({})).toBe("pplx_pro_upgraded");
|
||||||
expect(result.model).toBe("pplx_pro_upgraded");
|
|
||||||
expect(result.incognito).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("config file values override defaults", () => {
|
test("config file model overrides default", () => {
|
||||||
const result = resolveSearchDefaults({}, { model: "gpt54", incognito: false });
|
expect(resolveDefaultModel({ model: "gpt54" })).toBe("gpt54");
|
||||||
expect(result.model).toBe("gpt54");
|
|
||||||
expect(result.incognito).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("env var '0' disables incognito", () => {
|
test("env var overrides config file", () => {
|
||||||
const original = process.env.PI_PERPLEXITY_INCOGNITO;
|
|
||||||
try {
|
|
||||||
process.env.PI_PERPLEXITY_INCOGNITO = "0";
|
|
||||||
const result = resolveSearchDefaults({}, {});
|
|
||||||
expect(result.incognito).toBe(false);
|
|
||||||
} finally {
|
|
||||||
if (original === undefined) delete process.env.PI_PERPLEXITY_INCOGNITO;
|
|
||||||
else process.env.PI_PERPLEXITY_INCOGNITO = original;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("env vars override config file", () => {
|
|
||||||
const originalModel = process.env.PI_PERPLEXITY_MODEL;
|
const originalModel = process.env.PI_PERPLEXITY_MODEL;
|
||||||
const originalIncognito = process.env.PI_PERPLEXITY_INCOGNITO;
|
|
||||||
try {
|
try {
|
||||||
process.env.PI_PERPLEXITY_MODEL = "experimental";
|
process.env.PI_PERPLEXITY_MODEL = "experimental";
|
||||||
process.env.PI_PERPLEXITY_INCOGNITO = "false";
|
expect(resolveDefaultModel({ model: "gpt54" })).toBe("experimental");
|
||||||
|
|
||||||
const result = resolveSearchDefaults({}, { model: "gpt54", incognito: true });
|
|
||||||
expect(result.model).toBe("experimental");
|
|
||||||
expect(result.incognito).toBe(false);
|
|
||||||
} finally {
|
} finally {
|
||||||
if (originalModel === undefined) delete process.env.PI_PERPLEXITY_MODEL;
|
if (originalModel === undefined) delete process.env.PI_PERPLEXITY_MODEL;
|
||||||
else process.env.PI_PERPLEXITY_MODEL = originalModel;
|
else process.env.PI_PERPLEXITY_MODEL = originalModel;
|
||||||
if (originalIncognito === undefined) delete process.env.PI_PERPLEXITY_INCOGNITO;
|
|
||||||
else process.env.PI_PERPLEXITY_INCOGNITO = originalIncognito;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,26 +108,7 @@ describe("resolveSearchDefaults", () => {
|
|||||||
const originalModel = process.env.PI_PERPLEXITY_MODEL;
|
const originalModel = process.env.PI_PERPLEXITY_MODEL;
|
||||||
try {
|
try {
|
||||||
process.env.PI_PERPLEXITY_MODEL = " ";
|
process.env.PI_PERPLEXITY_MODEL = " ";
|
||||||
|
expect(resolveDefaultModel({ model: "gpt54" })).toBe("gpt54");
|
||||||
const result = resolveSearchDefaults({}, { model: "gpt54" });
|
|
||||||
expect(result.model).toBe("gpt54");
|
|
||||||
} finally {
|
|
||||||
if (originalModel === undefined) delete process.env.PI_PERPLEXITY_MODEL;
|
|
||||||
else process.env.PI_PERPLEXITY_MODEL = originalModel;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("per-call incognito overrides env/config without exposing model override", () => {
|
|
||||||
const originalModel = process.env.PI_PERPLEXITY_MODEL;
|
|
||||||
try {
|
|
||||||
process.env.PI_PERPLEXITY_MODEL = "experimental";
|
|
||||||
|
|
||||||
const result = resolveSearchDefaults(
|
|
||||||
{ incognito: false },
|
|
||||||
{ model: "gpt54", incognito: true },
|
|
||||||
);
|
|
||||||
expect(result.model).toBe("experimental");
|
|
||||||
expect(result.incognito).toBe(false);
|
|
||||||
} finally {
|
} finally {
|
||||||
if (originalModel === undefined) delete process.env.PI_PERPLEXITY_MODEL;
|
if (originalModel === undefined) delete process.env.PI_PERPLEXITY_MODEL;
|
||||||
else process.env.PI_PERPLEXITY_MODEL = originalModel;
|
else process.env.PI_PERPLEXITY_MODEL = originalModel;
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ describe("Perplexity model selection e2e", () => {
|
|||||||
{
|
{
|
||||||
query: "Say exactly OK",
|
query: "Say exactly OK",
|
||||||
model,
|
model,
|
||||||
incognito: true,
|
|
||||||
},
|
},
|
||||||
token,
|
token,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ describe("perplexity_search execute", () => {
|
|||||||
test("includes effective config values in the search request and result details", async () => {
|
test("includes effective config values in the search request and result details", async () => {
|
||||||
const authenticate = mock(async () => "jwt-token");
|
const authenticate = mock(async () => "jwt-token");
|
||||||
const saveBrowserAuthInput = mock(async () => ({ type: "oauth", access: "jwt-token" }));
|
const saveBrowserAuthInput = mock(async () => ({ type: "oauth", access: "jwt-token" }));
|
||||||
const loadConfig = mock(async () => ({ model: "gpt54", incognito: false }));
|
const loadConfig = mock(async () => ({ model: "gpt54" }));
|
||||||
const resolveSearchDefaults = mock(() => ({ model: "gpt54", incognito: false }));
|
const resolveDefaultModel = mock(() => "gpt54");
|
||||||
const searchPerplexity = mock(async () => ({
|
const searchPerplexity = mock(async () => ({
|
||||||
answer: "answer",
|
answer: "answer",
|
||||||
sources: [{ url: "https://example.com" }],
|
sources: [{ url: "https://example.com" }],
|
||||||
@@ -23,7 +23,7 @@ describe("perplexity_search execute", () => {
|
|||||||
mock.module("../src/config.js", () => ({
|
mock.module("../src/config.js", () => ({
|
||||||
getConfigPath: () => "/tmp/pi-perplexity-config.json",
|
getConfigPath: () => "/tmp/pi-perplexity-config.json",
|
||||||
loadConfig,
|
loadConfig,
|
||||||
resolveSearchDefaults,
|
resolveDefaultModel,
|
||||||
saveConfig: mock(async () => undefined),
|
saveConfig: mock(async () => undefined),
|
||||||
}));
|
}));
|
||||||
mock.module("../src/search/client.js", () => ({ searchPerplexity }));
|
mock.module("../src/search/client.js", () => ({ searchPerplexity }));
|
||||||
@@ -45,6 +45,7 @@ describe("perplexity_search execute", () => {
|
|||||||
|
|
||||||
expect(execute).toBeDefined();
|
expect(execute).toBeDefined();
|
||||||
expect(JSON.stringify(parameters)).not.toContain("model");
|
expect(JSON.stringify(parameters)).not.toContain("model");
|
||||||
|
expect(JSON.stringify(parameters)).not.toContain("incognito");
|
||||||
|
|
||||||
const result = await execute!(
|
const result = await execute!(
|
||||||
"tool-1",
|
"tool-1",
|
||||||
@@ -55,17 +56,15 @@ describe("perplexity_search execute", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(loadConfig).toHaveBeenCalledTimes(1);
|
expect(loadConfig).toHaveBeenCalledTimes(1);
|
||||||
expect(resolveSearchDefaults).toHaveBeenCalledWith({}, { model: "gpt54", incognito: false });
|
expect(resolveDefaultModel).toHaveBeenCalledWith({ model: "gpt54" });
|
||||||
expect(searchPerplexity).toHaveBeenCalledWith(
|
expect(searchPerplexity).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
query: "how many planets",
|
query: "how many planets",
|
||||||
model: "gpt54",
|
model: "gpt54",
|
||||||
incognito: false,
|
|
||||||
},
|
},
|
||||||
"jwt-token",
|
"jwt-token",
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
expect(result.details.incognito).toBe(false);
|
|
||||||
expect(result.details.model).toBe("gpt54");
|
expect(result.details.model).toBe("gpt54");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -74,7 +73,7 @@ describe("perplexity_search execute", () => {
|
|||||||
const saveBrowserAuthInput = mock(async () => ({ type: "oauth", access: "jwt-token" }));
|
const saveBrowserAuthInput = mock(async () => ({ type: "oauth", access: "jwt-token" }));
|
||||||
const clearToken = mock(async () => undefined);
|
const clearToken = mock(async () => undefined);
|
||||||
const loadConfig = mock(async () => ({}));
|
const loadConfig = mock(async () => ({}));
|
||||||
const resolveSearchDefaults = mock(() => ({ model: "pplx_pro_upgraded", incognito: true }));
|
const resolveDefaultModel = mock(() => "pplx_pro_upgraded");
|
||||||
const searchPerplexity = mock(async () => {
|
const searchPerplexity = mock(async () => {
|
||||||
throw new SearchError("AUTH", "Perplexity rejected authentication (401/403).");
|
throw new SearchError("AUTH", "Perplexity rejected authentication (401/403).");
|
||||||
});
|
});
|
||||||
@@ -84,7 +83,7 @@ describe("perplexity_search execute", () => {
|
|||||||
mock.module("../src/config.js", () => ({
|
mock.module("../src/config.js", () => ({
|
||||||
getConfigPath: () => "/tmp/pi-perplexity-config.json",
|
getConfigPath: () => "/tmp/pi-perplexity-config.json",
|
||||||
loadConfig,
|
loadConfig,
|
||||||
resolveSearchDefaults,
|
resolveDefaultModel,
|
||||||
saveConfig: mock(async () => undefined),
|
saveConfig: mock(async () => undefined),
|
||||||
}));
|
}));
|
||||||
mock.module("../src/search/client.js", () => ({ searchPerplexity }));
|
mock.module("../src/search/client.js", () => ({ searchPerplexity }));
|
||||||
|
|||||||
+15
-16
@@ -57,7 +57,7 @@ describe("searchPerplexity", () => {
|
|||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const result = await searchPerplexity(
|
const result = await searchPerplexity(
|
||||||
{ query: "latest Node release notes", recency: "week", model: "pplx_pro_upgraded", incognito: true },
|
{ query: "latest Node release notes", recency: "week", model: "pplx_pro_upgraded" },
|
||||||
"jwt-token",
|
"jwt-token",
|
||||||
controller.signal,
|
controller.signal,
|
||||||
);
|
);
|
||||||
@@ -98,7 +98,7 @@ describe("searchPerplexity", () => {
|
|||||||
expect(result.sources).toHaveLength(1);
|
expect(result.sources).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("passes model and incognito through to request body", async () => {
|
test("passes model through to request body", async () => {
|
||||||
let capturedInit: RequestInit | undefined;
|
let capturedInit: RequestInit | undefined;
|
||||||
|
|
||||||
globalThis.fetch = (async (_url: RequestInfo | URL, init?: RequestInit) => {
|
globalThis.fetch = (async (_url: RequestInfo | URL, init?: RequestInit) => {
|
||||||
@@ -109,15 +109,14 @@ describe("searchPerplexity", () => {
|
|||||||
}) as unknown as typeof fetch;
|
}) as unknown as typeof fetch;
|
||||||
|
|
||||||
await searchPerplexity(
|
await searchPerplexity(
|
||||||
{ query: "q", model: "claude46sonnetthinking", incognito: false },
|
{ query: "q", model: "claude46sonnetthinking" },
|
||||||
"jwt-token",
|
"jwt-token",
|
||||||
);
|
);
|
||||||
|
|
||||||
const body = JSON.parse(String(capturedInit?.body)) as {
|
const body = JSON.parse(String(capturedInit?.body)) as {
|
||||||
params: { model_preference: string; is_incognito: boolean };
|
params: { model_preference: string };
|
||||||
};
|
};
|
||||||
expect(body.params.model_preference).toBe("claude46sonnetthinking");
|
expect(body.params.model_preference).toBe("claude46sonnetthinking");
|
||||||
expect(body.params.is_incognito).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("uses Cookie header for browser-cookie credentials", async () => {
|
test("uses Cookie header for browser-cookie credentials", async () => {
|
||||||
@@ -131,7 +130,7 @@ describe("searchPerplexity", () => {
|
|||||||
}) as unknown as typeof fetch;
|
}) as unknown as typeof fetch;
|
||||||
|
|
||||||
await searchPerplexity(
|
await searchPerplexity(
|
||||||
{ query: "q", model: "pplx_pro_upgraded", incognito: true },
|
{ query: "q", model: "pplx_pro_upgraded" },
|
||||||
{ type: "oauth", cookies: "__Secure-next-auth.session-token=session; cf_clearance=clearance" },
|
{ type: "oauth", cookies: "__Secure-next-auth.session-token=session; cf_clearance=clearance" },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -140,7 +139,7 @@ describe("searchPerplexity", () => {
|
|||||||
expect(headers.get("Authorization")).toBeNull();
|
expect(headers.get("Authorization")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("passes incognito true through to request body", async () => {
|
test("always sends is_incognito true", async () => {
|
||||||
let capturedInit: RequestInit | undefined;
|
let capturedInit: RequestInit | undefined;
|
||||||
|
|
||||||
globalThis.fetch = (async (_url: RequestInfo | URL, init?: RequestInit) => {
|
globalThis.fetch = (async (_url: RequestInfo | URL, init?: RequestInit) => {
|
||||||
@@ -150,7 +149,7 @@ describe("searchPerplexity", () => {
|
|||||||
]);
|
]);
|
||||||
}) as unknown as typeof fetch;
|
}) as unknown as typeof fetch;
|
||||||
|
|
||||||
await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt-token");
|
await searchPerplexity({ query: "q", model: "pplx_pro_upgraded" }, "jwt-token");
|
||||||
|
|
||||||
const body = JSON.parse(String(capturedInit?.body)) as {
|
const body = JSON.parse(String(capturedInit?.body)) as {
|
||||||
params: { is_incognito: boolean };
|
params: { is_incognito: boolean };
|
||||||
@@ -175,7 +174,7 @@ describe("searchPerplexity", () => {
|
|||||||
headers: { "content-type": "text/event-stream" },
|
headers: { "content-type": "text/event-stream" },
|
||||||
})) as unknown as typeof fetch;
|
})) as unknown as typeof fetch;
|
||||||
|
|
||||||
const result = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt");
|
const result = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded" }, "jwt");
|
||||||
|
|
||||||
expect(result.answer).toBe("answer");
|
expect(result.answer).toBe("answer");
|
||||||
expect(cancelCalled).toBe(true);
|
expect(cancelCalled).toBe(true);
|
||||||
@@ -185,7 +184,7 @@ describe("searchPerplexity", () => {
|
|||||||
for (const status of [401, 403]) {
|
for (const status of [401, 403]) {
|
||||||
globalThis.fetch = (async () => new Response("auth fail", { status })) as unknown as typeof fetch;
|
globalThis.fetch = (async () => new Response("auth fail", { status })) as unknown as typeof fetch;
|
||||||
|
|
||||||
await expect(searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt")).rejects.toMatchObject({
|
await expect(searchPerplexity({ query: "q", model: "pplx_pro_upgraded" }, "jwt")).rejects.toMatchObject({
|
||||||
name: "SearchError",
|
name: "SearchError",
|
||||||
code: "AUTH",
|
code: "AUTH",
|
||||||
});
|
});
|
||||||
@@ -195,7 +194,7 @@ describe("searchPerplexity", () => {
|
|||||||
test("maps 429 responses to RATE_LIMIT error", async () => {
|
test("maps 429 responses to RATE_LIMIT error", async () => {
|
||||||
globalThis.fetch = (async () => new Response("rate limited", { status: 429 })) as unknown as typeof fetch;
|
globalThis.fetch = (async () => new Response("rate limited", { status: 429 })) as unknown as typeof fetch;
|
||||||
|
|
||||||
await expect(searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt")).rejects.toMatchObject({
|
await expect(searchPerplexity({ query: "q", model: "pplx_pro_upgraded" }, "jwt")).rejects.toMatchObject({
|
||||||
name: "SearchError",
|
name: "SearchError",
|
||||||
code: "RATE_LIMIT",
|
code: "RATE_LIMIT",
|
||||||
});
|
});
|
||||||
@@ -223,7 +222,7 @@ describe("searchPerplexity", () => {
|
|||||||
},
|
},
|
||||||
])) as unknown as typeof fetch;
|
])) as unknown as typeof fetch;
|
||||||
|
|
||||||
const result = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt");
|
const result = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded" }, "jwt");
|
||||||
|
|
||||||
expect(result.sources).toHaveLength(2);
|
expect(result.sources).toHaveLength(2);
|
||||||
expect(result.sources[0].url).toBe("https://example.com/path");
|
expect(result.sources[0].url).toBe("https://example.com/path");
|
||||||
@@ -245,7 +244,7 @@ describe("searchPerplexity", () => {
|
|||||||
},
|
},
|
||||||
])) as unknown as typeof fetch;
|
])) as unknown as typeof fetch;
|
||||||
|
|
||||||
const result = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt");
|
const result = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded" }, "jwt");
|
||||||
expect(result.answer).toBe("markdown answer");
|
expect(result.answer).toBe("markdown answer");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -263,7 +262,7 @@ describe("searchPerplexity", () => {
|
|||||||
},
|
},
|
||||||
])) as unknown as typeof fetch;
|
])) as unknown as typeof fetch;
|
||||||
|
|
||||||
const askTextResult = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt");
|
const askTextResult = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded" }, "jwt");
|
||||||
expect(askTextResult.answer).toBe("ask answer");
|
expect(askTextResult.answer).toBe("ask answer");
|
||||||
|
|
||||||
globalThis.fetch = (async () =>
|
globalThis.fetch = (async () =>
|
||||||
@@ -276,7 +275,7 @@ describe("searchPerplexity", () => {
|
|||||||
},
|
},
|
||||||
])) as unknown as typeof fetch;
|
])) as unknown as typeof fetch;
|
||||||
|
|
||||||
const textResult = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt");
|
const textResult = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded" }, "jwt");
|
||||||
expect(textResult.answer).toBe("text fallback");
|
expect(textResult.answer).toBe("text fallback");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -286,7 +285,7 @@ describe("searchPerplexity", () => {
|
|||||||
|
|
||||||
let thrown: unknown;
|
let thrown: unknown;
|
||||||
try {
|
try {
|
||||||
await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt");
|
await searchPerplexity({ query: "q", model: "pplx_pro_upgraded" }, "jwt");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
thrown = error;
|
thrown = error;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user