📄 Viewing rendered HTML — download raw .md

chat-howto

Chat toolkit — user’s guide

Generated 2026-06-09T10:16:30

This guide covers bin/chat.js (the CLI driver) and the OpenAI-interfacing
libraries in lib/. The CLI is a thin shell over the libs — anything you can
do from the command line you can do from Node by importing the lib function
directly.

Backends overview

bin/chat.js auto-discovers any lib/chatgpt*.js file (excluding
chatgpt_rest_sn.js, the ServiceNow-only port) and registers its first
exported function under the filename stem. Every backend exports the same
shape: chatgpt(prompt, opts) => { reply, messages }.

Backend OpenAI API Transport Tools (file_search) Multi-turn cache (CLI)
chatgpt Chat Completions openai SDK — —
chatgpt_rest_completions Chat Completions raw fetch() — —
chatgpt_rest_responses Responses raw fetch() yes yes

The raw-fetch() variants exist so the code can be ported to environments
without the openai SDK — most notably ServiceNow Script Includes. See
lib/chatgpt_rest_sn.js for the ServiceNow port, which covers both the
Completions and Responses APIs in one class; the ServiceNow usage
section below documents it.

Why pick one over another?

CLI: bin/chat.js

./bin/chat.js -a <backend> [opts] <<< 'prompt text'

The prompt is read from stdin so here-strings, here-docs, and pipes all work.
Reply goes to stdout; progress and errors go to stderr.

Flag reference

Flag Applies to Notes
-a <backend> all (required) Backend name; -h shows the live list.
-m, --model <id> all Overrides $OPENAI_MODEL and the backend default.
--system <text> all System prompt prepended to messages / input[].
-i, --instructions <text> responses only Top-level instructions field on the request.
--vector-store <id> responses only Adds to file_search vector_store_ids; repeatable.
-z, --zap responses only Discards the cached session before this call.
-h, --help — Live help listing registered backends and the cache path.

Env vars: OPENAI_API_KEY (required) and OPENAI_MODEL (optional override).

Examples

Basic single-turn calls, one per backend:

source .env

./bin/chat.js -a chatgpt                  <<< 'What is the capital of France?'
./bin/chat.js -a chatgpt_rest_completions <<< 'What is the capital of France?'
./bin/chat.js -a chatgpt_rest_responses   <<< 'What is the capital of France?'

Model + system prompt:

./bin/chat.js -a chatgpt_rest_responses --model gpt-4o --system 'Be terse.' \
  <<< 'Explain closures briefly.'

Responses-only top-level instructions (independent of --system; both can be
used together):

./bin/chat.js -a chatgpt_rest_responses --instructions 'Reply in all caps.' <<< 'Hi.'

Multi-turn conversations

The chatgpt_rest_responses backend persists conversation history between
invocations at ~/.cache/ai-support/responses-session.json. Each call appends
its prompt + reply, and the next call reloads the cached messages array and
passes it back to the Responses API. (The stateless input form is used; this
repo does not currently use previous_response_id chaining.)

./bin/chat.js -a chatgpt_rest_responses <<< 'My favorite Polish word is "dziękuję".'
./bin/chat.js -a chatgpt_rest_responses <<< 'What word did I tell you?'    # remembers

Pass -z to start a fresh conversation (the cache file is deleted before this
turn):

./bin/chat.js -a chatgpt_rest_responses -z <<< 'New topic: tell me about Polish nasal vowels.'

-z is only valid with chatgpt_rest_responses; other backends reject it.
The Completions backends do not carry state between CLI invocations — call the
lib function programmatically and pass messages yourself if you need
multi-turn there.

chatgpt_rest_responses can ground replies in a corpus you upload to OpenAI’s
File Search vector store, exposed in the Responses API as the file_search
tool. The end-to-end workflow is two scripts in this repo:

  1. bin/stageKnowledge.js uploads a file, creates (or reuses) a vector store,
    attaches the file, polls until indexing completes, and prints the vector
    store ID on stdout.
  2. ./bin/chat.js -a chatgpt_rest_responses --vector-store <id> references the
    store from chat calls.

Prerequisites

Step 1 — Stage the knowledge file

bin/stageKnowledge.js performs:

  1. POST /v1/files with purpose=assistants — uploads the raw file.
  2. POST /v1/vector_stores — creates the store (skipped when --reuse is
    given).
  3. POST /v1/vector_stores/{id}/files — attaches the file, carrying any
    --attr key=value metadata as the file’s attributes (used both on the
    new-store and --reuse paths).
  4. GET /v1/vector_stores/{id}/files/{file_id} polled until status is
    completed or failed.
  5. Vector store ID written to stdout; progress lines go to stderr so the ID
    can be captured cleanly.
VS_ID=$(./bin/stageKnowledge.js path/to/notes.md)
echo "$VS_ID"   # e.g. vs_6a1ce825e40c8191a52e8dbf44ee3270

Variants:

# Custom vector store name (default is the file stem):
./bin/stageKnowledge.js notes.md --name 'polish-notes-2026-05'

# Attach an additional file to an existing vector store:
./bin/stageKnowledge.js extra-notes.md --reuse "$VS_ID"

# Tag the file with metadata attributes (repeatable) for later file_search filters:
./bin/stageKnowledge.js template-rest.md --reuse "$VS_ID" \
  --attr kind=template --attr scenario=rest-integration

# Show full usage:
./bin/stageKnowledge.js -h

--attr values are coerced: true/false become booleans, numeric strings
become numbers, everything else stays a string — matching the value types the
file_search filters comparators expect. Attributes set here are what the
filters option on chatResponses (and the raw file_search tool) match
against at query time.

Step 2 — Reference from chat.js

./bin/chat.js -a chatgpt_rest_responses --vector-store "$VS_ID" \
  <<< 'Tell me my top 3 Polish pronunciation difficulties.'

Variants:

# Multiple knowledge bases — flag is repeatable:
... --vector-store "$VS_POLISH" --vector-store "$VS_GERMAN"

# Combine with system or instructions:
... --system 'Be terse.' --vector-store "$VS_ID"
... --instructions 'Cite source line numbers.' --vector-store "$VS_ID"

# Nudge the model to actually invoke file_search (it can choose not to):
... --instructions 'Use the file_search tool to ground your answer in my notes.' \
    --vector-store "$VS_ID"

# Override model:
... --model gpt-4o --vector-store "$VS_ID"

Multi-turn + File Search compose naturally — the session cache thread-throughs
prior turns while each new turn can still retrieve fresh chunks from the
vector store(s).

Metadata attributes and filtering

File Search can restrict retrieval to a subset of a vector store using per-file
metadata — a two-phase workflow:

  1. Tag at stage time — bin/stageKnowledge.js … --attr key=value
    (repeatable) records attributes on the file’s vector-store attachment.
  2. Filter at query time — pass a filters object on the file_search
    tool; only files whose attributes match are searched, and ranking then runs
    over that narrowed set.

A filter is a single comparison or a boolean combination of them:

// single comparison
{ type: 'eq', key: 'kind', value: 'template' }

// compound
{
  type: 'and',
  filters: [
    { type: 'eq', key: 'kind', value: 'template' },
    { type: 'eq', key: 'scenario', value: 'rest-integration' },
  ],
}

Comparators: eq, ne, gt, gte, lt, lte, combined with and / or.
Keys and value types must match what --attr wrote — which is why the flag
coerces true/false to boolean and numeric strings to number.

Where filters work: programmatically — the lib functions or the ServiceNow
ChatGPT.chatResponses filters option — not from the bin/chat.js CLI,
which has no --filter flag (it only forwards --vector-store). Reach for
filtering when you want deterministic scoping (“search only the templates”)
instead of trusting the model’s semantic query to stay in the right lane.

// ServiceNow: search only template-tagged files in a combined store
bot.chatResponses(request, {
    vectorStoreIds: [STORE_ALL],
    filters: { type: 'eq', key: 'kind', value: 'template' },
});

Cleanup / management

bin/stageKnowledge.js only creates artifacts; nothing deletes them. Vector
stores and their underlying files persist on your OpenAI account and continue
to incur storage costs until removed. Use the OpenAI platform dashboard at
https://platform.openai.com/storage, or the REST API directly:

# List vector stores
curl -sS -H "Authorization: Bearer $OPENAI_API_KEY" \
  https://api.openai.com/v1/vector_stores

# Inspect one
curl -sS -H "Authorization: Bearer $OPENAI_API_KEY" \
  "https://api.openai.com/v1/vector_stores/$VS_ID"

# Delete a vector store (detaches files; the underlying /v1/files records
# remain and must be deleted separately if no longer needed)
curl -sS -X DELETE -H "Authorization: Bearer $OPENAI_API_KEY" \
  "https://api.openai.com/v1/vector_stores/$VS_ID"

# List uploaded files
curl -sS -H "Authorization: Bearer $OPENAI_API_KEY" \
  https://api.openai.com/v1/files

# Delete an uploaded file
curl -sS -X DELETE -H "Authorization: Bearer $OPENAI_API_KEY" \
  "https://api.openai.com/v1/files/$FILE_ID"

File Search gotchas

Programmatic use

Each backend lib exports a single chatgpt(prompt, opts) => { reply, messages }
function. Import the one you want and call it directly:

import { chatgpt } from '../lib/chatgpt_rest_responses.js';

const { reply, messages } = await chatgpt('Hello.', {
    model: 'gpt-4o-mini',
    systemPrompt: 'Be terse.',
    instructions: 'Reply in all caps.',
    vectorStoreIds: [process.env.VS_ID],
    // messages: priorHistory,   // pass prior turn's messages to continue
});

The opts keys map cleanly onto CLI flags: model ↔︎ --model, systemPrompt
↔︎ --system, instructions ↔︎ --instructions, vectorStoreIds ↔︎ repeated
--vector-store. The session cache file used by the CLI is purely a CLI
concern — when calling the lib directly you manage messages yourself by
threading the returned array back into the next call.

Backend-by-backend opts cheat-sheet:

Backend Accepted opts
chatgpt model, systemPrompt, messages, apiKey
chatgpt_rest_completions model, systemPrompt, messages, apiKey
chatgpt_rest_responses + instructions, vectorStoreIds

All backends accept and return the same messages shape
({ role, content }[]), so they’re interchangeable for stateless multi-turn
use as long as you don’t depend on Responses-only fields.

ServiceNow usage

lib/chatgpt_rest_sn.js is the ServiceNow port: an es_latest scoped Script
Include exporting a ChatGPT class that talks to OpenAI through
sn_ws.RESTMessageV2 instead of fetch(). One class covers both APIs —
chatResponses() and chatCompletions() — so it’s the platform-side
equivalent of chatgpt_rest_responses and chatgpt_rest_completions combined.

It is not runnable in Node; run the examples below from a background script
(System Definition → Scripts - Background) in the application’s scope, or call
the class from any server-side script. From another scope, qualify the name —
new x_yourscope_yourapp.ChatGPT(...).

Prerequisites

Configuration vs. per-call parameters

Constructor options are stable config; the API-specific knobs are per-call
parameters, mirroring the CLI/lib split:

Constructor option Purpose
model Model ID (default gpt-4o-mini).
systemPrompt System message for new conversations.
apiKey Override the scoped openai.api_key property.
sessionId Resume a Responses conversation (see below).

chatResponses(prompt, { instructions, vectorStoreIds, maxNumResults, filters, rankingOptions }) returns { reply, sessionId }; chatCompletions(prompt, messages) returns { reply, messages }; clearSessionId() drops the
continuation token. The last three chatResponses options tune the
file_search tool — maxNumResults (chunks retrieved), filters (metadata
attribute filtering), and rankingOptions (e.g. { ranker, score_threshold })
— and are ignored when no vectorStoreIds are given.

listVectorStores(limit) and listFiles(limit) return trimmed inventory
arrays — the platform-side equivalent of the curl listing under
Cleanup, useful since a scoped script can’t shell out
to curl.

Multi-turn: server-side continuation

Unlike the CLI (which replays a cached messages array), the Script Include
continues a Responses conversation server-side via previous_response_id.
The returned sessionId is that response ID. Within one transaction, reuse the
same instance; the second turn sends only the new prompt and OpenAI supplies
the prior context:

const bot = new ChatGPT({ systemPrompt: 'You are a Polish tutor.' });
bot.chatResponses('My favorite Polish word is "dziękuję".');
const second = bot.chatResponses('What word did I tell you?');   // remembers
gs.info(second.reply);

To continue across transactions (e.g. a Virtual Agent topic or a series of
event-driven jobs), persist the returned sessionId — in a record field, user
preference, or system property — and hand it to a fresh instance later:

const first = new ChatGPT().chatResponses('Remember the number 42.');
// ... store first.sessionId somewhere durable ...

const resumed = new ChatGPT({ sessionId: first.sessionId });
gs.info(resumed.chatResponses('What number did I give you?').reply);

clearSessionId() (or simply a new instance with no sessionId) starts fresh.

Example calls

Single-turn, Responses API:

const bot = new ChatGPT();
gs.info(bot.chatResponses('What is the capital of France?').reply);

Model, system prompt, top-level instructions, and File Search over a staged
vector store (see Knowledge files for how to
create one — bin/stageKnowledge.js runs from your workstation, and the vector
store ID it prints is what you pass here):

const bot = new ChatGPT({ model: 'gpt-4o', systemPrompt: 'Be terse.' });
const res = bot.chatResponses('Summarize my notes.', {
    instructions: 'Use the file_search tool; cite source line numbers.',
    vectorStoreIds: ['vs_6a1ce825e40c8191a52e8dbf44ee3270'],
});
gs.info(res.reply);

Completions API (stateless — thread messages yourself to continue):

const bot = new ChatGPT();
const first = bot.chatCompletions('Explain JavaScript closures.');
const next = bot.chatCompletions('Now give a one-line example.', first.messages);
gs.info(next.reply);

Inventory vector stores and uploaded files (newest first; limit caps at 100):

const bot = new ChatGPT();
for (const vs of bot.listVectorStores())
    gs.info(`${vs.id}  ${vs.name}  files=${vs.fileCount}  ${vs.status}`);
for (const f of bot.listFiles())
    gs.info(`${f.id}  ${f.filename}  ${f.bytes}B  ${f.purpose}`);

Each store record carries { id, name, status, fileCount, bytes, createdAt }
and each file record { id, filename, bytes, purpose, status, createdAt }
(createdAt is OpenAI’s unix-epoch-seconds value).

ServiceNow notes

Worked example — rules-driven code generation

End-to-end walkthrough of a code generator grounded in a small policy corpus:
one conventions file, two rule files, and three template files, all in a single
vector store, tagged so retrieval can be scoped.

The corpus

File --attr tags
codingConventions.md kind=convention
templateRules-core.md kind=rule
templateRules-integration.md kind=rule
template-rest.md kind=template, scenario=rest
template-scheduledJob.md kind=template, scenario=scheduled-job
template-businessRule.md kind=template, scenario=business-rule

Step 1 — Stage all six into one store (workstation)

The first call creates the store and prints its ID; the rest attach with
--reuse. Progress goes to stderr, so the $(…) capture gets only the ID.

source .env

VS=$(./bin/stageKnowledge.js codingConventions.md --name codegen-kb --attr kind=convention)

./bin/stageKnowledge.js templateRules-core.md        --reuse "$VS" --attr kind=rule
./bin/stageKnowledge.js templateRules-integration.md --reuse "$VS" --attr kind=rule
./bin/stageKnowledge.js template-rest.md         --reuse "$VS" --attr kind=template --attr scenario=rest
./bin/stageKnowledge.js template-scheduledJob.md --reuse "$VS" --attr kind=template --attr scenario=scheduled-job
./bin/stageKnowledge.js template-businessRule.md --reuse "$VS" --attr kind=template --attr scenario=business-rule

echo "Vector store: $VS"   # -> vs_...; record this for the generator

Step 2 — The instructions (the manifest + procedure)

A thin filename/purpose manifest plus the procedure and precedence rules. This
is stable config — it names what each file is for, never restating the
content inside, so rule edits don’t force instruction edits.

ROLE
You generate ServiceNow scripts that strictly conform to the user's standards. A
file_search tool exposes their knowledge base; search it before writing any code.

KNOWLEDGE BASE (search by these filenames and the headings inside them)
  - codingConventions.md   — house style: naming, structure, error handling, logging.
                             Always applies.
  - templateRules-*.md     — rules that decide WHICH template to use and HOW to adapt
                             it. Each rule has an ID heading and may name the template
                             file(s) it governs.
  - template-*.md          — the actual templates. Each file is one template; its top
                             heading names the scenario it covers.

PROCEDURE (every request)
1. Search templateRules-*.md to determine the scenario and which template-*.md the
   rules direct you to. Rules are authoritative for this choice.
2. Retrieve that template-* file and treat its content as the verbatim starting point.
   If the rules name no matching template, say so and stop — do not improvise one.
3. Search codingConventions.md and apply every relevant convention.
4. Re-check every applicable rule and ensure compliance. Precedence on conflict:
   templateRules > codingConventions > the template's own defaults. Preserve the
   template's structure; adjust contents to comply and note each deviation in a comment.
5. Cite the source filename and heading for the template and each rule/convention applied
   (e.g. "templateRules-integration.md → R-014").
6. If a needed rule, convention, or template is absent from the knowledge base, state
   exactly what is missing rather than guessing.

OUTPUT
The final script only, unless asked to explain — plus the compliance/deviation comments.

Step 3 — Generate (ServiceNow, model picks the template)

Let the model choose the template via its own search. Bump maxNumResults so a
single turn can pull conventions + the relevant rules + a template together.

const STORE = 'vs_...';   // the codegen-kb ID from Step 1

const bot = new ChatGPT({ model: 'gpt-4o', systemPrompt: 'You generate ServiceNow scripts.' });
const { reply } = bot.chatResponses(
    'Write a scheduled job that purges x_myapp_log records older than 30 days.',
    { instructions: INSTRUCTIONS, vectorStoreIds: [STORE], maxNumResults: 20 },
);
gs.info(reply);

Step 3 (variant) — Pre-select the template with a filter

When your app already knows the scenario, scope retrieval determi