pplx-zero 2.2.1 → 2.2.2

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/README.md CHANGED
@@ -86,7 +86,6 @@ pplx --history
86
86
  |-------|-----|
87
87
  | `sonar` | Quick answers |
88
88
  | `sonar-pro` | Complex questions |
89
- | `sonar-reasoning` | Step-by-step |
90
89
  | `sonar-reasoning-pro` | Advanced reasoning |
91
90
  | `sonar-deep-research` | Research reports |
92
91
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pplx-zero",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "description": "Minimal Perplexity AI CLI - search from terminal",
5
5
  "author": "kenzo",
6
6
  "license": "MIT",
package/src/api.ts CHANGED
@@ -3,7 +3,7 @@ import type { FileAttachment } from './files';
3
3
 
4
4
  const API_URL = 'https://api.perplexity.ai/chat/completions';
5
5
 
6
- export const MODELS = ['sonar', 'sonar-pro', 'sonar-reasoning', 'sonar-reasoning-pro', 'sonar-deep-research'] as const;
6
+ export const MODELS = ['sonar', 'sonar-pro', 'sonar-reasoning-pro', 'sonar-deep-research'] as const;
7
7
  export type Model = (typeof MODELS)[number];
8
8
 
9
9
  export interface SearchResult {
@@ -19,9 +19,10 @@ export interface StreamCallbacks {
19
19
  }
20
20
 
21
21
  interface MessageContent {
22
- type: 'text' | 'file_url';
22
+ type: 'text' | 'file_url' | 'image_url';
23
23
  text?: string;
24
24
  file_url?: { url: string };
25
+ image_url?: { url: string };
25
26
  file_name?: string;
26
27
  }
27
28
 
@@ -32,12 +33,22 @@ function buildMessages(query: string, file?: FileAttachment): { role: string; co
32
33
 
33
34
  const content: MessageContent[] = [
34
35
  { type: 'text', text: query },
35
- {
36
+ ];
37
+
38
+ if (file.type === 'image') {
39
+ // Images: use image_url with data URL prefix per Perplexity API spec
40
+ content.push({
41
+ type: 'image_url',
42
+ image_url: { url: `data:${file.mimeType};base64,${file.data}` },
43
+ });
44
+ } else {
45
+ // Documents: use file_url with raw base64
46
+ content.push({
36
47
  type: 'file_url',
37
48
  file_url: { url: file.data },
38
49
  file_name: file.filename,
39
- },
40
- ];
50
+ });
51
+ }
41
52
 
42
53
  return [{ role: 'user', content }];
43
54
  }
package/src/files.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { extname } from 'node:path';
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { extname, basename, resolve } from 'node:path';
3
+
4
+ const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB - Perplexity API limit
3
5
 
4
6
  const MIME_TYPES: Record<string, string> = {
5
7
  '.pdf': 'application/pdf',
@@ -20,14 +22,26 @@ export interface FileAttachment {
20
22
  }
21
23
 
22
24
  export async function encodeFile(path: string): Promise<FileAttachment> {
23
- const ext = extname(path).toLowerCase();
25
+ // Security: prevent path traversal
26
+ if (path.includes('..')) {
27
+ throw new Error('Path traversal not allowed');
28
+ }
29
+
30
+ const resolved = resolve(path);
31
+ const ext = extname(resolved).toLowerCase();
24
32
  const mimeType = MIME_TYPES[ext];
25
33
 
26
34
  if (!mimeType) {
27
35
  throw new Error(`Unsupported file type: ${ext}`);
28
36
  }
29
37
 
30
- const buffer = await readFile(path);
38
+ // Security: check file size before reading into memory
39
+ const stats = await stat(resolved);
40
+ if (stats.size > MAX_FILE_SIZE) {
41
+ throw new Error(`File too large: ${(stats.size / 1024 / 1024).toFixed(1)}MB (max 50MB)`);
42
+ }
43
+
44
+ const buffer = await readFile(resolved);
31
45
  const data = buffer.toString('base64');
32
46
  const isImage = mimeType.startsWith('image/');
33
47
 
@@ -35,7 +49,7 @@ export async function encodeFile(path: string): Promise<FileAttachment> {
35
49
  type: isImage ? 'image' : 'file',
36
50
  data,
37
51
  mimeType,
38
- filename: path.split('/').pop() || 'file',
52
+ filename: basename(resolved),
39
53
  };
40
54
  }
41
55
 
package/src/index.ts CHANGED
@@ -72,8 +72,14 @@ if (positionals.length === 0 && !values.continue) {
72
72
  process.exit(2);
73
73
  }
74
74
 
75
+ // Validate model before proceeding
76
+ if (!MODELS.includes(values.model as Model)) {
77
+ console.error(fmt.error(`Invalid model: ${values.model}. Available: ${MODELS.join(', ')}`));
78
+ process.exit(2);
79
+ }
80
+ const model = values.model as Model;
81
+
75
82
  let query = positionals.join(' ');
76
- const model = (MODELS.includes(values.model as Model) ? values.model : 'sonar') as Model;
77
83
 
78
84
  if (values.continue) {
79
85
  const last = await getLastEntry();
@@ -90,7 +96,25 @@ if (values.continue) {
90
96
  }
91
97
 
92
98
  const filePath = values.file || values.image;
93
- const file = filePath ? await encodeFile(filePath) : undefined;
99
+ let file;
100
+ if (filePath) {
101
+ try {
102
+ file = await encodeFile(filePath);
103
+ } catch (err) {
104
+ let msg = err instanceof Error ? err.message : 'Unknown error reading file';
105
+ if (msg.includes('ENOENT')) {
106
+ msg = `File not found: ${filePath}`;
107
+ }
108
+ console.error(fmt.error(msg));
109
+ process.exit(2);
110
+ }
111
+ }
112
+
113
+ // Validate model supports image input
114
+ if (file?.type === 'image' && model === 'sonar-deep-research') {
115
+ console.error(fmt.error('sonar-deep-research does not support image input. Use sonar or sonar-pro.'));
116
+ process.exit(2);
117
+ }
94
118
 
95
119
  const startTime = Date.now();
96
120
  let fullContent = '';