pplx-npx-search 0.2.1 → 0.3.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 +18 -0
- package/README.md +44 -2
- package/package.json +2 -2
- package/src/artifacts.js +85 -0
- package/src/cli.js +227 -18
- package/src/computer.js +193 -0
- package/src/labs.js +3 -0
- package/src/search.js +3 -0
- package/src/timeout.js +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to pplx-cli will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-05-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Standard artifacts.** Query-producing commands now save run folders by default with `meta.json`, `query.txt`, `answer.md`, `result.json`, and `sources.json`.
|
|
12
|
+
- **Artifact controls.** `--out <dir>`, `--artifact-id <id>`, and `--no-artifact` support local agent workflows and deterministic run folders.
|
|
13
|
+
- **Perplexity Computer handoff.** `pplx computer` creates artifact-first task prompts for Perplexity Computer and validates `computer-result.json` outputs for local agents.
|
|
14
|
+
|
|
15
|
+
## [0.2.2] - 2026-05-21
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Configurable stream timeout.** `search`, `reason`, and `research` now accept `--timeout-ms <duration>`, with support for raw milliseconds plus `s` and `m` suffixes.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- `pplx research` now defaults to a 10-minute stream timeout so Deep Research can finish instead of hitting the old 2-minute ceiling.
|
|
22
|
+
- `pplx --version` now reads from `package.json`, keeping CLI output aligned with npm releases.
|
|
23
|
+
|
|
8
24
|
## [0.2.1] - 2026-05-18
|
|
9
25
|
|
|
10
26
|
First public release worth telling people about. (v0.2.0 was unpublished before this release; do not use it.)
|
|
@@ -46,6 +62,8 @@ First public release worth telling people about. (v0.2.0 was unpublished before
|
|
|
46
62
|
- SSE streaming for real-time answers
|
|
47
63
|
- Optional Playwright and Chrome CDP transports
|
|
48
64
|
|
|
65
|
+
[0.3.0]: https://github.com/thatsrajan/pplx-cli/compare/v0.2.2...v0.3.0
|
|
66
|
+
[0.2.2]: https://github.com/thatsrajan/pplx-cli/compare/v0.2.1...v0.2.2
|
|
49
67
|
[0.2.1]: https://github.com/thatsrajan/pplx-cli/compare/v0.1.1...v0.2.1
|
|
50
68
|
[0.1.1]: https://github.com/thatsrajan/pplx-cli/compare/v0.1.0...v0.1.1
|
|
51
69
|
[0.1.0]: https://github.com/thatsrajan/pplx-cli/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -101,6 +101,7 @@ pplx search "what is quantum computing"
|
|
|
101
101
|
pplx reason "explain the Riemann hypothesis"
|
|
102
102
|
pplx research "compare React vs Vue in 2026"
|
|
103
103
|
pplx labs "hello world" # free, no auth needed
|
|
104
|
+
pplx computer new "compare dinner options nearby"
|
|
104
105
|
pplx models # list available models
|
|
105
106
|
```
|
|
106
107
|
|
|
@@ -127,6 +128,8 @@ pplx search "query" | head -1
|
|
|
127
128
|
pplx search "query" --json || echo "failed"
|
|
128
129
|
```
|
|
129
130
|
|
|
131
|
+
Deep Research is slower than normal search. `pplx research` defaults to a 10-minute stream timeout; override it per run with `--timeout-ms 600000`, `--timeout-ms 120s`, or `--timeout-ms 10m`.
|
|
132
|
+
|
|
130
133
|
Recommended agent invocation:
|
|
131
134
|
|
|
132
135
|
```bash
|
|
@@ -143,12 +146,46 @@ pplx search "research this topic" --json --raw --mode pro
|
|
|
143
146
|
],
|
|
144
147
|
"query": "...",
|
|
145
148
|
"mode": "pro",
|
|
146
|
-
"model": "..."
|
|
149
|
+
"model": "...",
|
|
150
|
+
"artifactDir": "/Users/you/.config/pplx/artifacts/...",
|
|
151
|
+
"artifactId": "..."
|
|
147
152
|
}
|
|
148
153
|
```
|
|
149
154
|
|
|
150
155
|
---
|
|
151
156
|
|
|
157
|
+
## Artifacts
|
|
158
|
+
|
|
159
|
+
Query-producing commands save artifacts by default:
|
|
160
|
+
|
|
161
|
+
- `pplx search`
|
|
162
|
+
- `pplx reason`
|
|
163
|
+
- `pplx research`
|
|
164
|
+
- `pplx labs`
|
|
165
|
+
- bare `pplx "query"`
|
|
166
|
+
- `pplx computer new`
|
|
167
|
+
|
|
168
|
+
Each run gets a folder containing `meta.json`, `query.txt`, `answer.md`, `result.json`, and `sources.json`. Use `--out <dir>` to choose the destination for one run, or set `"artifactDir"` in `~/.config/pplx/config.json` to make it persistent. Use `--artifact-id <id>` when an agent needs a deterministic run folder, and `--no-artifact` to disable saving for one search-style run.
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
pplx search "compare laptops under $1500" --out ~/Dropbox/pplx-runs
|
|
172
|
+
pplx research "best places to stay in Kyoto" --artifact-id kyoto-research
|
|
173
|
+
pplx search "what is 2+2" --no-artifact --json --raw
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`pplx computer` is an artifact handoff for Perplexity Computer. It creates a task prompt and a result contract without calling private Computer APIs:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pplx computer new "compare dinner options nearby" --out ~/Dropbox/pplx-runs
|
|
180
|
+
pplx computer open <run-id> --copy
|
|
181
|
+
pplx computer status <run-id> --out ~/Dropbox/pplx-runs
|
|
182
|
+
pplx computer import <run-id> --out ~/Dropbox/pplx-runs --json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Computer runs include `task.md`, `result.schema.json`, and `computer-result.json`. Paste `task.md` into Perplexity Computer; when the task is done, place the structured result in `computer-result.json` so local agents can read it.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
152
189
|
## Options
|
|
153
190
|
|
|
154
191
|
| Flag | Description |
|
|
@@ -160,6 +197,10 @@ pplx search "research this topic" --json --raw --mode pro
|
|
|
160
197
|
| `--chrome` | Use Chrome CDP bridge instead of HTTP |
|
|
161
198
|
| `--playwright` | Use Playwright headless Chromium |
|
|
162
199
|
| `--no-playwright` | Force HTTP transport even if config enables Playwright |
|
|
200
|
+
| `--timeout-ms 120000\|120s\|10m` | Overall stream timeout |
|
|
201
|
+
| `--out <dir>` | Directory for saved artifacts |
|
|
202
|
+
| `--artifact-id <id>` | Deterministic artifact id for this run |
|
|
203
|
+
| `--no-artifact` | Disable artifact saving for one search-style run |
|
|
163
204
|
| `--curl` | Force curl-impersonate (auto-downloads if missing) |
|
|
164
205
|
| `--allow-anonymous` | Allow anonymous Perplexity responses when cookies are expired |
|
|
165
206
|
| `--incognito` | Do not save the query to Perplexity history |
|
|
@@ -190,7 +231,8 @@ Optional config file at `~/.config/pplx/config.json`:
|
|
|
190
231
|
"model": "claude-3.5-sonnet",
|
|
191
232
|
"lang": "en-US",
|
|
192
233
|
"playwright": true,
|
|
193
|
-
"playwrightHeadless": false
|
|
234
|
+
"playwrightHeadless": false,
|
|
235
|
+
"artifactDir": "/Users/you/Dropbox/pplx-runs"
|
|
194
236
|
}
|
|
195
237
|
```
|
|
196
238
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pplx-npx-search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "CLI for Perplexity AI with cookie-based auth. Headless, agent-friendly, no API key required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"pplx": "
|
|
7
|
+
"pplx": "bin/pplx.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/pplx.js",
|
package/src/artifacts.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { CONFIG_DIR } from './constants.js';
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_ARTIFACT_DIR = join(CONFIG_DIR, 'artifacts');
|
|
8
|
+
export const ARTIFACT_SCHEMA_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
function expandHome(value) {
|
|
11
|
+
if (!value) return value;
|
|
12
|
+
if (value === '~') return homedir();
|
|
13
|
+
if (value.startsWith('~/')) return join(homedir(), value.slice(2));
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveArtifactDir({ out, config = {} } = {}) {
|
|
18
|
+
const selected = out || config.artifactDir || DEFAULT_ARTIFACT_DIR;
|
|
19
|
+
const expanded = expandHome(selected);
|
|
20
|
+
return isAbsolute(expanded) ? resolve(expanded) : resolve(process.cwd(), expanded);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createArtifactId(id) {
|
|
24
|
+
if (id) {
|
|
25
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id) || id === '.' || id === '..') {
|
|
26
|
+
throw new Error('artifact id may only contain letters, numbers, dots, underscores, and hyphens');
|
|
27
|
+
}
|
|
28
|
+
return id;
|
|
29
|
+
}
|
|
30
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
31
|
+
return `${stamp}-${randomUUID().slice(0, 8)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function makeArtifactContext({ command, query, opts = {}, config = {} }) {
|
|
35
|
+
if (opts.artifact === false) return null;
|
|
36
|
+
const artifactId = createArtifactId(opts.artifactId);
|
|
37
|
+
const artifactRoot = resolveArtifactDir({ out: opts.out, config });
|
|
38
|
+
const artifactDir = join(artifactRoot, artifactId);
|
|
39
|
+
return { command, query, artifactId, artifactRoot, artifactDir };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function writeJson(file, value) {
|
|
43
|
+
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function writeStandardArtifact(ctx, payload) {
|
|
47
|
+
if (!ctx) return null;
|
|
48
|
+
mkdirSync(ctx.artifactDir, { recursive: true });
|
|
49
|
+
|
|
50
|
+
const createdAt = payload.createdAt || new Date().toISOString();
|
|
51
|
+
const sources = payload.sources || [];
|
|
52
|
+
const result = {
|
|
53
|
+
query: ctx.query,
|
|
54
|
+
answer: payload.answer || '',
|
|
55
|
+
sources,
|
|
56
|
+
command: ctx.command,
|
|
57
|
+
mode: payload.mode || null,
|
|
58
|
+
model: payload.model || null,
|
|
59
|
+
artifactId: ctx.artifactId,
|
|
60
|
+
artifactDir: ctx.artifactDir,
|
|
61
|
+
createdAt,
|
|
62
|
+
};
|
|
63
|
+
const meta = {
|
|
64
|
+
schemaVersion: ARTIFACT_SCHEMA_VERSION,
|
|
65
|
+
command: ctx.command,
|
|
66
|
+
query: ctx.query,
|
|
67
|
+
mode: payload.mode || null,
|
|
68
|
+
model: payload.model || null,
|
|
69
|
+
artifactId: ctx.artifactId,
|
|
70
|
+
artifactDir: ctx.artifactDir,
|
|
71
|
+
createdAt,
|
|
72
|
+
status: payload.status || 'complete',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
writeJson(join(ctx.artifactDir, 'meta.json'), meta);
|
|
76
|
+
writeFileSync(join(ctx.artifactDir, 'query.txt'), `${ctx.query}\n`, 'utf8');
|
|
77
|
+
writeFileSync(join(ctx.artifactDir, 'answer.md'), payload.answer || '', 'utf8');
|
|
78
|
+
writeJson(join(ctx.artifactDir, 'result.json'), result);
|
|
79
|
+
writeJson(join(ctx.artifactDir, 'sources.json'), sources);
|
|
80
|
+
return { artifactId: ctx.artifactId, artifactDir: ctx.artifactDir };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function readJsonFile(file) {
|
|
84
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
85
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
1
3
|
import { program } from 'commander';
|
|
2
4
|
import chalk from 'chalk';
|
|
3
5
|
import ora from 'ora';
|
|
@@ -10,6 +12,18 @@ import { formatSources } from './format.js';
|
|
|
10
12
|
import { LABS_MODELS, MODEL_MAP } from './constants.js';
|
|
11
13
|
import { setUseCurl } from './http.js';
|
|
12
14
|
import { loadConfig } from './config.js';
|
|
15
|
+
import { resolveTimeoutMs } from './timeout.js';
|
|
16
|
+
import { makeArtifactContext, resolveArtifactDir, writeStandardArtifact } from './artifacts.js';
|
|
17
|
+
import {
|
|
18
|
+
createComputerRun,
|
|
19
|
+
copyTextToClipboard,
|
|
20
|
+
importComputerResult,
|
|
21
|
+
inspectComputerRun,
|
|
22
|
+
openComputerUrl,
|
|
23
|
+
readTaskFile,
|
|
24
|
+
} from './computer.js';
|
|
25
|
+
|
|
26
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
13
27
|
|
|
14
28
|
// --- Output state ---
|
|
15
29
|
let rawMode = false;
|
|
@@ -84,11 +98,14 @@ async function extractAndValidateBrowser(browser, profile) {
|
|
|
84
98
|
program
|
|
85
99
|
.name('pplx')
|
|
86
100
|
.description('CLI for Perplexity AI')
|
|
87
|
-
.version(
|
|
101
|
+
.version(pkg.version);
|
|
88
102
|
|
|
89
103
|
program.option('--verbose', 'Enable verbose logging');
|
|
90
104
|
program.option('--proxy <url>', 'Set proxy URL (sets HTTPS_PROXY env var)');
|
|
91
105
|
program.option('--raw', 'Plain text output, no colors, no spinner');
|
|
106
|
+
program.option('--out <dir>', 'Directory for saved artifacts');
|
|
107
|
+
program.option('--no-artifact', 'Disable artifact saving for this run');
|
|
108
|
+
program.option('--artifact-id <id>', 'Deterministic artifact id for this run');
|
|
92
109
|
|
|
93
110
|
program.hook('preAction', (thisCmd) => {
|
|
94
111
|
const gopts = thisCmd.optsWithGlobals ? thisCmd.optsWithGlobals() : thisCmd.opts();
|
|
@@ -103,6 +120,35 @@ program.hook('preAction', (thisCmd) => {
|
|
|
103
120
|
}
|
|
104
121
|
});
|
|
105
122
|
|
|
123
|
+
function getOpts(commandOrOpts) {
|
|
124
|
+
const globals = program.opts();
|
|
125
|
+
const locals = commandOrOpts.optsWithGlobals
|
|
126
|
+
? { ...commandOrOpts.optsWithGlobals(), ...commandOrOpts.opts() }
|
|
127
|
+
: commandOrOpts;
|
|
128
|
+
const merged = { ...globals, ...locals };
|
|
129
|
+
if (globals.artifact === false || locals.artifact === false) merged.artifact = false;
|
|
130
|
+
return merged;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function addArtifactOptions(command, { allowDisable = true } = {}) {
|
|
134
|
+
command
|
|
135
|
+
.option('--out <dir>', 'Directory for saved artifacts')
|
|
136
|
+
.option('--artifact-id <id>', 'Deterministic artifact id for this run');
|
|
137
|
+
if (allowDisable) command.option('--no-artifact', 'Disable artifact saving for this run');
|
|
138
|
+
return command;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function maybePrintArtifactInfo(info, opts) {
|
|
142
|
+
if (!info || opts.json || rawMode) return;
|
|
143
|
+
console.log(chalk.dim(`\nArtifact: ${info.artifactDir}`));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveRunDir(runId, opts = {}) {
|
|
147
|
+
if (isAbsolute(runId) || runId.includes('/')) return resolve(runId);
|
|
148
|
+
const cfg = loadConfig();
|
|
149
|
+
return join(resolveArtifactDir({ out: opts.out, config: cfg }), runId);
|
|
150
|
+
}
|
|
151
|
+
|
|
106
152
|
// Auth command
|
|
107
153
|
program
|
|
108
154
|
.command('auth')
|
|
@@ -255,9 +301,9 @@ program
|
|
|
255
301
|
});
|
|
256
302
|
|
|
257
303
|
// Shared search logic
|
|
258
|
-
async function doSearch(query, opts) {
|
|
304
|
+
async function doSearch(query, opts, commandName = 'search') {
|
|
259
305
|
const cfg = loadConfig();
|
|
260
|
-
opts = { ...cfg, ...opts };
|
|
306
|
+
opts = { ...cfg, ...getOpts(opts) };
|
|
261
307
|
if (opts.curl) setUseCurl(true);
|
|
262
308
|
|
|
263
309
|
const cookies = loadCookies() || {};
|
|
@@ -274,8 +320,22 @@ async function doSearch(query, opts) {
|
|
|
274
320
|
}
|
|
275
321
|
|
|
276
322
|
const mode = opts.mode || 'pro';
|
|
323
|
+
let timeoutMs;
|
|
324
|
+
try {
|
|
325
|
+
timeoutMs = resolveTimeoutMs({ ...opts, mode });
|
|
326
|
+
} catch (e) {
|
|
327
|
+
console.error(chalk.red(e.message));
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
277
330
|
const sources = opts.sources ? opts.sources.split(',') : ['web'];
|
|
278
331
|
const lang = opts.lang || 'en-US';
|
|
332
|
+
let artifactCtx = null;
|
|
333
|
+
try {
|
|
334
|
+
artifactCtx = makeArtifactContext({ command: commandName, query, opts, config: cfg });
|
|
335
|
+
} catch (e) {
|
|
336
|
+
console.error(chalk.red(e.message));
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
279
339
|
|
|
280
340
|
try {
|
|
281
341
|
let lastAnswer = '';
|
|
@@ -291,6 +351,7 @@ async function doSearch(query, opts) {
|
|
|
291
351
|
chrome: opts.chrome,
|
|
292
352
|
playwright: opts.playwright,
|
|
293
353
|
curl: opts.curl,
|
|
354
|
+
timeoutMs,
|
|
294
355
|
})) {
|
|
295
356
|
lastData = data;
|
|
296
357
|
|
|
@@ -313,12 +374,21 @@ async function doSearch(query, opts) {
|
|
|
313
374
|
// Output single final JSON object
|
|
314
375
|
const answer = lastData?.answer || lastAnswer || '';
|
|
315
376
|
const webResults = lastData?.web_results || [];
|
|
377
|
+
const normalizedSources = webResults.map(r => ({ title: r.name || r.title, url: r.url }));
|
|
378
|
+
const artifactInfo = writeStandardArtifact(artifactCtx, {
|
|
379
|
+
answer,
|
|
380
|
+
sources: normalizedSources,
|
|
381
|
+
mode,
|
|
382
|
+
model: opts.model || 'default',
|
|
383
|
+
});
|
|
316
384
|
const jsonOut = {
|
|
317
385
|
answer,
|
|
318
|
-
sources:
|
|
386
|
+
sources: normalizedSources,
|
|
319
387
|
query,
|
|
320
388
|
mode,
|
|
321
389
|
model: opts.model || 'default',
|
|
390
|
+
artifactDir: artifactInfo?.artifactDir,
|
|
391
|
+
artifactId: artifactInfo?.artifactId,
|
|
322
392
|
};
|
|
323
393
|
console.log(JSON.stringify(jsonOut));
|
|
324
394
|
if (!answer) process.exit(1);
|
|
@@ -335,6 +405,14 @@ async function doSearch(query, opts) {
|
|
|
335
405
|
if (!rawMode && opts.citations !== false && lastData?.web_results) {
|
|
336
406
|
console.log(formatSources(lastData.web_results, { full: opts.citationsFull }));
|
|
337
407
|
}
|
|
408
|
+
const webResults = lastData?.web_results || [];
|
|
409
|
+
const artifactInfo = writeStandardArtifact(artifactCtx, {
|
|
410
|
+
answer: lastAnswer,
|
|
411
|
+
sources: webResults.map(r => ({ title: r.name || r.title, url: r.url })),
|
|
412
|
+
mode,
|
|
413
|
+
model: opts.model || 'default',
|
|
414
|
+
});
|
|
415
|
+
maybePrintArtifactInfo(artifactInfo, opts);
|
|
338
416
|
} catch (e) {
|
|
339
417
|
console.error(chalk.red('\nError:'), e.message);
|
|
340
418
|
if (e.message.includes('403')) {
|
|
@@ -345,7 +423,7 @@ async function doSearch(query, opts) {
|
|
|
345
423
|
}
|
|
346
424
|
|
|
347
425
|
// Search command
|
|
348
|
-
program
|
|
426
|
+
addArtifactOptions(program
|
|
349
427
|
.command('search [query]')
|
|
350
428
|
.description('Search with Perplexity (default: pro mode)')
|
|
351
429
|
.option('-m, --mode <mode>', 'Search mode: auto, pro, reasoning, deep-research', 'pro')
|
|
@@ -361,15 +439,17 @@ program
|
|
|
361
439
|
.option('--chrome', 'Use Chrome CDP bridge instead of HTTP')
|
|
362
440
|
.option('--playwright', 'Use Playwright headless Chromium instead of HTTP')
|
|
363
441
|
.option('--no-playwright', 'Disable Playwright even if config enables it')
|
|
364
|
-
.option('--
|
|
442
|
+
.option('--timeout-ms <duration>', 'Overall stream timeout: milliseconds by default, or use 120s / 10m')
|
|
443
|
+
.option('--allow-anonymous', 'Allow anonymous Perplexity responses when cookies are expired'))
|
|
365
444
|
.action(async (queryArg, opts) => {
|
|
445
|
+
opts = getOpts(opts);
|
|
366
446
|
if (opts.raw) { rawMode = true; chalk.level = 0; }
|
|
367
447
|
const query = await resolveQuery(queryArg);
|
|
368
|
-
await doSearch(query, opts);
|
|
448
|
+
await doSearch(query, opts, 'search');
|
|
369
449
|
});
|
|
370
450
|
|
|
371
451
|
// Shorthand: reason
|
|
372
|
-
program
|
|
452
|
+
addArtifactOptions(program
|
|
373
453
|
.command('reason [query]')
|
|
374
454
|
.description('Reasoning mode search')
|
|
375
455
|
.option('--model <model>', 'Model name')
|
|
@@ -378,14 +458,16 @@ program
|
|
|
378
458
|
.option('--chrome', 'Use Chrome CDP bridge')
|
|
379
459
|
.option('--playwright', 'Use Playwright headless Chromium')
|
|
380
460
|
.option('--no-playwright', 'Disable Playwright even if config enables it')
|
|
381
|
-
.option('--
|
|
461
|
+
.option('--timeout-ms <duration>', 'Overall stream timeout: milliseconds by default, or use 120s / 10m')
|
|
462
|
+
.option('--allow-anonymous', 'Allow anonymous Perplexity responses when cookies are expired'))
|
|
382
463
|
.action(async (queryArg, opts) => {
|
|
464
|
+
opts = getOpts(opts);
|
|
383
465
|
const query = await resolveQuery(queryArg);
|
|
384
|
-
await doSearch(query, { ...opts, mode: 'reasoning' });
|
|
466
|
+
await doSearch(query, { ...opts, mode: 'reasoning' }, 'reason');
|
|
385
467
|
});
|
|
386
468
|
|
|
387
469
|
// Shorthand: research
|
|
388
|
-
program
|
|
470
|
+
addArtifactOptions(program
|
|
389
471
|
.command('research [query]')
|
|
390
472
|
.description('Deep research mode')
|
|
391
473
|
.option('--json', 'Output raw JSON')
|
|
@@ -393,12 +475,14 @@ program
|
|
|
393
475
|
.option('--chrome', 'Use Chrome CDP bridge')
|
|
394
476
|
.option('--playwright', 'Use Playwright headless Chromium')
|
|
395
477
|
.option('--no-playwright', 'Disable Playwright even if config enables it')
|
|
396
|
-
.option('--
|
|
478
|
+
.option('--timeout-ms <duration>', 'Overall stream timeout: milliseconds by default, or use 120s / 10m')
|
|
479
|
+
.option('--allow-anonymous', 'Allow anonymous Perplexity responses when cookies are expired'))
|
|
397
480
|
.action(async (queryArg, opts) => {
|
|
481
|
+
opts = getOpts(opts);
|
|
398
482
|
const query = await resolveQuery(queryArg);
|
|
399
483
|
const spinner = makeSpinner('Deep research in progress...').start();
|
|
400
484
|
try {
|
|
401
|
-
await doSearch(query, { ...opts, mode: 'deep-research', _spinner: spinner });
|
|
485
|
+
await doSearch(query, { ...opts, mode: 'deep-research', _spinner: spinner }, 'research');
|
|
402
486
|
} catch (e) {
|
|
403
487
|
spinner.fail(e.message);
|
|
404
488
|
process.exit(1);
|
|
@@ -406,13 +490,22 @@ program
|
|
|
406
490
|
});
|
|
407
491
|
|
|
408
492
|
// Labs command
|
|
409
|
-
program
|
|
493
|
+
addArtifactOptions(program
|
|
410
494
|
.command('labs [query]')
|
|
411
495
|
.description('Query open-source models (no auth needed)')
|
|
412
496
|
.option('--model <model>', `Model: ${LABS_MODELS.join(', ')}`, 'sonar')
|
|
413
|
-
.option('--json', 'Output
|
|
497
|
+
.option('--json', 'Output single JSON object with answer, events, and artifact metadata'))
|
|
414
498
|
.action(async (queryArg, opts) => {
|
|
499
|
+
opts = getOpts(opts);
|
|
500
|
+
const cfg = loadConfig();
|
|
415
501
|
const query = await resolveQuery(queryArg);
|
|
502
|
+
let artifactCtx = null;
|
|
503
|
+
try {
|
|
504
|
+
artifactCtx = makeArtifactContext({ command: 'labs', query, opts, config: cfg });
|
|
505
|
+
} catch (e) {
|
|
506
|
+
console.error(chalk.red(e.message));
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
416
509
|
const spinner = makeSpinner('Connecting to labs...').start();
|
|
417
510
|
const client = new LabsClient();
|
|
418
511
|
try {
|
|
@@ -420,9 +513,10 @@ program
|
|
|
420
513
|
spinner.stop();
|
|
421
514
|
|
|
422
515
|
let lastOutput = '';
|
|
516
|
+
const events = [];
|
|
423
517
|
for await (const data of client.ask(query, opts.model)) {
|
|
518
|
+
events.push(data);
|
|
424
519
|
if (opts.json) {
|
|
425
|
-
console.log(JSON.stringify(data));
|
|
426
520
|
continue;
|
|
427
521
|
}
|
|
428
522
|
const output = data.output || '';
|
|
@@ -432,14 +526,129 @@ program
|
|
|
432
526
|
}
|
|
433
527
|
}
|
|
434
528
|
if (!opts.json) process.stdout.write('\n');
|
|
529
|
+
const artifactInfo = writeStandardArtifact(artifactCtx, {
|
|
530
|
+
answer: lastOutput,
|
|
531
|
+
sources: [],
|
|
532
|
+
mode: 'labs',
|
|
533
|
+
model: opts.model,
|
|
534
|
+
});
|
|
535
|
+
if (opts.json) {
|
|
536
|
+
console.log(JSON.stringify({
|
|
537
|
+
answer: lastOutput,
|
|
538
|
+
events,
|
|
539
|
+
query,
|
|
540
|
+
mode: 'labs',
|
|
541
|
+
model: opts.model,
|
|
542
|
+
artifactDir: artifactInfo?.artifactDir,
|
|
543
|
+
artifactId: artifactInfo?.artifactId,
|
|
544
|
+
}));
|
|
545
|
+
} else {
|
|
546
|
+
maybePrintArtifactInfo(artifactInfo, opts);
|
|
547
|
+
}
|
|
435
548
|
} catch (e) {
|
|
436
549
|
spinner.fail('Labs error: ' + e.message);
|
|
550
|
+
if (isQuiet()) console.error(chalk.red('Labs error:'), e.message);
|
|
437
551
|
process.exit(1);
|
|
438
552
|
} finally {
|
|
439
553
|
client.close();
|
|
440
554
|
}
|
|
441
555
|
});
|
|
442
556
|
|
|
557
|
+
// Computer artifact handoff workflow
|
|
558
|
+
const computer = program
|
|
559
|
+
.command('computer')
|
|
560
|
+
.description('Create and manage Perplexity Computer artifact handoffs');
|
|
561
|
+
|
|
562
|
+
addArtifactOptions(computer
|
|
563
|
+
.command('new [task]')
|
|
564
|
+
.description('Create a Perplexity Computer task artifact')
|
|
565
|
+
.option('--template <name>', 'Computer task template', 'compare')
|
|
566
|
+
.option('--json', 'Output run metadata as JSON'), { allowDisable: false })
|
|
567
|
+
.action(async (taskArg, opts) => {
|
|
568
|
+
opts = getOpts(opts);
|
|
569
|
+
const task = await resolveQuery(taskArg);
|
|
570
|
+
try {
|
|
571
|
+
const run = createComputerRun({
|
|
572
|
+
task,
|
|
573
|
+
template: opts.template,
|
|
574
|
+
opts,
|
|
575
|
+
config: loadConfig(),
|
|
576
|
+
});
|
|
577
|
+
if (opts.json) {
|
|
578
|
+
console.log(JSON.stringify(run));
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
console.log(chalk.green(`✓ Computer task artifact created: ${run.artifactDir}`));
|
|
582
|
+
console.log(chalk.dim(` Task: ${run.taskPath}`));
|
|
583
|
+
console.log(chalk.dim(` Result: ${run.resultPath}`));
|
|
584
|
+
} catch (e) {
|
|
585
|
+
console.error(chalk.red(e.message));
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
addArtifactOptions(computer
|
|
591
|
+
.command('open <run>')
|
|
592
|
+
.description('Open Perplexity Computer and optionally copy task.md')
|
|
593
|
+
.option('--copy', 'Copy task.md to the clipboard'), { allowDisable: false })
|
|
594
|
+
.action((runId, opts) => {
|
|
595
|
+
opts = getOpts(opts);
|
|
596
|
+
const runDir = resolveRunDir(runId, opts);
|
|
597
|
+
try {
|
|
598
|
+
if (opts.copy) {
|
|
599
|
+
const taskText = readTaskFile(runDir);
|
|
600
|
+
if (!copyTextToClipboard(taskText)) {
|
|
601
|
+
console.log(chalk.yellow('Clipboard copy is only supported on macOS.'));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (!openComputerUrl()) {
|
|
605
|
+
console.log(chalk.yellow('Opening Perplexity Computer is only supported on macOS.'));
|
|
606
|
+
console.log('https://www.perplexity.ai/computer');
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
console.log(chalk.green(`✓ Opened Perplexity Computer for ${runDir}`));
|
|
610
|
+
if (opts.copy) console.log(chalk.dim(' Copied task.md to clipboard.'));
|
|
611
|
+
} catch (e) {
|
|
612
|
+
console.error(chalk.red(e.message));
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
addArtifactOptions(computer
|
|
618
|
+
.command('status <run>')
|
|
619
|
+
.description('Inspect a Perplexity Computer artifact run')
|
|
620
|
+
.option('--json', 'Output status as JSON'), { allowDisable: false })
|
|
621
|
+
.action((runId, opts) => {
|
|
622
|
+
opts = getOpts(opts);
|
|
623
|
+
const status = inspectComputerRun(resolveRunDir(runId, opts));
|
|
624
|
+
if (opts.json) {
|
|
625
|
+
console.log(JSON.stringify(status));
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const label = status.status === 'complete'
|
|
629
|
+
? chalk.green('[✓] Complete')
|
|
630
|
+
: status.status === 'pending'
|
|
631
|
+
? chalk.yellow('[○] Pending')
|
|
632
|
+
: chalk.red(status.status === 'invalid' ? '[!] Invalid' : '[✗] Missing');
|
|
633
|
+
console.log(`${label} ${status.artifactDir}`);
|
|
634
|
+
if (status.reason) console.log(chalk.dim(` ${status.reason}`));
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
addArtifactOptions(computer
|
|
638
|
+
.command('import <run>')
|
|
639
|
+
.description('Print a completed Perplexity Computer result')
|
|
640
|
+
.option('--json', 'Output compact JSON'), { allowDisable: false })
|
|
641
|
+
.action((runId, opts) => {
|
|
642
|
+
opts = getOpts(opts);
|
|
643
|
+
try {
|
|
644
|
+
const result = importComputerResult(resolveRunDir(runId, opts));
|
|
645
|
+
console.log(opts.json ? JSON.stringify(result) : JSON.stringify(result, null, 2));
|
|
646
|
+
} catch (e) {
|
|
647
|
+
console.error(chalk.red(e.message));
|
|
648
|
+
process.exit(1);
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
443
652
|
// Models command
|
|
444
653
|
program
|
|
445
654
|
.command('models')
|
|
@@ -457,9 +666,9 @@ program
|
|
|
457
666
|
// Default: treat bare args as search
|
|
458
667
|
program
|
|
459
668
|
.argument('[query...]', 'Quick search (shorthand for pplx search)')
|
|
460
|
-
.action(async (query) => {
|
|
669
|
+
.action(async (query, opts) => {
|
|
461
670
|
if (query.length > 0) {
|
|
462
|
-
await doSearch(query.join(' '),
|
|
671
|
+
await doSearch(query.join(' '), getOpts(opts || program), 'search');
|
|
463
672
|
}
|
|
464
673
|
});
|
|
465
674
|
|
package/src/computer.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
ARTIFACT_SCHEMA_VERSION,
|
|
6
|
+
makeArtifactContext,
|
|
7
|
+
readJsonFile,
|
|
8
|
+
writeJson,
|
|
9
|
+
} from './artifacts.js';
|
|
10
|
+
|
|
11
|
+
export const COMPUTER_URL = 'https://www.perplexity.ai/computer';
|
|
12
|
+
export const COMPUTER_RESULT_FILE = 'computer-result.json';
|
|
13
|
+
export const PENDING_COMPUTER_RESULT = {
|
|
14
|
+
summary: '',
|
|
15
|
+
winner: '',
|
|
16
|
+
confidence: 'low',
|
|
17
|
+
items: [],
|
|
18
|
+
sources: [],
|
|
19
|
+
checked_at: '',
|
|
20
|
+
notes: [],
|
|
21
|
+
_status: 'pending',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const RESULT_SCHEMA = {
|
|
25
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
26
|
+
title: 'pplx computer comparison result',
|
|
27
|
+
type: 'object',
|
|
28
|
+
required: ['summary', 'winner', 'confidence', 'items', 'sources', 'checked_at', 'notes'],
|
|
29
|
+
properties: {
|
|
30
|
+
summary: { type: 'string' },
|
|
31
|
+
winner: { type: 'string' },
|
|
32
|
+
confidence: { enum: ['low', 'medium', 'high'] },
|
|
33
|
+
items: { type: 'array' },
|
|
34
|
+
sources: { type: 'array' },
|
|
35
|
+
checked_at: { type: 'string' },
|
|
36
|
+
notes: { type: 'array' },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function buildComputerTask({ task, template = 'compare', resultPath }) {
|
|
41
|
+
if (template !== 'compare') {
|
|
42
|
+
throw new Error(`unsupported computer template: ${template}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return `# Perplexity Computer Task
|
|
46
|
+
|
|
47
|
+
You are running a live comparison task for a local agent workflow.
|
|
48
|
+
|
|
49
|
+
## User request
|
|
50
|
+
|
|
51
|
+
${task}
|
|
52
|
+
|
|
53
|
+
## Instructions
|
|
54
|
+
|
|
55
|
+
- Use live web pages and direct source pages where possible, not only search snippets.
|
|
56
|
+
- Compare any relevant prices, fees, availability, location, timing, quality signals, eligibility rules, and constraints.
|
|
57
|
+
- This template is intentionally broad: it can cover products, real estate, restaurants, food prices, travel, rewards portals, services, and other comparison tasks.
|
|
58
|
+
- Preserve source URLs for every material claim.
|
|
59
|
+
- Include the time the information was checked.
|
|
60
|
+
- Mark uncertainty explicitly. Do not invent missing values.
|
|
61
|
+
- Prefer structured evidence over prose.
|
|
62
|
+
|
|
63
|
+
## Output target
|
|
64
|
+
|
|
65
|
+
Write the final result as JSON matching \`result.schema.json\`.
|
|
66
|
+
|
|
67
|
+
If you can access the local filesystem, save it here:
|
|
68
|
+
|
|
69
|
+
\`${resultPath}\`
|
|
70
|
+
|
|
71
|
+
If you cannot access the filesystem, return the JSON in the chat so the local agent or user can place it in that file.
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createComputerRun({ task, template = 'compare', opts = {}, config = {} }) {
|
|
76
|
+
const ctx = makeArtifactContext({ command: 'computer', query: task, opts, config });
|
|
77
|
+
if (!ctx) {
|
|
78
|
+
throw new Error('computer runs require artifacts; omit --no-artifact for this command');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
mkdirSync(ctx.artifactDir, { recursive: true });
|
|
82
|
+
const createdAt = new Date().toISOString();
|
|
83
|
+
const resultPath = join(ctx.artifactDir, COMPUTER_RESULT_FILE);
|
|
84
|
+
const taskText = buildComputerTask({ task, template, resultPath });
|
|
85
|
+
const meta = {
|
|
86
|
+
schemaVersion: ARTIFACT_SCHEMA_VERSION,
|
|
87
|
+
command: 'computer',
|
|
88
|
+
query: task,
|
|
89
|
+
template,
|
|
90
|
+
artifactId: ctx.artifactId,
|
|
91
|
+
artifactDir: ctx.artifactDir,
|
|
92
|
+
createdAt,
|
|
93
|
+
status: 'pending',
|
|
94
|
+
};
|
|
95
|
+
const result = {
|
|
96
|
+
query: task,
|
|
97
|
+
answer: '',
|
|
98
|
+
sources: [],
|
|
99
|
+
command: 'computer',
|
|
100
|
+
mode: 'computer',
|
|
101
|
+
model: null,
|
|
102
|
+
artifactId: ctx.artifactId,
|
|
103
|
+
artifactDir: ctx.artifactDir,
|
|
104
|
+
createdAt,
|
|
105
|
+
status: 'pending',
|
|
106
|
+
computerResultFile: COMPUTER_RESULT_FILE,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
writeJson(join(ctx.artifactDir, 'meta.json'), meta);
|
|
110
|
+
writeFileSync(join(ctx.artifactDir, 'query.txt'), `${task}\n`, 'utf8');
|
|
111
|
+
writeFileSync(join(ctx.artifactDir, 'answer.md'), taskText, 'utf8');
|
|
112
|
+
writeJson(join(ctx.artifactDir, 'result.json'), result);
|
|
113
|
+
writeJson(join(ctx.artifactDir, 'sources.json'), []);
|
|
114
|
+
writeFileSync(join(ctx.artifactDir, 'task.md'), taskText, 'utf8');
|
|
115
|
+
writeJson(join(ctx.artifactDir, 'result.schema.json'), RESULT_SCHEMA);
|
|
116
|
+
writeJson(resultPath, PENDING_COMPUTER_RESULT);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
artifactId: ctx.artifactId,
|
|
120
|
+
artifactDir: ctx.artifactDir,
|
|
121
|
+
taskPath: join(ctx.artifactDir, 'task.md'),
|
|
122
|
+
resultPath,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function assertComputerResult(value) {
|
|
127
|
+
const missing = ['summary', 'winner', 'confidence', 'items', 'sources', 'checked_at', 'notes']
|
|
128
|
+
.filter((key) => !(key in value));
|
|
129
|
+
if (missing.length) return { ok: false, reason: `missing fields: ${missing.join(', ')}` };
|
|
130
|
+
if (!['low', 'medium', 'high'].includes(value.confidence)) {
|
|
131
|
+
return { ok: false, reason: 'confidence must be low, medium, or high' };
|
|
132
|
+
}
|
|
133
|
+
if (!Array.isArray(value.items)) return { ok: false, reason: 'items must be an array' };
|
|
134
|
+
if (!Array.isArray(value.sources)) return { ok: false, reason: 'sources must be an array' };
|
|
135
|
+
if (!Array.isArray(value.notes)) return { ok: false, reason: 'notes must be an array' };
|
|
136
|
+
return { ok: true, reason: null };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function inspectComputerRun(runDir) {
|
|
140
|
+
const artifactDir = resolve(runDir);
|
|
141
|
+
const resultPath = join(artifactDir, COMPUTER_RESULT_FILE);
|
|
142
|
+
const metaPath = join(artifactDir, 'meta.json');
|
|
143
|
+
if (!existsSync(metaPath)) {
|
|
144
|
+
return { status: 'missing', artifactDir, resultPath, reason: 'meta.json not found' };
|
|
145
|
+
}
|
|
146
|
+
if (!existsSync(resultPath)) {
|
|
147
|
+
return { status: 'pending', artifactDir, resultPath, reason: `${COMPUTER_RESULT_FILE} not found` };
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const result = readJsonFile(resultPath);
|
|
151
|
+
if (result._status === 'pending') {
|
|
152
|
+
return { status: 'pending', artifactDir, resultPath, reason: `${COMPUTER_RESULT_FILE} is still pending` };
|
|
153
|
+
}
|
|
154
|
+
const validation = assertComputerResult(result);
|
|
155
|
+
return {
|
|
156
|
+
status: validation.ok ? 'complete' : 'invalid',
|
|
157
|
+
artifactDir,
|
|
158
|
+
resultPath,
|
|
159
|
+
reason: validation.reason,
|
|
160
|
+
result,
|
|
161
|
+
};
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return { status: 'invalid', artifactDir, resultPath, reason: e.message };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function importComputerResult(runDir) {
|
|
168
|
+
const status = inspectComputerRun(runDir);
|
|
169
|
+
if (status.status !== 'complete') {
|
|
170
|
+
throw new Error(`computer result is ${status.status}: ${status.reason}`);
|
|
171
|
+
}
|
|
172
|
+
return status.result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function copyTextToClipboard(text) {
|
|
176
|
+
if (process.platform === 'darwin') {
|
|
177
|
+
execFileSync('pbcopy', { input: text });
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function openComputerUrl() {
|
|
184
|
+
if (process.platform === 'darwin') {
|
|
185
|
+
execFileSync('open', [COMPUTER_URL]);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function readTaskFile(runDir) {
|
|
192
|
+
return readFileSync(join(resolve(runDir), 'task.md'), 'utf8');
|
|
193
|
+
}
|
package/src/labs.js
CHANGED
|
@@ -45,6 +45,9 @@ export class LabsClient {
|
|
|
45
45
|
headers: { 'user-agent': HEADERS['user-agent'] },
|
|
46
46
|
});
|
|
47
47
|
const pollText = await pollResp.text();
|
|
48
|
+
if (!pollResp.ok) {
|
|
49
|
+
throw new Error(`Labs polling failed (${pollResp.status}): ${pollText.slice(0, 120)}`);
|
|
50
|
+
}
|
|
48
51
|
|
|
49
52
|
const handshake = parseEngineIO(pollText);
|
|
50
53
|
this.sid = handshake.sid;
|
package/src/search.js
CHANGED
|
@@ -126,6 +126,7 @@ async function* searchWithChrome(query, cookies, opts) {
|
|
|
126
126
|
method: 'POST',
|
|
127
127
|
headers: { 'content-type': 'application/json' },
|
|
128
128
|
body,
|
|
129
|
+
timeout: opts.timeoutMs,
|
|
129
130
|
}
|
|
130
131
|
)) {
|
|
131
132
|
parser.feed(chunk);
|
|
@@ -177,6 +178,7 @@ async function* searchWithPlaywright(query, cookies, opts) {
|
|
|
177
178
|
method: 'POST',
|
|
178
179
|
headers: { 'content-type': 'application/json' },
|
|
179
180
|
body,
|
|
181
|
+
timeout: opts.timeoutMs,
|
|
180
182
|
}
|
|
181
183
|
)) {
|
|
182
184
|
parser.feed(chunk);
|
|
@@ -219,6 +221,7 @@ async function* searchWithHttp(query, cookies, opts) {
|
|
|
219
221
|
'cookie': cookieHeader(sessionCookies),
|
|
220
222
|
},
|
|
221
223
|
body,
|
|
224
|
+
timeout: opts.timeoutMs,
|
|
222
225
|
});
|
|
223
226
|
|
|
224
227
|
if (!resp.ok) {
|
package/src/timeout.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const DEFAULT_SEARCH_TIMEOUT_MS = 120000;
|
|
2
|
+
export const DEFAULT_RESEARCH_TIMEOUT_MS = 600000;
|
|
3
|
+
|
|
4
|
+
export function parseTimeoutMs(value, label = 'timeout') {
|
|
5
|
+
if (value == null || value === '') return null;
|
|
6
|
+
|
|
7
|
+
if (typeof value === 'number') {
|
|
8
|
+
if (Number.isFinite(value) && value > 0) return Math.trunc(value);
|
|
9
|
+
throw new Error(`${label} must be a positive number of milliseconds`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const text = String(value).trim().toLowerCase();
|
|
13
|
+
const match = text.match(/^(\d+(?:\.\d+)?)(ms|s|m)?$/);
|
|
14
|
+
if (!match) {
|
|
15
|
+
throw new Error(`${label} must be a positive duration like 120000, 120s, or 10m`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const amount = Number(match[1]);
|
|
19
|
+
const unit = match[2] ?? 'ms';
|
|
20
|
+
const multipliers = { ms: 1, s: 1000, m: 60000 };
|
|
21
|
+
const timeoutMs = amount * multipliers[unit];
|
|
22
|
+
|
|
23
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
24
|
+
throw new Error(`${label} must be a positive duration`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return Math.trunc(timeoutMs);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveTimeoutMs(opts = {}) {
|
|
31
|
+
const explicit = parseTimeoutMs(opts.timeoutMs, '--timeout-ms');
|
|
32
|
+
if (explicit != null) return explicit;
|
|
33
|
+
|
|
34
|
+
const configured = parseTimeoutMs(opts.timeout, 'config timeout');
|
|
35
|
+
if (configured != null) return configured;
|
|
36
|
+
|
|
37
|
+
return opts.mode === 'deep-research'
|
|
38
|
+
? DEFAULT_RESEARCH_TIMEOUT_MS
|
|
39
|
+
: DEFAULT_SEARCH_TIMEOUT_MS;
|
|
40
|
+
}
|