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/src/index.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { search, MODELS, type Model } from './api';
|
|
4
|
+
import { encodeFile } from './files';
|
|
5
|
+
import { getEnv } from './env';
|
|
6
|
+
import { fmt, write, writeLn } from './output';
|
|
7
|
+
|
|
8
|
+
getEnv();
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const { values, positionals } = parseArgs({
|
|
12
|
+
args: Bun.argv.slice(2),
|
|
13
|
+
options: {
|
|
14
|
+
model: { type: 'string', short: 'm', default: 'sonar' },
|
|
15
|
+
file: { type: 'string', short: 'f' },
|
|
16
|
+
image: { type: 'string', short: 'i' },
|
|
17
|
+
json: { type: 'boolean', default: false },
|
|
18
|
+
help: { type: 'boolean', short: 'h' },
|
|
19
|
+
},
|
|
20
|
+
allowPositionals: true,
|
|
21
|
+
strict: true,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (values.help || positionals.length === 0) {
|
|
25
|
+
console.log(`
|
|
26
|
+
pplx - Perplexity AI search from terminal
|
|
27
|
+
|
|
28
|
+
Usage: pplx [options] <query>
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
-m, --model <name> Model: ${MODELS.join(', ')} (default: sonar)
|
|
32
|
+
-f, --file <path> Attach a file (PDF, TXT, etc.)
|
|
33
|
+
-i, --image <path> Attach an image (PNG, JPG, etc.)
|
|
34
|
+
--json Output as JSON
|
|
35
|
+
-h, --help Show this help
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
pplx "what is bun"
|
|
39
|
+
pplx -m sonar-pro "explain quantum computing"
|
|
40
|
+
pplx -f report.pdf "summarize this document"
|
|
41
|
+
`);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const query = positionals.join(' ');
|
|
46
|
+
const model = (MODELS.includes(values.model as Model) ? values.model : 'sonar') as Model;
|
|
47
|
+
|
|
48
|
+
const filePath = values.file || values.image;
|
|
49
|
+
const file = filePath ? await encodeFile(filePath) : undefined;
|
|
50
|
+
|
|
51
|
+
const startTime = Date.now();
|
|
52
|
+
let fullContent = '';
|
|
53
|
+
|
|
54
|
+
if (!values.json) {
|
|
55
|
+
await write(fmt.model(model) + ' ');
|
|
56
|
+
await write(fmt.searching());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await search(query, model, {
|
|
60
|
+
onContent: async (text) => {
|
|
61
|
+
fullContent += text;
|
|
62
|
+
if (!values.json) {
|
|
63
|
+
await write(text);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
onDone: async (citations, usage) => {
|
|
67
|
+
const elapsed = Date.now() - startTime;
|
|
68
|
+
|
|
69
|
+
if (values.json) {
|
|
70
|
+
const output = {
|
|
71
|
+
answer: fullContent,
|
|
72
|
+
citations: citations.map((c) => c.url),
|
|
73
|
+
model,
|
|
74
|
+
tokens: usage.prompt_tokens + usage.completion_tokens,
|
|
75
|
+
latency_ms: elapsed,
|
|
76
|
+
};
|
|
77
|
+
console.log(JSON.stringify(output, null, 2));
|
|
78
|
+
} else {
|
|
79
|
+
if (citations.length > 0) {
|
|
80
|
+
await writeLn(fmt.sources());
|
|
81
|
+
for (let i = 0; i < citations.length; i++) {
|
|
82
|
+
await writeLn(fmt.citation(i + 1, citations[i]!.url));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
await write(fmt.stats(usage.prompt_tokens + usage.completion_tokens, elapsed));
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
onError: async (error) => {
|
|
89
|
+
if (values.json) {
|
|
90
|
+
console.error(JSON.stringify({ error: error.message }));
|
|
91
|
+
} else {
|
|
92
|
+
await write(fmt.error(error.message));
|
|
93
|
+
}
|
|
94
|
+
process.exit(1);
|
|
95
|
+
},
|
|
96
|
+
}, file);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { test, expect, describe } from 'bun:test';
|
|
2
|
+
import { fmt } from './output';
|
|
3
|
+
|
|
4
|
+
describe('fmt', () => {
|
|
5
|
+
test('model formats with cyan color', () => {
|
|
6
|
+
const result = fmt.model('sonar');
|
|
7
|
+
expect(result).toContain('[sonar]');
|
|
8
|
+
expect(result).toContain('\x1b[36m');
|
|
9
|
+
expect(result).toContain('\x1b[0m');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('searching shows dim text', () => {
|
|
13
|
+
const result = fmt.searching();
|
|
14
|
+
expect(result).toContain('Searching...');
|
|
15
|
+
expect(result).toContain('\x1b[2m');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('error formats with red color', () => {
|
|
19
|
+
const result = fmt.error('test error');
|
|
20
|
+
expect(result).toContain('Error: test error');
|
|
21
|
+
expect(result).toContain('\x1b[31m');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('citation formats with number and URL', () => {
|
|
25
|
+
const result = fmt.citation(1, 'https://example.com');
|
|
26
|
+
expect(result).toContain('1.');
|
|
27
|
+
expect(result).toContain('https://example.com');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('stats formats tokens and time', () => {
|
|
31
|
+
const result = fmt.stats(100, 1500);
|
|
32
|
+
expect(result).toContain('100 tokens');
|
|
33
|
+
expect(result).toContain('1.5s');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('sources shows yellow header', () => {
|
|
37
|
+
const result = fmt.sources();
|
|
38
|
+
expect(result).toContain('Sources:');
|
|
39
|
+
expect(result).toContain('\x1b[33m');
|
|
40
|
+
});
|
|
41
|
+
});
|
package/src/output.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const c = {
|
|
2
|
+
reset: '\x1b[0m',
|
|
3
|
+
dim: '\x1b[2m',
|
|
4
|
+
bold: '\x1b[1m',
|
|
5
|
+
cyan: '\x1b[36m',
|
|
6
|
+
green: '\x1b[32m',
|
|
7
|
+
yellow: '\x1b[33m',
|
|
8
|
+
red: '\x1b[31m',
|
|
9
|
+
gray: '\x1b[90m',
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export const fmt = {
|
|
13
|
+
model: (name: string) => `${c.cyan}[${name}]${c.reset}`,
|
|
14
|
+
searching: () => `${c.dim}Searching...${c.reset}\n`,
|
|
15
|
+
error: (msg: string) => `${c.red}Error: ${msg}${c.reset}\n`,
|
|
16
|
+
citation: (i: number, url: string) => `${c.dim} ${i}. ${url}${c.reset}`,
|
|
17
|
+
stats: (tokens: number, ms: number) =>
|
|
18
|
+
`\n${c.gray}[${tokens} tokens, ${(ms / 1000).toFixed(1)}s]${c.reset}\n`,
|
|
19
|
+
sources: () => `\n${c.yellow}Sources:${c.reset}`,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function write(text: string): Promise<void> {
|
|
23
|
+
await Bun.write(Bun.stdout, text);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function writeLn(text: string): Promise<void> {
|
|
27
|
+
await Bun.write(Bun.stdout, text + '\n');
|
|
28
|
+
}
|