pplx-zero 1.1.7 → 2.0.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.
package/LICENSE CHANGED
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,154 +1,46 @@
1
- # PPLX‑Zero
1
+ # pplx-zero
2
2
 
3
- Use the Perplexity API straight from the terminal — sub‑1s search API, ideal for coding agents and automation.
4
- Minimal setup, fast results, and practical flags for files, images, streaming, and batch workflows.
3
+ Minimal Perplexity AI CLI - search from terminal.
5
4
 
6
- ### What you can do
7
- - Ask, research, and stream answers instantly from the CLI with sensible defaults for rapid iteration.
8
- - Summarize documents and analyze images using a single command with optional model control.
9
- - Run batch jobs with concurrency and timeouts for agent pipelines and CI flows.
10
-
11
- ### Install
12
- Choose one:
5
+ ## Installation
13
6
 
14
7
  ```bash
15
- # npm or bun (global)
16
- npm install -g pplx-zero
17
- pplx --version
8
+ bun install -g pplx-zero
18
9
  ```
19
10
 
20
-
21
- or
11
+ Or with npm:
22
12
 
23
13
  ```bash
24
- # AUR (Arch Linux)
25
- yay -S pplx-zero
26
- pplx --version
27
- ```
28
-
29
-
30
- or
31
-
32
- ```bash
33
- # Build from source
34
- # 1) clone the repository
35
- # 2) enter the folder
36
- # 3) build and link the CLI
37
- bun install && bun run build
38
- sudo ln -s "$(pwd)/dist/cli.js" /usr/local/bin/pplx
39
- pplx --version
14
+ npm install -g pplx-zero
40
15
  ```
41
16
 
17
+ ## Setup
42
18
 
43
- ### Configure
44
- Set your API key as an environment variable before running commands.
45
-
46
- Linux/macOS:
47
19
  ```bash
48
20
  export PERPLEXITY_API_KEY="your-api-key"
49
21
  ```
50
22
 
23
+ ## Usage
51
24
 
52
- Windows:
53
- ```cmd
54
- setx PERPLEXITY_API_KEY "your-api-key"
55
- ```
56
-
57
-
58
- Get your key from your Perplexity account and keep it private to your machine or CI secrets manager.
59
-
60
- ### Quick examples
61
- Simple search (default model: sonar)
62
- ```bash
63
- pplx "python type hints best practices"
64
- ```
65
-
66
-
67
- Deep research or sonar-pro or reasoning when you need more steps or web context
68
- ```bash
69
- pplx -m sonar-pro "React 19 Hooks"
70
- pplx -m sonar-deep-research "best Rust web frameworks 2025"
71
- pplx -m sonar-reasoning "prove this algorithm runs in O(n log n)"
72
- ```
73
-
74
-
75
- Summarize a PDF report quickly
76
- ```bash
77
- pplx -f report.pdf "summarize key findings and risks"
78
- ```
79
-
80
-
81
- Understand an interface from a screenshot
82
25
  ```bash
83
- pplx -i screenshot.png "what is this UI and what are the next steps?"
26
+ pplx "what is bun"
27
+ pplx -m sonar-pro "explain quantum computing"
28
+ pplx -m sonar-deep-research "comprehensive analysis of AI trends"
29
+ pplx -f report.pdf "summarize this document"
30
+ pplx -i screenshot.png "what's in this image"
31
+ pplx --json "get structured response"
84
32
  ```
85
33
 
34
+ ## Options
86
35
 
87
- Combine a doc and an image in one prompt
88
- ```bash
89
- pplx -f data.csv -i chart.png "spot anomalies and explain the chart"
90
- ```
91
-
92
-
93
- Stream newline‑delimited JSON for agents or UNIX pipes
94
- ```bash
95
- pplx -o jsonl "ai trends"
96
- ```
97
-
98
-
99
- Batch from a JSON file (concurrency and timeout shown)
100
- ```json
101
- {
102
- "version": "1.0.0",
103
- "requests": [
104
- { "op": "search", "args": { "query": "AI trends", "maxResults": 5 } },
105
- { "op": "search", "args": { "query": "TypeScript patterns", "maxResults": 3 } }
106
- ],
107
- "options": { "concurrency": 5, "timeoutMs": 30000 }
108
- }
109
- ```
110
-
111
-
112
- ```bash
113
- pplx -I queries.json -o jsonl -c 5 -t 30000
114
- ```
115
-
116
-
117
- Fire‑and‑forget async with a webhook callback (agent workflows)
118
- ```bash
119
- pplx --async --webhook http://localhost:3000/callback "long research task"
120
- ```
121
-
122
-
123
- ### Flags
124
- - -m, --model: sonar | sonar-pro | sonar-deep-research | sonar-reasoning (default: sonar)
125
- - -f, --file: attach a document (PDF, DOC, DOCX, TXT, RTF, MD; up to ~50MB)
126
- - -i, --image: attach an image (PNG, JPEG, WebP, HEIF/HEIC, GIF; up to ~50MB)
127
- - -o, --format: json | jsonl (default: json)
128
- - -I, --input: read batch requests from a JSON file
129
- - -c, --concurrency: max parallel requests, e.g., 5 (default: 5)
130
- - -t, --timeout: request timeout in ms, e.g., 30000 (default: 30000)
131
- - --async: process requests asynchronously
132
- - --webhook: URL receiving async notifications
133
- - -h, --help: show help
134
- - -v, --version: show version
135
-
136
- ### Programmatic use (optional)
137
- Use the toolkit directly in TypeScript when embedding into agents or services.
138
- ```ts
139
- import { PerplexitySearchTool } from 'pplx-zero';
140
-
141
- const tool = new PerplexitySearchTool();
142
-
143
- const result = await tool.runBatch({
144
- version: "1.0.0",
145
- requests: [{ op: "search", args: { query: "TypeScript best practices", maxResults: 5 } }]
146
- });
147
-
148
- console.log(result);
149
- ```
36
+ | Flag | Description |
37
+ |------|-------------|
38
+ | `-m, --model` | Model: sonar, sonar-pro, sonar-reasoning, sonar-reasoning-pro, sonar-deep-research |
39
+ | `-f, --file` | Attach a file (PDF, TXT, etc.) |
40
+ | `-i, --image` | Attach an image (PNG, JPG, etc.) |
41
+ | `--json` | Output as JSON |
42
+ | `-h, --help` | Show help |
150
43
 
44
+ ## License
151
45
 
152
- ### Notes
153
- - Use pplx --help to see all available options and short flags without scanning long docs.
154
- - Keep output in jsonl for streaming pipelines and agent frameworks that consume line‑by‑line events.*Built with [Bun](https://bun.sh) and [Perplexity AI](https://www.perplexity.ai)**
46
+ MIT
package/bin/pplx.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn, execSync } = require('child_process');
4
+ const { join } = require('path');
5
+
6
+ const hasBun = () => {
7
+ try {
8
+ execSync('bun --version', { stdio: 'ignore' });
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ };
14
+
15
+ if (hasBun()) {
16
+ const scriptPath = join(__dirname, '..', 'src', 'index.ts');
17
+ const proc = spawn('bun', ['run', scriptPath, ...process.argv.slice(2)], {
18
+ stdio: 'inherit'
19
+ });
20
+ proc.on('exit', (code) => process.exit(code ?? 1));
21
+ } else {
22
+ console.error('\x1b[31mError: pplx-zero requires Bun to be installed.\x1b[0m');
23
+ console.error('\nInstall Bun:');
24
+ console.error(' curl -fsSL https://bun.sh/install | bash');
25
+ console.error('\nOr via npm:');
26
+ console.error(' npm install -g bun');
27
+ process.exit(1);
28
+ }
package/package.json CHANGED
@@ -1,80 +1,46 @@
1
1
  {
2
2
  "name": "pplx-zero",
3
- "version": "1.1.7",
4
- "description": "Fast Perplexity AI search CLI with multimodal support - minimal setup, maximal results",
3
+ "version": "2.0.0",
4
+ "description": "Minimal Perplexity AI CLI - search from terminal",
5
+ "author": "kenzo",
6
+ "license": "MIT",
5
7
  "type": "module",
6
- "main": "dist/index.js",
8
+ "module": "src/index.ts",
7
9
  "bin": {
8
- "pplx": "dist/index.js"
10
+ "pplx": "bin/pplx.js"
9
11
  },
10
12
  "scripts": {
11
- "dev": "bun run src/cli/index.ts",
12
- "dev:legacy": "bun run src/cli.ts",
13
- "build": "rm -rf dist && bun build src/cli/index.ts --target node --outdir dist",
14
- "build:legacy": "rm -rf dist && bun build src/cli.ts --target node --outdir dist",
15
- "build:binary": "bun build --compile src/cli/index.ts --outfile=dist/pplx",
16
- "build:binary:legacy": "bun build --compile src/cli.ts --outfile=dist/pplx-legacy",
17
- "test": "bun test",
18
- "test:watch": "bun test --watch",
19
- "typecheck": "bun tsc --noEmit",
20
- "lint": "bun run --bun eslint src/**/*.ts",
21
- "lint:fix": "bun run --bun eslint src/**/*.ts --fix",
22
- "clean": "rm -rf dist",
23
- "dev:debug": "bun --inspect src/cli/index.ts",
24
- "dev:debug:legacy": "bun --inspect src/cli.ts"
13
+ "build": "bun build src/index.ts --compile --outfile=pplx",
14
+ "dev": "bun run src/index.ts",
15
+ "test": "bun test"
25
16
  },
26
17
  "dependencies": {
27
- "@perplexity-ai/perplexity_ai": "^0.11.0",
28
- "abort-controller": "^3.0.0",
29
- "commander": "^12.0.0",
30
- "dotenv": "^16.3.1",
31
- "zod": "^3.22.4"
18
+ "zod": "^4.0.0"
32
19
  },
33
20
  "devDependencies": {
34
- "@types/node": "^20.0.0",
35
- "typescript": "^5.0.0",
36
- "bun-types": "latest",
37
- "@typescript-eslint/eslint-plugin": "^6.0.0",
38
- "@typescript-eslint/parser": "^6.0.0",
39
- "eslint": "^8.0.0"
21
+ "@types/bun": "latest"
40
22
  },
41
- "engines": {
42
- "bun": ">=1.0.0"
23
+ "peerDependencies": {
24
+ "typescript": "^5"
43
25
  },
44
- "keywords": [
45
- "pplx-zero",
46
- "perplexity",
47
- "search",
48
- "ai",
49
- "cli",
50
- "command-line",
51
- "tool",
52
- "typescript",
53
- "bun",
54
- "api",
55
- "minimal",
56
- "fast",
57
- "productivity",
58
- "zero-config",
59
- "multimodal",
60
- "attachments",
61
- "images",
62
- "documents",
63
- "sonar",
64
- "reasoning",
65
- "research",
66
- "async"
26
+ "files": [
27
+ "src",
28
+ "bin"
67
29
  ],
68
- "author": "Kenzo",
69
- "license": "MIT",
70
30
  "repository": {
71
31
  "type": "git",
72
32
  "url": "git+https://github.com/codewithkenzo/pplx-zero.git"
73
33
  },
74
- "files": [
75
- "dist",
76
- "README.md",
77
- "LICENSE",
78
- "package.json"
34
+ "bugs": {
35
+ "url": "https://github.com/codewithkenzo/pplx-zero/issues"
36
+ },
37
+ "homepage": "https://github.com/codewithkenzo/pplx-zero#readme",
38
+ "keywords": [
39
+ "perplexity",
40
+ "ai",
41
+ "search",
42
+ "cli",
43
+ "terminal",
44
+ "bun"
79
45
  ]
80
- }
46
+ }
@@ -0,0 +1,21 @@
1
+ import { test, expect, describe } from 'bun:test';
2
+ import { MODELS, type Model } from './api';
3
+
4
+ describe('MODELS', () => {
5
+ test('includes all expected models', () => {
6
+ expect(MODELS).toContain('sonar');
7
+ expect(MODELS).toContain('sonar-pro');
8
+ expect(MODELS).toContain('sonar-reasoning');
9
+ expect(MODELS).toContain('sonar-reasoning-pro');
10
+ expect(MODELS).toContain('sonar-deep-research');
11
+ });
12
+
13
+ test('has exactly 5 models', () => {
14
+ expect(MODELS).toHaveLength(5);
15
+ });
16
+
17
+ test('Model type matches MODELS array', () => {
18
+ const model: Model = MODELS[0]!;
19
+ expect(MODELS.includes(model)).toBe(true);
20
+ });
21
+ });
package/src/api.ts ADDED
@@ -0,0 +1,116 @@
1
+ import { getEnv } from './env';
2
+ import type { FileAttachment } from './files';
3
+
4
+ const API_URL = 'https://api.perplexity.ai/chat/completions';
5
+
6
+ export const MODELS = ['sonar', 'sonar-pro', 'sonar-reasoning', 'sonar-reasoning-pro', 'sonar-deep-research'] as const;
7
+ export type Model = (typeof MODELS)[number];
8
+
9
+ export interface SearchResult {
10
+ title: string;
11
+ url: string;
12
+ date?: string;
13
+ }
14
+
15
+ export interface StreamCallbacks {
16
+ onContent: (text: string) => void;
17
+ onDone: (citations: SearchResult[], usage: { prompt_tokens: number; completion_tokens: number }) => void;
18
+ onError: (error: Error) => void;
19
+ }
20
+
21
+ interface MessageContent {
22
+ type: 'text' | 'file_url';
23
+ text?: string;
24
+ file_url?: { url: string };
25
+ file_name?: string;
26
+ }
27
+
28
+ function buildMessages(query: string, file?: FileAttachment): { role: string; content: string | MessageContent[] }[] {
29
+ if (!file) {
30
+ return [{ role: 'user', content: query }];
31
+ }
32
+
33
+ const content: MessageContent[] = [
34
+ { type: 'text', text: query },
35
+ {
36
+ type: 'file_url',
37
+ file_url: { url: file.data },
38
+ file_name: file.filename,
39
+ },
40
+ ];
41
+
42
+ return [{ role: 'user', content }];
43
+ }
44
+
45
+ export async function search(
46
+ query: string,
47
+ model: Model,
48
+ callbacks: StreamCallbacks,
49
+ file?: FileAttachment
50
+ ): Promise<void> {
51
+ const body = JSON.stringify({
52
+ model,
53
+ messages: buildMessages(query, file),
54
+ stream: true,
55
+ });
56
+
57
+ const response = await fetch(API_URL, {
58
+ method: 'POST',
59
+ headers: {
60
+ 'Authorization': `Bearer ${getEnv().PERPLEXITY_API_KEY}`,
61
+ 'Content-Type': 'application/json',
62
+ },
63
+ body,
64
+ });
65
+
66
+ if (!response.ok) {
67
+ const text = await response.text();
68
+ callbacks.onError(new Error(`API error ${response.status}: ${text}`));
69
+ return;
70
+ }
71
+
72
+ if (!response.body) {
73
+ callbacks.onError(new Error('No response body'));
74
+ return;
75
+ }
76
+
77
+ const reader = response.body.getReader();
78
+ const decoder = new TextDecoder();
79
+ let buffer = '';
80
+ let citations: SearchResult[] = [];
81
+ let usage = { prompt_tokens: 0, completion_tokens: 0 };
82
+
83
+ while (true) {
84
+ const { done, value } = await reader.read();
85
+ if (done) break;
86
+
87
+ buffer += decoder.decode(value, { stream: true });
88
+ const lines = buffer.split('\n');
89
+ buffer = lines.pop() || '';
90
+
91
+ for (const line of lines) {
92
+ if (!line.startsWith('data: ')) continue;
93
+ const data = line.slice(6).trim();
94
+ if (data === '[DONE]') continue;
95
+
96
+ try {
97
+ const parsed = JSON.parse(data);
98
+ const delta = parsed.choices?.[0]?.delta?.content;
99
+ if (delta) {
100
+ callbacks.onContent(delta);
101
+ }
102
+
103
+ if (parsed.search_results) {
104
+ citations = parsed.search_results;
105
+ }
106
+ if (parsed.usage) {
107
+ usage = parsed.usage;
108
+ }
109
+ } catch {
110
+ continue;
111
+ }
112
+ }
113
+ }
114
+
115
+ callbacks.onDone(citations, usage);
116
+ }
package/src/env.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod';
2
+
3
+ const c = {
4
+ reset: '\x1b[0m',
5
+ red: '\x1b[31m',
6
+ yellow: '\x1b[33m',
7
+ cyan: '\x1b[36m',
8
+ dim: '\x1b[2m',
9
+ } as const;
10
+
11
+ const envSchema = z.object({
12
+ PERPLEXITY_API_KEY: z.string().min(1),
13
+ });
14
+
15
+ let _env: z.infer<typeof envSchema> | null = null;
16
+
17
+ export function getEnv() {
18
+ if (_env) return _env;
19
+
20
+ const key = process.env.PERPLEXITY_API_KEY || process.env.PERPLEXITY_AI_API_KEY;
21
+
22
+ if (!key) {
23
+ console.error(`
24
+ ${c.red}✗ Missing API Key${c.reset}
25
+
26
+ Set your Perplexity API key:
27
+
28
+ ${c.cyan}export PERPLEXITY_API_KEY="pplx-..."${c.reset}
29
+
30
+ ${c.dim}Get one at: https://perplexity.ai/settings/api${c.reset}
31
+ `);
32
+ process.exit(2);
33
+ }
34
+
35
+ _env = { PERPLEXITY_API_KEY: key };
36
+ return _env;
37
+ }
@@ -0,0 +1,71 @@
1
+ import { test, expect, describe } from 'bun:test';
2
+ import { encodeFile, toDataUrl, type FileAttachment } from './files';
3
+ import { writeFile, unlink } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+
6
+ const TMP_DIR = '/tmp';
7
+
8
+ describe('encodeFile', () => {
9
+ test('encodes text file correctly', async () => {
10
+ const testPath = join(TMP_DIR, 'test.txt');
11
+ await writeFile(testPath, 'hello world');
12
+
13
+ const result = await encodeFile(testPath);
14
+
15
+ expect(result.type).toBe('file');
16
+ expect(result.mimeType).toBe('text/plain');
17
+ expect(result.filename).toBe('test.txt');
18
+ expect(result.data).toBe(Buffer.from('hello world').toString('base64'));
19
+
20
+ await unlink(testPath);
21
+ });
22
+
23
+ test('encodes PDF as file type', async () => {
24
+ const testPath = join(TMP_DIR, 'test.pdf');
25
+ await writeFile(testPath, '%PDF-1.4 test');
26
+
27
+ const result = await encodeFile(testPath);
28
+
29
+ expect(result.type).toBe('file');
30
+ expect(result.mimeType).toBe('application/pdf');
31
+
32
+ await unlink(testPath);
33
+ });
34
+
35
+ test('encodes PNG as image type', async () => {
36
+ const testPath = join(TMP_DIR, 'test.png');
37
+ const pngHeader = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
38
+ await writeFile(testPath, pngHeader);
39
+
40
+ const result = await encodeFile(testPath);
41
+
42
+ expect(result.type).toBe('image');
43
+ expect(result.mimeType).toBe('image/png');
44
+
45
+ await unlink(testPath);
46
+ });
47
+
48
+ test('throws on unsupported file type', async () => {
49
+ const testPath = join(TMP_DIR, 'test.xyz');
50
+ await writeFile(testPath, 'test');
51
+
52
+ await expect(encodeFile(testPath)).rejects.toThrow('Unsupported file type: .xyz');
53
+
54
+ await unlink(testPath);
55
+ });
56
+ });
57
+
58
+ describe('toDataUrl', () => {
59
+ test('creates valid data URL', () => {
60
+ const attachment: FileAttachment = {
61
+ type: 'image',
62
+ data: 'aGVsbG8gd29ybGQ=',
63
+ mimeType: 'image/png',
64
+ filename: 'test.png',
65
+ };
66
+
67
+ const result = toDataUrl(attachment);
68
+
69
+ expect(result).toBe('data:image/png;base64,aGVsbG8gd29ybGQ=');
70
+ });
71
+ });
package/src/files.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { extname } from 'node:path';
3
+
4
+ const MIME_TYPES: Record<string, string> = {
5
+ '.pdf': 'application/pdf',
6
+ '.txt': 'text/plain',
7
+ '.md': 'text/markdown',
8
+ '.png': 'image/png',
9
+ '.jpg': 'image/jpeg',
10
+ '.jpeg': 'image/jpeg',
11
+ '.gif': 'image/gif',
12
+ '.webp': 'image/webp',
13
+ };
14
+
15
+ export interface FileAttachment {
16
+ type: 'file' | 'image';
17
+ data: string;
18
+ mimeType: string;
19
+ filename: string;
20
+ }
21
+
22
+ export async function encodeFile(path: string): Promise<FileAttachment> {
23
+ const ext = extname(path).toLowerCase();
24
+ const mimeType = MIME_TYPES[ext];
25
+
26
+ if (!mimeType) {
27
+ throw new Error(`Unsupported file type: ${ext}`);
28
+ }
29
+
30
+ const buffer = await readFile(path);
31
+ const data = buffer.toString('base64');
32
+ const isImage = mimeType.startsWith('image/');
33
+
34
+ return {
35
+ type: isImage ? 'image' : 'file',
36
+ data,
37
+ mimeType,
38
+ filename: path.split('/').pop() || 'file',
39
+ };
40
+ }
41
+
42
+ export function toDataUrl(attachment: FileAttachment): string {
43
+ return `data:${attachment.mimeType};base64,${attachment.data}`;
44
+ }