Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.perplexity.ai/llms.txt

Use this file to discover all available pages before exploring further.

TypeScript Agent CLI

A TypeScript-first interactive command-line interface that connects to the Perplexity Agent API with streaming responses, runtime model selection, and integrated web search.

Features

  • Interactive REPL with streaming token output
  • Runtime model selection from a curated list
  • Web search integration via the web_search tool
  • TypeScript-specific patterns: type narrowing, const assertions, typed error classes
  • Conversation history for multi-turn interactions
  • Graceful error handling and clean shutdown

Installation

mkdir typescript-agent-cli && cd typescript-agent-cli
npm init -y
npm install @perplexity-ai/perplexity_ai
npm install -D typescript @types/node tsx
export PERPLEXITY_API_KEY="your_api_key_here"

Usage

npx tsx src/cli.ts
The CLI prompts you to select a model, then enters an interactive loop:
Available models:
  1. OpenAI GPT-5.1 (openai/gpt-5.4)
  2. Google Gemini 3 Flash (google/gemini-3.1-flash-lite)

Select a model (1-2): 1

> What is the current state of quantum computing?
Commands: /search (enable web search), /nosearch (disable), /clear (reset history), /quit (exit).

Full Code

Save as src/cli.ts:
import Perplexity from "@perplexity-ai/perplexity_ai";
import * as readline from "readline";

// --- Configuration ---

const AVAILABLE_MODELS = [
  { name: "openai/gpt-5.4", label: "OpenAI GPT-5.1" },
  { name: "google/gemini-3.1-flash-lite", label: "Google Gemini 3 Flash" },
] as const;

type ModelName = (typeof AVAILABLE_MODELS)[number]["name"];

interface Message {
  role: "user" | "assistant";
  content: string;
}

// --- Helpers ---

function createRL(): readline.Interface {
  return readline.createInterface({ input: process.stdin, output: process.stdout });
}

function ask(rl: readline.Interface, prompt: string): Promise<string> {
  return new Promise((resolve) => rl.question(prompt, (a) => resolve(a.trim())));
}

// --- Model selection ---

async function selectModel(rl: readline.Interface): Promise<ModelName> {
  console.log("\nAvailable models:");
  AVAILABLE_MODELS.forEach((m, i) =>
    console.log(`  ${i + 1}. ${m.label} (${m.name})`)
  );
  while (true) {
    const idx = parseInt(await ask(rl, `\nSelect a model (1-${AVAILABLE_MODELS.length}): `), 10) - 1;
    if (idx >= 0 && idx < AVAILABLE_MODELS.length) {
      console.log(`Using model: ${AVAILABLE_MODELS[idx].name}\n`);
      return AVAILABLE_MODELS[idx].name;
    }
    console.log("Invalid selection.");
  }
}

// --- Streaming query ---

async function streamQuery(
  client: Perplexity,
  model: ModelName,
  history: Message[],
  userMessage: string,
  useWebSearch: boolean
): Promise<string> {
  const input = [
    ...history.map((m) => ({ role: m.role as "user" | "assistant", content: m.content })),
    { role: "user" as const, content: userMessage },
  ];

  const tools: Array<{ type: "web_search" }> = useWebSearch
    ? [{ type: "web_search" as const }]
    : [];

  const stream = await client.responses.create({
    model,
    input,
    tools,
    stream: true,
    instructions: "You are a helpful assistant. Use web search when available for current events. Be concise.",
    max_output_tokens: 2048,
  });

  let fullResponse = "";
  for await (const chunk of stream) {
    if (chunk.type === "response.output_text.delta") {
      const delta = (chunk as any).delta as string;
      process.stdout.write(delta);
      fullResponse += delta;
    }
    if (chunk.type === "response.output_item.added") {
      const item = (chunk as any).item;
      if (item?.type === "search_results") {
        process.stdout.write("\n[Searching the web...]\n");
      }
    }
    if (chunk.type === "response.completed") {
      const usage = (chunk as any).response?.usage;
      if (usage) {
        process.stdout.write(`\n\n[Tokens: ${usage.input_tokens} in / ${usage.output_tokens} out]`);
      }
    }
  }
  process.stdout.write("\n");
  return fullResponse;
}

// --- Command handling ---

function handleCommand(cmd: string, state: { webSearch: boolean; history: Message[] }): boolean {
  switch (cmd.toLowerCase()) {
    case "/quit": case "/exit":
      console.log("Goodbye."); return true;
    case "/search":
      state.webSearch = true; console.log("Web search enabled."); return false;
    case "/nosearch":
      state.webSearch = false; console.log("Web search disabled."); return false;
    case "/clear":
      state.history = []; console.log("History cleared."); return false;
    case "/help":
      console.log("\n  /search  /nosearch  /clear  /quit  /help\n"); return false;
    default:
      console.log(`Unknown command: ${cmd}. Type /help.`); return false;
  }
}

// --- Main ---

async function main(): Promise<void> {
  const client = new Perplexity();
  const rl = createRL();
  const model = await selectModel(rl);
  const state = { webSearch: true, history: [] as Message[] };

  console.log("Type a message to chat, or /help for commands. Web search is ON.\n");
  process.on("SIGINT", () => { console.log("\nGoodbye."); rl.close(); process.exit(0); });

  while (true) {
    const input = await ask(rl, "> ");
    if (!input) continue;
    if (input.startsWith("/")) { if (handleCommand(input, state)) break; continue; }

    try {
      const response = await streamQuery(client, model, state.history, input, state.webSearch);
      state.history.push({ role: "user", content: input });
      state.history.push({ role: "assistant", content: response });
      if (state.history.length > 20) state.history = state.history.slice(-20);
    } catch (error: unknown) {
      if (error instanceof Perplexity.APIConnectionError) {
        console.error("\nConnection error. Check your network.");
      } else if (error instanceof Perplexity.RateLimitError) {
        console.error("\nRate limit exceeded. Wait and retry.");
      } else if (error instanceof Perplexity.APIStatusError) {
        console.error(`\nAPI error: ${(error as any).message}`);
      } else {
        console.error("\nUnexpected error:", error);
      }
    }
    console.log();
  }
  rl.close();
}

main();

Example Session

Available models:
  1. OpenAI GPT-5.1 (openai/gpt-5.4)
  2. Google Gemini 3 Flash (google/gemini-3.1-flash-lite)

Select a model (1-2): 1
Using model: openai/gpt-5.4

Type a message to chat, or /help for commands. Web search is ON.

> What were the major AI announcements this week?
[Searching the web...]
This week saw several notable AI developments:
1. Anthropic released Claude 4 with improved reasoning...
2. Google DeepMind published new protein folding results...
3. OpenAI announced enterprise partnerships for GPT-5...

[Tokens: 1420 in / 287 out]

> /nosearch
Web search disabled.

> Explain transformers in simple terms
A transformer is a neural network architecture that processes all
parts of an input simultaneously rather than sequentially...

[Tokens: 2580 in / 195 out]

> /quit
Goodbye.

Key TypeScript Patterns

Const assertions for tool types

Use as const to narrow tool type literals:
const tools = [{ type: "web_search" as const }];

Streaming event type narrowing

Check chunk.type before accessing event-specific fields:
for await (const chunk of stream) {
  if (chunk.type === "response.output_text.delta") {
    process.stdout.write((chunk as any).delta);
  }
}

Typed error handling

try {
  // API call
} catch (error) {
  if (error instanceof Perplexity.APIConnectionError) {
    // Handle network issues
  } else if (error instanceof Perplexity.RateLimitError) {
    // Handle rate limits
  }
}
Conversation history is preserved across turns, so the model can reference earlier messages. Use /clear to start a fresh conversation without restarting the CLI.

Limitations

  • The CLI uses Node.js readline, which does not support arrow-key history navigation. For a richer experience, consider inquirer or prompts.
  • Conversation history is in-memory only and lost when the process exits.
  • Streaming events may vary by model provider. The response.output_text.delta event is consistent across all models.