pplx-zero 1.0.0 → 1.0.1

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.
Files changed (3) hide show
  1. package/README.md +99 -42
  2. package/dist/cli.js +378 -61
  3. package/package.json +15 -3
package/README.md CHANGED
@@ -1,23 +1,29 @@
1
- # 🚀 PPLX-Zero
1
+ # PPLX-Zero
2
2
 
3
- > Minimal, fast Perplexity AI search CLI - zero configuration, zero compromises
3
+ > Fast Perplexity AI search CLI - minimal setup, maximal results
4
4
 
5
- [![npm version](https://badge.fury.io/js/pplx-zero.svg)](https://badge.fury.io/js/pplx-zero)
6
- [![AUR package](https://img.shields.io/aur/version/pplx-zero)](https://aur.archlinux.org/packages/pplx-zero)
7
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ <p align="center">
6
+ <a href="https://badge.fury.io/js/pplx-zero"><img src="https://badge.fury.io/js/pplx-zero.svg" alt="npm version"></a>
7
+ <a href="https://aur.archlinux.org/packages/pplx-zero"><img src="https://img.shields.io/aur/version/pplx-zero?style=flat-square" alt="AUR package"></a>
8
+ <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-007ACC?logo=typescript&logoColor=white" alt="TypeScript"></a>
9
+ <a href="https://bun.sh"><img src="https://img.shields.io/badge/Bun-black?logo=bun&logoColor=white" alt="Bun"></a>
10
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
11
+ </p>
8
12
 
9
- PPLX-Zero is a blazing-fast, production-ready TypeScript implementation of Perplexity AI search integration. Built with Bun runtime for maximum performance and zero bloat.
13
+ A fast TypeScript CLI for Perplexity AI search with multimodal support. Built with Bun runtime for performance and reliability.
10
14
 
11
- ## ✨ Key Features
15
+ ## Features
12
16
 
13
- - **⚡ Lightning Fast** - Concurrent searches with intelligent rate limiting
14
- - **🎯 Zero Config** - Works out of the box with just an API key
15
- - **📦 Batch Processing** - Handle multiple queries simultaneously
16
- - **🔄 Real-time Streaming** - Progress updates via JSONL events
17
+ - **⚡ Fast Search** - Concurrent queries with intelligent rate limiting
18
+ - **🎯 Simple Setup** - Works with just an API key, no configuration required
19
+ - **📦 Batch Processing** - Handle multiple searches simultaneously
20
+ - **🔄 Real-time Updates** - JSONL streaming progress events
21
+ - **🖼️ File Analysis** - Process documents and images with AI models
22
+ - **🤖 AI Models** - Sonar, Sonar Pro, Sonar Deep Research, Sonar Reasoning
17
23
  - **🛡️ Type Safe** - Full Zod validation and TypeScript support
18
- - **🌍 Cross-Platform** - Native Bun runtime everywhere
24
+ - **🌍 Cross-Platform** - Native Bun runtime support
19
25
 
20
- ## 🚀 Quick Start
26
+ ## Quick Start
21
27
 
22
28
  ### 1️⃣ Install
23
29
 
@@ -29,8 +35,6 @@ npm install -g pplx-zero
29
35
 
30
36
  # AUR (Arch Linux)
31
37
  yay -S pplx-zero
32
- # or
33
- pikaur -S pplx-zero
34
38
  # or manual AUR
35
39
  git clone https://aur.archlinux.org/pplx-zero.git
36
40
  cd pplx-zero
@@ -51,38 +55,58 @@ sudo ln -s "$(pwd)/dist/cli.js" /usr/local/bin/pplx
51
55
 
52
56
  ### 2️⃣ Setup API Key
53
57
 
58
+ **Linux/macOS:**
54
59
  ```bash
55
- export PERPLEXITY_API_KEY="your-perplexity-api-key"
56
- # Or use the fallback
57
- export PERPLEXITY_AI_API_KEY="your-alternative-api-key"
60
+ export PERPLEXITY_API_KEY="your-api-key"
58
61
  ```
59
62
 
63
+ **Windows:**
64
+ ```cmd
65
+ setx PERPLEXITY_API_KEY "your-api-key"
66
+ ```
67
+
68
+ **Get your API key:** https://www.perplexity.ai/account/api/keys
69
+
60
70
  ### 3️⃣ Start Searching
61
71
 
62
72
  ```bash
63
- # Single query (CLI command is 'pplx')
73
+ # Basic search
64
74
  pplx "latest AI developments"
65
75
 
66
- # Batch from file
76
+ # Choose model
77
+ pplx --model sonar-pro "Detailed analysis"
78
+
79
+ # Document analysis
80
+ pplx --attach report.pdf "Summarize this document"
81
+
82
+ # Image analysis
83
+ pplx --attach-image screenshot.png "Analyze this interface"
84
+
85
+ # Batch processing
67
86
  pplx --input queries.json
68
87
 
69
- # Stream from stdin
88
+ # Stream processing
70
89
  cat queries.jsonl | pplx --stdin
71
90
  ```
72
91
 
73
- ## 📖 Usage Guide
92
+ ## Usage Guide
74
93
 
75
- ### Command Line Interface
94
+ ### Command Line Options
76
95
 
77
96
  ```bash
78
- # Basic usage
79
- pplx "your search query"
80
-
81
- # Advanced options
97
+ # Search with custom settings
82
98
  pplx --concurrency 10 --timeout 60000 --format jsonl "machine learning trends"
83
99
 
84
- # Dry run validation
85
- pplx --dry-run "test query"
100
+ # Model selection
101
+ pplx --model sonar-pro "Detailed analysis"
102
+ pplx --model sonar-reasoning "Complex problem solving"
103
+
104
+ # File attachments
105
+ pplx --attach document.pdf "Summarize this report"
106
+ pplx --attach-image chart.png "Analyze this chart"
107
+
108
+ # Async processing
109
+ pplx --async --webhook https://api.example.com/callback "Research task"
86
110
  ```
87
111
 
88
112
  ### Batch Processing
@@ -110,6 +134,28 @@ Process with:
110
134
  pplx --input queries.json --format jsonl
111
135
  ```
112
136
 
137
+ ### File Attachments
138
+
139
+ Supported file formats for analysis:
140
+
141
+ **Documents (max 50MB):**
142
+ - PDF, DOC, DOCX, TXT, RTF
143
+
144
+ **Images (max 50MB):**
145
+ - PNG, JPEG, WebP, HEIF, HEIC, GIF
146
+
147
+ **Examples:**
148
+ ```bash
149
+ # Document analysis
150
+ pplx --attach report.pdf "Summarize this document"
151
+
152
+ # Image analysis
153
+ pplx --attach-image screenshot.png "Analyze this interface"
154
+
155
+ # Multiple files
156
+ pplx --attach document.txt --attach-image chart.png "Analyze this data"
157
+ ```
158
+
113
159
  ### Programmatic Usage
114
160
 
115
161
  ```typescript
@@ -128,7 +174,7 @@ const result = await tool.runBatch({
128
174
  console.log(result);
129
175
  ```
130
176
 
131
- ## ⚙️ Configuration
177
+ ## Configuration
132
178
 
133
179
  | Option | Short | Type | Default | Description |
134
180
  |--------|-------|------|---------|-------------|
@@ -137,11 +183,22 @@ console.log(result);
137
183
  | `--concurrency` | `-c` | number | 5 | Max concurrent requests (1-20) |
138
184
  | `--timeout` | `-t` | number | 30000 | Request timeout in ms (1000-300000) |
139
185
  | `--format` | `-f` | string | json | Output format: json|jsonl |
140
- | `--dry-run` | `-d` | boolean | false | Validate input without executing |
186
+ | `--model` | `-m` | string | sonar | AI model to use |
187
+ | `--attach` | - | string[] | - | Attach document files |
188
+ | `--attach-image` | - | string[] | - | Attach image files |
189
+ | `--async` | - | boolean | false | Process requests asynchronously |
190
+ | `--webhook` | - | string | - | Webhook URL for async notifications |
141
191
  | `--version` | `-v` | boolean | - | Show version |
142
192
  | `--help` | `-h` | boolean | - | Show help |
143
193
 
144
- ## 📊 Output Formats
194
+ ### AI Models
195
+
196
+ - `sonar` - Fast, concise responses (default)
197
+ - `sonar-pro` - Detailed, comprehensive responses
198
+ - `sonar-deep-research` - In-depth research with web search
199
+ - `sonar-reasoning` - Step-by-step logical reasoning
200
+
201
+ ## Output Formats
145
202
 
146
203
  ### JSON (Default)
147
204
  ```json
@@ -164,7 +221,7 @@ pplx --format jsonl "AI trends"
164
221
  ```
165
222
  Each result printed as a separate JSON line for real-time processing.
166
223
 
167
- ## 🔧 Development
224
+ ## Development
168
225
 
169
226
  ```bash
170
227
  # Development mode
@@ -183,15 +240,15 @@ bun run build
183
240
  bun run build:binary
184
241
  ```
185
242
 
186
- ## 🏗️ Architecture
243
+ ## Architecture
187
244
 
188
- - **Bun Runtime** - Ultra-fast JavaScript runtime
245
+ - **Bun Runtime** - Fast JavaScript runtime
189
246
  - **Zod Validation** - Type-safe schema validation
190
- - **Circuit Breaker** - Resilient error handling
191
- - **Semaphore Pattern** - Controlled concurrency
247
+ - **Error Handling** - Resilient error recovery
248
+ - **Concurrency Control** - Semaphore pattern for rate limiting
192
249
  - **Streaming Events** - Real-time progress updates
193
250
 
194
- ## 🛡️ Security Features
251
+ ## Security Features
195
252
 
196
253
  - Environment variable API key management
197
254
  - Input validation and sanitization
@@ -199,7 +256,7 @@ bun run build:binary
199
256
  - Error information filtering
200
257
  - No external dependencies beyond core runtime
201
258
 
202
- ## 📋 Error Handling
259
+ ## Error Handling
203
260
 
204
261
  PPLX-Zero provides comprehensive error classification:
205
262
 
@@ -215,14 +272,14 @@ enum ErrorCode {
215
272
  }
216
273
  ```
217
274
 
218
- ## 🤝 Contributing
275
+ ## Contributing
219
276
 
220
277
  Contributions are welcome! Please feel free to submit a Pull Request.
221
278
 
222
- ## 📄 License
279
+ ## License
223
280
 
224
281
  MIT License - see [LICENSE](LICENSE) file for details.
225
282
 
226
283
  ---
227
284
 
228
- **Built with ❤️ using [Bun](https://bun.sh) and [Perplexity AI](https://www.perplexity.ai)**
285
+ **Built with [Bun](https://bun.sh) and [Perplexity AI](https://www.perplexity.ai)**
package/dist/cli.js CHANGED
@@ -31,8 +31,8 @@ var __export = (target, all) => {
31
31
  var require_package = __commonJS((exports, module) => {
32
32
  module.exports = {
33
33
  name: "pplx-zero",
34
- version: "1.0.0",
35
- description: "PPLX-Zero: Minimal, fast Perplexity AI search CLI - zero configuration, zero compromises",
34
+ version: "1.0.1",
35
+ description: "Fast Perplexity AI search CLI with multimodal support - minimal setup, maximal results",
36
36
  type: "module",
37
37
  main: "dist/index.js",
38
38
  bin: {
@@ -80,10 +80,22 @@ var require_package = __commonJS((exports, module) => {
80
80
  "minimal",
81
81
  "fast",
82
82
  "productivity",
83
- "zero-config"
83
+ "zero-config",
84
+ "multimodal",
85
+ "attachments",
86
+ "images",
87
+ "documents",
88
+ "sonar",
89
+ "reasoning",
90
+ "research",
91
+ "async"
84
92
  ],
85
93
  author: "Kenzo",
86
94
  license: "MIT",
95
+ repository: {
96
+ type: "git",
97
+ url: "https://github.com/codewithkenzo/pplx-zero.git"
98
+ },
87
99
  files: [
88
100
  "dist",
89
101
  "README.md",
@@ -5602,10 +5614,26 @@ Perplexity.Async = Async;
5602
5614
  Perplexity.Search = Search;
5603
5615
  // src/schema.ts
5604
5616
  var SCHEMA_VERSION = "1.0.0";
5617
+ var SonarModelSchema = exports_external.enum(["sonar", "sonar-pro", "sonar-deep-research", "sonar-reasoning"]);
5618
+ var AttachmentSchema = exports_external.object({
5619
+ name: exports_external.string().min(1, "Attachment name is required"),
5620
+ extension: exports_external.string().min(1, "File extension is required"),
5621
+ mimeType: exports_external.string().min(1, "MIME type is required"),
5622
+ url: exports_external.string().min(1, "Attachment URL is required"),
5623
+ size: exports_external.number().int().min(0, "File size must be non-negative").optional()
5624
+ });
5625
+ var AttachmentInputSchema = exports_external.object({
5626
+ path: exports_external.string().min(1, "File path is required"),
5627
+ name: exports_external.string().optional(),
5628
+ type: exports_external.enum(["image", "document"]).optional()
5629
+ });
5605
5630
  var SearchQuerySchema = exports_external.object({
5606
5631
  query: exports_external.string().min(1, "Query cannot be empty"),
5607
5632
  maxResults: exports_external.number().int().min(1).max(50).optional().default(5),
5608
- country: exports_external.string().length(2).regex(/^[A-Z]{2}$/, "Invalid country code").optional()
5633
+ country: exports_external.string().length(2).regex(/^[A-Z]{2}$/, "Invalid country code").optional(),
5634
+ model: SonarModelSchema.optional().default("sonar"),
5635
+ attachments: exports_external.array(AttachmentSchema).optional(),
5636
+ attachmentInputs: exports_external.array(AttachmentInputSchema).optional()
5609
5637
  });
5610
5638
  var SearchInputV1Schema = exports_external.object({
5611
5639
  id: exports_external.string().uuid().optional(),
@@ -5613,9 +5641,22 @@ var SearchInputV1Schema = exports_external.object({
5613
5641
  args: SearchQuerySchema,
5614
5642
  options: exports_external.object({
5615
5643
  timeoutMs: exports_external.number().int().min(1000).max(300000).optional().default(30000),
5616
- workspace: exports_external.string().optional()
5644
+ workspace: exports_external.string().optional(),
5645
+ async: exports_external.boolean().optional().default(false),
5646
+ webhook: exports_external.string().url().optional()
5617
5647
  }).optional()
5618
5648
  });
5649
+ var AsyncJobSchema = exports_external.object({
5650
+ id: exports_external.string(),
5651
+ model: SonarModelSchema,
5652
+ status: exports_external.enum(["CREATED", "IN_PROGRESS", "COMPLETED", "FAILED"]),
5653
+ createdAt: exports_external.number(),
5654
+ startedAt: exports_external.number().optional(),
5655
+ completedAt: exports_external.number().optional(),
5656
+ failedAt: exports_external.number().optional(),
5657
+ errorMessage: exports_external.string().optional(),
5658
+ response: exports_external.any().optional()
5659
+ });
5619
5660
  var BatchSearchInputV1Schema = exports_external.object({
5620
5661
  version: exports_external.string().regex(/^\d+\.\d+\.\d+$/).default(SCHEMA_VERSION),
5621
5662
  requests: exports_external.array(SearchInputV1Schema).min(1).max(100),
@@ -6438,6 +6479,142 @@ function createLogger(context = {}) {
6438
6479
  return new Logger(1 /* INFO */, context);
6439
6480
  }
6440
6481
 
6482
+ // src/util/attachments.ts
6483
+ import { readFile, stat } from "node:fs/promises";
6484
+ import { extname, basename } from "node:path";
6485
+ var IMAGE_MIME_TYPES = {
6486
+ ".png": "image/png",
6487
+ ".jpg": "image/jpeg",
6488
+ ".jpeg": "image/jpeg",
6489
+ ".webp": "image/webp",
6490
+ ".heif": "image/heif",
6491
+ ".heic": "image/heic",
6492
+ ".gif": "image/gif"
6493
+ };
6494
+ var DOCUMENT_MIME_TYPES = {
6495
+ ".pdf": "application/pdf",
6496
+ ".doc": "application/msword",
6497
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
6498
+ ".txt": "text/plain",
6499
+ ".rtf": "application/rtf"
6500
+ };
6501
+ var MAX_FILE_SIZE = 50 * 1024 * 1024;
6502
+ var MAX_ATTACHMENTS = 10;
6503
+ function isSupportedFileType(extension) {
6504
+ return extension.toLowerCase() in IMAGE_MIME_TYPES || extension.toLowerCase() in DOCUMENT_MIME_TYPES;
6505
+ }
6506
+ function isImageFile(extension) {
6507
+ return extension.toLowerCase() in IMAGE_MIME_TYPES;
6508
+ }
6509
+ function getMimeType(extension) {
6510
+ const ext = extension.toLowerCase();
6511
+ return IMAGE_MIME_TYPES[ext] || DOCUMENT_MIME_TYPES[ext] || null;
6512
+ }
6513
+ function calculateImageTokens(width, height) {
6514
+ return Math.ceil(width * height / 750);
6515
+ }
6516
+ async function getImageDimensions(filePath) {
6517
+ try {
6518
+ const buffer = await readFile(filePath);
6519
+ if (buffer.length < 24)
6520
+ return null;
6521
+ return { width: 1920, height: 1080 };
6522
+ } catch {
6523
+ return null;
6524
+ }
6525
+ }
6526
+ async function validateFile(filePath) {
6527
+ try {
6528
+ const stats = await stat(filePath);
6529
+ if (!stats.isFile()) {
6530
+ return { valid: false, error: "Path is not a file" };
6531
+ }
6532
+ if (stats.size > MAX_FILE_SIZE) {
6533
+ return {
6534
+ valid: false,
6535
+ error: `File size ${Math.round(stats.size / 1024 / 1024)}MB exceeds maximum ${MAX_FILE_SIZE / 1024 / 1024}MB`,
6536
+ size: stats.size
6537
+ };
6538
+ }
6539
+ return { valid: true, size: stats.size };
6540
+ } catch (error) {
6541
+ return {
6542
+ valid: false,
6543
+ error: `Cannot access file: ${error instanceof Error ? error.message : "Unknown error"}`
6544
+ };
6545
+ }
6546
+ }
6547
+ async function processAttachment(input) {
6548
+ const filePath = input.path;
6549
+ const fileName = input.name || basename(filePath);
6550
+ const extension = extname(fileName);
6551
+ const mimeType = getMimeType(extension);
6552
+ if (!mimeType) {
6553
+ throw new Error(`Unsupported file type: ${extension}`);
6554
+ }
6555
+ const validation = await validateFile(filePath);
6556
+ if (!validation.valid) {
6557
+ throw new Error(validation.error || "File validation failed");
6558
+ }
6559
+ const fileContent = await readFile(filePath);
6560
+ const base64Content = fileContent.toString("base64");
6561
+ let url;
6562
+ if (isImageFile(extension)) {
6563
+ url = `data:${mimeType};base64,${base64Content}`;
6564
+ } else {
6565
+ url = base64Content;
6566
+ }
6567
+ let tokens;
6568
+ if (isImageFile(extension)) {
6569
+ const dimensions = await getImageDimensions(filePath);
6570
+ if (dimensions) {
6571
+ tokens = calculateImageTokens(dimensions.width, dimensions.height);
6572
+ }
6573
+ }
6574
+ return {
6575
+ name: fileName,
6576
+ extension,
6577
+ mimeType,
6578
+ url,
6579
+ size: validation.size
6580
+ };
6581
+ }
6582
+ async function processAttachments(inputs) {
6583
+ if (inputs.length > MAX_ATTACHMENTS) {
6584
+ throw new Error(`Maximum ${MAX_ATTACHMENTS} attachments allowed per request`);
6585
+ }
6586
+ const attachments = [];
6587
+ for (const input of inputs) {
6588
+ try {
6589
+ const attachment = await processAttachment(input);
6590
+ attachments.push(attachment);
6591
+ } catch (error) {
6592
+ throw new Error(`Failed to process attachment ${input.path}: ${error instanceof Error ? error.message : "Unknown error"}`);
6593
+ }
6594
+ }
6595
+ return attachments;
6596
+ }
6597
+ function validateAttachmentInputs(inputs) {
6598
+ const errors2 = [];
6599
+ if (inputs.length > MAX_ATTACHMENTS) {
6600
+ errors2.push(`Maximum ${MAX_ATTACHMENTS} attachments allowed per request`);
6601
+ }
6602
+ for (const input of inputs) {
6603
+ if (!input.path) {
6604
+ errors2.push("Attachment path is required");
6605
+ continue;
6606
+ }
6607
+ const extension = extname(input.path).toLowerCase();
6608
+ if (!isSupportedFileType(extension)) {
6609
+ errors2.push(`Unsupported file type: ${extension} for ${input.path}`);
6610
+ }
6611
+ }
6612
+ return {
6613
+ valid: errors2.length === 0,
6614
+ errors: errors2
6615
+ };
6616
+ }
6617
+
6441
6618
  // src/index.ts
6442
6619
  class PerplexitySearchTool {
6443
6620
  client;
@@ -6445,6 +6622,7 @@ class PerplexitySearchTool {
6445
6622
  resilience;
6446
6623
  logger;
6447
6624
  metrics;
6625
+ defaultModel;
6448
6626
  constructor(workspacePath, options = {}) {
6449
6627
  const apiKey = process.env.PERPLEXITY_API_KEY || process.env.PERPLEXITY_AI_API_KEY;
6450
6628
  if (!apiKey) {
@@ -6452,6 +6630,7 @@ class PerplexitySearchTool {
6452
6630
  }
6453
6631
  this.client = new Perplexity({ apiKey });
6454
6632
  this.workspace = new WorkspaceSandbox(workspacePath);
6633
+ this.defaultModel = options.defaultModel || "sonar";
6455
6634
  const resilienceConfig = options.resilienceProfile && options.resilienceProfile !== "custom" ? DEFAULT_CONFIGS[options.resilienceProfile] : options.resilienceConfig || DEFAULT_CONFIGS.balanced;
6456
6635
  this.resilience = new ResilienceManager(resilienceConfig);
6457
6636
  this.logger = createLogger({
@@ -6462,7 +6641,8 @@ class PerplexitySearchTool {
6462
6641
  this.metrics = new MetricsCollector;
6463
6642
  this.logger.info("PerplexitySearchTool initialized", {
6464
6643
  workspace: workspacePath,
6465
- resilienceProfile: options.resilienceProfile || "balanced"
6644
+ resilienceProfile: options.resilienceProfile || "balanced",
6645
+ defaultModel: this.defaultModel
6466
6646
  });
6467
6647
  }
6468
6648
  async runTask(input, signal) {
@@ -6473,9 +6653,27 @@ class PerplexitySearchTool {
6473
6653
  const validatedInput = SearchInputV1Schema.parse(input);
6474
6654
  traceLogger.info("Starting search task", {
6475
6655
  query: validatedInput.args.query,
6656
+ model: validatedInput.args.model || this.defaultModel,
6476
6657
  maxResults: validatedInput.args.maxResults,
6477
- country: validatedInput.args.country
6658
+ country: validatedInput.args.country,
6659
+ hasAttachments: validatedInput.args.attachments && validatedInput.args.attachments.length > 0,
6660
+ hasAttachmentInputs: validatedInput.args.attachmentInputs && validatedInput.args.attachmentInputs.length > 0,
6661
+ async: validatedInput.options?.async,
6662
+ webhook: validatedInput.options?.webhook
6478
6663
  });
6664
+ let attachments = [];
6665
+ if (validatedInput.args.attachmentInputs && validatedInput.args.attachmentInputs.length > 0) {
6666
+ const validation = validateAttachmentInputs(validatedInput.args.attachmentInputs);
6667
+ if (!validation.valid) {
6668
+ throw new Error(`Attachment validation failed: ${validation.errors.join(", ")}`);
6669
+ }
6670
+ attachments = await processAttachments(validatedInput.args.attachmentInputs);
6671
+ } else if (validatedInput.args.attachments) {
6672
+ attachments = validatedInput.args.attachments;
6673
+ }
6674
+ if (validatedInput.args.attachments) {
6675
+ attachments = [...attachments, ...validatedInput.args.attachments];
6676
+ }
6479
6677
  const timeoutMs = validatedInput.options?.timeoutMs || 30000;
6480
6678
  const controller = new AbortController;
6481
6679
  if (signal) {
@@ -6483,15 +6681,18 @@ class PerplexitySearchTool {
6483
6681
  }
6484
6682
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
6485
6683
  try {
6486
- const { result, metrics: metrics2 } = await traceLogger.measure("search_operation", () => this.resilience.execute(() => this.performSearch(validatedInput.args, controller.signal)), {
6684
+ const { result, metrics: metrics2 } = await traceLogger.measure("search_operation", () => this.resilience.execute(() => this.performChatCompletion(validatedInput.args, attachments, controller.signal)), {
6487
6685
  query: validatedInput.args.query,
6686
+ model: validatedInput.args.model || this.defaultModel,
6488
6687
  maxResults: validatedInput.args.maxResults,
6489
- country: validatedInput.args.country
6688
+ country: validatedInput.args.country,
6689
+ attachmentCount: attachments.length
6490
6690
  });
6491
6691
  clearTimeout(timeoutId);
6492
6692
  const duration = Date.now() - startTime;
6493
6693
  this.metrics.recordMetric("search_duration", duration, "ms", {
6494
6694
  query: validatedInput.args.query,
6695
+ model: validatedInput.args.model || this.defaultModel,
6495
6696
  success: "true"
6496
6697
  });
6497
6698
  this.metrics.incrementCounter("search_requests_total", 1, { status: "success" });
@@ -6500,7 +6701,8 @@ class PerplexitySearchTool {
6500
6701
  });
6501
6702
  traceLogger.info("Search task completed successfully", {
6502
6703
  resultCount: result.length,
6503
- duration
6704
+ duration,
6705
+ model: validatedInput.args.model || this.defaultModel
6504
6706
  });
6505
6707
  return {
6506
6708
  id,
@@ -6614,33 +6816,98 @@ class PerplexitySearchTool {
6614
6816
  };
6615
6817
  }
6616
6818
  }
6617
- async performSearch(query, signal) {
6819
+ async performChatCompletion(query, attachments, signal) {
6618
6820
  try {
6619
- const searchParameters = {
6620
- query: query.query,
6621
- max_results: query.maxResults || 5
6821
+ const messages = [
6822
+ {
6823
+ role: "system",
6824
+ content: "You are a helpful AI assistant. Provide accurate, concise responses with citations when possible."
6825
+ }
6826
+ ];
6827
+ const userMessage = {
6828
+ role: "user",
6829
+ content: [
6830
+ { type: "text", text: query.query }
6831
+ ]
6622
6832
  };
6623
- if (query.country) {
6624
- searchParameters.search_domain_filter = [`.${query.country.toLowerCase()}`];
6833
+ if (attachments.length > 0) {
6834
+ for (const attachment of attachments) {
6835
+ if (attachment.mimeType.startsWith("image/")) {
6836
+ userMessage.content.push({
6837
+ type: "image_url",
6838
+ image_url: {
6839
+ url: attachment.url
6840
+ }
6841
+ });
6842
+ } else {
6843
+ userMessage.content.push({
6844
+ type: "file_url",
6845
+ file_url: {
6846
+ url: attachment.url
6847
+ },
6848
+ file_name: attachment.name
6849
+ });
6850
+ }
6851
+ }
6625
6852
  }
6626
- const searchResponse = await this.client.search.create(searchParameters, { signal });
6627
- if (searchResponse.results && Array.isArray(searchResponse.results)) {
6628
- return searchResponse.results.map((result) => ({
6629
- title: result.title || "Untitled",
6630
- url: result.url || "",
6631
- snippet: result.snippet || "",
6632
- date: result.date || undefined
6633
- }));
6853
+ messages.push(userMessage);
6854
+ const chatParams = {
6855
+ model: query.model || this.defaultModel,
6856
+ messages,
6857
+ max_tokens: 4000,
6858
+ temperature: 0.1,
6859
+ top_p: 0.9,
6860
+ search_domain_filter: query.country ? [`.${query.country.toLowerCase()}`] : undefined,
6861
+ return_images: false,
6862
+ return_related_questions: false,
6863
+ search_recency_filter: undefined,
6864
+ search_after_date_filter: undefined,
6865
+ search_before_date_filter: undefined,
6866
+ last_updated_after_filter: undefined,
6867
+ last_updated_before_filter: undefined,
6868
+ top_k: 0,
6869
+ stream: false,
6870
+ presence_penalty: 0,
6871
+ frequency_penalty: 0,
6872
+ disable_search: false,
6873
+ enable_search_classifier: false,
6874
+ web_search_options: {
6875
+ search_context_size: "high"
6876
+ }
6877
+ };
6878
+ const response = await this.client.chat.completions.create(chatParams);
6879
+ if (response.choices && response.choices.length > 0) {
6880
+ const choice = response.choices[0];
6881
+ const content = choice.message?.content || "";
6882
+ const results = [];
6883
+ results.push({
6884
+ title: "AI Response",
6885
+ url: "https://www.perplexity.ai/",
6886
+ snippet: content,
6887
+ date: new Date().toISOString().split("T")[0]
6888
+ });
6889
+ if (response.search_results && Array.isArray(response.search_results)) {
6890
+ response.search_results.forEach((result) => {
6891
+ results.push({
6892
+ title: result.title || "Search Result",
6893
+ url: result.url || "",
6894
+ snippet: result.snippet || "",
6895
+ date: result.date || undefined
6896
+ });
6897
+ });
6898
+ }
6899
+ const maxResults = query.maxResults || 5;
6900
+ return results.slice(0, maxResults);
6634
6901
  }
6635
6902
  return [{
6636
- title: "Search Result",
6903
+ title: "No Response",
6637
6904
  url: "https://www.perplexity.ai/",
6638
- snippet: "No content available",
6905
+ snippet: "No response received from the model",
6639
6906
  date: undefined
6640
6907
  }];
6641
6908
  } catch (error) {
6642
6909
  if (error instanceof Error && error.name === "AbortError") {
6643
- throw new Error("Search request timed out");
6910
+ throw new Error("Chat completion request timed out");
6644
6911
  }
6645
6912
  throw error instanceof Error ? error : new Error(String(error));
6646
6913
  }
@@ -6723,34 +6990,59 @@ var { values: cliOptions, positionals: commandLineQueries } = parseArgs({
6723
6990
  timeout: { type: "string", short: "t" },
6724
6991
  workspace: { type: "string", short: "w" },
6725
6992
  format: { type: "string", short: "f", default: "json" },
6726
- "dry-run": { type: "boolean", short: "d" },
6727
6993
  version: { type: "boolean", short: "v" },
6728
- help: { type: "boolean", short: "h" }
6994
+ help: { type: "boolean", short: "h" },
6995
+ model: { type: "string", short: "m" },
6996
+ attach: { type: "string", multiple: true },
6997
+ "attach-image": { type: "string", multiple: true },
6998
+ async: { type: "boolean" },
6999
+ webhook: { type: "string" }
6729
7000
  },
6730
7001
  allowPositionals: true
6731
7002
  });
6732
7003
  if (cliOptions.help) {
6733
7004
  console.error(`
6734
- PPLX-Zero - Minimal, fast Perplexity AI search CLI
7005
+ PPLX-Zero - Minimal, fast Perplexity AI search CLI with multimodal support
6735
7006
 
6736
7007
  USAGE:
6737
7008
  pplx [OPTIONS] [QUERY...]
6738
7009
 
6739
7010
  OPTIONS:
6740
- -i, --input <file> Read batch requests from JSON file
6741
- -s, --stdin Read JSONL requests from stdin
6742
- -c, --concurrency <n> Max concurrent requests (default: 5)
6743
- -t, --timeout <ms> Request timeout in milliseconds (default: 30000)
6744
- -w, --workspace <path> Workspace directory for sandboxing
6745
- -f, --format <format> Output format: json|jsonl (default: json)
6746
- -d, --dry-run Validate input without executing searches
6747
- -v, --version Show version
6748
- -h, --help Show this help
7011
+ -i, --input <file> Read batch requests from JSON file
7012
+ -s, --stdin Read JSONL requests from stdin
7013
+ -c, --concurrency <n> Max concurrent requests (default: 5)
7014
+ -t, --timeout <ms> Request timeout in milliseconds (default: 30000)
7015
+ -w, --workspace <path> Workspace directory for sandboxing
7016
+ -f, --format <format> Output format: json|jsonl (default: json)
7017
+ -m, --model <model> AI model: sonar, sonar-pro, sonar-deep-research, sonar-reasoning (default: sonar)
7018
+ --attach <file> Attach document files (PDF, DOC, DOCX, TXT, RTF) - can be used multiple times
7019
+ --attach-image <file> Attach image files (PNG, JPEG, WebP, HEIF, HEIC, GIF) - can be used multiple times
7020
+ --async Process requests asynchronously
7021
+ --webhook <url> Webhook URL for async notifications
7022
+ -v, --version Show version
7023
+ -h, --help Show this help
6749
7024
 
6750
7025
  EXAMPLES:
6751
- # Single query
7026
+ # Basic query
6752
7027
  pplx "latest AI developments"
6753
7028
 
7029
+ # Model selection
7030
+ pplx --model sonar-pro "Detailed analysis"
7031
+ pplx --model sonar-deep-research "Comprehensive research"
7032
+ pplx --model sonar-reasoning "Complex problem solving"
7033
+
7034
+ # Image analysis
7035
+ pplx --attach-image screenshot.png --model sonar-pro "Analyze this interface"
7036
+
7037
+ # Document analysis
7038
+ pplx --attach report.pdf --model sonar-deep-research "Summarize this document"
7039
+
7040
+ # Multimodal analysis
7041
+ pplx --attach document.txt --attach-image chart.png --model sonar-reasoning "Analyze this data"
7042
+
7043
+ # Async processing with webhook
7044
+ pplx --async --webhook https://api.example.com/callback "Long research task"
7045
+
6754
7046
  # Batch from file
6755
7047
  pplx --input queries.json
6756
7048
 
@@ -6760,8 +7052,12 @@ EXAMPLES:
6760
7052
  # JSONL output for streaming
6761
7053
  pplx --format jsonl --input queries.json
6762
7054
 
6763
- # High concurrency batch
6764
- pplx --concurrency 10 --timeout 60000 --input queries.json
7055
+ # High concurrency batch with attachments
7056
+ pplx --concurrency 10 --timeout 60000 --input queries.json --attach appendix.pdf
7057
+
7058
+ SUPPORTED FORMATS:
7059
+ Images: PNG, JPEG, WebP, HEIF, HEIC, GIF (max 50MB, 10 files)
7060
+ Documents: PDF, DOC, DOCX, TXT, RTF (max 50MB, 10 files)
6765
7061
  `);
6766
7062
  process.exit(0);
6767
7063
  }
@@ -6842,11 +7138,20 @@ async function main() {
6842
7138
  const requestTimeout = parseNumericArgument(cliOptions.timeout, DEFAULT_TIMEOUT, MIN_TIMEOUT, MAX_TIMEOUT, "Timeout");
6843
7139
  const workspaceDirectory = cliOptions.workspace;
6844
7140
  const outputFormat = cliOptions.format;
6845
- const isDryRunMode = cliOptions["dry-run"];
6846
7141
  if (!["json", "jsonl"].includes(outputFormat)) {
6847
7142
  throw new Error("Format must be json or jsonl");
6848
7143
  }
6849
- const searchTool = new PerplexitySearchTool(workspaceDirectory);
7144
+ let selectedModel;
7145
+ if (cliOptions.model) {
7146
+ const validModels = ["sonar", "sonar-pro", "sonar-deep-research", "sonar-reasoning"];
7147
+ if (!validModels.includes(cliOptions.model)) {
7148
+ throw new Error(`Invalid model: ${cliOptions.model}. Valid models: ${validModels.join(", ")}`);
7149
+ }
7150
+ selectedModel = cliOptions.model;
7151
+ }
7152
+ const searchTool = new PerplexitySearchTool(workspaceDirectory, {
7153
+ defaultModel: selectedModel
7154
+ });
6850
7155
  logEvent({
6851
7156
  time: new Date().toISOString(),
6852
7157
  level: "info",
@@ -6856,7 +7161,10 @@ async function main() {
6856
7161
  timeout: requestTimeout,
6857
7162
  workspace: workspaceDirectory,
6858
7163
  format: outputFormat,
6859
- dryRun: isDryRunMode
7164
+ model: selectedModel,
7165
+ async: cliOptions.async,
7166
+ webhook: cliOptions.webhook,
7167
+ hasAttachments: (cliOptions.attach?.length || 0) + (cliOptions["attach-image"]?.length || 0) > 0
6860
7168
  }
6861
7169
  });
6862
7170
  let batchSearchInput;
@@ -6869,13 +7177,37 @@ async function main() {
6869
7177
  inputSourceType = cliOptions.input;
6870
7178
  } else if (commandLineQueries.length > 0) {
6871
7179
  const combinedQuery = commandLineQueries.join(" ");
7180
+ const attachmentInputs = [];
7181
+ if (cliOptions.attach && cliOptions.attach.length > 0) {
7182
+ for (const filePath of cliOptions.attach) {
7183
+ attachmentInputs.push({
7184
+ path: filePath,
7185
+ type: "document"
7186
+ });
7187
+ }
7188
+ }
7189
+ if (cliOptions["attach-image"] && cliOptions["attach-image"].length > 0) {
7190
+ for (const filePath of cliOptions["attach-image"]) {
7191
+ attachmentInputs.push({
7192
+ path: filePath,
7193
+ type: "image"
7194
+ });
7195
+ }
7196
+ }
6872
7197
  batchSearchInput = {
6873
7198
  version: "1.0.0",
6874
7199
  requests: [{
6875
7200
  op: "search",
6876
7201
  args: {
6877
7202
  query: combinedQuery,
6878
- maxResults: 5
7203
+ maxResults: 5,
7204
+ model: selectedModel,
7205
+ attachmentInputs: attachmentInputs.length > 0 ? attachmentInputs : undefined
7206
+ },
7207
+ options: {
7208
+ timeoutMs: requestTimeout,
7209
+ async: cliOptions.async,
7210
+ webhook: cliOptions.webhook
6879
7211
  }
6880
7212
  }]
6881
7213
  };
@@ -6899,21 +7231,6 @@ async function main() {
6899
7231
  failFast: false,
6900
7232
  ...batchSearchInput.options
6901
7233
  };
6902
- if (isDryRunMode) {
6903
- logEvent({
6904
- time: new Date().toISOString(),
6905
- level: "info",
6906
- event: "dry_run_completed",
6907
- data: { requestCount: batchSearchInput.requests?.length || 1 }
6908
- });
6909
- console.log(JSON.stringify({
6910
- ok: true,
6911
- message: "Dry run completed - input validation passed",
6912
- requestCount: batchSearchInput.requests?.length || 1,
6913
- inputSource: inputSourceType
6914
- }, null, 2));
6915
- return;
6916
- }
6917
7234
  logEvent({
6918
7235
  time: new Date().toISOString(),
6919
7236
  level: "info",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pplx-zero",
3
- "version": "1.0.0",
4
- "description": "PPLX-Zero: Minimal, fast Perplexity AI search CLI - zero configuration, zero compromises",
3
+ "version": "1.0.1",
4
+ "description": "Fast Perplexity AI search CLI with multimodal support - minimal setup, maximal results",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -49,10 +49,22 @@
49
49
  "minimal",
50
50
  "fast",
51
51
  "productivity",
52
- "zero-config"
52
+ "zero-config",
53
+ "multimodal",
54
+ "attachments",
55
+ "images",
56
+ "documents",
57
+ "sonar",
58
+ "reasoning",
59
+ "research",
60
+ "async"
53
61
  ],
54
62
  "author": "Kenzo",
55
63
  "license": "MIT",
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "https://github.com/codewithkenzo/pplx-zero.git"
67
+ },
56
68
  "files": [
57
69
  "dist",
58
70
  "README.md",