@oh-my-pi/pi-coding-agent 15.9.5 → 15.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +98 -1
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/gallery-cli.d.ts +43 -0
- package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
- package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
- package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
- package/dist/types/cli/gallery-screenshot.d.ts +35 -0
- package/dist/types/commands/gallery.d.ts +47 -0
- package/dist/types/config/keybindings.d.ts +10 -2
- package/dist/types/config/model-id-affixes.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +8 -1
- package/dist/types/config/settings-schema.d.ts +43 -7
- package/dist/types/edit/file-snapshot-store.d.ts +1 -1
- package/dist/types/eval/backend.d.ts +6 -6
- package/dist/types/eval/bridge-timeout.d.ts +27 -0
- package/dist/types/eval/idle-timeout.d.ts +16 -14
- package/dist/types/eval/js/executor.d.ts +3 -3
- package/dist/types/eval/py/executor.d.ts +2 -2
- package/dist/types/eval/py/spawn-options.d.ts +58 -0
- package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
- package/dist/types/lsp/types.d.ts +10 -0
- package/dist/types/main.d.ts +3 -2
- package/dist/types/memory-backend/index.d.ts +2 -1
- package/dist/types/memory-backend/resolve.d.ts +1 -1
- package/dist/types/memory-backend/types.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +5 -0
- package/dist/types/modes/components/copy-selector.d.ts +22 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +18 -0
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/index.d.ts +5 -4
- package/dist/types/modes/interactive-mode.d.ts +2 -2
- package/dist/types/modes/setup-version.d.ts +11 -0
- package/dist/types/modes/setup-wizard/index.d.ts +2 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/modes/utils/copy-targets.d.ts +53 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/telemetry-export.d.ts +1 -1
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +15 -7
- package/dist/types/tools/render-utils.d.ts +33 -0
- package/dist/types/tools/renderers.d.ts +16 -2
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/tui/code-cell.d.ts +6 -0
- package/dist/types/tui/output-block.d.ts +11 -0
- package/dist/types/web/scrapers/github.d.ts +22 -0
- package/dist/types/web/search/providers/perplexity.d.ts +8 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +9 -9
- package/scripts/dev-launch +42 -0
- package/scripts/dev-launch-preload.ts +19 -0
- package/src/autoresearch/dashboard.ts +11 -21
- package/src/cli/args.ts +2 -2
- package/src/cli/claude-trace-cli.ts +13 -1
- package/src/cli/gallery-cli.ts +223 -0
- package/src/cli/gallery-fixtures/agentic.ts +292 -0
- package/src/cli/gallery-fixtures/codeintel.ts +188 -0
- package/src/cli/gallery-fixtures/edit.ts +194 -0
- package/src/cli/gallery-fixtures/fs.ts +153 -0
- package/src/cli/gallery-fixtures/index.ts +40 -0
- package/src/cli/gallery-fixtures/interaction.ts +49 -0
- package/src/cli/gallery-fixtures/memory.ts +81 -0
- package/src/cli/gallery-fixtures/misc.ts +221 -0
- package/src/cli/gallery-fixtures/search.ts +213 -0
- package/src/cli/gallery-fixtures/shell.ts +167 -0
- package/src/cli/gallery-fixtures/types.ts +41 -0
- package/src/cli/gallery-fixtures/web.ts +158 -0
- package/src/cli/gallery-screenshot.ts +279 -0
- package/src/cli-commands.ts +1 -0
- package/src/commands/gallery.ts +52 -0
- package/src/commands/launch.ts +1 -1
- package/src/config/keybindings.ts +68 -2
- package/src/config/model-equivalence.ts +35 -12
- package/src/config/model-id-affixes.ts +39 -22
- package/src/config/model-registry.ts +16 -16
- package/src/config/settings-schema.ts +29 -6
- package/src/config/settings.ts +11 -0
- package/src/dap/client.ts +14 -16
- package/src/debug/raw-sse.ts +18 -4
- package/src/edit/file-snapshot-store.ts +1 -1
- package/src/edit/index.ts +1 -1
- package/src/edit/renderer.ts +43 -55
- package/src/edit/streaming.ts +1 -1
- package/src/eval/__tests__/agent-bridge.test.ts +102 -58
- package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
- package/src/eval/__tests__/idle-timeout.test.ts +26 -12
- package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
- package/src/eval/__tests__/llm-bridge.test.ts +10 -10
- package/src/eval/agent-bridge.ts +38 -12
- package/src/eval/backend.ts +6 -6
- package/src/eval/bridge-timeout.ts +44 -0
- package/src/eval/idle-timeout.ts +33 -15
- package/src/eval/js/executor.ts +10 -10
- package/src/eval/llm-bridge.ts +4 -5
- package/src/eval/py/executor.ts +6 -6
- package/src/eval/py/kernel.ts +11 -1
- package/src/eval/py/spawn-options.ts +126 -0
- package/src/export/ttsr.ts +9 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/plugins/doctor.ts +0 -1
- package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
- package/src/goals/tools/goal-tool.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/lsp/client.ts +179 -52
- package/src/lsp/index.ts +38 -4
- package/src/lsp/render.ts +3 -3
- package/src/lsp/types.ts +10 -0
- package/src/main.ts +47 -52
- package/src/memory-backend/index.ts +13 -1
- package/src/memory-backend/resolve.ts +3 -5
- package/src/memory-backend/types.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +13 -4
- package/src/modes/components/assistant-message.ts +22 -1
- package/src/modes/components/copy-selector.ts +249 -0
- package/src/modes/components/custom-editor.ts +10 -1
- package/src/modes/components/extensions/extension-list.ts +17 -8
- package/src/modes/components/history-search.ts +19 -11
- package/src/modes/components/model-selector.ts +125 -29
- package/src/modes/components/oauth-selector.ts +28 -12
- package/src/modes/components/session-observer-overlay.ts +13 -15
- package/src/modes/components/session-selector.ts +24 -13
- package/src/modes/components/status-line.ts +3 -5
- package/src/modes/components/tool-execution.ts +83 -24
- package/src/modes/components/tree-selector.ts +19 -7
- package/src/modes/components/user-message-selector.ts +25 -14
- package/src/modes/controllers/command-controller.ts +13 -118
- package/src/modes/controllers/event-controller.ts +26 -10
- package/src/modes/controllers/input-controller.ts +11 -3
- package/src/modes/controllers/selector-controller.ts +40 -3
- package/src/modes/index.ts +5 -4
- package/src/modes/interactive-mode.ts +21 -7
- package/src/modes/setup-version.ts +11 -0
- package/src/modes/setup-wizard/index.ts +3 -2
- package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
- package/src/modes/theme/theme.ts +46 -10
- package/src/modes/types.ts +2 -2
- package/src/modes/utils/context-usage.ts +10 -6
- package/src/modes/utils/copy-targets.ts +254 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/sdk.ts +21 -23
- package/src/session/agent-session.ts +13 -9
- package/src/slash-commands/builtin-registry.ts +4 -12
- package/src/slash-commands/helpers/usage-report.ts +2 -0
- package/src/task/executor.ts +20 -2
- package/src/task/render.ts +37 -11
- package/src/telemetry-export.ts +25 -7
- package/src/tools/bash.ts +18 -8
- package/src/tools/browser/render.ts +5 -4
- package/src/tools/debug.ts +3 -3
- package/src/tools/eval-backends.ts +6 -17
- package/src/tools/eval-render.ts +28 -10
- package/src/tools/eval.ts +19 -23
- package/src/tools/fetch.ts +99 -89
- package/src/tools/read.ts +7 -7
- package/src/tools/render-utils.ts +63 -3
- package/src/tools/renderers.ts +16 -1
- package/src/tools/report-tool-issue.ts +1 -1
- package/src/tools/search.ts +173 -81
- package/src/tools/ssh.ts +21 -8
- package/src/tools/todo.ts +20 -7
- package/src/tools/write.ts +39 -9
- package/src/tui/code-cell.ts +19 -4
- package/src/tui/output-block.ts +14 -0
- package/src/web/scrapers/github.ts +255 -3
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/providers/perplexity.ts +199 -51
- package/src/web/search/render.ts +42 -57
- package/src/web/search/types.ts +5 -1
- package/dist/types/eval/heartbeat.d.ts +0 -45
- package/src/eval/__tests__/heartbeat.test.ts +0 -84
- package/src/eval/__tests__/shared-executors.test.ts +0 -609
- package/src/eval/heartbeat.ts +0 -74
- /package/dist/types/eval/__tests__/{heartbeat.test.d.ts → bridge-timeout.test.d.ts} +0 -0
- /package/dist/types/eval/__tests__/{shared-executors.test.d.ts → kernel-spawn.test.d.ts} +0 -0
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { $env, ptree } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
|
-
import { buildResult, loadPage } from "./types";
|
|
3
|
+
import { buildResult, formatMediaDuration, loadPage } from "./types";
|
|
4
4
|
|
|
5
5
|
interface GitHubUrl {
|
|
6
|
-
type:
|
|
6
|
+
type:
|
|
7
|
+
| "blob"
|
|
8
|
+
| "tree"
|
|
9
|
+
| "repo"
|
|
10
|
+
| "issue"
|
|
11
|
+
| "issues"
|
|
12
|
+
| "pull"
|
|
13
|
+
| "pulls"
|
|
14
|
+
| "discussion"
|
|
15
|
+
| "discussions"
|
|
16
|
+
| "actions-run"
|
|
17
|
+
| "actions-job"
|
|
18
|
+
| "other";
|
|
7
19
|
owner: string;
|
|
8
20
|
repo: string;
|
|
9
21
|
ref?: string;
|
|
10
22
|
path?: string;
|
|
11
23
|
number?: number;
|
|
24
|
+
runId?: number;
|
|
25
|
+
jobId?: number;
|
|
12
26
|
}
|
|
13
27
|
|
|
14
28
|
interface GitHubIssueComment {
|
|
@@ -20,7 +34,7 @@ interface GitHubIssueComment {
|
|
|
20
34
|
/**
|
|
21
35
|
* Parse GitHub URL into components
|
|
22
36
|
*/
|
|
23
|
-
function parseGitHubUrl(url: string): GitHubUrl | null {
|
|
37
|
+
export function parseGitHubUrl(url: string): GitHubUrl | null {
|
|
24
38
|
try {
|
|
25
39
|
const parsed = new URL(url);
|
|
26
40
|
if (parsed.hostname !== "github.com") return null;
|
|
@@ -54,6 +68,20 @@ function parseGitHubUrl(url: string): GitHubUrl | null {
|
|
|
54
68
|
return { type: "pulls", owner, repo };
|
|
55
69
|
case "pulls":
|
|
56
70
|
return { type: "pulls", owner, repo };
|
|
71
|
+
case "actions": {
|
|
72
|
+
// /actions/runs/{runId} → run summary + jobs
|
|
73
|
+
// /actions/runs/{runId}/job/{jobId} → single job (web URL uses singular "job")
|
|
74
|
+
// /actions/runs/{runId}/jobs/{jobId} → single job (API-style plural)
|
|
75
|
+
if (subParts[0] === "runs" && /^\d+$/.test(subParts[1] ?? "")) {
|
|
76
|
+
const runId = parseInt(subParts[1], 10);
|
|
77
|
+
const seg = subParts[2];
|
|
78
|
+
if ((seg === "job" || seg === "jobs") && /^\d+$/.test(subParts[3] ?? "")) {
|
|
79
|
+
return { type: "actions-job", owner, repo, runId, jobId: parseInt(subParts[3], 10) };
|
|
80
|
+
}
|
|
81
|
+
return { type: "actions-run", owner, repo, runId };
|
|
82
|
+
}
|
|
83
|
+
return { type: "other", owner, repo };
|
|
84
|
+
}
|
|
57
85
|
case "discussions":
|
|
58
86
|
if (subParts.length > 0 && /^\d+$/.test(subParts[0])) {
|
|
59
87
|
return { type: "discussion", owner, repo, number: parseInt(subParts[0], 10) };
|
|
@@ -371,6 +399,212 @@ async function renderGitHubRepo(
|
|
|
371
399
|
return { content: md, ok: true };
|
|
372
400
|
}
|
|
373
401
|
|
|
402
|
+
interface GitHubActionsStep {
|
|
403
|
+
name: string;
|
|
404
|
+
status: string;
|
|
405
|
+
conclusion: string | null;
|
|
406
|
+
number: number;
|
|
407
|
+
started_at: string | null;
|
|
408
|
+
completed_at: string | null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
interface GitHubActionsJob {
|
|
412
|
+
id: number;
|
|
413
|
+
run_id: number;
|
|
414
|
+
name: string;
|
|
415
|
+
status: string;
|
|
416
|
+
conclusion: string | null;
|
|
417
|
+
started_at: string | null;
|
|
418
|
+
completed_at: string | null;
|
|
419
|
+
html_url: string | null;
|
|
420
|
+
steps?: GitHubActionsStep[];
|
|
421
|
+
runner_name?: string | null;
|
|
422
|
+
labels?: string[];
|
|
423
|
+
workflow_name?: string | null;
|
|
424
|
+
head_branch?: string | null;
|
|
425
|
+
head_sha?: string;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
interface GitHubActionsRun {
|
|
429
|
+
id: number;
|
|
430
|
+
name?: string | null;
|
|
431
|
+
display_title?: string;
|
|
432
|
+
run_number: number;
|
|
433
|
+
run_attempt?: number;
|
|
434
|
+
event: string;
|
|
435
|
+
status: string;
|
|
436
|
+
conclusion: string | null;
|
|
437
|
+
head_branch?: string | null;
|
|
438
|
+
head_sha?: string;
|
|
439
|
+
html_url: string;
|
|
440
|
+
created_at: string;
|
|
441
|
+
updated_at: string;
|
|
442
|
+
run_started_at?: string;
|
|
443
|
+
actor?: { login: string };
|
|
444
|
+
triggering_actor?: { login: string };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Combine status + conclusion into a single label, e.g. `completed (failure)`. */
|
|
448
|
+
function statusLabel(status: string, conclusion: string | null | undefined): string {
|
|
449
|
+
return conclusion ? `${status} (${conclusion})` : status;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Wall-clock duration between two ISO timestamps, formatted HH:MM:SS / MM:SS. Empty when unknown. */
|
|
453
|
+
function actionDuration(start?: string | null, end?: string | null): string {
|
|
454
|
+
if (!start || !end) return "";
|
|
455
|
+
const ms = Date.parse(end) - Date.parse(start);
|
|
456
|
+
if (!Number.isFinite(ms) || ms < 0) return "";
|
|
457
|
+
return formatMediaDuration(Math.round(ms / 1000));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Escape `|` so step/job names can't break a markdown table row. */
|
|
461
|
+
function escapeCell(text: string): string {
|
|
462
|
+
return text.replaceAll("|", "\\|");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Strip the per-line ISO-8601 timestamp prefix GitHub prepends to every job log line.
|
|
467
|
+
* Cuts ~28 bytes/line of noise while preserving the message text. Also drops the leading
|
|
468
|
+
* UTF-8 BOM GitHub puts at the start of the log file (otherwise the first line's timestamp
|
|
469
|
+
* survives because `^` no longer sits before a digit).
|
|
470
|
+
*/
|
|
471
|
+
export function stripActionsLogTimestamps(logs: string): string {
|
|
472
|
+
return logs.replace(/^\uFEFF/, "").replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z /gm, "");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Render a job's steps as a markdown table. Empty string when there are no steps. */
|
|
476
|
+
function renderActionsSteps(steps?: GitHubActionsStep[]): string {
|
|
477
|
+
if (!steps || steps.length === 0) return "";
|
|
478
|
+
let md = "| # | Step | Status | Conclusion | Duration |\n";
|
|
479
|
+
md += "|---|------|--------|------------|----------|\n";
|
|
480
|
+
for (const step of steps) {
|
|
481
|
+
const dur = actionDuration(step.started_at, step.completed_at) || "-";
|
|
482
|
+
md += `| ${step.number} | ${escapeCell(step.name)} | ${step.status} | ${step.conclusion ?? "-"} | ${dur} |\n`;
|
|
483
|
+
}
|
|
484
|
+
return `${md}\n`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Run-level metadata lines shared by the run and job renderers. */
|
|
488
|
+
function renderActionsRunMeta(run: GitHubActionsRun): string {
|
|
489
|
+
let md = `**Workflow:** ${run.name ?? "(unknown)"}\n`;
|
|
490
|
+
md += `**Run:** #${run.run_number}`;
|
|
491
|
+
if (run.run_attempt && run.run_attempt > 1) md += ` (attempt ${run.run_attempt})`;
|
|
492
|
+
md += ` · ${statusLabel(run.status, run.conclusion)}\n`;
|
|
493
|
+
if (run.head_branch) {
|
|
494
|
+
md += `**Branch:** ${run.head_branch}${run.head_sha ? ` @ ${run.head_sha.slice(0, 7)}` : ""}\n`;
|
|
495
|
+
}
|
|
496
|
+
const actor = run.triggering_actor?.login ?? run.actor?.login;
|
|
497
|
+
md += `**Event:** ${run.event}${actor ? ` · by @${actor}` : ""}\n`;
|
|
498
|
+
const started = run.run_started_at ?? run.created_at;
|
|
499
|
+
const dur = actionDuration(started, run.updated_at);
|
|
500
|
+
md += `Started: ${started}${dur ? ` · Duration: ${dur}` : ""}\n`;
|
|
501
|
+
md += `URL: ${run.html_url}\n`;
|
|
502
|
+
return md;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** Fetch a job's plain-text logs. Returns null when unavailable (no token / expired / private). */
|
|
506
|
+
async function fetchGitHubJobLogs(
|
|
507
|
+
owner: string,
|
|
508
|
+
repo: string,
|
|
509
|
+
jobId: number,
|
|
510
|
+
timeout: number,
|
|
511
|
+
signal?: AbortSignal,
|
|
512
|
+
): Promise<string | null> {
|
|
513
|
+
const headers: Record<string, string> = {
|
|
514
|
+
Accept: "application/vnd.github+json",
|
|
515
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
516
|
+
};
|
|
517
|
+
const token = $env.GITHUB_TOKEN || $env.GH_TOKEN;
|
|
518
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
519
|
+
|
|
520
|
+
// 302 → signed log URL on a different origin; fetch strips Authorization on the cross-origin hop.
|
|
521
|
+
const result = await loadPage(`https://api.github.com/repos/${owner}/${repo}/actions/jobs/${jobId}/logs`, {
|
|
522
|
+
timeout,
|
|
523
|
+
headers,
|
|
524
|
+
signal,
|
|
525
|
+
});
|
|
526
|
+
return result.ok && result.content ? result.content : null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Render a workflow run: run metadata plus a per-job breakdown. Steps are listed for any job that
|
|
531
|
+
* did not succeed (the debugging-relevant ones); successful jobs collapse to a single line.
|
|
532
|
+
*/
|
|
533
|
+
async function renderGitHubActionsRun(
|
|
534
|
+
gh: GitHubUrl,
|
|
535
|
+
timeout: number,
|
|
536
|
+
signal?: AbortSignal,
|
|
537
|
+
): Promise<{ content: string; ok: boolean }> {
|
|
538
|
+
const runResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}/actions/runs/${gh.runId}`, timeout, signal);
|
|
539
|
+
if (!runResult.ok || !runResult.data) return { content: "", ok: false };
|
|
540
|
+
|
|
541
|
+
const run = runResult.data as GitHubActionsRun;
|
|
542
|
+
let md = `# ${run.display_title || run.name || `Run #${run.run_number}`}\n\n`;
|
|
543
|
+
md += renderActionsRunMeta(run);
|
|
544
|
+
md += `\n---\n\n`;
|
|
545
|
+
|
|
546
|
+
const jobsResult = await fetchGitHubApi(
|
|
547
|
+
`/repos/${gh.owner}/${gh.repo}/actions/runs/${gh.runId}/jobs?per_page=100`,
|
|
548
|
+
timeout,
|
|
549
|
+
signal,
|
|
550
|
+
);
|
|
551
|
+
if (jobsResult.ok && jobsResult.data) {
|
|
552
|
+
const jobs = (jobsResult.data as { jobs?: GitHubActionsJob[] }).jobs ?? [];
|
|
553
|
+
md += `## Jobs (${jobs.length})\n\n`;
|
|
554
|
+
for (const job of jobs) {
|
|
555
|
+
const dur = actionDuration(job.started_at, job.completed_at);
|
|
556
|
+
md += `### ${escapeCell(job.name)} — ${statusLabel(job.status, job.conclusion)}${dur ? ` (${dur})` : ""}\n\n`;
|
|
557
|
+
if (job.conclusion !== "success") {
|
|
558
|
+
md += renderActionsSteps(job.steps);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return { content: md, ok: true };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Render a single workflow job: run context, step table, and the full job logs.
|
|
568
|
+
*/
|
|
569
|
+
async function renderGitHubActionsJob(
|
|
570
|
+
gh: GitHubUrl,
|
|
571
|
+
timeout: number,
|
|
572
|
+
signal?: AbortSignal,
|
|
573
|
+
): Promise<{ content: string; ok: boolean }> {
|
|
574
|
+
const jobResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}/actions/jobs/${gh.jobId}`, timeout, signal);
|
|
575
|
+
if (!jobResult.ok || !jobResult.data) return { content: "", ok: false };
|
|
576
|
+
|
|
577
|
+
const job = jobResult.data as GitHubActionsJob;
|
|
578
|
+
|
|
579
|
+
// Best-effort run context for nicer headers; the job render stands on its own without it.
|
|
580
|
+
const runResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}/actions/runs/${job.run_id}`, timeout, signal);
|
|
581
|
+
const run = runResult.ok && runResult.data ? (runResult.data as GitHubActionsRun) : null;
|
|
582
|
+
|
|
583
|
+
let md = `# ${escapeCell(job.name)}\n\n`;
|
|
584
|
+
if (run) {
|
|
585
|
+
md += renderActionsRunMeta(run);
|
|
586
|
+
} else if (job.workflow_name) {
|
|
587
|
+
md += `**Workflow:** ${job.workflow_name}\n`;
|
|
588
|
+
if (job.head_branch) md += `**Branch:** ${job.head_branch}\n`;
|
|
589
|
+
}
|
|
590
|
+
const dur = actionDuration(job.started_at, job.completed_at);
|
|
591
|
+
md += `**Job:** ${escapeCell(job.name)} · ${statusLabel(job.status, job.conclusion)}${dur ? ` · ${dur}` : ""}\n`;
|
|
592
|
+
if (job.runner_name) md += `**Runner:** ${job.runner_name}\n`;
|
|
593
|
+
if (job.html_url) md += `URL: ${job.html_url}\n`;
|
|
594
|
+
md += `\n---\n\n`;
|
|
595
|
+
|
|
596
|
+
const steps = renderActionsSteps(job.steps);
|
|
597
|
+
if (steps) md += `## Steps\n\n${steps}`;
|
|
598
|
+
|
|
599
|
+
const logs = await fetchGitHubJobLogs(gh.owner, gh.repo, job.id, timeout, signal);
|
|
600
|
+
md += `## Logs\n\n`;
|
|
601
|
+
md += logs
|
|
602
|
+
? stripActionsLogTimestamps(logs)
|
|
603
|
+
: "*Logs unavailable — requires a GITHUB_TOKEN/GH_TOKEN with read access, or the run's logs have expired.*\n";
|
|
604
|
+
|
|
605
|
+
return { content: md, ok: true };
|
|
606
|
+
}
|
|
607
|
+
|
|
374
608
|
/**
|
|
375
609
|
* Handle GitHub URLs specially
|
|
376
610
|
*/
|
|
@@ -445,6 +679,24 @@ export const handleGitHub: SpecialHandler = async (
|
|
|
445
679
|
}
|
|
446
680
|
break;
|
|
447
681
|
}
|
|
682
|
+
|
|
683
|
+
case "actions-run": {
|
|
684
|
+
notes.push(`Fetched via GitHub API`);
|
|
685
|
+
const result = await renderGitHubActionsRun(gh, timeout, signal);
|
|
686
|
+
if (result.ok) {
|
|
687
|
+
return buildResult(result.content, { url, method: "github-actions-run", fetchedAt, notes });
|
|
688
|
+
}
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
case "actions-job": {
|
|
693
|
+
notes.push(`Fetched via GitHub API`);
|
|
694
|
+
const result = await renderGitHubActionsJob(gh, timeout, signal);
|
|
695
|
+
if (result.ok) {
|
|
696
|
+
return buildResult(result.content, { url, method: "github-actions-job", fetchedAt, notes });
|
|
697
|
+
}
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
448
700
|
}
|
|
449
701
|
|
|
450
702
|
// Fall back to null (let normal rendering handle it)
|
|
@@ -113,8 +113,9 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
113
113
|
const notes: string[] = [];
|
|
114
114
|
const videoUrl = `https://www.youtube.com/watch?v=${yt.videoId}`;
|
|
115
115
|
|
|
116
|
-
// Prefer Parallel extract when
|
|
117
|
-
|
|
116
|
+
// Prefer Parallel extract when it sits in the reader chain and creds exist
|
|
117
|
+
const fetchPreference = settings.get("providers.fetch");
|
|
118
|
+
if ((fetchPreference === "auto" || fetchPreference === "parallel") && findParallelApiKey(storage)) {
|
|
118
119
|
try {
|
|
119
120
|
const parallelResult = await extractWithParallel(
|
|
120
121
|
[videoUrl],
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Perplexity Web Search Provider
|
|
3
3
|
*
|
|
4
|
-
* Supports
|
|
4
|
+
* Supports four auth modes:
|
|
5
5
|
* - Cookies (`PERPLEXITY_COOKIES`) via `www.perplexity.ai/rest/sse/perplexity_ask`
|
|
6
6
|
* - OAuth/session bearer via `AuthStorage` and `www.perplexity.ai/rest/sse/perplexity_ask`
|
|
7
7
|
* - API key (`PERPLEXITY_API_KEY`) via `api.perplexity.ai/chat/completions`
|
|
8
|
+
* - Anonymous via `www.perplexity.ai/rest/sse/perplexity_ask`
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { type AuthStorage, getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
@@ -32,6 +33,8 @@ const DEFAULT_NUM_SEARCH_RESULTS = 20;
|
|
|
32
33
|
const OAUTH_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
|
33
34
|
const OAUTH_API_VERSION = "2.18";
|
|
34
35
|
const OAUTH_USER_AGENT = "Perplexity/641 CFNetwork/1568 Darwin/25.2.0";
|
|
36
|
+
const ANONYMOUS_USER_AGENT =
|
|
37
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
|
|
35
38
|
|
|
36
39
|
type PerplexityAuth =
|
|
37
40
|
| {
|
|
@@ -45,6 +48,9 @@ type PerplexityAuth =
|
|
|
45
48
|
| {
|
|
46
49
|
type: "cookies";
|
|
47
50
|
cookies: string;
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
type: "anonymous";
|
|
48
54
|
};
|
|
49
55
|
|
|
50
56
|
interface PerplexityOAuthStreamMarkdownBlock {
|
|
@@ -149,6 +155,112 @@ function mergeOAuthEventSnapshot(
|
|
|
149
155
|
|
|
150
156
|
return merged;
|
|
151
157
|
}
|
|
158
|
+
|
|
159
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
160
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
161
|
+
return value as Record<string, unknown>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parseJson(text: string): unknown | null {
|
|
165
|
+
try {
|
|
166
|
+
return JSON.parse(text);
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function textFromChunks(value: unknown): string | null {
|
|
173
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
174
|
+
let text = "";
|
|
175
|
+
for (const chunk of value) {
|
|
176
|
+
if (typeof chunk !== "string") return null;
|
|
177
|
+
text += chunk;
|
|
178
|
+
}
|
|
179
|
+
return text.length > 0 ? text : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function textFromStructuredAnswer(value: unknown): string | null {
|
|
183
|
+
if (!Array.isArray(value)) return null;
|
|
184
|
+
for (const item of value) {
|
|
185
|
+
const record = asRecord(item);
|
|
186
|
+
if (!record) continue;
|
|
187
|
+
const text = record.text;
|
|
188
|
+
if (typeof text === "string" && text.length > 0) return text;
|
|
189
|
+
const chunks = textFromChunks(record.chunks);
|
|
190
|
+
if (chunks) return chunks;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function answerFromTextPayload(payload: Record<string, unknown>): string | null {
|
|
196
|
+
const structured = textFromStructuredAnswer(payload.structured_answer);
|
|
197
|
+
if (structured) return structured;
|
|
198
|
+
const chunks = textFromChunks(payload.chunks);
|
|
199
|
+
if (chunks) return chunks;
|
|
200
|
+
const answer = payload.answer;
|
|
201
|
+
return typeof answer === "string" && answer.length > 0 ? answer : null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function parseOAuthTextPayload(text: string): Record<string, unknown> | null {
|
|
205
|
+
const parsed = parseJson(text);
|
|
206
|
+
const direct = asRecord(parsed);
|
|
207
|
+
if (direct) return direct;
|
|
208
|
+
if (!Array.isArray(parsed)) return null;
|
|
209
|
+
|
|
210
|
+
for (const item of parsed) {
|
|
211
|
+
const step = asRecord(item);
|
|
212
|
+
const content = asRecord(step?.content);
|
|
213
|
+
const answer = content?.answer;
|
|
214
|
+
if (typeof answer !== "string" || answer.length === 0) continue;
|
|
215
|
+
const payload = asRecord(parseJson(answer));
|
|
216
|
+
if (payload) return payload;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function parseOAuthTextAnswer(text: string): string {
|
|
222
|
+
const payload = parseOAuthTextPayload(text);
|
|
223
|
+
if (payload) {
|
|
224
|
+
const answer = answerFromTextPayload(payload);
|
|
225
|
+
if (answer) return answer;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const parsed = parseJson(text);
|
|
229
|
+
if (!Array.isArray(parsed)) return text;
|
|
230
|
+
for (const item of parsed) {
|
|
231
|
+
const step = asRecord(item);
|
|
232
|
+
const content = asRecord(step?.content);
|
|
233
|
+
const answer = content?.answer;
|
|
234
|
+
if (typeof answer === "string" && answer.length > 0) return answer;
|
|
235
|
+
}
|
|
236
|
+
return text;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function sourcesFromTextPayload(text: string | undefined): SearchSource[] {
|
|
240
|
+
if (!text) return [];
|
|
241
|
+
const payload = parseOAuthTextPayload(text);
|
|
242
|
+
const webResults = payload?.web_results;
|
|
243
|
+
if (!Array.isArray(webResults) || webResults.length === 0) return [];
|
|
244
|
+
|
|
245
|
+
const sources: SearchSource[] = [];
|
|
246
|
+
for (const value of webResults) {
|
|
247
|
+
const result = asRecord(value);
|
|
248
|
+
if (!result) continue;
|
|
249
|
+
const url = result.url;
|
|
250
|
+
if (typeof url !== "string" || url.length === 0) continue;
|
|
251
|
+
const name = result.name ?? result.title;
|
|
252
|
+
const snippet = result.snippet;
|
|
253
|
+
const timestamp = result.timestamp;
|
|
254
|
+
sources.push({
|
|
255
|
+
title: typeof name === "string" && name.length > 0 ? name : url,
|
|
256
|
+
url,
|
|
257
|
+
snippet: typeof snippet === "string" ? snippet : undefined,
|
|
258
|
+
publishedDate: typeof timestamp === "string" ? timestamp : undefined,
|
|
259
|
+
ageSeconds: dateToAgeSeconds(typeof timestamp === "string" ? timestamp : undefined),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return sources;
|
|
263
|
+
}
|
|
152
264
|
export interface PerplexitySearchParams {
|
|
153
265
|
signal?: AbortSignal;
|
|
154
266
|
query: string;
|
|
@@ -216,7 +328,7 @@ async function findPerplexityAuth(
|
|
|
216
328
|
authStorage: AuthStorage,
|
|
217
329
|
sessionId: string | undefined,
|
|
218
330
|
signal: AbortSignal | undefined,
|
|
219
|
-
): Promise<PerplexityAuth
|
|
331
|
+
): Promise<PerplexityAuth> {
|
|
220
332
|
// 1. PERPLEXITY_COOKIES env var
|
|
221
333
|
const cookies = $env.PERPLEXITY_COOKIES?.trim();
|
|
222
334
|
if (cookies) {
|
|
@@ -235,7 +347,9 @@ async function findPerplexityAuth(
|
|
|
235
347
|
if (apiKey) {
|
|
236
348
|
return { type: "api_key", token: apiKey };
|
|
237
349
|
}
|
|
238
|
-
|
|
350
|
+
|
|
351
|
+
// 4. The consumer ask endpoint currently accepts unauthenticated browser-style requests.
|
|
352
|
+
return { type: "anonymous" };
|
|
239
353
|
}
|
|
240
354
|
|
|
241
355
|
/** Call Perplexity API-key endpoint. */
|
|
@@ -284,7 +398,7 @@ function buildOAuthSources(event: PerplexityOAuthStreamEvent): SearchSource[] {
|
|
|
284
398
|
}));
|
|
285
399
|
}
|
|
286
400
|
|
|
287
|
-
|
|
401
|
+
const sources = (event.sources_list ?? [])
|
|
288
402
|
.filter(source => typeof source.url === "string" && source.url.length > 0)
|
|
289
403
|
.map(source => ({
|
|
290
404
|
title: source.title ?? source.url ?? "",
|
|
@@ -293,11 +407,13 @@ function buildOAuthSources(event: PerplexityOAuthStreamEvent): SearchSource[] {
|
|
|
293
407
|
publishedDate: source.date,
|
|
294
408
|
ageSeconds: dateToAgeSeconds(source.date),
|
|
295
409
|
}));
|
|
410
|
+
if (sources.length > 0) return sources;
|
|
411
|
+
return sourcesFromTextPayload(event.text);
|
|
296
412
|
}
|
|
297
413
|
|
|
298
414
|
function buildOAuthAnswer(event: PerplexityOAuthStreamEvent): string {
|
|
299
415
|
if (!event.blocks?.length) {
|
|
300
|
-
return typeof event.text === "string" ? event.text : "";
|
|
416
|
+
return typeof event.text === "string" ? parseOAuthTextAnswer(event.text) : "";
|
|
301
417
|
}
|
|
302
418
|
|
|
303
419
|
const markdownBlock = event.blocks.find(
|
|
@@ -324,51 +440,77 @@ function buildOAuthAnswer(event: PerplexityOAuthStreamEvent): string {
|
|
|
324
440
|
}
|
|
325
441
|
}
|
|
326
442
|
if (typeof event.text === "string" && event.text.length > 0) {
|
|
327
|
-
return event.text;
|
|
443
|
+
return parseOAuthTextAnswer(event.text);
|
|
328
444
|
}
|
|
329
445
|
return "";
|
|
330
446
|
}
|
|
331
447
|
|
|
332
|
-
async function
|
|
333
|
-
auth: { type: "oauth"; token: string } | { type: "cookies"; cookies: string },
|
|
448
|
+
async function callPerplexityAsk(
|
|
449
|
+
auth: { type: "oauth"; token: string } | { type: "cookies"; cookies: string } | { type: "anonymous" },
|
|
334
450
|
params: PerplexitySearchParams,
|
|
335
451
|
): Promise<{ answer: string; sources: SearchSource[]; model?: string; requestId?: string }> {
|
|
336
452
|
const requestId = crypto.randomUUID();
|
|
337
|
-
|
|
453
|
+
// The consumer `perplexity_ask` endpoint is itself a research assistant and
|
|
454
|
+
// has no system-message slot. Prepending the API-style system prompt to the
|
|
455
|
+
// query makes the model read it as a meta-instruction and refuse with
|
|
456
|
+
// "I don't have access to web-search tools in this turn", so ask-endpoint
|
|
457
|
+
// searches send the bare query. (The API-key path still uses system_prompt
|
|
458
|
+
// as a proper `system` message.)
|
|
459
|
+
const effectiveQuery = params.query;
|
|
460
|
+
|
|
461
|
+
const headers: Record<string, string> = {
|
|
462
|
+
"Content-Type": "application/json",
|
|
463
|
+
Accept: "text/event-stream",
|
|
464
|
+
Origin: "https://www.perplexity.ai",
|
|
465
|
+
Referer: "https://www.perplexity.ai/",
|
|
466
|
+
"User-Agent": auth.type === "anonymous" ? ANONYMOUS_USER_AGENT : OAUTH_USER_AGENT,
|
|
467
|
+
"X-Request-ID": requestId,
|
|
468
|
+
};
|
|
469
|
+
if (auth.type === "oauth") {
|
|
470
|
+
// The ask endpoint authenticates via the next-auth session cookie, NOT a
|
|
471
|
+
// bearer header — a bearer (even a garbage one) is ignored and the request
|
|
472
|
+
// silently falls back to the anonymous free `turbo` model regardless of
|
|
473
|
+
// `model_preference`. The stored OAuth token IS the Perplexity session JWT
|
|
474
|
+
// (the native app injects the same value as this cookie), so sending it as
|
|
475
|
+
// the cookie is what unlocks the account's Pro model selection.
|
|
476
|
+
headers.Cookie = `__Secure-next-auth.session-token=${auth.token}`;
|
|
477
|
+
} else if (auth.type === "cookies") {
|
|
478
|
+
headers.Cookie = auth.cookies;
|
|
479
|
+
}
|
|
480
|
+
if (auth.type !== "anonymous") {
|
|
481
|
+
headers["X-App-ApiClient"] = "default";
|
|
482
|
+
headers["X-App-ApiVersion"] = OAUTH_API_VERSION;
|
|
483
|
+
headers["X-Perplexity-Request-Reason"] = "submit";
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const requestParams: Record<string, unknown> = {
|
|
487
|
+
query_str: effectiveQuery,
|
|
488
|
+
search_focus: "internet",
|
|
489
|
+
mode: "copilot",
|
|
490
|
+
model_preference: "experimental",
|
|
491
|
+
sources: ["web"],
|
|
492
|
+
attachments: [],
|
|
493
|
+
frontend_uuid: crypto.randomUUID(),
|
|
494
|
+
frontend_context_uuid: crypto.randomUUID(),
|
|
495
|
+
version: OAUTH_API_VERSION,
|
|
496
|
+
language: "en-US",
|
|
497
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
498
|
+
search_recency_filter: params.search_recency_filter ?? null,
|
|
499
|
+
is_incognito: true,
|
|
500
|
+
use_schematized_api: true,
|
|
501
|
+
skip_search_enabled: true,
|
|
502
|
+
};
|
|
503
|
+
if (auth.type === "anonymous") {
|
|
504
|
+
requestParams.send_back_text_in_streaming_api = true;
|
|
505
|
+
requestParams.source = "default";
|
|
506
|
+
}
|
|
338
507
|
|
|
339
508
|
const response = await fetch(PERPLEXITY_OAUTH_ASK_URL, {
|
|
340
509
|
method: "POST",
|
|
341
|
-
headers
|
|
342
|
-
...(auth.type === "cookies" ? { Cookie: auth.cookies } : { Authorization: `Bearer ${auth.token}` }),
|
|
343
|
-
"Content-Type": "application/json",
|
|
344
|
-
Accept: "text/event-stream",
|
|
345
|
-
Origin: "https://www.perplexity.ai",
|
|
346
|
-
Referer: "https://www.perplexity.ai/",
|
|
347
|
-
"User-Agent": OAUTH_USER_AGENT,
|
|
348
|
-
"X-App-ApiClient": "default",
|
|
349
|
-
"X-App-ApiVersion": OAUTH_API_VERSION,
|
|
350
|
-
"X-Perplexity-Request-Reason": "submit",
|
|
351
|
-
"X-Request-ID": requestId,
|
|
352
|
-
},
|
|
510
|
+
headers,
|
|
353
511
|
body: JSON.stringify({
|
|
354
512
|
query_str: effectiveQuery,
|
|
355
|
-
params:
|
|
356
|
-
query_str: effectiveQuery,
|
|
357
|
-
search_focus: "internet",
|
|
358
|
-
mode: "copilot",
|
|
359
|
-
model_preference: "pplx_pro_upgraded",
|
|
360
|
-
sources: ["web"],
|
|
361
|
-
attachments: [],
|
|
362
|
-
frontend_uuid: crypto.randomUUID(),
|
|
363
|
-
frontend_context_uuid: crypto.randomUUID(),
|
|
364
|
-
version: OAUTH_API_VERSION,
|
|
365
|
-
language: "en-US",
|
|
366
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
367
|
-
search_recency_filter: params.search_recency_filter ?? null,
|
|
368
|
-
is_incognito: true,
|
|
369
|
-
use_schematized_api: true,
|
|
370
|
-
skip_search_enabled: true,
|
|
371
|
-
},
|
|
513
|
+
params: requestParams,
|
|
372
514
|
}),
|
|
373
515
|
signal: withHardTimeout(params.signal),
|
|
374
516
|
});
|
|
@@ -379,13 +521,13 @@ async function callPerplexityOAuth(
|
|
|
379
521
|
if (classified) throw classified;
|
|
380
522
|
throw new SearchProviderError(
|
|
381
523
|
"perplexity",
|
|
382
|
-
`Perplexity
|
|
524
|
+
`Perplexity ask API error (${response.status}): ${errorText}`,
|
|
383
525
|
response.status,
|
|
384
526
|
);
|
|
385
527
|
}
|
|
386
528
|
|
|
387
529
|
if (!response.body) {
|
|
388
|
-
throw new SearchProviderError("perplexity", "Perplexity
|
|
530
|
+
throw new SearchProviderError("perplexity", "Perplexity ask API returned no response body", 500);
|
|
389
531
|
}
|
|
390
532
|
|
|
391
533
|
let answer = "";
|
|
@@ -397,7 +539,7 @@ async function callPerplexityOAuth(
|
|
|
397
539
|
for await (const event of readSseJson<PerplexityOAuthStreamEvent>(response.body, params.signal)) {
|
|
398
540
|
if (event.error_code) {
|
|
399
541
|
const message = event.error_message ?? event.error_code;
|
|
400
|
-
throw new SearchProviderError("perplexity", `Perplexity
|
|
542
|
+
throw new SearchProviderError("perplexity", `Perplexity ask stream error: ${message}`, 400);
|
|
401
543
|
}
|
|
402
544
|
|
|
403
545
|
mergedEvent = mergeOAuthEventSnapshot(mergedEvent, event);
|
|
@@ -500,20 +642,17 @@ function applySourceLimit(result: SearchResponse, limit?: number): SearchRespons
|
|
|
500
642
|
/** Execute Perplexity web search */
|
|
501
643
|
export async function searchPerplexity(params: PerplexitySearchParams): Promise<SearchResponse> {
|
|
502
644
|
const auth = await findPerplexityAuth(params.authStorage, params.sessionId, params.signal);
|
|
503
|
-
if (!auth) {
|
|
504
|
-
throw new Error("Perplexity auth not found. Set PERPLEXITY_COOKIES, PERPLEXITY_API_KEY, or login via OAuth.");
|
|
505
|
-
}
|
|
506
645
|
|
|
507
|
-
if (auth.type
|
|
508
|
-
const
|
|
646
|
+
if (auth.type !== "api_key") {
|
|
647
|
+
const askResult = await callPerplexityAsk(auth, params);
|
|
509
648
|
return applySourceLimit(
|
|
510
649
|
{
|
|
511
650
|
provider: "perplexity",
|
|
512
|
-
answer:
|
|
513
|
-
sources:
|
|
514
|
-
model:
|
|
515
|
-
requestId:
|
|
516
|
-
authMode: "oauth",
|
|
651
|
+
answer: askResult.answer || undefined,
|
|
652
|
+
sources: askResult.sources,
|
|
653
|
+
model: askResult.model,
|
|
654
|
+
requestId: askResult.requestId,
|
|
655
|
+
authMode: auth.type === "anonymous" ? "anonymous" : "oauth",
|
|
517
656
|
},
|
|
518
657
|
params.num_results,
|
|
519
658
|
);
|
|
@@ -562,6 +701,15 @@ export class PerplexityProvider extends SearchProvider {
|
|
|
562
701
|
return !!$env.PERPLEXITY_COOKIES?.trim() || authStorage.hasAuth("perplexity") || !!findApiKey();
|
|
563
702
|
}
|
|
564
703
|
|
|
704
|
+
/**
|
|
705
|
+
* Perplexity accepts anonymous browser-style ask requests, but keep auto
|
|
706
|
+
* provider selection credential-gated so a configured provider keeps priority
|
|
707
|
+
* over the anonymous fallback.
|
|
708
|
+
*/
|
|
709
|
+
isExplicitlyAvailable(_authStorage: AuthStorage): boolean {
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
|
|
565
713
|
search(params: SearchParams): Promise<SearchResponse> {
|
|
566
714
|
return searchPerplexity({
|
|
567
715
|
signal: params.signal,
|