@moikapy/lich 0.3.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/CHANGELOG.md +24 -0
- package/README.md +186 -0
- package/dist/chunk-P52U5M3L.js +3431 -0
- package/dist/chunk-P52U5M3L.js.map +1 -0
- package/dist/chunk-ZVK3MUPC.js +7 -0
- package/dist/chunk-ZVK3MUPC.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +409 -0
- package/dist/cli.js.map +1 -0
- package/dist/gateway-CWPVIU3W.js +752 -0
- package/dist/gateway-CWPVIU3W.js.map +1 -0
- package/dist/index.d.ts +542 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/tui-V7ATLIKW.js +430 -0
- package/dist/tui-V7ATLIKW.js.map +1 -0
- package/docs/.vitepress/config.mts +55 -0
- package/docs/architecture/agent-loop.md +234 -0
- package/docs/architecture/extending.md +284 -0
- package/docs/architecture/overview.md +188 -0
- package/docs/architecture/plugins.md +91 -0
- package/docs/architecture/providers.md +273 -0
- package/docs/architecture/tools.md +180 -0
- package/docs/design/council/architecture-review.md +47 -0
- package/docs/design/council/security-review.md +39 -0
- package/docs/design/council/simplicity-review.md +45 -0
- package/docs/design/self-improvement-loop.md +166 -0
- package/docs/getting-started.md +133 -0
- package/docs/index.md +68 -0
- package/docs/user-guide/cli.md +182 -0
- package/docs/user-guide/gateway.md +168 -0
- package/docs/user-guide/library.md +181 -0
- package/docs/user-guide/plugins.md +120 -0
- package/docs/user-guide/tui.md +76 -0
- package/package.json +54 -0
package/docs/index.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
outline: [2, 3]
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Lich documentation
|
|
6
|
+
|
|
7
|
+
> What you'll learn: what Lich is, what it ships, and which page to read next — plus a 60-second quickstart.
|
|
8
|
+
|
|
9
|
+
Lich is a TypeScript AI agent harness: a library and a CLI that run a chat model inside a Think-Act-Observe loop. A chat wrapper forwards one prompt and prints one completion. A harness keeps going: the model plans (think), calls tools such as `read_file` or `terminal` (act), reads the tool results (observe), and repeats until it can produce a final answer. Lich wraps that loop with the machinery real deployments need: provider failover with bounded retries, path confinement and output clamps on every tool, context compression when the transcript grows past a token budget, and append-only JSONL session transcripts.
|
|
10
|
+
|
|
11
|
+
One package, four ways to drive the same agent: a one-shot CLI, an interactive chat REPL, an ink-based terminal UI, and a long-running messaging gateway that bridges Telegram, Discord, Twitch, and a zero-config HTTP webhook. All four share the same twelve builtin tools, the same provider configuration, and the same session store.
|
|
12
|
+
|
|
13
|
+
## Feature overview
|
|
14
|
+
|
|
15
|
+
| Capability | What it gives you |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| Providers | `openai_compat`, `anthropic`, and `ollama` with automatic failover between configured providers; 429/5xx and network errors retry with backoff before failing over. |
|
|
18
|
+
| Tools | Twelve builtins (file read/write/edit, directory listing, shell, grep, HTTP fetch/request, web search, process list, disk usage, env inspection), all confined to the working directory. |
|
|
19
|
+
| Context compression | Transcript summarized in place when estimated tokens cross `compress_threshold` of `context_budget_tokens`; the 8 most recent turns always stay verbatim. |
|
|
20
|
+
| Sessions | Every run persists a `.jsonl` transcript under `.lich/sessions/`, labeled by origin (`tui`, `gw:<platform>:<chat>`). |
|
|
21
|
+
| CLI | One-shot tasks, chat REPL, TUI, gateway, and a `config` template command, all with flag/env/config-file configuration. |
|
|
22
|
+
| TUI | Live ink transcript with tool-call rows, status bar (model, turns, tokens, session path), and slash commands. |
|
|
23
|
+
| Gateway | One shared agent behind webhook/Telegram/Discord/Twitch with per-conversation memory (40-message history cap) and per-platform message splitting. |
|
|
24
|
+
| Library | `create_agent` / `run_agent` with typed events (`AgentEmitter`), multi-turn history, and `ProviderError` kinds for error handling. |
|
|
25
|
+
| Plugins | User-supplied tools and lifecycle hooks (`before_tool_call` veto, run lifecycle) loaded at startup from explicit module paths. |
|
|
26
|
+
|
|
27
|
+
## Page map
|
|
28
|
+
|
|
29
|
+
| Page | Read it to |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| [Getting started](getting-started.md) | Install, configure a provider, and get your first reply in any mode. |
|
|
32
|
+
| [CLI reference](user-guide/cli.md) | Master all four modes, flags, provider resolution, and config files. |
|
|
33
|
+
| [TUI guide](user-guide/tui.md) | Run the terminal UI and use slash commands and the status bar. |
|
|
34
|
+
| [Gateway guide](user-guide/gateway.md) | Wire Telegram, Discord, Twitch, and the HTTP webhook to one agent. |
|
|
35
|
+
| [Library guide](user-guide/library.md) | Embed the agent in TypeScript with events and multi-turn history. |
|
|
36
|
+
| [Plugins guide](user-guide/plugins.md) | Add your own tools and lifecycle hooks to the agent. |
|
|
37
|
+
| [Architecture overview](architecture/overview.md) | Understand how the harness works inside. |
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
For the internals — the agent loop, provider failover, tool guardrails, and how to extend each layer — read the architecture track: [overview](architecture/overview.md), [agent loop](architecture/agent-loop.md), [providers](architecture/providers.md), [tools](architecture/tools.md), [plugins](architecture/plugins.md), and [extending](architecture/extending.md).
|
|
42
|
+
|
|
43
|
+
## 60-second quickstart
|
|
44
|
+
|
|
45
|
+
Requires Node >= 20 (or Bun) and access to one model endpoint (local Ollama, OpenAI, Anthropic, or any OpenAI-compatible API such as OpenRouter).
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
# from a clone of the repository
|
|
49
|
+
bun install
|
|
50
|
+
|
|
51
|
+
# generate a starter config, then edit the model name
|
|
52
|
+
mkdir -p .lich && bun src/cli.ts config > .lich/config.json
|
|
53
|
+
|
|
54
|
+
# chat TUI (exit with /exit or Ctrl+C)
|
|
55
|
+
bun src/cli.ts tui
|
|
56
|
+
|
|
57
|
+
# or a one-shot task
|
|
58
|
+
bun src/cli.ts "list the files in this repo and summarize it"
|
|
59
|
+
|
|
60
|
+
# or a messaging gateway on http://localhost:8089
|
|
61
|
+
bun src/cli.ts gateway webhook
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
After `npm install`-ing the built package the same commands work as `lich tui`, `lich "task"`, and `lich gateway webhook`.
|
|
65
|
+
|
|
66
|
+
## Version compatibility
|
|
67
|
+
|
|
68
|
+
Documented for **v0.3.0**. Requires Node >= 20 (`engines` in `package.json`); Bun is the recommended runtime for development (`bun src/cli.ts ...`) and Node 20+ works for the built `dist/cli.js`. The TUI needs a TTY; the gateway and library run headless on both runtimes.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# CLI reference
|
|
2
|
+
|
|
3
|
+
> What you'll learn: every CLI mode, flag, and default; how provider/model resolution works; the config file schema; session files, exit codes, and log levels; and practical recipes.
|
|
4
|
+
|
|
5
|
+
## The four modes
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
lich "one shot task" # run a single task and print the reply
|
|
9
|
+
lich chat # interactive chat (commands: /exit, /quit)
|
|
10
|
+
lich tui # interactive terminal UI (ink)
|
|
11
|
+
lich gateway <plat..> # messaging gateway (webhook|telegram|discord|twitch)
|
|
12
|
+
lich config # print a starter config template
|
|
13
|
+
lich --help # usage text
|
|
14
|
+
lich --version # print 0.2.0
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- **One-shot** joins all positional words into a single task, runs the agent loop, prints the final answer to stdout, and exits. Progress (turn numbers, tool results) goes to stderr.
|
|
18
|
+
- **Chat** is a readline REPL over one long-lived agent: each line is a turn, memory persists across lines, and an empty line, `/exit`, or `/quit` ends the session. After each turn it prints a `[turns N | tokens M]` footer.
|
|
19
|
+
- **TUI** launches the ink interface. See the [TUI guide](tui.md).
|
|
20
|
+
- **Gateway** runs platform adapters (defaults to `webhook` when no platform is given). See the [Gateway guide](gateway.md). Unknown platform names are skipped with a warning; if none remain, the CLI exits `1`.
|
|
21
|
+
|
|
22
|
+
`bun src/cli.ts` and the installed `lich` binary accept identical arguments.
|
|
23
|
+
|
|
24
|
+
## Flags
|
|
25
|
+
|
|
26
|
+
Flags work before or after the subcommand. Every value flag can also be set via an environment variable or the config file (precedence below).
|
|
27
|
+
|
|
28
|
+
| Flag | Meaning | Default |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `--config <path>` | JSON config file (must exist). Disables config discovery. | discovery chain |
|
|
31
|
+
| `--work-dir <path>` | Working directory for tools and config/session resolution. | process cwd |
|
|
32
|
+
| `--max-turns <n>` | Turn budget for the agent loop. | `25` |
|
|
33
|
+
| `--model <m>` | Model name for `providers[0]`. | `LICH_MODEL` |
|
|
34
|
+
| `--provider-kind <k>` | `openai_compat` \| `anthropic` \| `ollama`. | `LICH_PROVIDER_KIND`, else `openai_compat` |
|
|
35
|
+
| `--base-url <u>` | Provider base URL for `providers[0]`. | `LICH_BASE_URL`, else per-kind default |
|
|
36
|
+
| `--api-key-env <NAME>` | Env var holding the api key for `providers[0]`. | `LICH_API_KEY_ENV`, else per-kind default |
|
|
37
|
+
| `--system-prompt <s>` | Replaces the default system prompt. | built-in concise-assistant prompt |
|
|
38
|
+
| `--session-dir <path>` | Transcript directory. | `<work_dir>/.lich/sessions` |
|
|
39
|
+
| `--log-level <level>` | `debug` \| `info` \| `warn` \| `error`. | `info` |
|
|
40
|
+
|
|
41
|
+
Passing `--max-turns 0` or a non-integer fails with `--max-turns must be a positive integer`. Unknown flags fail with `unknown flag: --foo`. A flag missing its value fails with `<flag> requires a value`.
|
|
42
|
+
|
|
43
|
+
## Provider resolution
|
|
44
|
+
|
|
45
|
+
The effective provider for a run is decided in this order:
|
|
46
|
+
|
|
47
|
+
1. If `--config <path>` was passed, that file is the whole configuration (it must exist, or the CLI fails with `config not found`).
|
|
48
|
+
2. Otherwise the discovery chain is walked: `./.lich/config.json`, then `~/.config/lich/config.json`. The first file found becomes the config. `LICH_*` env vars are *not* merged into a discovered file.
|
|
49
|
+
3. If no config file exists, one is built from the environment: `--provider-kind` / `LICH_PROVIDER_KIND` (default `openai_compat`), `--model` / `LICH_MODEL` (required — without it the CLI fails with `no model configured`), `--base-url` / `LICH_BASE_URL`, and `--api-key-env` / `LICH_API_KEY_ENV`, each falling back to the per-kind defaults below.
|
|
50
|
+
4. Provider override flags (`--model`, `--provider-kind`, `--base-url`, `--api-key-env`) always win over the chosen source: with a config file present they patch `providers[0]` in place; without one they seed a fresh provider from the environment.
|
|
51
|
+
|
|
52
|
+
Per-kind defaults:
|
|
53
|
+
|
|
54
|
+
| Kind | Default base URL | Default key env | Notes |
|
|
55
|
+
| --- | --- | --- | --- |
|
|
56
|
+
| `openai_compat` | `https://api.openai.com/v1` | `OPENAI_API_KEY` | Works with any OpenAI-shaped `/chat/completions` API. |
|
|
57
|
+
| `anthropic` | `https://api.anthropic.com` | `ANTHROPIC_API_KEY` | |
|
|
58
|
+
| `ollama` | `http://localhost:11434` | none | No key needed; `api_key`/`api_key_env` are sent as a Bearer header for cloud proxies when set. |
|
|
59
|
+
|
|
60
|
+
## Config file reference
|
|
61
|
+
|
|
62
|
+
Validated by zod (top-level unknown keys are silently stripped; extra keys inside a `providers[]` entry are passed through). Full example with every field:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"providers": [
|
|
67
|
+
{
|
|
68
|
+
"kind": "openai_compat",
|
|
69
|
+
"name": "openrouter",
|
|
70
|
+
"model": "anthropic/claude-sonnet-4",
|
|
71
|
+
"base_url": "https://openrouter.ai/api/v1",
|
|
72
|
+
"api_key_env": "OPENROUTER_API_KEY"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"kind": "ollama",
|
|
76
|
+
"name": "local",
|
|
77
|
+
"model": "llama3.2:latest",
|
|
78
|
+
"base_url": "http://localhost:11434",
|
|
79
|
+
"keep_alive": "10m"
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"system_prompt": "You are a careful code reviewer.",
|
|
83
|
+
"max_turns": 40,
|
|
84
|
+
"work_dir": "/home/me/project",
|
|
85
|
+
"tools_enabled": ["read_file", "grep_files", "terminal"],
|
|
86
|
+
"temperature": 0.2,
|
|
87
|
+
"max_tokens": 4096,
|
|
88
|
+
"context_budget_tokens": 100000,
|
|
89
|
+
"compress_threshold": 0.8,
|
|
90
|
+
"session_dir": "/home/me/project/.lich/sessions",
|
|
91
|
+
"terminal_timeout_ms": 60000,
|
|
92
|
+
"log_level": "info"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| Field | Type | Default | Meaning |
|
|
97
|
+
| --- | --- | --- | --- |
|
|
98
|
+
| `providers` | array, min 1 | required | Failover chain, tried in order. |
|
|
99
|
+
| `providers[].kind` | `"openai_compat" \| "anthropic" \| "ollama"` | required | Wire protocol. |
|
|
100
|
+
| `providers[].name` | string | required | Label used in logs and `ChatResult.provider_name`. |
|
|
101
|
+
| `providers[].model` | string | required | Model name sent to the provider. |
|
|
102
|
+
| `providers[].base_url` | string | per kind | Endpoint base (see table above). |
|
|
103
|
+
| `providers[].api_key` | string | – | Inline key (prefer `api_key_env`). |
|
|
104
|
+
| `providers[].api_key_env` | string | per kind | Env var to read the key from. |
|
|
105
|
+
| `providers[].timeout_ms` | positive int | none | Per-request abort deadline. |
|
|
106
|
+
| `providers[].think` | boolean | – | Ollama only: request thinking mode. |
|
|
107
|
+
| `providers[].keep_alive` | string | – | Ollama only: model residency (e.g. `"10m"`). |
|
|
108
|
+
| `system_prompt` | string | built-in | Replaces the default system prompt. |
|
|
109
|
+
| `max_turns` | int >= 1 | `25` | Turn budget per run. |
|
|
110
|
+
| `work_dir` | string | cwd | Root for all file tools; paths outside are rejected. |
|
|
111
|
+
| `tools_enabled` | `"all"` or name array | `"all"` | Restrict the registry to these builtin tools. |
|
|
112
|
+
| `temperature` | 0–2 | – | Sampling temperature. |
|
|
113
|
+
| `max_tokens` | positive int | – | Completion cap. |
|
|
114
|
+
| `context_budget_tokens` | positive int | `100000` | Estimated budget before compression triggers. |
|
|
115
|
+
| `compress_threshold` | 0.1–0.95 | `0.8` | Compress when usage >= this fraction of the budget. |
|
|
116
|
+
| `session_dir` | string | `<work_dir>/.lich/sessions` | Transcript directory. |
|
|
117
|
+
| `terminal_timeout_ms` | positive int | `60000` | Default timeout injected into the `terminal` tool. |
|
|
118
|
+
| `log_level` | enum | `info` | Logger verbosity. |
|
|
119
|
+
|
|
120
|
+
Minimal per-provider examples:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{ "providers": [{ "kind": "ollama", "name": "local", "model": "llama3.2" }] }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{ "providers": [{ "kind": "anthropic", "name": "main", "model": "claude-sonnet-4", "api_key_env": "ANTHROPIC_API_KEY" }] }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Listed providers form a failover chain: the router walks them in order, retrying `rate_limit`/`network` errors (bounded backoff) on the current provider before moving on, and failing over immediately on `auth`, `overflow`, and `bad_request`.
|
|
131
|
+
|
|
132
|
+
## Session files
|
|
133
|
+
|
|
134
|
+
Each run writes `.lich/sessions/<timestamp36>-<counter>[-label].jsonl` where the label is the run origin: `-tui`, or `-gw-<platform>-<chat_id>` for gateway conversations. One-shot and chat runs get no label. Records are JSON lines of two kinds: `{"ts","kind":"meta","meta":{...}}` (run start, budget exhaustion) and `{"ts","kind":"message","message":{...}}` for each system/user/assistant/tool message.
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
# follow the newest session
|
|
138
|
+
ls -t .lich/sessions/*.jsonl | head -1
|
|
139
|
+
|
|
140
|
+
# print the conversation
|
|
141
|
+
jq -r 'select(.kind=="message") | "\(.message.role): \(.message.content // "(tool call)")"' .lich/sessions/<file>.jsonl
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Exit codes
|
|
145
|
+
|
|
146
|
+
| Code | Meaning |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `0` | Success: final answer produced (also `--help`, `--version`, `config`). |
|
|
149
|
+
| `1` | Any failure: unknown flag, missing model, unreadable config, provider error after failover, aborted run, or budget exhaustion (`[lich] budget exhausted after N turns` is printed to stderr). |
|
|
150
|
+
|
|
151
|
+
## Log levels
|
|
152
|
+
|
|
153
|
+
`--log-level` / config `log_level` sets the logger: `debug` (tool calls, retries, compression detail), `info` (lifecycle: gateway starts, adapter starts), `warn` (failover, degraded adapters, failed session persistence), `error` (provider and loop failures). Logger output goes to stderr. The one-shot/chat `[lich] turn N` / `tool: ok` progress lines are separate and always shown.
|
|
154
|
+
|
|
155
|
+
## Recipes
|
|
156
|
+
|
|
157
|
+
Review a file with a scoped working directory:
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
bun src/cli.ts --work-dir ./myproject --max-turns 15 \
|
|
161
|
+
"Review src/payments/retry.ts for correctness bugs. List each with a line number and a suggested fix."
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Web research (search, then fetch the promising pages):
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
bun src/cli.ts \
|
|
168
|
+
"Find the current LTS version of Node.js using web_search, fetch the release page with fetch_url, and summarize the support schedule."
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Note: `web_search` scrapes DuckDuckGo's HTML endpoint without an api key and can be blocked with `search_failed` errors when rate-limited; retrying later usually works.
|
|
172
|
+
|
|
173
|
+
Batch one-shots from a script, checking each exit code:
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
#!/usr/bin/env bash
|
|
177
|
+
set -u
|
|
178
|
+
for task in "summarize README.md" "list the largest files with disk_usage" "grep for TODO comments"; do
|
|
179
|
+
echo "== $task"
|
|
180
|
+
LICH_PROVIDER_KIND=ollama LICH_MODEL=llama3.2 bun src/cli.ts --max-turns 10 "$task" || echo "FAILED ($?)"
|
|
181
|
+
done
|
|
182
|
+
```
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Gateway guide
|
|
2
|
+
|
|
3
|
+
> What you'll learn: how `lich gateway` bridges Telegram, Discord, Twitch, and an HTTP webhook into one shared agent, how to set up each platform, and the webhook API reference.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
`lich gateway <platform...>` runs a long-lived process that forwards inbound chat messages to **one shared agent** and routes replies back. Per-conversation memory is keyed `platform:chat_id` (Telegram/Discord chat ids, Twitch channel names, webhook `chat_id` field): each conversation keeps its own bounded history capped at 40 messages (oldest evicted; conversations beyond 200 are evicted oldest-first). Messages for the same conversation are serialized, so overlapping messages never interleave histories; different conversations can run concurrently. Failures become a safe one-line reply: `agent error: <flattened message, 300 chars max>`.
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
flowchart LR
|
|
11
|
+
A[Webhook / TG / Discord / Twitch] -->|inbound msg| B[platform adapter]
|
|
12
|
+
B -->|platform, chat_id, user_id, text| C[GatewayBus]
|
|
13
|
+
C -->|history for platform:chat_id| D[shared Agent]
|
|
14
|
+
D -->|tools, failover, compression| E[reply text]
|
|
15
|
+
E -->|split to platform limit| F[adapter send]
|
|
16
|
+
F --> G[user]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Platforms: `webhook` (HTTP server), `telegram` (long-poll), `discord` (gateway WebSocket), `twitch` (IRC over WebSocket). Pick any combination:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
bun src/cli.ts gateway webhook # http only
|
|
23
|
+
bun src/cli.ts gateway webhook telegram # http + telegram polling
|
|
24
|
+
bun src/cli.ts gateway telegram discord twitch # no webhook server
|
|
25
|
+
bun src/cli.ts gateway # defaults to webhook
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The gateway is silent after startup: Telegram/Discord/Twitch respond only in chats, channels, or servers the bot can see or has joined, and the webhook only serves HTTP. Telegram media messages arrive as the placeholder text `media not supported yet`; other non-text events are ignored. Telegram `/start` is answered like a plain "hello".
|
|
29
|
+
|
|
30
|
+
## Setup: webhook
|
|
31
|
+
|
|
32
|
+
Zero configuration — the server binds `0.0.0.0:$LICH_GATEWAY_PORT` (default 8089).
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
bun src/cli.ts gateway webhook
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
curl -s -X POST http://localhost:8089/message \
|
|
40
|
+
-H "content-type: application/json" -d '{"text": "hello"}'
|
|
41
|
+
# -> {"reply":"...","usage":null}
|
|
42
|
+
|
|
43
|
+
curl -s http://localhost:8089/health
|
|
44
|
+
# -> {"status":"ok"}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
With token auth, every POST must carry the exact `x-lich-token` header; mismatched or missing tokens get `401 {"error":"unauthorized"}`:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
LICH_GATEWAY_TOKEN=s3cret bun src/cli.ts gateway webhook
|
|
51
|
+
curl -s -X POST http://localhost:8089/message \
|
|
52
|
+
-H "x-lich-token: s3cret" -H "content-type: application/json" -d '{"text": "hello"}'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Payload fields (all optional except `text`): `platform` (default `"webhook"`), `chat_id` (default `"default"`), `user_id` (default `"anonymous"`), `text` (required; missing `text` is a `400`). Use distinct `chat_id` values to keep independent conversation memories.
|
|
56
|
+
|
|
57
|
+
## Setup: Telegram
|
|
58
|
+
|
|
59
|
+
1. Message [@BotFather](https://t.me/BotFather) → `/newbot` → copy the token.
|
|
60
|
+
2. Export it and run:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
export LICH_TELEGRAM_BOT_TOKEN=123456:ABC-your-token
|
|
64
|
+
bun src/cli.ts gateway telegram
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
3. Open your bot in Telegram, send a message, get a reply. Media messages arrive as the text `media not supported yet`; the bot replies from there.
|
|
68
|
+
|
|
69
|
+
Telegram uses long polling (no public URL needed). Replies split at 4096 chars.
|
|
70
|
+
|
|
71
|
+
## Setup: Discord
|
|
72
|
+
|
|
73
|
+
1. Create an application at the [Discord developer portal](https://discord.com/developers/applications), add a **Bot**, and copy the bot token.
|
|
74
|
+
2. Enable the **Message Content Intent** (Bot settings → Privileged Gateway Intents) — the adapter requests intents `512 | 32768`, which includes message content.
|
|
75
|
+
3. Invite the bot with the `bot` scope (OAuth2 → URL Generator; no extra permissions needed beyond sending messages in target channels).
|
|
76
|
+
4. Export the token (and the bot's application/user id, so leading `<@BOT_ID>` mentions are stripped) and run:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
export LICH_DISCORD_BOT_TOKEN=your-bot-token
|
|
80
|
+
export LICH_DISCORD_BOT_ID=123456789012345678
|
|
81
|
+
bun src.cli.ts gateway discord
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
5. Send the bot a message (DM or any channel it can read — every non-bot message gets a reply); each channel has its own conversation memory (keyed by `channel_id`). Replies split at 2000 chars. Bot-authored messages are ignored (no loops).
|
|
85
|
+
|
|
86
|
+
**Known limitation:** the Discord adapter has no reconnect resume. If its gateway WebSocket drops, messages sent while offline are missed permanently; the adapter reconnects fresh after 5s. If guaranteed delivery across disconnects matters, run webhook or Telegram instead.
|
|
87
|
+
|
|
88
|
+
## Setup: Twitch
|
|
89
|
+
|
|
90
|
+
1. Generate an OAuth token with the `chat:read` and `chat:edit` scopes (for example via [twitchtokengen](https://twitchtokengen.com)).
|
|
91
|
+
2. Export it, your bot account's nickname, and the channels to join (comma-separated, lowercased by the adapter):
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
export LICH_TWITCH_OAUTH_TOKEN=oauth:abc123...
|
|
95
|
+
export LICH_TWITCH_NICK=mylichbot
|
|
96
|
+
export LICH_TWITCH_CHANNELS=channelone,channeltwo
|
|
97
|
+
bun src/cli.ts gateway twitch
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
3. The bot joins `#channelone` and `#channeltwo` and replies in chat (own messages are ignored). Replies split at 512 chars; IRC PING/PONG is answered automatically.
|
|
101
|
+
|
|
102
|
+
All three fields (`token`, `nick`, `channels`) are required — a missing one idles the adapter.
|
|
103
|
+
|
|
104
|
+
## Running multiple platforms at once
|
|
105
|
+
|
|
106
|
+
List the platforms in one command; all configured adapters start together and share the agent:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
LICH_TELEGRAM_BOT_TOKEN=... LICH_DISCORD_BOT_TOKEN=... \
|
|
110
|
+
bun src/cli.ts gateway webhook telegram discord
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Adapters whose credentials are missing start **idle** (a warning is logged, e.g. `gateway discord adapter idle: LICH_DISCORD_BOT_TOKEN not set`) and the rest keep running — so the same command works on machines with partial credentials. If no platform name is valid, the CLI exits `1` with `gateway needs at least one valid platform`.
|
|
114
|
+
|
|
115
|
+
## Environment variables
|
|
116
|
+
|
|
117
|
+
| Variable | Default | Purpose |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| `LICH_GATEWAY_PORT` | `8089` | Webhook server port (invalid/empty values fall back to 8089). |
|
|
120
|
+
| `LICH_GATEWAY_TOKEN` | unset | If set, POST `/message` requires header `x-lich-token` to match; else 401. |
|
|
121
|
+
| `LICH_TELEGRAM_BOT_TOKEN` | unset | Bot token from BotFather; adapter idles without it. |
|
|
122
|
+
| `LICH_DISCORD_BOT_TOKEN` | unset | Bot token from the developer portal; adapter idles without it. |
|
|
123
|
+
| `LICH_DISCORD_BOT_ID` | unset | Bot user id; strips a leading `<@id>` mention from messages. |
|
|
124
|
+
| `LICH_TWITCH_OAUTH_TOKEN` | unset | IRC oauth token (`oauth:` prefix optional); adapter idles without it. |
|
|
125
|
+
| `LICH_TWITCH_NICK` | unset | Bot account nickname; required with the token. |
|
|
126
|
+
| `LICH_TWITCH_CHANNELS` | unset | Comma-separated channels to join; required. |
|
|
127
|
+
|
|
128
|
+
Provider/model configuration comes from the same resolution as every mode (`LICH_MODEL` or config file).
|
|
129
|
+
|
|
130
|
+
## Operational notes
|
|
131
|
+
|
|
132
|
+
- **Graceful stop:** SIGINT (Ctrl+C) or SIGTERM stops every adapter, then exits `0`. No other signals are handled.
|
|
133
|
+
- **Idle adapters:** any adapter missing its token/credentials logs a warning once and does nothing; the process stays up for the others.
|
|
134
|
+
- **Message splitting:** replies are split per platform — Telegram 4096 chars, Discord 2000, Twitch 512. Telegram/Discord splits prefer whitespace; Twitch hard-cuts at the limit.
|
|
135
|
+
- **One shared agent:** all platforms share one agent instance, tool set, and provider failover chain; only conversation memory is per-chat.
|
|
136
|
+
- **Debugging tool calls:** with `--log-level debug`, completed tool calls are logged as `tool <name> ok|failed`.
|
|
137
|
+
- **Restart loses Discord messages:** see the [Discord note](#setup-discord) — the offline window is not replayed.
|
|
138
|
+
|
|
139
|
+
## Webhook API reference
|
|
140
|
+
|
|
141
|
+
### `POST /message`
|
|
142
|
+
|
|
143
|
+
Request:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{"text": "hello", "platform": "webhook", "chat_id": "default", "user_id": "anonymous"}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Only `text` is required. Success (`200`):
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{"reply":"Hello! How can I help you today? ...","usage":null}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`reply` is the agent's final answer; `usage` is always `null` on this endpoint (webhook replies are formatted without usage stats, unlike the chat/TUI footers). Errors:
|
|
156
|
+
|
|
157
|
+
| Status | When |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| `400` | Body missing or has no `text` field: `{"error":"text is required"}`. |
|
|
160
|
+
| `401` | `LICH_GATEWAY_TOKEN` is set and the `x-lich-token` header does not match. |
|
|
161
|
+
| `404` | Anything other than `POST /message` or `GET /health`. |
|
|
162
|
+
| `500` | Internal dispatch failure: `{"error":"internal error"}`. |
|
|
163
|
+
|
|
164
|
+
Agent-level failures (e.g. every provider failed) return `200` with `reply` set to a sanitized one-line `agent error: ...` string, so callers always get a deliverable text.
|
|
165
|
+
|
|
166
|
+
### `GET /health`
|
|
167
|
+
|
|
168
|
+
`200 {"status":"ok"}` unconditionally — the webhook server itself is alive; it does not reflect platform adapters or provider health.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Library guide
|
|
2
|
+
|
|
3
|
+
> What you'll learn: how to install the package and embed the `Agent` class in TypeScript — basic runs, event subscription, multi-turn history, config, tool filtering, error handling, and session access.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
From a checkout of this repository (or a published tarball):
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install /path/to/lich-0.2.0.tgz # after `npm run build` in the lich repo
|
|
11
|
+
# or point package.json at the git repo
|
|
12
|
+
npm install git+ssh://example.com/you/lich.git
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The package ships ESM (`dist/index.js`, types at `dist/index.d.ts`, binary at `dist/cli.js`); `main`/`types`/`bin` are wired in `package.json`.
|
|
16
|
+
|
|
17
|
+
## Minimal example
|
|
18
|
+
|
|
19
|
+
`create_agent(raw_config)` validates the config (zod, defaults applied, frozen result) and returns an `Agent` with a `.run()` loop:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { create_agent } from "lich";
|
|
23
|
+
|
|
24
|
+
const agent = create_agent({
|
|
25
|
+
providers: [{ kind: "ollama", name: "local", model: "llama3.2:latest" }],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const result = await agent.run({ input: "Use list_dir to list the files, then summarize." });
|
|
29
|
+
console.log(result.outcome.final?.content);
|
|
30
|
+
console.log(`tokens: ${result.usage_total.total_tokens}`);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The one-liner `run_agent(config, input)` is equivalent when you only need a single run:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { run_agent } from "lich";
|
|
37
|
+
|
|
38
|
+
const result = await run_agent(
|
|
39
|
+
{ providers: [{ kind: "ollama", name: "local", model: "llama3.2:latest" }] },
|
|
40
|
+
"Reply with ok",
|
|
41
|
+
);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Agent class
|
|
45
|
+
|
|
46
|
+
`new Agent(config)` (or `create_agent(raw)`) builds the provider router, registers the twelve builtin tools (filtered by `tools_enabled`), and exposes:
|
|
47
|
+
|
|
48
|
+
| Member | Type | Purpose |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `run(options)` | `(AgentRunOptions) => Promise<AgentRunResult>` | Run the loop to a final answer, budget exhaustion, or abort. |
|
|
51
|
+
| `events` | `AgentEmitter` | Subscribe with `events.on(handler)`; the returned function unsubscribes. |
|
|
52
|
+
| `config` | `AgentConfig` | Frozen, fully-resolved config (defaults filled in). |
|
|
53
|
+
|
|
54
|
+
`AgentRunOptions`:
|
|
55
|
+
|
|
56
|
+
| Field | Type | Meaning |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| `input` | `string` | Required user message for this run. |
|
|
59
|
+
| `history` | `Message[]` | Prior conversation to continue (multi-turn). |
|
|
60
|
+
| `signal` | `AbortSignal` | Cooperative cancellation; the loop returns `outcome.stopped_reason: "aborted"`. |
|
|
61
|
+
| `label` | `string` | Origin tag for the session filename (e.g. `"tui"`, `"gw:webhook:default"`). |
|
|
62
|
+
|
|
63
|
+
`AgentRunResult`:
|
|
64
|
+
|
|
65
|
+
| Field | Type | Meaning |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| `outcome.final` | `AssistantMessage \| undefined` | The final assistant reply (undefined on abort/budget without content). |
|
|
68
|
+
| `outcome.turns_used` | `number` | Turns consumed this run. |
|
|
69
|
+
| `outcome.stopped_reason` | `"final" \| "budget" \| "aborted"` | Why the loop ended. |
|
|
70
|
+
| `messages` | `Message[]` | Full transcript: your `history` plus the new exchange. |
|
|
71
|
+
| `usage_total` | `Usage` | Summed `{prompt_tokens, completion_tokens, total_tokens}`. |
|
|
72
|
+
| `session_path` | `string \| undefined` | Session JSONL path, or `undefined` if persistence failed (logged warning, never throws). |
|
|
73
|
+
|
|
74
|
+
## Events
|
|
75
|
+
|
|
76
|
+
Handlers receive a discriminated `AgentEvent` union; throwing handlers are logged, never fatal:
|
|
77
|
+
|
|
78
|
+
| Event | Payload |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `turn_start` / `turn_end` | `{ turn }` |
|
|
81
|
+
| `llm_start` | `{ turn }` |
|
|
82
|
+
| `llm_end` | `{ turn, result: ChatResult }` — carries `usage` per call. |
|
|
83
|
+
| `tool_call_start` | `{ turn, call: ToolCall }` |
|
|
84
|
+
| `tool_call_end` | `{ turn, call, result: ToolResult }` |
|
|
85
|
+
| `compress_start` | `{ estimated_tokens }` |
|
|
86
|
+
| `compress_end` | `{ summary_chars }` |
|
|
87
|
+
| `final` | `{ message, result }` — the answer that ends the run. |
|
|
88
|
+
| `budget_exhausted` | `{ turns_used }` |
|
|
89
|
+
| `error` | `{ error }` — provider/loop errors; the run may still recover via failover. |
|
|
90
|
+
|
|
91
|
+
Print every tool call as it happens:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const agent = create_agent(config);
|
|
95
|
+
const unsubscribe = agent.events.on((event) => {
|
|
96
|
+
if (event.type === "tool_call_end") {
|
|
97
|
+
const status = event.result.ok ? "ok" : `error: ${event.result.error}`;
|
|
98
|
+
console.log(`[tool] ${event.call.name}(${JSON.stringify(event.call.args)}) -> ${status}`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
try {
|
|
102
|
+
await agent.run({ input: "List the repo and find TODO comments" });
|
|
103
|
+
} finally {
|
|
104
|
+
unsubscribe();
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Multi-turn conversations
|
|
109
|
+
|
|
110
|
+
Pass prior `result.messages` back in as `history`:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
let history: Message[] = [];
|
|
114
|
+
for (const question of ["What files are in the repo?", "Which one is largest?"]) {
|
|
115
|
+
const result = await agent.run({ input: question, history });
|
|
116
|
+
console.log(result.outcome.final?.content);
|
|
117
|
+
history = result.messages;
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Config reference
|
|
122
|
+
|
|
123
|
+
Same schema as the CLI config file — see the [config file reference](cli.md#config-file-reference) for the full field table. As a library caller you normally construct it directly:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const config = {
|
|
127
|
+
providers: [
|
|
128
|
+
{ kind: "openai_compat", name: "openrouter", model: "meta-llama/llama-3.1-8b-instruct",
|
|
129
|
+
base_url: "https://openrouter.ai/api/v1", api_key_env: "OPENROUTER_API_KEY" },
|
|
130
|
+
{ kind: "ollama", name: "local", model: "llama3.2" }, // failover target
|
|
131
|
+
],
|
|
132
|
+
max_turns: 25,
|
|
133
|
+
tools_enabled: ["read_file", "list_dir", "terminal", "web_search", "fetch_url"],
|
|
134
|
+
session_dir: "./.lich/sessions",
|
|
135
|
+
};
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Listed providers form a failover chain tried in order: `rate_limit`/`network` errors retry with backoff (3 attempts) on the current provider before failing over; `auth`, `overflow`, and `bad_request` fail over immediately. The last error is rethrown when all providers fail.
|
|
139
|
+
|
|
140
|
+
## Custom tool filtering
|
|
141
|
+
|
|
142
|
+
`tools_enabled` accepts `"all"` (default) or an array of builtin tool names to register; everything else stays unregistered and invisible to the model:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const agent = create_agent({
|
|
146
|
+
providers: [{ kind: "ollama", name: "local", model: "llama3.2" }],
|
|
147
|
+
tools_enabled: ["read_file", "grep_files", "list_dir"],
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Error handling
|
|
152
|
+
|
|
153
|
+
Provider failures throw `ProviderError`, an `Error` subclass with `kind`, `provider_name`, optional `status` and `retry_after_ms`:
|
|
154
|
+
|
|
155
|
+
| `kind` | Meaning | Failover behavior |
|
|
156
|
+
| --- | --- | --- |
|
|
157
|
+
| `auth` | 401/403 or bad credentials. | Immediate failover to the next provider. |
|
|
158
|
+
| `rate_limit` | 429 or 5xx (with `retry_after_ms` when the server sends it). | 3 attempts with backoff, then failover. |
|
|
159
|
+
| `network` | Fetch failed, timeout, or abort while connecting. | 3 attempts with backoff, then failover. |
|
|
160
|
+
| `overflow` | Request exceeded the model's context window. | Immediate failover. |
|
|
161
|
+
| `bad_request` | 400/422 or an unparseable success payload. | Immediate failover. |
|
|
162
|
+
| `unknown` | Non-provider errors (e.g. tool crashes surfaced as strings). | Treated as fatal for the provider. |
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { ProviderError } from "lich";
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await agent.run({ input: "hello" });
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof ProviderError) {
|
|
171
|
+
console.error(`${error.provider_name} failed: ${error.kind} — ${error.message}`);
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
When every configured provider fails, the last `ProviderError` is thrown. Tool failures are *not* exceptions: they return `{ ok: false, output, error }` into the loop as tool messages for the model to react to. Cancellation via `signal` ends the run with `stopped_reason: "aborted"` rather than throwing.
|
|
178
|
+
|
|
179
|
+
## Session access
|
|
180
|
+
|
|
181
|
+
Each `run()` appends a transcript line-by-line under `config.session_dir` (default `<work_dir>/.lich/sessions`); `result.session_path` gives the exact file. Records carry `{ts, kind: "message"|"meta", message?, meta?}`; read them with `jq` or the exported `read_session_messages(path)` helper from `src/session/store.ts`. Persistence is best-effort: a write failure logs a warning, returns `session_path: undefined`, and never fails the run.
|