pplx-npx-search 0.3.2 → 0.3.3
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 +7 -0
- package/README.md +15 -0
- package/package.json +1 -1
- package/src/cli.js +104 -1
- package/src/computer.js +58 -5
- package/src/council.js +282 -0
- package/src/mcp-server.js +86 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ 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.3] - 2026-05-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Perplexity Model Council handoff.** Added `pplx council` artifact workflows plus MCP tools for creating, reading, checking, and importing Model Council review runs.
|
|
12
|
+
- **Competitive-analysis template.** Added a `competitive-analysis` Perplexity Computer template that produces evidence briefs with a Council review prompt.
|
|
13
|
+
|
|
8
14
|
## [0.3.2] - 2026-05-26
|
|
9
15
|
|
|
10
16
|
### Added
|
|
@@ -73,6 +79,7 @@ First public release worth telling people about. (v0.2.0 was unpublished before
|
|
|
73
79
|
- SSE streaming for real-time answers
|
|
74
80
|
- Optional Playwright and Chrome CDP transports
|
|
75
81
|
|
|
82
|
+
[0.3.3]: https://github.com/thatsrajan/pplx-cli/compare/v0.3.2...v0.3.3
|
|
76
83
|
[0.3.2]: https://github.com/thatsrajan/pplx-cli/compare/v0.3.1...v0.3.2
|
|
77
84
|
[0.3.1]: https://github.com/thatsrajan/pplx-cli/compare/v0.3.0...v0.3.1
|
|
78
85
|
[0.3.0]: https://github.com/thatsrajan/pplx-cli/compare/v0.2.2...v0.3.0
|
package/README.md
CHANGED
|
@@ -102,6 +102,7 @@ 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
104
|
pplx computer new "compare dinner options nearby"
|
|
105
|
+
pplx council new "critique the competitive threat from Acme in enterprise search"
|
|
105
106
|
pplx models # list available models
|
|
106
107
|
```
|
|
107
108
|
|
|
@@ -187,6 +188,7 @@ The MCP server exposes:
|
|
|
187
188
|
- `pplx_auth_status`: validates the stored Perplexity browser cookies.
|
|
188
189
|
- `pplx_models`: lists known model aliases.
|
|
189
190
|
- `pplx_computer_create`, `pplx_computer_status`, `pplx_computer_read_task`, and `pplx_computer_import`: Perplexity Computer artifact handoff tools.
|
|
191
|
+
- `pplx_council_create`, `pplx_council_status`, `pplx_council_read_task`, and `pplx_council_import`: Perplexity Model Council artifact handoff tools.
|
|
190
192
|
|
|
191
193
|
---
|
|
192
194
|
|
|
@@ -200,6 +202,7 @@ Query-producing commands save artifacts by default:
|
|
|
200
202
|
- `pplx labs`
|
|
201
203
|
- bare `pplx "query"`
|
|
202
204
|
- `pplx computer new`
|
|
205
|
+
- `pplx council new`
|
|
203
206
|
|
|
204
207
|
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.
|
|
205
208
|
|
|
@@ -220,6 +223,18 @@ pplx computer import <run-id> --out ~/Dropbox/pplx-runs --json
|
|
|
220
223
|
|
|
221
224
|
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.
|
|
222
225
|
|
|
226
|
+
`pplx council` is an artifact handoff for Perplexity Model Council. It requires Model Council access in the Perplexity web UI and creates a task prompt plus result contract without claiming a private Council API:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
pplx council new "critique the competitive threat from Acme in enterprise search" --out ~/Dropbox/pplx-runs
|
|
230
|
+
pplx council new "review this evidence" --evidence ~/Dropbox/pplx-runs/acme/computer-result.json
|
|
231
|
+
pplx council open <run-id> --copy
|
|
232
|
+
pplx council status <run-id> --out ~/Dropbox/pplx-runs
|
|
233
|
+
pplx council import <run-id> --out ~/Dropbox/pplx-runs --json
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Council runs include `task.md`, `result.schema.json`, and `council-result.json`. Paste `task.md` into Perplexity Model Council; when the review is done, place the structured result in `council-result.json` so local agents can read it.
|
|
237
|
+
|
|
223
238
|
---
|
|
224
239
|
|
|
225
240
|
## Options
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { loadConfig } from './config.js';
|
|
|
15
15
|
import { resolveTimeoutMs } from './timeout.js';
|
|
16
16
|
import { makeArtifactContext, resolveArtifactDir, writeStandardArtifact } from './artifacts.js';
|
|
17
17
|
import {
|
|
18
|
+
COMPUTER_TEMPLATES,
|
|
18
19
|
createComputerRun,
|
|
19
20
|
copyTextToClipboard,
|
|
20
21
|
importComputerResult,
|
|
@@ -22,6 +23,14 @@ import {
|
|
|
22
23
|
openComputerUrl,
|
|
23
24
|
readTaskFile,
|
|
24
25
|
} from './computer.js';
|
|
26
|
+
import {
|
|
27
|
+
COUNCIL_TEMPLATES,
|
|
28
|
+
copyCouncilTaskToClipboard,
|
|
29
|
+
createCouncilRun,
|
|
30
|
+
importCouncilResult,
|
|
31
|
+
inspectCouncilRun,
|
|
32
|
+
openCouncilUrl,
|
|
33
|
+
} from './council.js';
|
|
25
34
|
|
|
26
35
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
27
36
|
|
|
@@ -562,7 +571,7 @@ const computer = program
|
|
|
562
571
|
addArtifactOptions(computer
|
|
563
572
|
.command('new [task]')
|
|
564
573
|
.description('Create a Perplexity Computer task artifact')
|
|
565
|
-
.option('--template <name>',
|
|
574
|
+
.option('--template <name>', `Computer task template: ${COMPUTER_TEMPLATES.join(', ')}`, 'compare')
|
|
566
575
|
.option('--json', 'Output run metadata as JSON'), { allowDisable: false })
|
|
567
576
|
.action(async (taskArg, opts) => {
|
|
568
577
|
opts = getOpts(opts);
|
|
@@ -649,6 +658,100 @@ addArtifactOptions(computer
|
|
|
649
658
|
}
|
|
650
659
|
});
|
|
651
660
|
|
|
661
|
+
// Council artifact handoff workflow
|
|
662
|
+
const council = program
|
|
663
|
+
.command('council')
|
|
664
|
+
.description('Create and manage Perplexity Model Council artifact handoffs');
|
|
665
|
+
|
|
666
|
+
addArtifactOptions(council
|
|
667
|
+
.command('new [task]')
|
|
668
|
+
.description('Create a Perplexity Model Council task artifact')
|
|
669
|
+
.option('--template <name>', `Council task template: ${COUNCIL_TEMPLATES.join(', ')}`, 'competitive-analysis')
|
|
670
|
+
.option('--evidence <path>', 'Optional local evidence artifact path to reference in the Council task')
|
|
671
|
+
.option('--json', 'Output run metadata as JSON'), { allowDisable: false })
|
|
672
|
+
.action(async (taskArg, opts) => {
|
|
673
|
+
opts = getOpts(opts);
|
|
674
|
+
const task = await resolveQuery(taskArg);
|
|
675
|
+
try {
|
|
676
|
+
const run = createCouncilRun({
|
|
677
|
+
task,
|
|
678
|
+
template: opts.template,
|
|
679
|
+
evidencePath: opts.evidence,
|
|
680
|
+
opts,
|
|
681
|
+
config: loadConfig(),
|
|
682
|
+
});
|
|
683
|
+
if (opts.json) {
|
|
684
|
+
console.log(JSON.stringify(run));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
console.log(chalk.green(`✓ Council task artifact created: ${run.artifactDir}`));
|
|
688
|
+
console.log(chalk.dim(` Task: ${run.taskPath}`));
|
|
689
|
+
console.log(chalk.dim(` Result: ${run.resultPath}`));
|
|
690
|
+
} catch (e) {
|
|
691
|
+
console.error(chalk.red(e.message));
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
addArtifactOptions(council
|
|
697
|
+
.command('open <run>')
|
|
698
|
+
.description('Open Perplexity and optionally copy task.md for Model Council')
|
|
699
|
+
.option('--copy', 'Copy task.md to the clipboard'), { allowDisable: false })
|
|
700
|
+
.action((runId, opts) => {
|
|
701
|
+
opts = getOpts(opts);
|
|
702
|
+
const runDir = resolveRunDir(runId, opts);
|
|
703
|
+
try {
|
|
704
|
+
if (opts.copy && !copyCouncilTaskToClipboard(runDir)) {
|
|
705
|
+
console.log(chalk.yellow('Clipboard copy is only supported on macOS.'));
|
|
706
|
+
}
|
|
707
|
+
if (!openCouncilUrl()) {
|
|
708
|
+
console.log(chalk.yellow('Opening Perplexity is only supported on macOS.'));
|
|
709
|
+
console.log('https://www.perplexity.ai/');
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
console.log(chalk.green(`✓ Opened Perplexity for ${runDir}`));
|
|
713
|
+
if (opts.copy) console.log(chalk.dim(' Copied task.md to clipboard.'));
|
|
714
|
+
} catch (e) {
|
|
715
|
+
console.error(chalk.red(e.message));
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
addArtifactOptions(council
|
|
721
|
+
.command('status <run>')
|
|
722
|
+
.description('Inspect a Perplexity Model Council artifact run')
|
|
723
|
+
.option('--json', 'Output status as JSON'), { allowDisable: false })
|
|
724
|
+
.action((runId, opts) => {
|
|
725
|
+
opts = getOpts(opts);
|
|
726
|
+
const status = inspectCouncilRun(resolveRunDir(runId, opts));
|
|
727
|
+
if (opts.json) {
|
|
728
|
+
console.log(JSON.stringify(status));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const label = status.status === 'complete'
|
|
732
|
+
? chalk.green('[✓] Complete')
|
|
733
|
+
: status.status === 'pending'
|
|
734
|
+
? chalk.yellow('[○] Pending')
|
|
735
|
+
: chalk.red(status.status === 'invalid' ? '[!] Invalid' : '[✗] Missing');
|
|
736
|
+
console.log(`${label} ${status.artifactDir}`);
|
|
737
|
+
if (status.reason) console.log(chalk.dim(` ${status.reason}`));
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
addArtifactOptions(council
|
|
741
|
+
.command('import <run>')
|
|
742
|
+
.description('Print a completed Perplexity Model Council result')
|
|
743
|
+
.option('--json', 'Output compact JSON'), { allowDisable: false })
|
|
744
|
+
.action((runId, opts) => {
|
|
745
|
+
opts = getOpts(opts);
|
|
746
|
+
try {
|
|
747
|
+
const result = importCouncilResult(resolveRunDir(runId, opts));
|
|
748
|
+
console.log(opts.json ? JSON.stringify(result) : JSON.stringify(result, null, 2));
|
|
749
|
+
} catch (e) {
|
|
750
|
+
console.error(chalk.red(e.message));
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
652
755
|
// Models command
|
|
653
756
|
program
|
|
654
757
|
.command('models')
|
package/src/computer.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
|
|
11
11
|
export const COMPUTER_URL = 'https://www.perplexity.ai/computer';
|
|
12
12
|
export const COMPUTER_RESULT_FILE = 'computer-result.json';
|
|
13
|
+
export const COMPUTER_TEMPLATES = ['compare', 'competitive-analysis'];
|
|
13
14
|
export const PENDING_COMPUTER_RESULT = {
|
|
14
15
|
summary: '',
|
|
15
16
|
winner: '',
|
|
@@ -18,6 +19,7 @@ export const PENDING_COMPUTER_RESULT = {
|
|
|
18
19
|
sources: [],
|
|
19
20
|
checked_at: '',
|
|
20
21
|
notes: [],
|
|
22
|
+
council_review_prompt: '',
|
|
21
23
|
_status: 'pending',
|
|
22
24
|
};
|
|
23
25
|
|
|
@@ -34,14 +36,11 @@ const RESULT_SCHEMA = {
|
|
|
34
36
|
sources: { type: 'array' },
|
|
35
37
|
checked_at: { type: 'string' },
|
|
36
38
|
notes: { type: 'array' },
|
|
39
|
+
council_review_prompt: { type: 'string' },
|
|
37
40
|
},
|
|
38
41
|
};
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
if (template !== 'compare') {
|
|
42
|
-
throw new Error(`unsupported computer template: ${template}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
43
|
+
function buildCompareTask({ task, resultPath }) {
|
|
45
44
|
return `# Perplexity Computer Task
|
|
46
45
|
|
|
47
46
|
You are running a live comparison task for a local agent workflow.
|
|
@@ -72,6 +71,60 @@ If you cannot access the filesystem, return the JSON in the chat so the local ag
|
|
|
72
71
|
`;
|
|
73
72
|
}
|
|
74
73
|
|
|
74
|
+
function buildCompetitiveAnalysisTask({ task, resultPath }) {
|
|
75
|
+
return `# Perplexity Computer Task
|
|
76
|
+
|
|
77
|
+
You are running a live competitive-analysis task for a local agent workflow.
|
|
78
|
+
|
|
79
|
+
## User request
|
|
80
|
+
|
|
81
|
+
${task}
|
|
82
|
+
|
|
83
|
+
## Instructions
|
|
84
|
+
|
|
85
|
+
- Treat this as "one competitor or competitor set" plus "one topic, market, product surface, capability, pricing motion, GTM motion, or customer segment."
|
|
86
|
+
- Use live web pages and direct source pages where possible, not only search snippets.
|
|
87
|
+
- Check competitor-owned pages first: home page, product docs, pricing, changelog, blog, help center, release notes, status pages, public roadmaps, job postings, and app marketplace listings.
|
|
88
|
+
- Add independent evidence where useful: customer reviews, forum threads, analyst commentary, news, benchmark posts, partner pages, search ads, and social posts.
|
|
89
|
+
- Preserve source URLs for every material claim.
|
|
90
|
+
- Separate facts from interpretation.
|
|
91
|
+
- Identify:
|
|
92
|
+
- current positioning
|
|
93
|
+
- recent changes or launches
|
|
94
|
+
- pricing and packaging implications
|
|
95
|
+
- GTM and distribution signals
|
|
96
|
+
- customer pain or praise signals
|
|
97
|
+
- likely strategic intent
|
|
98
|
+
- threat level to the user request's context
|
|
99
|
+
- evidence gaps that need follow-up
|
|
100
|
+
- Include the time the information was checked.
|
|
101
|
+
- Mark uncertainty explicitly. Do not invent missing values.
|
|
102
|
+
- Prefer structured evidence over prose.
|
|
103
|
+
- Include \`council_review_prompt\`: a concise prompt that can be pasted into Perplexity Model Council to critique the evidence and strategy read.
|
|
104
|
+
|
|
105
|
+
## Output target
|
|
106
|
+
|
|
107
|
+
Write the final result as JSON matching \`result.schema.json\`.
|
|
108
|
+
|
|
109
|
+
If you can access the local filesystem, save it here:
|
|
110
|
+
|
|
111
|
+
\`${resultPath}\`
|
|
112
|
+
|
|
113
|
+
If you cannot access the filesystem, return the JSON in the chat so the local agent or user can place it in that file.
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function buildComputerTask({ task, template = 'compare', resultPath }) {
|
|
118
|
+
if (!COMPUTER_TEMPLATES.includes(template)) {
|
|
119
|
+
throw new Error(`unsupported computer template: ${template}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (template === 'competitive-analysis') {
|
|
123
|
+
return buildCompetitiveAnalysisTask({ task, resultPath });
|
|
124
|
+
}
|
|
125
|
+
return buildCompareTask({ task, resultPath });
|
|
126
|
+
}
|
|
127
|
+
|
|
75
128
|
export function createComputerRun({ task, template = 'compare', opts = {}, config = {} }) {
|
|
76
129
|
const ctx = makeArtifactContext({ command: 'computer', query: task, opts, config });
|
|
77
130
|
if (!ctx) {
|
package/src/council.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
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
|
+
import { copyTextToClipboard } from './computer.js';
|
|
11
|
+
|
|
12
|
+
export const COUNCIL_URL = 'https://www.perplexity.ai/';
|
|
13
|
+
export const COUNCIL_RESULT_FILE = 'council-result.json';
|
|
14
|
+
export const COUNCIL_TEMPLATES = ['competitive-analysis', 'strategy-review'];
|
|
15
|
+
export const PENDING_COUNCIL_RESULT = {
|
|
16
|
+
summary: '',
|
|
17
|
+
consensus: [],
|
|
18
|
+
disagreements: [],
|
|
19
|
+
risks: [],
|
|
20
|
+
recommendations: [],
|
|
21
|
+
followups: [],
|
|
22
|
+
sources: [],
|
|
23
|
+
confidence: 'low',
|
|
24
|
+
checked_at: '',
|
|
25
|
+
notes: [],
|
|
26
|
+
_status: 'pending',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const RESULT_SCHEMA = {
|
|
30
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
31
|
+
title: 'pplx council review result',
|
|
32
|
+
type: 'object',
|
|
33
|
+
required: [
|
|
34
|
+
'summary',
|
|
35
|
+
'consensus',
|
|
36
|
+
'disagreements',
|
|
37
|
+
'risks',
|
|
38
|
+
'recommendations',
|
|
39
|
+
'followups',
|
|
40
|
+
'sources',
|
|
41
|
+
'confidence',
|
|
42
|
+
'checked_at',
|
|
43
|
+
'notes',
|
|
44
|
+
],
|
|
45
|
+
properties: {
|
|
46
|
+
summary: { type: 'string' },
|
|
47
|
+
consensus: { type: 'array' },
|
|
48
|
+
disagreements: { type: 'array' },
|
|
49
|
+
risks: { type: 'array' },
|
|
50
|
+
recommendations: { type: 'array' },
|
|
51
|
+
followups: { type: 'array' },
|
|
52
|
+
sources: { type: 'array' },
|
|
53
|
+
confidence: { enum: ['low', 'medium', 'high'] },
|
|
54
|
+
checked_at: { type: 'string' },
|
|
55
|
+
notes: { type: 'array' },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function buildCompetitiveAnalysisCouncilTask({ task, evidencePath, resultPath }) {
|
|
60
|
+
const evidenceBlock = evidencePath
|
|
61
|
+
? `\n## Evidence brief\n\nUse this local evidence artifact if available:\n\n\`${evidencePath}\`\n`
|
|
62
|
+
: '';
|
|
63
|
+
|
|
64
|
+
return `# Perplexity Model Council Task
|
|
65
|
+
|
|
66
|
+
Use Perplexity Model Council for a multi-model review. This is a judgment and synthesis pass, not the primary web-browsing pass.
|
|
67
|
+
|
|
68
|
+
## User request
|
|
69
|
+
|
|
70
|
+
${task}
|
|
71
|
+
${evidenceBlock}
|
|
72
|
+
## Instructions
|
|
73
|
+
|
|
74
|
+
- Ask the Council to evaluate the same evidence from multiple model perspectives before synthesizing.
|
|
75
|
+
- Focus on the competitor, topic, market, product capability, pricing motion, GTM motion, or customer segment named in the request.
|
|
76
|
+
- Separate source-backed facts from strategic interpretation.
|
|
77
|
+
- Identify where the models agree and where they disagree.
|
|
78
|
+
- Challenge the strongest assumption in the analysis.
|
|
79
|
+
- Surface missing evidence that would change the conclusion.
|
|
80
|
+
- Produce concrete recommendations and monitoring follow-ups.
|
|
81
|
+
- Preserve source URLs from the evidence brief and add any new source URLs used by Council.
|
|
82
|
+
- Mark uncertainty explicitly. Do not invent missing values.
|
|
83
|
+
|
|
84
|
+
## Output target
|
|
85
|
+
|
|
86
|
+
Return the final result as JSON matching \`result.schema.json\`.
|
|
87
|
+
|
|
88
|
+
If you can access the local filesystem, save it here:
|
|
89
|
+
|
|
90
|
+
\`${resultPath}\`
|
|
91
|
+
|
|
92
|
+
If you cannot access the filesystem, return the JSON in the chat so the local agent or user can place it in that file.
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildStrategyReviewCouncilTask({ task, evidencePath, resultPath }) {
|
|
97
|
+
const evidenceBlock = evidencePath
|
|
98
|
+
? `\n## Evidence brief\n\nUse this local evidence artifact if available:\n\n\`${evidencePath}\`\n`
|
|
99
|
+
: '';
|
|
100
|
+
|
|
101
|
+
return `# Perplexity Model Council Task
|
|
102
|
+
|
|
103
|
+
Use Perplexity Model Council for a multi-model strategy review.
|
|
104
|
+
|
|
105
|
+
## User request
|
|
106
|
+
|
|
107
|
+
${task}
|
|
108
|
+
${evidenceBlock}
|
|
109
|
+
## Instructions
|
|
110
|
+
|
|
111
|
+
- Ask the Council to evaluate the request from multiple reasoning perspectives before synthesizing.
|
|
112
|
+
- Identify consensus, disagreements, risks, blind spots, and decision criteria.
|
|
113
|
+
- Separate source-backed facts from interpretation.
|
|
114
|
+
- Challenge the strongest assumption.
|
|
115
|
+
- Surface missing evidence that would change the conclusion.
|
|
116
|
+
- Produce concrete recommendations and follow-up questions.
|
|
117
|
+
- Preserve source URLs from the evidence brief and add any new source URLs used by Council.
|
|
118
|
+
- Mark uncertainty explicitly. Do not invent missing values.
|
|
119
|
+
|
|
120
|
+
## Output target
|
|
121
|
+
|
|
122
|
+
Return the final result as JSON matching \`result.schema.json\`.
|
|
123
|
+
|
|
124
|
+
If you can access the local filesystem, save it here:
|
|
125
|
+
|
|
126
|
+
\`${resultPath}\`
|
|
127
|
+
|
|
128
|
+
If you cannot access the filesystem, return the JSON in the chat so the local agent or user can place it in that file.
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function buildCouncilTask({
|
|
133
|
+
task,
|
|
134
|
+
template = 'competitive-analysis',
|
|
135
|
+
evidencePath,
|
|
136
|
+
resultPath,
|
|
137
|
+
}) {
|
|
138
|
+
if (!COUNCIL_TEMPLATES.includes(template)) {
|
|
139
|
+
throw new Error(`unsupported council template: ${template}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (template === 'strategy-review') {
|
|
143
|
+
return buildStrategyReviewCouncilTask({ task, evidencePath, resultPath });
|
|
144
|
+
}
|
|
145
|
+
return buildCompetitiveAnalysisCouncilTask({ task, evidencePath, resultPath });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createCouncilRun({
|
|
149
|
+
task,
|
|
150
|
+
template = 'competitive-analysis',
|
|
151
|
+
evidencePath,
|
|
152
|
+
opts = {},
|
|
153
|
+
config = {},
|
|
154
|
+
}) {
|
|
155
|
+
const ctx = makeArtifactContext({ command: 'council', query: task, opts, config });
|
|
156
|
+
if (!ctx) {
|
|
157
|
+
throw new Error('council runs require artifacts; omit --no-artifact for this command');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
mkdirSync(ctx.artifactDir, { recursive: true });
|
|
161
|
+
const createdAt = new Date().toISOString();
|
|
162
|
+
const resultPath = join(ctx.artifactDir, COUNCIL_RESULT_FILE);
|
|
163
|
+
const taskText = buildCouncilTask({ task, template, evidencePath, resultPath });
|
|
164
|
+
const meta = {
|
|
165
|
+
schemaVersion: ARTIFACT_SCHEMA_VERSION,
|
|
166
|
+
command: 'council',
|
|
167
|
+
query: task,
|
|
168
|
+
template,
|
|
169
|
+
evidencePath: evidencePath || null,
|
|
170
|
+
artifactId: ctx.artifactId,
|
|
171
|
+
artifactDir: ctx.artifactDir,
|
|
172
|
+
createdAt,
|
|
173
|
+
status: 'pending',
|
|
174
|
+
};
|
|
175
|
+
const result = {
|
|
176
|
+
query: task,
|
|
177
|
+
answer: '',
|
|
178
|
+
sources: [],
|
|
179
|
+
command: 'council',
|
|
180
|
+
mode: 'council',
|
|
181
|
+
model: 'model-council',
|
|
182
|
+
template,
|
|
183
|
+
evidencePath: evidencePath || null,
|
|
184
|
+
artifactId: ctx.artifactId,
|
|
185
|
+
artifactDir: ctx.artifactDir,
|
|
186
|
+
createdAt,
|
|
187
|
+
status: 'pending',
|
|
188
|
+
councilResultFile: COUNCIL_RESULT_FILE,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
writeJson(join(ctx.artifactDir, 'meta.json'), meta);
|
|
192
|
+
writeFileSync(join(ctx.artifactDir, 'query.txt'), `${task}\n`, 'utf8');
|
|
193
|
+
writeFileSync(join(ctx.artifactDir, 'answer.md'), taskText, 'utf8');
|
|
194
|
+
writeJson(join(ctx.artifactDir, 'result.json'), result);
|
|
195
|
+
writeJson(join(ctx.artifactDir, 'sources.json'), []);
|
|
196
|
+
writeFileSync(join(ctx.artifactDir, 'task.md'), taskText, 'utf8');
|
|
197
|
+
writeJson(join(ctx.artifactDir, 'result.schema.json'), RESULT_SCHEMA);
|
|
198
|
+
writeJson(join(ctx.artifactDir, COUNCIL_RESULT_FILE), PENDING_COUNCIL_RESULT);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
artifactId: ctx.artifactId,
|
|
202
|
+
artifactDir: ctx.artifactDir,
|
|
203
|
+
taskPath: join(ctx.artifactDir, 'task.md'),
|
|
204
|
+
resultPath,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function assertCouncilResult(value) {
|
|
209
|
+
const missing = [
|
|
210
|
+
'summary',
|
|
211
|
+
'consensus',
|
|
212
|
+
'disagreements',
|
|
213
|
+
'risks',
|
|
214
|
+
'recommendations',
|
|
215
|
+
'followups',
|
|
216
|
+
'sources',
|
|
217
|
+
'confidence',
|
|
218
|
+
'checked_at',
|
|
219
|
+
'notes',
|
|
220
|
+
].filter((key) => !(key in value));
|
|
221
|
+
if (missing.length) return { ok: false, reason: `missing fields: ${missing.join(', ')}` };
|
|
222
|
+
if (!['low', 'medium', 'high'].includes(value.confidence)) {
|
|
223
|
+
return { ok: false, reason: 'confidence must be low, medium, or high' };
|
|
224
|
+
}
|
|
225
|
+
for (const key of ['consensus', 'disagreements', 'risks', 'recommendations', 'followups', 'sources', 'notes']) {
|
|
226
|
+
if (!Array.isArray(value[key])) return { ok: false, reason: `${key} must be an array` };
|
|
227
|
+
}
|
|
228
|
+
return { ok: true, reason: null };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function inspectCouncilRun(runDir) {
|
|
232
|
+
const artifactDir = resolve(runDir);
|
|
233
|
+
const resultPath = join(artifactDir, COUNCIL_RESULT_FILE);
|
|
234
|
+
const metaPath = join(artifactDir, 'meta.json');
|
|
235
|
+
if (!existsSync(metaPath)) {
|
|
236
|
+
return { status: 'missing', artifactDir, resultPath, reason: 'meta.json not found' };
|
|
237
|
+
}
|
|
238
|
+
if (!existsSync(resultPath)) {
|
|
239
|
+
return { status: 'pending', artifactDir, resultPath, reason: `${COUNCIL_RESULT_FILE} not found` };
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const result = readJsonFile(resultPath);
|
|
243
|
+
if (result._status === 'pending') {
|
|
244
|
+
return { status: 'pending', artifactDir, resultPath, reason: `${COUNCIL_RESULT_FILE} is still pending` };
|
|
245
|
+
}
|
|
246
|
+
const validation = assertCouncilResult(result);
|
|
247
|
+
return {
|
|
248
|
+
status: validation.ok ? 'complete' : 'invalid',
|
|
249
|
+
artifactDir,
|
|
250
|
+
resultPath,
|
|
251
|
+
reason: validation.reason,
|
|
252
|
+
result,
|
|
253
|
+
};
|
|
254
|
+
} catch (e) {
|
|
255
|
+
return { status: 'invalid', artifactDir, resultPath, reason: e.message };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function importCouncilResult(runDir) {
|
|
260
|
+
const status = inspectCouncilRun(runDir);
|
|
261
|
+
if (status.status !== 'complete') {
|
|
262
|
+
throw new Error(`council result is ${status.status}: ${status.reason}`);
|
|
263
|
+
}
|
|
264
|
+
return status.result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function openCouncilUrl() {
|
|
268
|
+
if (process.platform === 'darwin') {
|
|
269
|
+
execFileSync('open', [COUNCIL_URL]);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function copyCouncilTaskToClipboard(runDir) {
|
|
276
|
+
const taskText = readCouncilTaskFile(runDir);
|
|
277
|
+
return copyTextToClipboard(taskText);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function readCouncilTaskFile(runDir) {
|
|
281
|
+
return readFileSync(join(resolve(runDir), 'task.md'), 'utf8');
|
|
282
|
+
}
|
package/src/mcp-server.js
CHANGED
|
@@ -11,11 +11,19 @@ import { LabsClient } from './labs.js';
|
|
|
11
11
|
import { resolveTimeoutMs } from './timeout.js';
|
|
12
12
|
import { makeArtifactContext, resolveArtifactDir, writeStandardArtifact } from './artifacts.js';
|
|
13
13
|
import {
|
|
14
|
+
COMPUTER_TEMPLATES,
|
|
14
15
|
createComputerRun,
|
|
15
16
|
importComputerResult,
|
|
16
17
|
inspectComputerRun,
|
|
17
18
|
readTaskFile,
|
|
18
19
|
} from './computer.js';
|
|
20
|
+
import {
|
|
21
|
+
COUNCIL_TEMPLATES,
|
|
22
|
+
createCouncilRun,
|
|
23
|
+
importCouncilResult,
|
|
24
|
+
inspectCouncilRun,
|
|
25
|
+
readCouncilTaskFile,
|
|
26
|
+
} from './council.js';
|
|
19
27
|
import { MODEL_MAP, LABS_MODELS } from './constants.js';
|
|
20
28
|
|
|
21
29
|
const SERVER_NAME = 'pplx';
|
|
@@ -271,7 +279,7 @@ export function createPplxMcpServer() {
|
|
|
271
279
|
description: 'Create a Perplexity Computer artifact handoff folder containing task.md, result.schema.json, and computer-result.json.',
|
|
272
280
|
inputSchema: {
|
|
273
281
|
task: z.string().min(1).describe('Live web task for Perplexity Computer.'),
|
|
274
|
-
template: z.
|
|
282
|
+
template: z.enum(COMPUTER_TEMPLATES).optional().default('compare').describe('Computer task template.'),
|
|
275
283
|
out: z.string().optional().describe('Artifact root for this run.'),
|
|
276
284
|
artifactId: z.string().optional().describe('Deterministic artifact id for this run.'),
|
|
277
285
|
},
|
|
@@ -290,6 +298,83 @@ export function createPplxMcpServer() {
|
|
|
290
298
|
return toTextResult(run);
|
|
291
299
|
});
|
|
292
300
|
|
|
301
|
+
server.registerTool('pplx_council_create', {
|
|
302
|
+
title: 'Create Perplexity Model Council Handoff',
|
|
303
|
+
description: 'Create a Perplexity Model Council artifact handoff folder containing task.md, result.schema.json, and council-result.json.',
|
|
304
|
+
inputSchema: {
|
|
305
|
+
task: z.string().min(1).describe('Judgment, strategy, or competitive-analysis task for Model Council.'),
|
|
306
|
+
template: z.enum(COUNCIL_TEMPLATES).optional().default('competitive-analysis').describe('Council task template.'),
|
|
307
|
+
evidencePath: z.string().optional().describe('Optional local evidence artifact path to reference in the Council task.'),
|
|
308
|
+
out: z.string().optional().describe('Artifact root for this run.'),
|
|
309
|
+
artifactId: z.string().optional().describe('Deterministic artifact id for this run.'),
|
|
310
|
+
},
|
|
311
|
+
annotations: {
|
|
312
|
+
title: 'Create Perplexity Model Council Handoff',
|
|
313
|
+
readOnlyHint: false,
|
|
314
|
+
openWorldHint: false,
|
|
315
|
+
},
|
|
316
|
+
}, async (args) => {
|
|
317
|
+
const run = createCouncilRun({
|
|
318
|
+
task: args.task,
|
|
319
|
+
template: args.template || 'competitive-analysis',
|
|
320
|
+
evidencePath: args.evidencePath,
|
|
321
|
+
opts: { out: args.out, artifactId: args.artifactId },
|
|
322
|
+
config: loadConfig(),
|
|
323
|
+
});
|
|
324
|
+
return toTextResult(run);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
server.registerTool('pplx_council_status', {
|
|
328
|
+
title: 'Perplexity Model Council Status',
|
|
329
|
+
description: 'Inspect a Perplexity Model Council artifact run.',
|
|
330
|
+
inputSchema: {
|
|
331
|
+
run: z.string().min(1).describe('Run id or absolute run folder path.'),
|
|
332
|
+
out: z.string().optional().describe('Artifact root used when run is an id.'),
|
|
333
|
+
},
|
|
334
|
+
annotations: {
|
|
335
|
+
title: 'Perplexity Model Council Status',
|
|
336
|
+
readOnlyHint: true,
|
|
337
|
+
openWorldHint: false,
|
|
338
|
+
},
|
|
339
|
+
}, async (args) => {
|
|
340
|
+
const runDir = resolveRunDir(args.run, args.out, loadConfig());
|
|
341
|
+
return toTextResult(inspectCouncilRun(runDir));
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
server.registerTool('pplx_council_import', {
|
|
345
|
+
title: 'Import Perplexity Model Council Result',
|
|
346
|
+
description: 'Read and validate a completed council-result.json from a Perplexity Model Council artifact run.',
|
|
347
|
+
inputSchema: {
|
|
348
|
+
run: z.string().min(1).describe('Run id or absolute run folder path.'),
|
|
349
|
+
out: z.string().optional().describe('Artifact root used when run is an id.'),
|
|
350
|
+
},
|
|
351
|
+
annotations: {
|
|
352
|
+
title: 'Import Perplexity Model Council Result',
|
|
353
|
+
readOnlyHint: true,
|
|
354
|
+
openWorldHint: false,
|
|
355
|
+
},
|
|
356
|
+
}, async (args) => {
|
|
357
|
+
const runDir = resolveRunDir(args.run, args.out, loadConfig());
|
|
358
|
+
return toTextResult(importCouncilResult(runDir));
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
server.registerTool('pplx_council_read_task', {
|
|
362
|
+
title: 'Read Perplexity Model Council Task',
|
|
363
|
+
description: 'Read the task.md prompt from a Perplexity Model Council artifact run.',
|
|
364
|
+
inputSchema: {
|
|
365
|
+
run: z.string().min(1).describe('Run id or absolute run folder path.'),
|
|
366
|
+
out: z.string().optional().describe('Artifact root used when run is an id.'),
|
|
367
|
+
},
|
|
368
|
+
annotations: {
|
|
369
|
+
title: 'Read Perplexity Model Council Task',
|
|
370
|
+
readOnlyHint: true,
|
|
371
|
+
openWorldHint: false,
|
|
372
|
+
},
|
|
373
|
+
}, async (args) => {
|
|
374
|
+
const runDir = resolveRunDir(args.run, args.out, loadConfig());
|
|
375
|
+
return toTextResult({ runDir, task: readCouncilTaskFile(runDir) });
|
|
376
|
+
});
|
|
377
|
+
|
|
293
378
|
server.registerTool('pplx_computer_status', {
|
|
294
379
|
title: 'Perplexity Computer Status',
|
|
295
380
|
description: 'Inspect a Perplexity Computer artifact run.',
|