pplx-zero 2.2.0 → 2.2.1
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/bin/pplx.js +5 -2
- package/package.json +3 -2
- package/src/api.test.ts +0 -21
- package/src/files.test.ts +0 -71
- package/src/history.test.ts +0 -84
- package/src/output.test.ts +0 -41
package/bin/pplx.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { spawn, execSync } from 'child_process';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
8
|
|
|
6
9
|
const hasBun = () => {
|
|
7
10
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pplx-zero",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Minimal Perplexity AI CLI - search from terminal",
|
|
5
5
|
"author": "kenzo",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"src",
|
|
28
|
-
"bin"
|
|
28
|
+
"bin",
|
|
29
|
+
"!src/**/*.test.ts"
|
|
29
30
|
],
|
|
30
31
|
"repository": {
|
|
31
32
|
"type": "git",
|
package/src/api.test.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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/files.test.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
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/history.test.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { test, expect, beforeEach, afterAll } from 'bun:test';
|
|
2
|
-
import { appendHistory, readHistory, getLastEntry, clearHistory } from './history';
|
|
3
|
-
|
|
4
|
-
beforeEach(async () => {
|
|
5
|
-
await clearHistory();
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
afterAll(async () => {
|
|
9
|
-
await clearHistory();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
test('appendHistory creates entry', async () => {
|
|
13
|
-
await appendHistory({ q: 'test query', m: 'sonar', a: 'test answer' });
|
|
14
|
-
const entries = await readHistory();
|
|
15
|
-
expect(entries.length).toBe(1);
|
|
16
|
-
expect(entries[0]!.q).toBe('test query');
|
|
17
|
-
expect(entries[0]!.m).toBe('sonar');
|
|
18
|
-
expect(entries[0]!.a).toBe('test answer');
|
|
19
|
-
expect(entries[0]!.ts).toBeGreaterThan(0);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test('readHistory returns entries in reverse order', async () => {
|
|
23
|
-
await appendHistory({ q: 'first', m: 'sonar', a: 'a1' });
|
|
24
|
-
await appendHistory({ q: 'second', m: 'sonar-pro', a: 'a2' });
|
|
25
|
-
await appendHistory({ q: 'third', m: 'sonar', a: 'a3' });
|
|
26
|
-
|
|
27
|
-
const entries = await readHistory();
|
|
28
|
-
expect(entries.length).toBe(3);
|
|
29
|
-
expect(entries[0]!.q).toBe('third');
|
|
30
|
-
expect(entries[1]!.q).toBe('second');
|
|
31
|
-
expect(entries[2]!.q).toBe('first');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('readHistory respects limit', async () => {
|
|
35
|
-
await appendHistory({ q: 'one', m: 'sonar', a: 'a' });
|
|
36
|
-
await appendHistory({ q: 'two', m: 'sonar', a: 'a' });
|
|
37
|
-
await appendHistory({ q: 'three', m: 'sonar', a: 'a' });
|
|
38
|
-
|
|
39
|
-
const entries = await readHistory(2);
|
|
40
|
-
expect(entries.length).toBe(2);
|
|
41
|
-
expect(entries[0]!.q).toBe('three');
|
|
42
|
-
expect(entries[1]!.q).toBe('two');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('getLastEntry returns most recent', async () => {
|
|
46
|
-
await appendHistory({ q: 'old', m: 'sonar', a: 'old answer' });
|
|
47
|
-
await appendHistory({ q: 'new', m: 'sonar-pro', a: 'new answer' });
|
|
48
|
-
|
|
49
|
-
const last = await getLastEntry();
|
|
50
|
-
expect(last?.q).toBe('new');
|
|
51
|
-
expect(last?.m).toBe('sonar-pro');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('getLastEntry returns null when empty', async () => {
|
|
55
|
-
const last = await getLastEntry();
|
|
56
|
-
expect(last).toBeNull();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('clearHistory removes all entries', async () => {
|
|
60
|
-
await appendHistory({ q: 'test', m: 'sonar', a: 'answer' });
|
|
61
|
-
await clearHistory();
|
|
62
|
-
const entries = await readHistory();
|
|
63
|
-
expect(entries.length).toBe(0);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('appendHistory stores citations', async () => {
|
|
67
|
-
await appendHistory({
|
|
68
|
-
q: 'query',
|
|
69
|
-
m: 'sonar',
|
|
70
|
-
a: 'answer',
|
|
71
|
-
citations: ['https://example.com', 'https://test.com']
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const entries = await readHistory();
|
|
75
|
-
expect(entries[0]!.citations).toEqual(['https://example.com', 'https://test.com']);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('appendHistory truncates long answers', async () => {
|
|
79
|
-
const longAnswer = 'x'.repeat(3000);
|
|
80
|
-
await appendHistory({ q: 'query', m: 'sonar', a: longAnswer });
|
|
81
|
-
|
|
82
|
-
const entries = await readHistory();
|
|
83
|
-
expect(entries[0]!.a.length).toBe(2000);
|
|
84
|
-
});
|
package/src/output.test.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
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
|
-
});
|