pplx-npx-search 0.2.2 → 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 +8 -0
- package/README.md +41 -2
- package/package.json +1 -1
- package/src/artifacts.js +85 -0
- package/src/cli.js +211 -17
- package/src/computer.js +193 -0
- package/src/labs.js +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ 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
|
+
|
|
8
15
|
## [0.2.2] - 2026-05-21
|
|
9
16
|
|
|
10
17
|
### Added
|
|
@@ -55,6 +62,7 @@ First public release worth telling people about. (v0.2.0 was unpublished before
|
|
|
55
62
|
- SSE streaming for real-time answers
|
|
56
63
|
- Optional Playwright and Chrome CDP transports
|
|
57
64
|
|
|
65
|
+
[0.3.0]: https://github.com/thatsrajan/pplx-cli/compare/v0.2.2...v0.3.0
|
|
58
66
|
[0.2.2]: https://github.com/thatsrajan/pplx-cli/compare/v0.2.1...v0.2.2
|
|
59
67
|
[0.2.1]: https://github.com/thatsrajan/pplx-cli/compare/v0.1.1...v0.2.1
|
|
60
68
|
[0.1.1]: https://github.com/thatsrajan/pplx-cli/compare/v0.1.0...v0.1.1
|
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
|
|
|
@@ -145,12 +146,46 @@ pplx search "research this topic" --json --raw --mode pro
|
|
|
145
146
|
],
|
|
146
147
|
"query": "...",
|
|
147
148
|
"mode": "pro",
|
|
148
|
-
"model": "..."
|
|
149
|
+
"model": "...",
|
|
150
|
+
"artifactDir": "/Users/you/.config/pplx/artifacts/...",
|
|
151
|
+
"artifactId": "..."
|
|
149
152
|
}
|
|
150
153
|
```
|
|
151
154
|
|
|
152
155
|
---
|
|
153
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
|
+
|
|
154
189
|
## Options
|
|
155
190
|
|
|
156
191
|
| Flag | Description |
|
|
@@ -163,6 +198,9 @@ pplx search "research this topic" --json --raw --mode pro
|
|
|
163
198
|
| `--playwright` | Use Playwright headless Chromium |
|
|
164
199
|
| `--no-playwright` | Force HTTP transport even if config enables Playwright |
|
|
165
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 |
|
|
166
204
|
| `--curl` | Force curl-impersonate (auto-downloads if missing) |
|
|
167
205
|
| `--allow-anonymous` | Allow anonymous Perplexity responses when cookies are expired |
|
|
168
206
|
| `--incognito` | Do not save the query to Perplexity history |
|
|
@@ -193,7 +231,8 @@ Optional config file at `~/.config/pplx/config.json`:
|
|
|
193
231
|
"model": "claude-3.5-sonnet",
|
|
194
232
|
"lang": "en-US",
|
|
195
233
|
"playwright": true,
|
|
196
|
-
"playwrightHeadless": false
|
|
234
|
+
"playwrightHeadless": false,
|
|
235
|
+
"artifactDir": "/Users/you/Dropbox/pplx-runs"
|
|
197
236
|
}
|
|
198
237
|
```
|
|
199
238
|
|
package/package.json
CHANGED
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,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
2
3
|
import { program } from 'commander';
|
|
3
4
|
import chalk from 'chalk';
|
|
4
5
|
import ora from 'ora';
|
|
@@ -12,6 +13,15 @@ import { LABS_MODELS, MODEL_MAP } from './constants.js';
|
|
|
12
13
|
import { setUseCurl } from './http.js';
|
|
13
14
|
import { loadConfig } from './config.js';
|
|
14
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';
|
|
15
25
|
|
|
16
26
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
17
27
|
|
|
@@ -93,6 +103,9 @@ program
|
|
|
93
103
|
program.option('--verbose', 'Enable verbose logging');
|
|
94
104
|
program.option('--proxy <url>', 'Set proxy URL (sets HTTPS_PROXY env var)');
|
|
95
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');
|
|
96
109
|
|
|
97
110
|
program.hook('preAction', (thisCmd) => {
|
|
98
111
|
const gopts = thisCmd.optsWithGlobals ? thisCmd.optsWithGlobals() : thisCmd.opts();
|
|
@@ -107,6 +120,35 @@ program.hook('preAction', (thisCmd) => {
|
|
|
107
120
|
}
|
|
108
121
|
});
|
|
109
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
|
+
|
|
110
152
|
// Auth command
|
|
111
153
|
program
|
|
112
154
|
.command('auth')
|
|
@@ -259,9 +301,9 @@ program
|
|
|
259
301
|
});
|
|
260
302
|
|
|
261
303
|
// Shared search logic
|
|
262
|
-
async function doSearch(query, opts) {
|
|
304
|
+
async function doSearch(query, opts, commandName = 'search') {
|
|
263
305
|
const cfg = loadConfig();
|
|
264
|
-
opts = { ...cfg, ...opts };
|
|
306
|
+
opts = { ...cfg, ...getOpts(opts) };
|
|
265
307
|
if (opts.curl) setUseCurl(true);
|
|
266
308
|
|
|
267
309
|
const cookies = loadCookies() || {};
|
|
@@ -287,6 +329,13 @@ async function doSearch(query, opts) {
|
|
|
287
329
|
}
|
|
288
330
|
const sources = opts.sources ? opts.sources.split(',') : ['web'];
|
|
289
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
|
+
}
|
|
290
339
|
|
|
291
340
|
try {
|
|
292
341
|
let lastAnswer = '';
|
|
@@ -325,12 +374,21 @@ async function doSearch(query, opts) {
|
|
|
325
374
|
// Output single final JSON object
|
|
326
375
|
const answer = lastData?.answer || lastAnswer || '';
|
|
327
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
|
+
});
|
|
328
384
|
const jsonOut = {
|
|
329
385
|
answer,
|
|
330
|
-
sources:
|
|
386
|
+
sources: normalizedSources,
|
|
331
387
|
query,
|
|
332
388
|
mode,
|
|
333
389
|
model: opts.model || 'default',
|
|
390
|
+
artifactDir: artifactInfo?.artifactDir,
|
|
391
|
+
artifactId: artifactInfo?.artifactId,
|
|
334
392
|
};
|
|
335
393
|
console.log(JSON.stringify(jsonOut));
|
|
336
394
|
if (!answer) process.exit(1);
|
|
@@ -347,6 +405,14 @@ async function doSearch(query, opts) {
|
|
|
347
405
|
if (!rawMode && opts.citations !== false && lastData?.web_results) {
|
|
348
406
|
console.log(formatSources(lastData.web_results, { full: opts.citationsFull }));
|
|
349
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);
|
|
350
416
|
} catch (e) {
|
|
351
417
|
console.error(chalk.red('\nError:'), e.message);
|
|
352
418
|
if (e.message.includes('403')) {
|
|
@@ -357,7 +423,7 @@ async function doSearch(query, opts) {
|
|
|
357
423
|
}
|
|
358
424
|
|
|
359
425
|
// Search command
|
|
360
|
-
program
|
|
426
|
+
addArtifactOptions(program
|
|
361
427
|
.command('search [query]')
|
|
362
428
|
.description('Search with Perplexity (default: pro mode)')
|
|
363
429
|
.option('-m, --mode <mode>', 'Search mode: auto, pro, reasoning, deep-research', 'pro')
|
|
@@ -374,15 +440,16 @@ program
|
|
|
374
440
|
.option('--playwright', 'Use Playwright headless Chromium instead of HTTP')
|
|
375
441
|
.option('--no-playwright', 'Disable Playwright even if config enables it')
|
|
376
442
|
.option('--timeout-ms <duration>', 'Overall stream timeout: milliseconds by default, or use 120s / 10m')
|
|
377
|
-
.option('--allow-anonymous', 'Allow anonymous Perplexity responses when cookies are expired')
|
|
443
|
+
.option('--allow-anonymous', 'Allow anonymous Perplexity responses when cookies are expired'))
|
|
378
444
|
.action(async (queryArg, opts) => {
|
|
445
|
+
opts = getOpts(opts);
|
|
379
446
|
if (opts.raw) { rawMode = true; chalk.level = 0; }
|
|
380
447
|
const query = await resolveQuery(queryArg);
|
|
381
|
-
await doSearch(query, opts);
|
|
448
|
+
await doSearch(query, opts, 'search');
|
|
382
449
|
});
|
|
383
450
|
|
|
384
451
|
// Shorthand: reason
|
|
385
|
-
program
|
|
452
|
+
addArtifactOptions(program
|
|
386
453
|
.command('reason [query]')
|
|
387
454
|
.description('Reasoning mode search')
|
|
388
455
|
.option('--model <model>', 'Model name')
|
|
@@ -392,14 +459,15 @@ program
|
|
|
392
459
|
.option('--playwright', 'Use Playwright headless Chromium')
|
|
393
460
|
.option('--no-playwright', 'Disable Playwright even if config enables it')
|
|
394
461
|
.option('--timeout-ms <duration>', 'Overall stream timeout: milliseconds by default, or use 120s / 10m')
|
|
395
|
-
.option('--allow-anonymous', 'Allow anonymous Perplexity responses when cookies are expired')
|
|
462
|
+
.option('--allow-anonymous', 'Allow anonymous Perplexity responses when cookies are expired'))
|
|
396
463
|
.action(async (queryArg, opts) => {
|
|
464
|
+
opts = getOpts(opts);
|
|
397
465
|
const query = await resolveQuery(queryArg);
|
|
398
|
-
await doSearch(query, { ...opts, mode: 'reasoning' });
|
|
466
|
+
await doSearch(query, { ...opts, mode: 'reasoning' }, 'reason');
|
|
399
467
|
});
|
|
400
468
|
|
|
401
469
|
// Shorthand: research
|
|
402
|
-
program
|
|
470
|
+
addArtifactOptions(program
|
|
403
471
|
.command('research [query]')
|
|
404
472
|
.description('Deep research mode')
|
|
405
473
|
.option('--json', 'Output raw JSON')
|
|
@@ -408,12 +476,13 @@ program
|
|
|
408
476
|
.option('--playwright', 'Use Playwright headless Chromium')
|
|
409
477
|
.option('--no-playwright', 'Disable Playwright even if config enables it')
|
|
410
478
|
.option('--timeout-ms <duration>', 'Overall stream timeout: milliseconds by default, or use 120s / 10m')
|
|
411
|
-
.option('--allow-anonymous', 'Allow anonymous Perplexity responses when cookies are expired')
|
|
479
|
+
.option('--allow-anonymous', 'Allow anonymous Perplexity responses when cookies are expired'))
|
|
412
480
|
.action(async (queryArg, opts) => {
|
|
481
|
+
opts = getOpts(opts);
|
|
413
482
|
const query = await resolveQuery(queryArg);
|
|
414
483
|
const spinner = makeSpinner('Deep research in progress...').start();
|
|
415
484
|
try {
|
|
416
|
-
await doSearch(query, { ...opts, mode: 'deep-research', _spinner: spinner });
|
|
485
|
+
await doSearch(query, { ...opts, mode: 'deep-research', _spinner: spinner }, 'research');
|
|
417
486
|
} catch (e) {
|
|
418
487
|
spinner.fail(e.message);
|
|
419
488
|
process.exit(1);
|
|
@@ -421,13 +490,22 @@ program
|
|
|
421
490
|
});
|
|
422
491
|
|
|
423
492
|
// Labs command
|
|
424
|
-
program
|
|
493
|
+
addArtifactOptions(program
|
|
425
494
|
.command('labs [query]')
|
|
426
495
|
.description('Query open-source models (no auth needed)')
|
|
427
496
|
.option('--model <model>', `Model: ${LABS_MODELS.join(', ')}`, 'sonar')
|
|
428
|
-
.option('--json', 'Output
|
|
497
|
+
.option('--json', 'Output single JSON object with answer, events, and artifact metadata'))
|
|
429
498
|
.action(async (queryArg, opts) => {
|
|
499
|
+
opts = getOpts(opts);
|
|
500
|
+
const cfg = loadConfig();
|
|
430
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
|
+
}
|
|
431
509
|
const spinner = makeSpinner('Connecting to labs...').start();
|
|
432
510
|
const client = new LabsClient();
|
|
433
511
|
try {
|
|
@@ -435,9 +513,10 @@ program
|
|
|
435
513
|
spinner.stop();
|
|
436
514
|
|
|
437
515
|
let lastOutput = '';
|
|
516
|
+
const events = [];
|
|
438
517
|
for await (const data of client.ask(query, opts.model)) {
|
|
518
|
+
events.push(data);
|
|
439
519
|
if (opts.json) {
|
|
440
|
-
console.log(JSON.stringify(data));
|
|
441
520
|
continue;
|
|
442
521
|
}
|
|
443
522
|
const output = data.output || '';
|
|
@@ -447,14 +526,129 @@ program
|
|
|
447
526
|
}
|
|
448
527
|
}
|
|
449
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
|
+
}
|
|
450
548
|
} catch (e) {
|
|
451
549
|
spinner.fail('Labs error: ' + e.message);
|
|
550
|
+
if (isQuiet()) console.error(chalk.red('Labs error:'), e.message);
|
|
452
551
|
process.exit(1);
|
|
453
552
|
} finally {
|
|
454
553
|
client.close();
|
|
455
554
|
}
|
|
456
555
|
});
|
|
457
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
|
+
|
|
458
652
|
// Models command
|
|
459
653
|
program
|
|
460
654
|
.command('models')
|
|
@@ -472,9 +666,9 @@ program
|
|
|
472
666
|
// Default: treat bare args as search
|
|
473
667
|
program
|
|
474
668
|
.argument('[query...]', 'Quick search (shorthand for pplx search)')
|
|
475
|
-
.action(async (query) => {
|
|
669
|
+
.action(async (query, opts) => {
|
|
476
670
|
if (query.length > 0) {
|
|
477
|
-
await doSearch(query.join(' '),
|
|
671
|
+
await doSearch(query.join(' '), getOpts(opts || program), 'search');
|
|
478
672
|
}
|
|
479
673
|
});
|
|
480
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;
|