askpplx 1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Łukasz Jerciński
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # askpplx
2
+
3
+ `askpplx` is a minimal Unix-style CLI for querying Perplexity Sonar.
4
+
5
+ > **Perplexity AI** is an AI-powered search engine and answer engine that delivers concise, accurate responses to user queries by combining real-time web searches with advanced language models.
6
+
7
+ - Command name: `askpplx`
8
+ - Output: plain text or JSON to stdout
9
+ - No MCPs, no agents, no plugins, no TUI
10
+ - Just a thin, script-friendly wrapper around the Perplexity API
11
+
12
+ ## Setup
13
+
14
+ Provide your Perplexity API key via environment variable or persistent storage:
15
+
16
+ ```bash
17
+ # Option 1: Environment variable
18
+ export PERPLEXITY_API_KEY="pplx-..."
19
+
20
+ # Option 2: Store persistently
21
+ npx -y askpplx config --set-api-key "pplx-..."
22
+ ```
23
+
24
+ ## Examples
25
+
26
+ ```bash
27
+ # simple question
28
+ askpplx "Explain Raft vs Paxos in simple terms"
29
+
30
+ # web-enabled search with local context
31
+ askpplx "What are breaking changes in React 19 that affect this code? $(cat src/app.tsx)"
32
+ ```
33
+
34
+ ## Agent Rule
35
+
36
+ Add this rule to your `CLAUDE.md` or `AGENTS.md` to enable automatic Perplexity lookups, no need to configure MCPs:
37
+
38
+ ````markdown
39
+ # Rule: askpplx CLI Usage
40
+
41
+ Use `askpplx` to query Perplexity, an AI search engine combining real-time web search with advanced language models. Run it via `npx -y askpplx`.
42
+
43
+ Use concise prompts for quick facts and focused questions for deeper topics. If results are unexpected, refine your query and ask again.
44
+
45
+ Verification is fast and cheap, so prefer looking up information over making assumptions. Before first use, run `npx -y askpplx --help`.
46
+ ````
package/bin/askpplx ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/cli.js");
@@ -0,0 +1,18 @@
1
+ import { streamText } from "ai";
2
+ export type SearchContextSize = "low" | "medium" | "high";
3
+ type AskPerplexityOptions = {
4
+ apiKey: string;
5
+ model: string;
6
+ prompt: string;
7
+ system?: string;
8
+ searchContextSize?: SearchContextSize;
9
+ };
10
+ export type StreamPerplexityResult = ReturnType<typeof streamText>;
11
+ export type AskPerplexityResult = {
12
+ text: string;
13
+ sources: Awaited<StreamPerplexityResult["sources"]>;
14
+ usage: Awaited<StreamPerplexityResult["usage"]>;
15
+ providerMetadata: Awaited<StreamPerplexityResult["providerMetadata"]>;
16
+ };
17
+ export declare function streamPerplexity(options: AskPerplexityOptions): StreamPerplexityResult;
18
+ export {};
@@ -0,0 +1,17 @@
1
+ import { createPerplexity } from "@ai-sdk/perplexity";
2
+ import { streamText } from "ai";
3
+ export function streamPerplexity(options) {
4
+ const perplexity = createPerplexity({ apiKey: options.apiKey });
5
+ return streamText({
6
+ model: perplexity(options.model),
7
+ system: options.system,
8
+ prompt: options.prompt,
9
+ providerOptions: {
10
+ perplexity: {
11
+ web_search_options: {
12
+ search_context_size: options.searchContextSize ?? "high",
13
+ },
14
+ },
15
+ },
16
+ });
17
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import packageJson from "../package.json" with { type: "json" };
4
+ import { clearPerplexityApiKey, getConfigPath, getPerplexityApiKey, maskApiKey, setPerplexityApiKey, } from "./config.js";
5
+ import { runCli } from "./run-cli.js";
6
+ const usageExamples = `
7
+ About Perplexity:
8
+ Perplexity AI is an AI-powered search engine and answer engine that delivers
9
+ concise, accurate responses to user queries by combining real-time web
10
+ searches with advanced language models.
11
+
12
+ Models:
13
+ sonar Fast, lightweight for quick searches (128K context)
14
+ sonar-pro Advanced multi-step research queries
15
+ sonar-reasoning-pro Deep reasoning with R1-1776 backend (default)
16
+
17
+ JSON output (--json):
18
+ Returns { text, sources[], usage, providerMetadata } - not structured AI output.
19
+ Use jq to extract fields: --json | jq -r '.text' or '.sources[].url'
20
+
21
+ System prompt:
22
+ Default prompt is optimized for technical/coding questions.
23
+ Use -s <file> or -S <text> to customize. Use -S "" to disable.
24
+
25
+ Examples:
26
+ askpplx "What is the capital of France?" -S ""
27
+ askpplx "Explain quantum computing" --model sonar-pro
28
+ askpplx "Latest news on AI" -c medium
29
+ askpplx "$(cat article.txt)" -s ./summarize.md
30
+ askpplx "$(cat article.txt)" -S "Summarize this article"
31
+ askpplx "Node.js LTS version" --json | jq -r '.text'
32
+ askpplx "Show reasoning" --show-thinking`;
33
+ const program = new Command()
34
+ .name("askpplx")
35
+ .description(packageJson.description)
36
+ .version(packageJson.version)
37
+ .argument("[prompt]", "The prompt to send to Perplexity Sonar")
38
+ .option("-m, --model <model>", "Model to use", "sonar-reasoning-pro")
39
+ .option("-s, --system <path>", "Path to custom system prompt file")
40
+ .option("-S, --system-text <text>", "System prompt text (overrides -s)")
41
+ .option("-c, --context <size>", "Search context size: low, medium, high", "high")
42
+ .option("--json", "Output full API response as JSON (text, sources, usage)")
43
+ .option("--show-thinking", "Show model thinking/reasoning blocks")
44
+ .option("--no-stream, --no-streaming", "Disable streaming output")
45
+ .addHelpText("after", usageExamples)
46
+ .action(async (prompt, options) => {
47
+ if (!prompt) {
48
+ program.help();
49
+ return;
50
+ }
51
+ try {
52
+ await runCli(prompt, options);
53
+ }
54
+ catch (error) {
55
+ const message = error instanceof Error ? error.message : "An unexpected error occurred";
56
+ console.error(`Error: ${message}`);
57
+ process.exitCode = 1;
58
+ }
59
+ });
60
+ program
61
+ .command("config")
62
+ .description("Manage stored configuration")
63
+ .option("--set-api-key <key>", "Store Perplexity API key")
64
+ .option("--show-api-key", "Show stored API key (masked)")
65
+ .option("--clear-api-key", "Remove stored API key")
66
+ .option("--path", "Show config file path")
67
+ .action((options) => {
68
+ try {
69
+ if (options.setApiKey) {
70
+ setPerplexityApiKey(options.setApiKey);
71
+ console.log("API key stored successfully.");
72
+ }
73
+ else if (options.showApiKey) {
74
+ const key = getPerplexityApiKey();
75
+ const masked = maskApiKey(key);
76
+ console.log(masked ? `API key: ${masked}` : "No API key configured.");
77
+ }
78
+ else if (options.clearApiKey) {
79
+ clearPerplexityApiKey();
80
+ console.log("API key cleared.");
81
+ }
82
+ else if (options.path) {
83
+ console.log(getConfigPath());
84
+ }
85
+ else {
86
+ const configCmd = program.commands.find((c) => c.name() === "config");
87
+ configCmd?.help();
88
+ }
89
+ }
90
+ catch (error) {
91
+ const message = error instanceof Error
92
+ ? error.message
93
+ : "An unexpected error occurred";
94
+ console.error(`Error: ${message}`);
95
+ process.exitCode = 1;
96
+ }
97
+ });
98
+ program.parse();
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Get API key: env var takes precedence over stored config.
3
+ * Note: An empty PERPLEXITY_API_KEY="" explicitly disables stored config.
4
+ */
5
+ export declare function getPerplexityApiKey(): string | undefined;
6
+ /** Store API key in persistent config. */
7
+ export declare function setPerplexityApiKey(apiKey: string): void;
8
+ /** Remove stored API key from config. */
9
+ export declare function clearPerplexityApiKey(): void;
10
+ /** Get path to config file. */
11
+ export declare function getConfigPath(): string;
12
+ /**
13
+ * Mask API key for display: shows first 4 and last 4 characters for keys longer than 16 chars.
14
+ * For keys of length 16 or less, returns "****".
15
+ * Returns undefined if key is undefined or empty.
16
+ */
17
+ export declare function maskApiKey(key?: string): string | undefined;
package/dist/config.js ADDED
@@ -0,0 +1,49 @@
1
+ import Conf from "conf";
2
+ import packageJson from "../package.json" with { type: "json" };
3
+ const schema = {
4
+ perplexityApiKey: {
5
+ type: "string",
6
+ },
7
+ };
8
+ let configInstance;
9
+ function getConfig() {
10
+ if (!configInstance) {
11
+ configInstance = new Conf({
12
+ projectName: packageJson.name,
13
+ projectVersion: packageJson.version,
14
+ schema,
15
+ });
16
+ }
17
+ return configInstance;
18
+ }
19
+ /**
20
+ * Get API key: env var takes precedence over stored config.
21
+ * Note: An empty PERPLEXITY_API_KEY="" explicitly disables stored config.
22
+ */
23
+ export function getPerplexityApiKey() {
24
+ return (process.env["PERPLEXITY_API_KEY"] ?? getConfig().get("perplexityApiKey"));
25
+ }
26
+ /** Store API key in persistent config. */
27
+ export function setPerplexityApiKey(apiKey) {
28
+ getConfig().set("perplexityApiKey", apiKey);
29
+ }
30
+ /** Remove stored API key from config. */
31
+ export function clearPerplexityApiKey() {
32
+ getConfig().delete("perplexityApiKey");
33
+ }
34
+ /** Get path to config file. */
35
+ export function getConfigPath() {
36
+ return getConfig().path;
37
+ }
38
+ /**
39
+ * Mask API key for display: shows first 4 and last 4 characters for keys longer than 16 chars.
40
+ * For keys of length 16 or less, returns "****".
41
+ * Returns undefined if key is undefined or empty.
42
+ */
43
+ export function maskApiKey(key) {
44
+ if (!key)
45
+ return undefined;
46
+ if (key.length <= 16)
47
+ return "****";
48
+ return `${key.slice(0, 4)}...${key.slice(-4)}`;
49
+ }
@@ -0,0 +1 @@
1
+ export declare function loadSystemPrompt(customPath?: string): Promise<string>;
@@ -0,0 +1,22 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ const DEFAULT_SYSTEM_PROMPT_PATH = path.join(__dirname, "prompts", "default-system.md");
6
+ export async function loadSystemPrompt(customPath) {
7
+ const promptPath = customPath ?? DEFAULT_SYSTEM_PROMPT_PATH;
8
+ try {
9
+ const content = await readFile(promptPath, "utf8");
10
+ return content.trim();
11
+ }
12
+ catch (error) {
13
+ const code = error.code;
14
+ if (code === "ENOENT") {
15
+ throw new Error(`System prompt file not found: ${promptPath}`);
16
+ }
17
+ if (code === "EACCES") {
18
+ throw new Error(`Permission denied reading system prompt: ${promptPath}`);
19
+ }
20
+ throw new Error(`Failed to read system prompt file: ${promptPath}`);
21
+ }
22
+ }
@@ -0,0 +1,71 @@
1
+ # Role: Technical Decision & Analysis Agent
2
+
3
+ Research complex questions, compare approaches, and provide actionable recommendations. Optimized for:
4
+
5
+ - Architecture decisions and design patterns
6
+ - Library/framework selection and migration paths
7
+ - Performance optimization strategies
8
+ - Debugging complex issues across systems
9
+ - Best practices and trade-off analysis
10
+
11
+ # Instructions
12
+
13
+ - Start with a brief analysis plan (3-5 conceptual steps) to structure your research
14
+ - Search multiple sources to compare different approaches
15
+ - Analyze real-world usage patterns in popular repositories
16
+ - Weigh trade-offs based on the user's specific constraints
17
+ - Provide a decisive recommendation with clear justification
18
+
19
+ # Output Structure
20
+
21
+ - **Recommendation:** Your advised approach in 1-2 sentences
22
+ - **Why:** Key reasons with evidence from source code or benchmarks
23
+ - **Implementation:** Practical steps with working code example
24
+ - **Trade-offs:** What you gain vs what you sacrifice
25
+ - **Alternatives:** Other viable options if constraints change
26
+
27
+ # Authoritative Sources
28
+
29
+ ## Code as Truth - Priority Order
30
+
31
+ 1. **GitHub Repository Source Code**: Search actual implementation files first
32
+ - Find exact usage locations of parameters, methods, and configurations
33
+ - Look for test files showing real-world usage patterns
34
+ - Check example directories and demo code
35
+ - Trace through type definitions and interfaces
36
+ - Remember: Code is truth - implementation details override documentation
37
+
38
+ 2. **GitHub Repository Documentation**
39
+ - README files, CHANGELOG, release notes
40
+ - API documentation within repositories
41
+ - Configuration examples and setup guides
42
+
43
+ 3. **Official Documentation**
44
+ - TypeScript Handbook, Node.js docs, MDN, WHATWG, TC39
45
+ - npm registry entries (versions, files, types, exports)
46
+ - Library/framework official sites
47
+
48
+ 4. **Verification Resources**
49
+ - Stack Overflow: only to clarify rare edge cases and always verify against source code
50
+
51
+ ## Search Strategy
52
+
53
+ - When looking for how a specific parameter or API works, prioritize finding its actual usage in the source code over reading its description
54
+ - Documentation can be outdated, but code execution paths are always current
55
+ - Look for patterns: if multiple repositories use the same approach, it's likely correct
56
+
57
+ ## Curated JavaScript & TypeScript References
58
+
59
+ - [Total TypeScript articles](https://www.totaltypescript.com/articles)
60
+ - [2ality blog](https://2ality.com)
61
+ - [Exploring JS book](https://exploringjs.com/js/book/index.html)
62
+ - [Deep JavaScript book](https://exploringjs.com/deep-js/toc.html)
63
+ - [Node.js Shell Scripting](https://exploringjs.com/nodejs-shell-scripting/toc.html)
64
+
65
+ - Default to using modern ESM and TypeScript for examples when relevant.
66
+
67
+ # Guidance
68
+
69
+ - Use modern ESM and TypeScript for examples by default, but adapt language and examples as appropriate to the question.
70
+ - Be decisive in your conclusions, but transparent about any uncertainty.
71
+ - Present only your final conclusions and justification—avoid extraneous commentary or process narration.
@@ -0,0 +1,28 @@
1
+ import type { AskPerplexityResult, SearchContextSize } from "./ask-perplexity.js";
2
+ import { streamPerplexity } from "./ask-perplexity.js";
3
+ import { loadSystemPrompt } from "./load-system-prompt.js";
4
+ export type CliOptions = {
5
+ model: string;
6
+ json?: boolean;
7
+ system?: string;
8
+ systemText?: string;
9
+ context?: SearchContextSize;
10
+ showThinking?: boolean;
11
+ stream?: boolean;
12
+ };
13
+ export type CliDependencies = {
14
+ streamPerplexity: typeof streamPerplexity;
15
+ loadSystemPrompt: typeof loadSystemPrompt;
16
+ getApiKey: () => string | undefined;
17
+ output: (message: string) => void;
18
+ writeStream: (chunk: string) => void;
19
+ errorOutput: (message: string) => void;
20
+ exit: (code: number) => never;
21
+ };
22
+ type FormatOptions = {
23
+ json: boolean;
24
+ showThinking: boolean;
25
+ };
26
+ export declare function formatResult(result: AskPerplexityResult, options: FormatOptions): string;
27
+ export declare function runCli(prompt: string, options: CliOptions, deps?: CliDependencies): Promise<void>;
28
+ export {};
@@ -0,0 +1,63 @@
1
+ import { streamPerplexity } from "./ask-perplexity.js";
2
+ import { getPerplexityApiKey } from "./config.js";
3
+ import { loadSystemPrompt } from "./load-system-prompt.js";
4
+ import { collectStreamToResult, formatSources, handleStreamingOutput, } from "./stream-output.js";
5
+ import { stripThinkContent } from "./strip-think-content.js";
6
+ const defaultDependencies = {
7
+ streamPerplexity,
8
+ loadSystemPrompt,
9
+ getApiKey: getPerplexityApiKey,
10
+ output: (message) => {
11
+ console.log(message);
12
+ },
13
+ writeStream: (chunk) => {
14
+ process.stdout.write(chunk);
15
+ },
16
+ errorOutput: (message) => {
17
+ console.error(message);
18
+ },
19
+ // eslint-disable-next-line unicorn/no-process-exit
20
+ exit: (code) => process.exit(code),
21
+ };
22
+ export function formatResult(result, options) {
23
+ const text = options.showThinking
24
+ ? result.text
25
+ : stripThinkContent(result.text);
26
+ if (options.json) {
27
+ return JSON.stringify({
28
+ text,
29
+ sources: result.sources,
30
+ usage: result.usage,
31
+ providerMetadata: result.providerMetadata,
32
+ }, undefined, 2);
33
+ }
34
+ return text.trim() + formatSources(result.sources);
35
+ }
36
+ export async function runCli(prompt, options, deps = defaultDependencies) {
37
+ const apiKey = deps.getApiKey();
38
+ if (!apiKey) {
39
+ deps.errorOutput("Error: Perplexity API key is required\n" +
40
+ "Set it with: export PERPLEXITY_API_KEY='your-api-key'\n" +
41
+ "Or store it: askpplx config --set-api-key 'your-api-key'");
42
+ deps.exit(1);
43
+ }
44
+ const systemPrompt = options.systemText ?? (await deps.loadSystemPrompt(options.system));
45
+ const stream = deps.streamPerplexity({
46
+ apiKey,
47
+ model: options.model,
48
+ prompt,
49
+ system: systemPrompt,
50
+ searchContextSize: options.context,
51
+ });
52
+ const useStreaming = options.stream !== false && !options.json;
53
+ if (useStreaming) {
54
+ await handleStreamingOutput(stream, { showThinking: options.showThinking ?? false }, deps);
55
+ }
56
+ else {
57
+ const result = await collectStreamToResult(stream);
58
+ deps.output(formatResult(result, {
59
+ json: options.json ?? false,
60
+ showThinking: options.showThinking ?? false,
61
+ }));
62
+ }
63
+ }
@@ -0,0 +1,11 @@
1
+ import type { AskPerplexityResult, StreamPerplexityResult } from "./ask-perplexity.js";
2
+ type StreamDependencies = {
3
+ writeStream: (chunk: string) => void;
4
+ output: (message: string) => void;
5
+ };
6
+ export declare function formatSources(sources: AskPerplexityResult["sources"]): string;
7
+ export declare function handleStreamingOutput(stream: StreamPerplexityResult, options: {
8
+ showThinking: boolean;
9
+ }, deps: StreamDependencies): Promise<void>;
10
+ export declare function collectStreamToResult(stream: StreamPerplexityResult): Promise<AskPerplexityResult>;
11
+ export {};
@@ -0,0 +1,73 @@
1
+ export function formatSources(sources) {
2
+ if (sources.length === 0) {
3
+ return "";
4
+ }
5
+ const lines = [];
6
+ for (const [index, source] of sources.entries()) {
7
+ if (source.sourceType === "url") {
8
+ lines.push(`[${String(index + 1)}] ${source.url}`);
9
+ }
10
+ }
11
+ if (lines.length === 0) {
12
+ return "";
13
+ }
14
+ return `\n\nSources:\n${lines.join("\n")}`;
15
+ }
16
+ async function streamWithThinking(stream, deps) {
17
+ for await (const chunk of stream.textStream) {
18
+ deps.writeStream(chunk);
19
+ }
20
+ }
21
+ async function streamWithoutThinking(stream, deps) {
22
+ let buffer = "";
23
+ let insideThink = false;
24
+ let thinkEnded = false;
25
+ for await (const chunk of stream.textStream) {
26
+ if (thinkEnded) {
27
+ deps.writeStream(chunk);
28
+ continue;
29
+ }
30
+ buffer += chunk;
31
+ if (!insideThink && buffer.includes("<think>")) {
32
+ insideThink = true;
33
+ }
34
+ if (insideThink && buffer.includes("</think>")) {
35
+ thinkEnded = true;
36
+ const thinkEnd = buffer.lastIndexOf("</think>") + "</think>".length;
37
+ const afterThink = buffer.slice(thinkEnd);
38
+ if (afterThink) {
39
+ deps.writeStream(afterThink);
40
+ }
41
+ buffer = "";
42
+ }
43
+ else if (!insideThink) {
44
+ deps.writeStream(buffer);
45
+ buffer = "";
46
+ }
47
+ }
48
+ }
49
+ export async function handleStreamingOutput(stream, options, deps) {
50
+ await (options.showThinking
51
+ ? streamWithThinking(stream, deps)
52
+ : streamWithoutThinking(stream, deps));
53
+ const sources = await stream.sources;
54
+ const sourcesOutput = formatSources(sources);
55
+ if (sourcesOutput) {
56
+ deps.output(sourcesOutput);
57
+ }
58
+ else {
59
+ deps.writeStream("\n");
60
+ }
61
+ }
62
+ export async function collectStreamToResult(stream) {
63
+ let text = "";
64
+ for await (const chunk of stream.textStream) {
65
+ text += chunk;
66
+ }
67
+ const [sources, usage, providerMetadata] = await Promise.all([
68
+ stream.sources,
69
+ stream.usage,
70
+ stream.providerMetadata,
71
+ ]);
72
+ return { text, sources, usage, providerMetadata };
73
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Removes <think> blocks from the response content.
3
+ * The sonar-reasoning-pro model outputs reasoning tokens in <think> blocks
4
+ * that should be filtered out before returning to the client.
5
+ *
6
+ * Intentional behavior: We remove everything from the first "<think>" to the
7
+ * last "</think>", treating any nested or stray think tags as a single block.
8
+ * For example, the input "A <think>1</think> B <think>2</think> C" becomes
9
+ * "A C" (note that " B " is removed). This is by design: there should only be
10
+ * one reasoning block, and any accidental inner think tags must not interfere
11
+ * with the primary goal of stripping the reasoning block entirely.
12
+ *
13
+ * @param {string} input - The raw content from the API response
14
+ * @returns {string} The content with <think> blocks removed
15
+ */
16
+ export declare function stripThinkContent(input: string): string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Removes <think> blocks from the response content.
3
+ * The sonar-reasoning-pro model outputs reasoning tokens in <think> blocks
4
+ * that should be filtered out before returning to the client.
5
+ *
6
+ * Intentional behavior: We remove everything from the first "<think>" to the
7
+ * last "</think>", treating any nested or stray think tags as a single block.
8
+ * For example, the input "A <think>1</think> B <think>2</think> C" becomes
9
+ * "A C" (note that " B " is removed). This is by design: there should only be
10
+ * one reasoning block, and any accidental inner think tags must not interfere
11
+ * with the primary goal of stripping the reasoning block entirely.
12
+ *
13
+ * @param {string} input - The raw content from the API response
14
+ * @returns {string} The content with <think> blocks removed
15
+ */
16
+ export function stripThinkContent(input) {
17
+ const open = "<think>";
18
+ const close = "</think>";
19
+ const firstOpen = input.indexOf(open);
20
+ if (firstOpen === -1)
21
+ return input;
22
+ const lastClose = input.lastIndexOf(close);
23
+ if (lastClose === -1 || lastClose < firstOpen)
24
+ return input;
25
+ const end = lastClose + close.length;
26
+ return input.slice(0, firstOpen) + input.slice(end);
27
+ }
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "askpplx",
3
+ "author": "Łukasz Jerciński",
4
+ "license": "MIT",
5
+ "version": "1.0.0",
6
+ "description": "Minimal Unix-style CLI for querying Perplexity Sonar API.",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Jercik/askpplx.git"
10
+ },
11
+ "type": "module",
12
+ "bin": {
13
+ "askpplx": "bin/askpplx"
14
+ },
15
+ "files": [
16
+ "bin/",
17
+ "dist/",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "start": "pnpm -s run rebuild && node --env-file=.env bin/askpplx",
23
+ "build": "tsc -p tsconfig.app.json && cp -r src/prompts dist/",
24
+ "clean": "rm -rf dist *.tsbuildinfo",
25
+ "rebuild": "pnpm run clean && pnpm run build",
26
+ "prepare": "husky",
27
+ "prepublishOnly": "pnpm run rebuild",
28
+ "typecheck": "tsc -b --noEmit",
29
+ "format": "prettier --write .",
30
+ "format:check": "prettier --check .",
31
+ "lint": "eslint",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "test:coverage": "vitest run --coverage",
35
+ "knip": "knip",
36
+ "fta:check": "fta-check"
37
+ },
38
+ "keywords": [],
39
+ "packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b",
40
+ "engines": {
41
+ "node": ">=22.14.0"
42
+ },
43
+ "dependencies": {
44
+ "@ai-sdk/perplexity": "^2.0.21",
45
+ "ai": "^5.0.106",
46
+ "commander": "^14.0.2",
47
+ "conf": "^15.0.2",
48
+ "zod": "^4.1.13"
49
+ },
50
+ "devDependencies": {
51
+ "@commitlint/cli": "^20.1.0",
52
+ "@commitlint/config-conventional": "^20.0.0",
53
+ "@eslint/compat": "^1.4.1",
54
+ "@eslint/js": "^9.39.1",
55
+ "@total-typescript/ts-reset": "^0.6.1",
56
+ "@types/node": "^24.10.1",
57
+ "@vitest/coverage-v8": "^3.2.4",
58
+ "@vitest/eslint-plugin": "^1.4.4",
59
+ "eslint": "^9.39.1",
60
+ "eslint-config-prettier": "^10.1.8",
61
+ "eslint-plugin-unicorn": "^62.0.0",
62
+ "fta-check": "^1.2.0",
63
+ "fta-cli": "^3.0.0",
64
+ "globals": "^16.5.0",
65
+ "husky": "^9.1.7",
66
+ "knip": "^5.70.1",
67
+ "prettier": "3.6.2",
68
+ "semantic-release": "^25.0.2",
69
+ "typescript": "^5.9.3",
70
+ "typescript-eslint": "^8.47.0",
71
+ "vitest": "^3.2.4"
72
+ }
73
+ }