pplx-zero 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +188 -51
  2. package/dist/cli.js +378 -61
  3. package/package.json +15 -3
package/README.md CHANGED
@@ -1,40 +1,53 @@
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
 
24
30
  **📦 Package Manager Installation (Recommended)**
25
31
 
32
+ <p align="center">
33
+ <a href="https://badge.fury.io/js/pplx-zero"><img src="https://badge.fury.io/js/pplx-zero.svg" alt="npm version"></a>
34
+ <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>
35
+ </p>
36
+
26
37
  ```bash
27
- # npm (Node.js package manager)
38
+ # npm (Node.js package manager) - Global installation
28
39
  npm install -g pplx-zero
29
40
 
30
- # AUR (Arch Linux)
41
+ # AUR (Arch Linux) - Binary package
31
42
  yay -S pplx-zero
32
- # or
33
- pikaur -S pplx-zero
34
- # or manual AUR
43
+
44
+ # AUR (Arch Linux) - Manual build
35
45
  git clone https://aur.archlinux.org/pplx-zero.git
36
46
  cd pplx-zero
37
47
  makepkg -si
48
+
49
+ # Verify installation
50
+ pplx --version
38
51
  ```
39
52
 
40
53
  **🔨 Manual Installation**
@@ -47,42 +60,109 @@ bun install && bun run build
47
60
 
48
61
  # Add to PATH
49
62
  sudo ln -s "$(pwd)/dist/cli.js" /usr/local/bin/pplx
63
+
64
+ # Verify installation
65
+ pplx --version
50
66
  ```
51
67
 
52
68
  ### 2️⃣ Setup API Key
53
69
 
70
+ **Linux/macOS:**
54
71
  ```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"
72
+ export PERPLEXITY_API_KEY="your-api-key"
58
73
  ```
59
74
 
60
- ### 3️⃣ Start Searching
75
+ **Windows:**
76
+ ```cmd
77
+ setx PERPLEXITY_API_KEY "your-api-key"
78
+ ```
79
+
80
+ **Get your API key:** https://www.perplexity.ai/account/api/keys
81
+
82
+ ### 3️⃣ Start Searching (Simplified Interface)
61
83
 
62
84
  ```bash
63
- # Single query (CLI command is 'pplx')
85
+ # Simple search
64
86
  pplx "latest AI developments"
65
87
 
66
- # Batch from file
67
- pplx --input queries.json
88
+ # Choose model for detailed analysis
89
+ pplx --model sonar-pro "Explain quantum computing"
68
90
 
69
- # Stream from stdin
70
- cat queries.jsonl | pplx --stdin
91
+ # Analyze document (simplified syntax)
92
+ pplx --file report.pdf "Summarize this document"
93
+
94
+ # Analyze image (simplified syntax)
95
+ pplx --image screenshot.png "What does this interface do?"
96
+
97
+ # Document + image analysis
98
+ pplx --file data.csv --image chart.png "Analyze this data"
99
+
100
+ # Advanced AI models
101
+ pplx --model sonar-reasoning "Solve this math problem"
102
+ pplx --model sonar-deep-research "History of artificial intelligence"
103
+
104
+ # See basic help
105
+ pplx --help
106
+
107
+ # See advanced options
108
+ pplx --help-advanced
71
109
  ```
72
110
 
73
- ## 📖 Usage Guide
111
+ **🔄 Migration from v1.0.x:**
112
+ ```bash
113
+ # OLD: pplx --format jsonl --input queries.json "search"
114
+ # NEW: pplx --format jsonl --input queries.json "search" # format flag unchanged
74
115
 
75
- ### Command Line Interface
116
+ # OLD: pplx --attach document.pdf "analyze"
117
+ # NEW: pplx --file document.pdf "analyze" # simplified syntax
118
+ ```
119
+
120
+ ## Usage Guide
76
121
 
122
+ ### Command Line Options
123
+
124
+ **Simplified Interface (Everyday Usage):**
77
125
  ```bash
78
- # Basic usage
79
- pplx "your search query"
126
+ # Basic search with model selection
127
+ pplx --model sonar-pro "Detailed analysis"
128
+
129
+ # File attachments (simplified syntax)
130
+ pplx --file document.pdf "Summarize this report"
131
+ pplx --image chart.png "Analyze this chart"
80
132
 
81
- # Advanced options
133
+ # Multiple attachments
134
+ pplx --file doc1.pdf --image img1.png "Analyze these files"
135
+
136
+ # Output format selection
137
+ pplx --format jsonl "Machine learning trends"
138
+
139
+ # Choose AI models
140
+ pplx --model sonar-pro "Detailed analysis"
141
+ pplx --model sonar-reasoning "Complex problem solving"
142
+ ```
143
+
144
+ **Advanced Interface (Power Users):**
145
+ ```bash
146
+ # Custom concurrency and timeout
82
147
  pplx --concurrency 10 --timeout 60000 --format jsonl "machine learning trends"
83
148
 
84
- # Dry run validation
85
- pplx --dry-run "test query"
149
+ # Batch processing
150
+ pplx --input queries.json --concurrency 5
151
+
152
+ # Stream processing
153
+ cat queries.jsonl | pplx --stdin
154
+
155
+ # Advanced attachments (multiple files)
156
+ pplx --attach doc1.pdf --attach doc2.txt --attach-image img1.png "Analyze all files"
157
+
158
+ # Async processing with webhook
159
+ pplx --async --webhook https://api.example.com/callback "Research task"
160
+
161
+ # Custom workspace
162
+ pplx --workspace /tmp/research "Custom workspace search"
163
+
164
+ # See all advanced options
165
+ pplx --help-advanced
86
166
  ```
87
167
 
88
168
  ### Batch Processing
@@ -110,6 +190,28 @@ Process with:
110
190
  pplx --input queries.json --format jsonl
111
191
  ```
112
192
 
193
+ ### File Attachments
194
+
195
+ Supported file formats for analysis:
196
+
197
+ **Documents (max 50MB):**
198
+ - PDF, DOC, DOCX, TXT, RTF
199
+
200
+ **Images (max 50MB):**
201
+ - PNG, JPEG, WebP, HEIF, HEIC, GIF
202
+
203
+ **Examples:**
204
+ ```bash
205
+ # Document analysis
206
+ pplx --attach report.pdf "Summarize this document"
207
+
208
+ # Image analysis
209
+ pplx --attach-image screenshot.png "Analyze this interface"
210
+
211
+ # Multiple files
212
+ pplx --attach document.txt --attach-image chart.png "Analyze this data"
213
+ ```
214
+
113
215
  ### Programmatic Usage
114
216
 
115
217
  ```typescript
@@ -128,20 +230,55 @@ const result = await tool.runBatch({
128
230
  console.log(result);
129
231
  ```
130
232
 
131
- ## ⚙️ Configuration
233
+ ## Configuration
234
+
235
+ ### Simplified Options (Everyday Usage)
236
+
237
+ | Option | Short | Type | Default | Description |
238
+ |--------|-------|------|---------|-------------|
239
+ | `--file` | `-f` | string | - | Attach document (PDF, DOC, DOCX, TXT, RTF) |
240
+ | `--image` | `-i` | string | - | Attach image (PNG, JPEG, WebP, HEIF, HEIC, GIF) |
241
+ | `--format` | `-o` | string | json | Output format: json|jsonl |
242
+ | `--model` | `-m` | string | sonar | AI model: sonar, sonar-pro, sonar-deep-research, sonar-reasoning |
243
+ | `--version` | `-v` | boolean | - | Show version |
244
+ | `--help` | `-h` | boolean | - | Show basic help |
245
+
246
+ ### Advanced Options (Power Users)
132
247
 
133
248
  | Option | Short | Type | Default | Description |
134
249
  |--------|-------|------|---------|-------------|
135
- | `--input` | `-i` | string | - | Read batch requests from JSON file |
250
+ | `--input` | `-I` | string | - | Read batch requests from JSON file |
136
251
  | `--stdin` | `-s` | boolean | false | Read JSONL requests from stdin |
137
252
  | `--concurrency` | `-c` | number | 5 | Max concurrent requests (1-20) |
138
253
  | `--timeout` | `-t` | number | 30000 | Request timeout in ms (1000-300000) |
139
- | `--format` | `-f` | string | json | Output format: json|jsonl |
140
- | `--dry-run` | `-d` | boolean | false | Validate input without executing |
141
- | `--version` | `-v` | boolean | - | Show version |
142
- | `--help` | `-h` | boolean | - | Show help |
254
+ | `--workspace` | `-w` | string | - | Workspace directory for sandboxing |
255
+ | `--attach` | - | string[] | - | Attach document files (multiple) |
256
+ | `--attach-image` | - | string[] | - | Attach image files (multiple) |
257
+ | `--async` | - | boolean | false | Process requests asynchronously |
258
+ | `--webhook` | - | string | - | Webhook URL for async notifications |
259
+ | `--help-advanced` | - | boolean | - | Show advanced help with all options |
260
+
261
+ ### Quick Reference
262
+
263
+ ```bash
264
+ # Basic usage (simplified)
265
+ pplx -f doc.pdf -m sonar-pro "analyze this"
266
+
267
+ # Advanced usage (full control)
268
+ pplx -I batch.json -c 10 -t 60000 --format jsonl "process all"
269
+
270
+ # See all available options
271
+ pplx --help-advanced
272
+ ```
273
+
274
+ ### AI Models
275
+
276
+ - `sonar` - Fast, concise responses (default)
277
+ - `sonar-pro` - Detailed, comprehensive responses
278
+ - `sonar-deep-research` - In-depth research with web search
279
+ - `sonar-reasoning` - Step-by-step logical reasoning
143
280
 
144
- ## 📊 Output Formats
281
+ ## Output Formats
145
282
 
146
283
  ### JSON (Default)
147
284
  ```json
@@ -164,7 +301,7 @@ pplx --format jsonl "AI trends"
164
301
  ```
165
302
  Each result printed as a separate JSON line for real-time processing.
166
303
 
167
- ## 🔧 Development
304
+ ## Development
168
305
 
169
306
  ```bash
170
307
  # Development mode
@@ -183,15 +320,15 @@ bun run build
183
320
  bun run build:binary
184
321
  ```
185
322
 
186
- ## 🏗️ Architecture
323
+ ## Architecture
187
324
 
188
- - **Bun Runtime** - Ultra-fast JavaScript runtime
325
+ - **Bun Runtime** - Fast JavaScript runtime
189
326
  - **Zod Validation** - Type-safe schema validation
190
- - **Circuit Breaker** - Resilient error handling
191
- - **Semaphore Pattern** - Controlled concurrency
327
+ - **Error Handling** - Resilient error recovery
328
+ - **Concurrency Control** - Semaphore pattern for rate limiting
192
329
  - **Streaming Events** - Real-time progress updates
193
330
 
194
- ## 🛡️ Security Features
331
+ ## Security Features
195
332
 
196
333
  - Environment variable API key management
197
334
  - Input validation and sanitization
@@ -199,7 +336,7 @@ bun run build:binary
199
336
  - Error information filtering
200
337
  - No external dependencies beyond core runtime
201
338
 
202
- ## 📋 Error Handling
339
+ ## Error Handling
203
340
 
204
341
  PPLX-Zero provides comprehensive error classification:
205
342
 
@@ -215,14 +352,14 @@ enum ErrorCode {
215
352
  }
216
353
  ```
217
354
 
218
- ## 🤝 Contributing
355
+ ## Contributing
219
356
 
220
357
  Contributions are welcome! Please feel free to submit a Pull Request.
221
358
 
222
- ## 📄 License
359
+ ## License
223
360
 
224
361
  MIT License - see [LICENSE](LICENSE) file for details.
225
362
 
226
363
  ---
227
364
 
228
- **Built with ❤️ using [Bun](https://bun.sh) and [Perplexity AI](https://www.perplexity.ai)**
365
+ **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.1.0",
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": "git+https://github.com/codewithkenzo/pplx-zero.git"
67
+ },
56
68
  "files": [
57
69
  "dist",
58
70
  "README.md",