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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pplx-npx-search",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
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": {
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>', 'Computer task template', 'compare')
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
- export function buildComputerTask({ task, template = 'compare', resultPath }) {
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.literal('compare').optional().default('compare').describe('Computer task template.'),
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.',