A useful chat CLI needs a small contract: accept a prompt, call the chat endpoint, and print the answer. In this first lap, you will build that tool with Node.js and its built-in fetch function. The result fits in one file and works in shell scripts without installing an SDK.
SwiftRelay provides an OpenAI-compatible API for several model providers, making it a practical starting point for this project. Billing is per token, so keep your first prompts short while checking the wiring.
1. Prepare Node.js and your API credentials
Use Node.js 20 or later. Run node --version to check your installation, then create a working directory with mkdir tiny-chat and enter it with cd tiny-chat.
Sign up for SwiftRelay to get an API key and a small gift credit for your first requests. In your account, find an available model and copy its exact API identifier. A model's display name may differ from the identifier accepted by the endpoint.
In a Bash or Zsh terminal, set export SWIFTRELAY_API_KEY='YOUR_API_KEY' and export SWIFTRELAY_MODEL='YOUR_MODEL_ID', replacing both placeholders. Keep credentials out of your source file and version control. These variables apply to commands launched from this terminal.
2. Point the CLI at chat completions
The API host is https://api.fastapirelay.com. This example calls the HTTP endpoint directly, so its complete request URL is https://api.fastapirelay.com/v1/chat/completions. If you later switch to an OpenAI-compatible SDK, configure its base URL with the /v1 suffix.
Each request needs a bearer token, a model identifier, and a messages array. Start with one user message. This keeps the CLI stateless: every invocation sends only the prompt you supply.
3. Save the complete CLI in chat.mjs
Create chat.mjs and paste the following JavaScript into it. The .mjs extension enables module syntax and top-level await. The code can stay on one line, although formatting it in your editor makes later changes easier.
const prompt = process.argv.slice(2).join(' ').trim(); const key = process.env.SWIFTRELAY_API_KEY; const model = process.env.SWIFTRELAY_MODEL; if (!prompt || !key || !model) { console.error('Usage: node chat.mjs "Your prompt"; set SWIFTRELAY_API_KEY and SWIFTRELAY_MODEL.'); process.exit(1); } try { const response = await fetch('https://api.fastapirelay.com/v1/chat/completions', { method: 'POST', headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model, messages: [{ role: 'user', content: prompt }], stream: false }), signal: AbortSignal.timeout(60000) }); const raw = await response.text(); let data; try { data = JSON.parse(raw); } catch { throw new Error(`HTTP ${response.status}: unexpected response format`); } if (!response.ok) throw new Error(`HTTP ${response.status}: ${data.error?.message || 'Request failed'}`); const answer = data.choices?.[0]?.message?.content; if (typeof answer !== 'string' || !answer.trim()) throw new Error('The response contained no text answer.'); process.stdout.write(answer + '\n'); } catch (error) { console.error(`Chat failed: ${error.message}`); process.exitCode = 1; }
4. Run a prompt and check shell behavior
Run node chat.mjs "Explain environment variables in three sentences.". Quoting the prompt keeps shell characters from being interpreted before Node.js receives them. A successful request prints the returned text followed by a newline.
Next, run node chat.mjs without a prompt. It should print usage guidance and exit unsuccessfully. This check makes sure missing input fails before making a billable API request.
Answers go to standard output; diagnostics go to standard error. That separation lets you save a response with node chat.mjs "Draft a concise release note." > answer.txt while keeping errors visible in your terminal.
5. Diagnose failures before adding features
If authentication fails, check the key in your current terminal. If the API rejects the model, confirm its exact identifier and availability. A timeout means the request did not finish within the configured deadline; it does not establish whether the server processed it.
Keep retries manual initially, since another request may incur another charge. Once basic requests work, add standard-input support or conversation history as separate changes. History requires resending earlier messages, which increases the tokens included in subsequent requests.