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 +1 -1
- package/README.md +24 -132
- package/bin/pplx.js +28 -0
- package/package.json +28 -62
- package/src/api.test.ts +21 -0
- package/src/api.ts +116 -0
- package/src/env.ts +37 -0
- package/src/files.test.ts +71 -0
- package/src/files.ts +44 -0
- package/src/index.ts +96 -0
- package/src/output.test.ts +41 -0
- package/src/output.ts +28 -0
- package/dist/index.js +0 -10443
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
|
-
#
|
|
1
|
+
# pplx-zero
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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": "
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
8
|
+
"module": "src/index.ts",
|
|
7
9
|
"bin": {
|
|
8
|
-
"pplx": "
|
|
10
|
+
"pplx": "bin/pplx.js"
|
|
9
11
|
},
|
|
10
12
|
"scripts": {
|
|
11
|
-
"
|
|
12
|
-
"dev
|
|
13
|
-
"
|
|
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
|
-
"
|
|
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/
|
|
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
|
-
"
|
|
42
|
-
"
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"typescript": "^5"
|
|
43
25
|
},
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
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
|
-
"
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
}
|
package/src/api.test.ts
ADDED
|
@@ -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
|
+
}
|