ChatGML is a TypeScript agentic coding assistant for your codebase. It navigates and edits
your project through tools (glob, grep, read, semantic search, graph/temporal recall) over any
OpenAI-compatible endpoint — a local llama-server, an OpenAI-style gateway like
pylon, or the OpenAI API itself. It is GameMaker-first (understands
GML files, objects, and events, and ships a GMEdit plugin) but works on any
repository.
Chat and embeddings are configured as separate lanes (independent baseURL / apiKey /
model), so you can embed locally and chat against a bigger model, or vice versa. Retrieval/memory
is pluggable: a local on-disk vector store by default, or a read-hybrid hippo
backend.
This is a ground-up TypeScript ESM rewrite of the old Python talk-codebase tool. It is a different
program: no Python, no pip, no pickle, no LangChain.
Shortest path from clone to a working chat (assumes a local OpenAI-compatible server on
http://localhost:8080/v1):
git clone https://github.com/jolionlands/ChatGML.git
cd ChatGML
npm ci
npm run build
npm link # puts `chatgml` on PATH (or use `node dist/cli.js …`)
# Point both lanes at your local server and name a chat + embed model and a scope.
export CHATGML_CHAT_BASE_URL=http://localhost:8080/v1
export CHATGML_CHAT_MODEL=qwen2.5-coder
export CHATGML_EMBED_BASE_URL=http://localhost:8080/v1
export CHATGML_EMBED_MODEL=nomic-embed-text
export CHATGML_SCOPE=myproject
# Index the repo, then chat.
chatgml index .
chatgml chat .chat opens an interactive REPL (chatgml> ). Edits are approval-gated: the agent shows a diff
and asks before writing.
See docs/usage.md for the full command/flag/env/config reference.
Requirements:
- Node.js >= 24 (uses the stable global
fetch/streaming; nonode-fetch/undici). - npm (for the build).
- Access to an OpenAI-compatible chat endpoint and an embeddings endpoint.
git clone https://github.com/jolionlands/ChatGML.git
cd ChatGML
npm ci
npm run build # compiles TS -> dist/ (dist/cli.js is the `chatgml` bin)Then either npm link to put chatgml on your PATH, or invoke it directly as
node dist/cli.js <command>.
ChatGML resolves config from four layers, highest precedence first:
- CLI flags (
--chat-base-url,--chat-model, …) CHATGML_*environment variables- config files — the per-project
<root>/.chatgml.json(UNTRUSTED) then the user-global~/.config/chatgml/config.json(trusted) - built-in defaults
Chat and embeddings are configured separately. The embed lane falls back to the chat lane's
baseURL/apiKey, but never the model — embed.model is always required.
Required fields (or ChatGML exits with a config error, code 3):
chat.baseURL,chat.modelembed.modelscope(a label for this codebase's memory, e.g.myproject; supports arepo::subform)memory.hippo.url(only whenmemory.providerishippo)
| Variable | Maps to |
|---|---|
CHATGML_CHAT_BASE_URL |
chat.baseURL |
CHATGML_CHAT_API_KEY |
chat.apiKey |
CHATGML_CHAT_MODEL |
chat.model |
CHATGML_EMBED_BASE_URL |
embed.baseURL |
CHATGML_EMBED_API_KEY |
embed.apiKey |
CHATGML_EMBED_MODEL |
embed.model |
CHATGML_SCOPE |
scope |
CHATGML_APPROVAL |
approval (gated | auto) |
The user-global file ~/.config/chatgml/config.json (honoring XDG_CONFIG_HOME) is the trusted,
durable place for settings. Example:
{
"chat": { "baseURL": "http://localhost:8080/v1", "model": "qwen2.5-coder", "apiKey": "env:OPENAI_API_KEY" },
"embed": { "model": "nomic-embed-text" },
"memory": { "provider": "local" },
"scope": "myproject",
"approval": "gated"
}Set fields durably with chatgml config set <field> <value> (writes the user-global file only).
Secret fields (chat.apiKey, embed.apiKey, memory.hippo.key) must be written as an
env:NAME reference, never a literal key. ChatGML refuses to persist a literal secret to disk:
chatgml config set chat.apiKey env:OPENAI_API_KEY # OK — resolves from $OPENAI_API_KEY at runtime
chatgml config set chat.apiKey sk-abc123 # REFUSED (exit 2)Keys are never logged and are redacted (***) in chatgml config show.
chatgml <index|chat|serve|config> [dir] [options]
[dir] defaults to . (the current directory). Global options apply to every subcommand and, when
combined with positional subcommands, must precede the subcommand.
| Command | What it does |
|---|---|
chatgml index [dir] |
Build or incrementally update the local index. |
chatgml chat [dir] |
Start an interactive chat REPL. |
chatgml serve [dir] |
Expose the agent over NDJSON-on-stdio (for editors/automation). |
chatgml config show [dir] |
Print the resolved config (secrets redacted) + the files searched. |
chatgml config set <field> <value> |
Persist one field to the user-global config (refuses literal secrets). |
Global options:
--chat-base-url <url> --embed-base-url <url>
--chat-api-key <key> --embed-api-key <key>
--chat-model <model> --embed-model <model>
--scope <scope> --approval <gated|auto>
--no-color --trust-project-config
Examples:
# Index, pointing the chat + embed lanes at a local server via flags.
chatgml --chat-base-url http://localhost:8080/v1 --chat-model qwen2.5-coder \
--embed-base-url http://localhost:8080/v1 --embed-model nomic-embed-text \
--scope myproject index .
# Chat in auto-approve mode (skips the per-edit prompt).
chatgml --approval auto chat .
# Serve the agent for an editor.
chatgml serve /path/to/gamemaker/projectMore detail and copy-pasteable recipes live in docs/usage.md.
ChatGML ships a GMEdit plugin (in plugin/) that spawns the core as a child process and renders the
streaming agent events in a side panel — chat, tool activity, and approve/reject for gated edits.
See docs/gmedit-plugin.md for install and architecture.
Retrieval is pluggable via memory.provider:
local(default) — an on-disk JSON vector store built bychatgml index. No external service; your code never leaves your machine (beyond the embeddings/chat endpoints you point at). Citations carry file paths with line ranges.hippo— a read-hybrid adapter over a running hippo memory service (memory.hippo.url, optionalmemory.hippo.keyas anenv:NAMEref). Used for retrieval/recall; graph/memory hits may carry a snippet + score without a file path.
- Approval-gated, sandboxed edits. The default
approvalmode isgated: the agent emits a unified diff and an approval request, and nothing is written until you approve. All file writes are confined to the project root through a single sandbox chokepoint (lexical + realpath/symlink checks).approval: 'auto'exists but can never be sourced from the untrusted project config. - Untrusted project config. A per-project
.chatgml.jsonmay not override a secret-bearing endpoint (chat.baseURL/embed.baseURL/memory.hippo.url) while the matching key resolves from anenv:reference, unless you pass--trust-project-config. This blocks key-exfiltration / SSRF via a checked-in config. - No pickle / no eval. Persistence is JSON + base64
Float32Array; config is zod-validated; tool args go throughsafeParse. Nothing is deserialized into code. - Keys are never logged and are redacted everywhere they would otherwise print.
v1 — milestones M1–M6 complete (461 tests green; npm run ci green):
- M1–M3 — foundation, config (deep-merge + secret resolution + untrusted-config hardening),
index + local memory, OpenAI-compatible streaming LLM client, read-only tools, the NDJSON
protocol,
serve, and the CLI/REPL. - M4 — real edit engine: unified-diff apply + the approval round-trip, realpath-validated sandboxed writes.
- M5 — hippo READ adapter (retrieval/recall hybrid).
- M6 — the GMEdit plugin (NDJSON-over-stdio).
The GMEdit plugin's visual UX (DOM/side panel/diff view inside a real GMEdit/Electron install) cannot run in CI and needs manual verification; all protocol/framing/transport logic is covered by headless tests.
See DEVELOPMENT.md. Common scripts:
npm run ci # typecheck + lint + build + coverage
npm run typecheck
npm test # vitest
npm run buildSee the repository for license details.