@moikapy/lich 0.5.1 → 0.7.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 +33 -0
- package/README.md +84 -4
- package/dist/chunk-JYURFAGB.js +92 -0
- package/dist/chunk-JYURFAGB.js.map +1 -0
- package/dist/{chunk-CV2YH3FH.js → chunk-QVJCIZIF.js} +748 -13
- package/dist/chunk-QVJCIZIF.js.map +1 -0
- package/dist/chunk-SAEB3QL3.js +58 -0
- package/dist/chunk-SAEB3QL3.js.map +1 -0
- package/dist/cli.js +263 -22
- package/dist/cli.js.map +1 -1
- package/dist/{gateway-W6S43ETE.js → gateway-5BG3YCZF.js} +2 -2
- package/dist/index.d.ts +229 -118
- package/dist/index.js +5 -3
- package/dist/{tui-2VO6LAFM.js → tui-L6RABP2J.js} +43 -41
- package/dist/tui-L6RABP2J.js.map +1 -0
- package/docs/.vitepress/config.mts +6 -1
- package/docs/architecture/agent-loop.md +5 -3
- package/docs/architecture/overview.md +36 -22
- package/docs/architecture/plugins.md +1 -1
- package/docs/architecture/tools.md +7 -2
- package/docs/getting-started.md +9 -7
- package/docs/index.md +6 -4
- package/docs/user-guide/cli.md +24 -9
- package/docs/user-guide/games.md +93 -0
- package/docs/user-guide/godot.md +3 -1
- package/docs/user-guide/library.md +8 -2
- package/docs/user-guide/plugins.md +2 -2
- package/docs/user-guide/redot.md +93 -0
- package/docs/user-guide/tui.md +11 -11
- package/examples/game_bridge/README.md +2 -0
- package/optional-mcps/godot/manifest.json +6 -0
- package/optional-mcps/redot/manifest.json +18 -0
- package/package.json +2 -1
- package/dist/chunk-6M6OAQGN.js +0 -17
- package/dist/chunk-6M6OAQGN.js.map +0 -1
- package/dist/chunk-CV2YH3FH.js.map +0 -1
- package/dist/tui-2VO6LAFM.js.map +0 -1
- /package/dist/{gateway-W6S43ETE.js.map → gateway-5BG3YCZF.js.map} +0 -0
package/docs/user-guide/cli.md
CHANGED
|
@@ -13,19 +13,25 @@ lich tui # interactive terminal UI (ink)
|
|
|
13
13
|
lich gateway <plat..> # messaging gateway (webhook|telegram|discord|twitch)
|
|
14
14
|
lich config # print a starter config template
|
|
15
15
|
lich update # install a newer npm release, if one exists
|
|
16
|
+
lich mcp list # list servers in this work dir's .lich/config.json
|
|
17
|
+
lich mcp add <name> # catalog or --command/--url; stays disabled
|
|
18
|
+
lich mcp enable <name> # set enabled true
|
|
19
|
+
lich mcp disable <name>
|
|
20
|
+
lich mcp remove <name>
|
|
16
21
|
lich --help # usage text
|
|
17
|
-
lich --version #
|
|
22
|
+
lich --version # package.json version (published package and this tree: 0.6.0)
|
|
18
23
|
```
|
|
19
24
|
|
|
20
|
-
- **Bare `lich`** opens the same TUI as `lich tui`. It does not print usage. On a TTY, if neither `.lich/config.json` nor `~/.config/lich/config.json` exists
|
|
25
|
+
- **Bare `lich`** opens the same TUI as `lich tui`. It does not print usage. On a TTY, if neither `.lich/config.json` nor `~/.config/lich/config.json` exists, a setup wizard runs first (name, provider, optional gateway env-var names, optional plugins) and writes `.lich/config.json` once. `LICH_MODEL` / `--model` prefills the model prompt; it does not skip the wizard. An existing config in that chain skips the wizard and is not replaced. Non-TTY stdin skips the wizard and prints guidance instead of hanging. `lich --help` still prints usage.
|
|
21
26
|
- **`lich init`** writes that starter file without prompts, using the same writer as the wizard. Existing flags such as `--model` are written into the file and win over `LICH_MODEL`. It never overwrites an existing `.lich/config.json`. `.lich/` is gitignored.
|
|
22
27
|
- **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.
|
|
23
28
|
- **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.
|
|
24
29
|
- **TUI** launches the ink interface. See the [TUI guide](tui.md).
|
|
25
30
|
- **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`.
|
|
26
31
|
- **Update** compares the installed version to the npm registry and, when a newer release exists, runs `npm install -g @moikapy/lich@latest`. Exit any running TUI or gateway first; npm cannot replace the package while those processes are running. A git clone is told to `git pull`. See [Updating](../getting-started.md#updating).
|
|
32
|
+
- **MCP** is in this source (changelog 0.7.0, unreleased). The published 0.6.0 npm package does not include `lich mcp`. It edits only `mcp_servers` in `<work-dir>/.lich/config.json` through the same writer as `lich init` (`update` mode, so other keys stay). A missing file lists as empty; `add` creates the file if needed. New entries stay disabled until `enable`. Names must match `^[a-z][a-z0-9_]*$`. Catalog names use `optional-mcps/`; otherwise pass `--command` and repeatable `--arg`, or `--url` (loopback only), not both. Redot still needs `--project-path` for the catalog args, and the command basename must be `redot`. No prompts. See the [Redot guide](redot.md).
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
A repository clone's `bun src/cli.ts` matches that checkout. The published 0.6.0 binary does not include `lich mcp`.
|
|
29
35
|
|
|
30
36
|
## Flags
|
|
31
37
|
|
|
@@ -43,6 +49,11 @@ Flags work before or after the subcommand. Every value flag can also be set via
|
|
|
43
49
|
| `--system-prompt <s>` | Replaces the default system prompt. | built-in concise-assistant prompt |
|
|
44
50
|
| `--session-dir <path>` | Transcript directory. | `<work_dir>/.lich/sessions` |
|
|
45
51
|
| `--log-level <level>` | `debug` \| `info` \| `warn` \| `error`. | `info` |
|
|
52
|
+
| `--theme <name>` | Display theme loaded once at startup. `lich` is built-in; other names read `~/.lich/themes/<name>.json`. | `lich` |
|
|
53
|
+
| `--command <bin>` | `lich mcp add` only: local stdio binary. | – |
|
|
54
|
+
| `--arg <value>` | `lich mcp add` only: repeatable stdio arg. May start with `--`. | – |
|
|
55
|
+
| `--url <url>` | `lich mcp add` only: loopback HTTP MCP URL. | – |
|
|
56
|
+
| `--project-path <path>` | `lich mcp add` only: catalog `${project_path}` substitute. | – |
|
|
46
57
|
|
|
47
58
|
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`.
|
|
48
59
|
|
|
@@ -50,7 +61,7 @@ Passing `--max-turns 0` or a non-integer fails with `--max-turns must be a posit
|
|
|
50
61
|
|
|
51
62
|
The effective provider for a run is decided in this order:
|
|
52
63
|
|
|
53
|
-
1. If `--config <path>` was passed, that file is the whole configuration (it must exist, or the CLI fails with `config
|
|
64
|
+
1. If `--config <path>` was passed, that file is the whole configuration (it must exist and be a JSON object, or the CLI fails with `cannot use config file <path>: ...`).
|
|
54
65
|
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.
|
|
55
66
|
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.
|
|
56
67
|
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.
|
|
@@ -65,7 +76,7 @@ Per-kind defaults:
|
|
|
65
76
|
|
|
66
77
|
## Config file reference
|
|
67
78
|
|
|
68
|
-
Validated by zod (top-level unknown keys are silently stripped; extra keys inside a `providers[]` entry are passed through
|
|
79
|
+
Validated by zod (top-level unknown keys are silently stripped; extra keys inside a `providers[]` entry are passed through; each `mcp_servers` entry is strict). Example:
|
|
69
80
|
|
|
70
81
|
```json
|
|
71
82
|
{
|
|
@@ -95,7 +106,8 @@ Validated by zod (top-level unknown keys are silently stripped; extra keys insid
|
|
|
95
106
|
"compress_threshold": 0.8,
|
|
96
107
|
"session_dir": "/home/me/project/.lich/sessions",
|
|
97
108
|
"terminal_timeout_ms": 60000,
|
|
98
|
-
"log_level": "info"
|
|
109
|
+
"log_level": "info",
|
|
110
|
+
"theme": "lich"
|
|
99
111
|
}
|
|
100
112
|
```
|
|
101
113
|
|
|
@@ -111,12 +123,15 @@ Validated by zod (top-level unknown keys are silently stripped; extra keys insid
|
|
|
111
123
|
| `providers[].timeout_ms` | positive int | none | Per-request abort deadline. |
|
|
112
124
|
| `providers[].think` | boolean | – | Ollama only: request thinking mode. |
|
|
113
125
|
| `providers[].keep_alive` | string | – | Ollama only: model residency (e.g. `"10m"`). |
|
|
114
|
-
| `agent_name` | string | `lich` |
|
|
126
|
+
| `agent_name` | string | `lich` | Wizard label. The TUI banner uses the active theme welcome string, not this field. |
|
|
127
|
+
| `theme` | string | `lich` | Display theme name. See [Themes](https://github.com/Moikapy/lich/blob/main/README.md#themes). |
|
|
115
128
|
| `gateway` | object | omitted | Optional. `platforms` (`webhook` \| `telegram` \| `discord` \| `twitch`) and `token_envs` (platform → env-var name). Secrets stay in the environment. |
|
|
129
|
+
| `plugins` | string array | `[]` | Module paths relative to `work_dir` or absolute. Bare `lich`, one-shot, chat, tui, and gateway load them through `create_agent_with_plugins`. `run_agent` does too. `create_agent` does not. See the [plugins guide](plugins.md). |
|
|
130
|
+
| `mcp_servers` | object | omitted | Optional. Closed record of named servers. Each entry is stdio `{command, args, env?}` or loopback http `{url}`. `enabled` defaults to false. Unknown keys are rejected. In this source only (0.7.0 unreleased; not in the published 0.6.0 package). See the [Redot guide](redot.md). |
|
|
116
131
|
| `system_prompt` | string | built-in | Replaces the default system prompt. |
|
|
117
132
|
| `max_turns` | int >= 1 | `25` | Turn budget per run. |
|
|
118
133
|
| `work_dir` | string | cwd | Root for all file tools; paths outside are rejected. |
|
|
119
|
-
| `tools_enabled` | `"all"` or name array | `"all"` | Restrict the registry to these
|
|
134
|
+
| `tools_enabled` | `"all"` or name array | `"all"` | Restrict the builtin registry to these names. `[]` drops builtins and MCP tools and does not connect to MCP servers. Plugin tools still register afterward, including the gatekeeper's `git_commit`. |
|
|
120
135
|
| `temperature` | 0–2 | – | Sampling temperature. |
|
|
121
136
|
| `max_tokens` | positive int | – | Completion cap. |
|
|
122
137
|
| `context_budget_tokens` | positive int | `100000` | Estimated budget before compression triggers. |
|
|
@@ -166,7 +181,7 @@ jq -r 'select(.kind=="message") | "\(.message.role): \(.message.content // "(too
|
|
|
166
181
|
|
|
167
182
|
| Code | Meaning |
|
|
168
183
|
| --- | --- |
|
|
169
|
-
| `0` | Success: final answer produced (also `--help`, `--version`, `config`, `init`, and a TUI that exits cleanly). |
|
|
184
|
+
| `0` | Success: final answer produced (also `--help`, `--version`, `config`, `init`, a successful `mcp` action, `update` when nothing newer is installed or the install succeeds, and a TUI that exits cleanly). |
|
|
170
185
|
| `1` | Any failure: unknown flag, missing model, unreadable config, provider error after failover, aborted run, budget exhaustion, non-TTY bare `lich`, or a cancelled setup wizard. |
|
|
171
186
|
|
|
172
187
|
## Log levels
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Games
|
|
2
|
+
|
|
3
|
+
> What you'll learn: how to treat session JSONL as a combat log, which fields the recipes read, and how a playthrough's token spend shows up after a run.
|
|
4
|
+
|
|
5
|
+
lich does not replay combat from RNG seeds. The commander's choices are sampled. The JSONL transcript is the replay. Godot still drains `.lich/game/`; these recipes read `.lich/sessions/`, not the order file.
|
|
6
|
+
|
|
7
|
+
Tool shapes live in [`examples/game_bridge/README.md`](https://github.com/Moikapy/lich/blob/main/examples/game_bridge/README.md). This page does not repeat them. The plugin entry is `examples/game_bridge/game_bridge.plugin.mjs`, loaded through `config.plugins` and `create_agent_with_plugins`. A per-persona HTTP front for that plugin is the [orchestrator example](https://github.com/Moikapy/lich/blob/main/examples/persona_orchestrator/README.md) — a pattern the game repo copies, not a second agent core.
|
|
8
|
+
|
|
9
|
+
## Session files as combat logs
|
|
10
|
+
|
|
11
|
+
Each `Agent.run` appends one `.jsonl` file under `session_dir` (default `<work_dir>/.lich/sessions`). Records are `{ts, kind: "message"|"meta", message?, meta?}`.
|
|
12
|
+
|
|
13
|
+
| What you want | Where it is |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| What the commander saw | `kind: "message"`, `message.role: "user"` — the battle digest you posted |
|
|
16
|
+
| What it chose | assistant `message.tool_calls[]` with `name`, `args` |
|
|
17
|
+
| Why | `args.rationale` on `enemy_actions` |
|
|
18
|
+
| What happened | `message.role: "tool"`, `content`, optional `is_error` |
|
|
19
|
+
| Token spend | `kind: "meta"`, `meta.event: "run_end"`, `meta.usage` |
|
|
20
|
+
|
|
21
|
+
Assistant tool calls are the internal shape `{id, name, args}`, not the provider wire format. Tool failures are `JSON.stringify({ok, output, error})` in `content`, with `is_error: true`. A plugin veto is that object with `error` starting `blocked_by_plugin:` (the meteor gate uses `blocked_by_plugin: meteor_gates_closed_until_round_3`). Non-JSON tool content is not an error for the recipes: `fromjson?` skips it.
|
|
22
|
+
|
|
23
|
+
`run_end` is written for every completed loop (`stopped_reason` `final`, `budget`, or `aborted`). `usage` is `{prompt_tokens, completion_tokens, total_tokens}` and matches the `usage_total` returned to the caller. A budget stop also writes `meta.event: "budget_exhausted"` before `run_end`. Recipes that filter `kind=="message"` stay valid as meta events change. A provider throw never reaches persistence — there is no outcome to close. A failed persist logs a warning and returns `session_path: undefined`; a run missing from disk is a gap, not a zero-spend run.
|
|
24
|
+
|
|
25
|
+
Filenames are `<base36-timestamp>-<counter>[-<label>].jsonl`. The timestamp prefix sorts chronologically. The gateway (and the orchestrator example) labels `gw:<platform>:<chat_id>`. With `chat_id` = run id, files for one playthrough share that label. The label slug is truncated to 40 characters.
|
|
26
|
+
|
|
27
|
+
`read_session_messages(path)` in `src/session/store.ts` is the programmatic reader for a source checkout. It is not a package export. It returns messages only and skips meta. Offline analysis should use `jq` (a user tool, not a lich dependency).
|
|
28
|
+
|
|
29
|
+
## Recipes
|
|
30
|
+
|
|
31
|
+
`jq` is a prerequisite for these commands, not a package dependency. Paths assume you are in `work_dir`.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# (1) rationale
|
|
35
|
+
jq -r 'select(.kind=="message") | select(.message.role=="assistant")
|
|
36
|
+
| .message.tool_calls[]? | select(.name=="enemy_actions")
|
|
37
|
+
| .args.rationale' .lich/sessions/*.jsonl
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# (2) histogram
|
|
42
|
+
jq -s '[.[] | .message? | select(.role=="assistant")
|
|
43
|
+
| .tool_calls[]? | select(.name=="enemy_actions")
|
|
44
|
+
| .args.actions[]?.action]
|
|
45
|
+
| group_by(.) | map({action: .[0], uses: length}) | sort_by(-.uses)' \
|
|
46
|
+
.lich/sessions/*.jsonl
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# (3) veto
|
|
51
|
+
jq -r 'select(.kind=="message") | select(.message.role=="tool")
|
|
52
|
+
| .message.content | fromjson? | select(.error? // "" | startswith("blocked_by_plugin"))
|
|
53
|
+
| .error' .lich/sessions/*.jsonl
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# (4) is_error
|
|
58
|
+
jq -r 'select(.kind=="message") | select(.message.role=="tool" and .message.is_error==true)
|
|
59
|
+
| [.ts, .message.name, .message.content] | @tsv' .lich/sessions/*.jsonl
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# (5) pacing
|
|
64
|
+
jq -r 'select(.kind=="message") | select(.message.role=="user" or .message.role=="assistant")
|
|
65
|
+
| [.ts, .message.role] | @tsv' .lich/sessions/<run>.jsonl
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# (usage) run_end tokens
|
|
70
|
+
jq -s '[.[] | select(.kind=="meta" and .meta.event=="run_end") | .meta.usage.total_tokens] | add' \
|
|
71
|
+
.lich/sessions/*.jsonl
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Recipe 2 counts assistant tool-call arguments, including orders a hook later vetoed. It is not the line set Godot applied. Recipe 3 is not a veto table. The only gate in the shipped plugin is meteor before round 3. Other `blocked_by_plugin:` strings, if a game plugin adds them, show up in the same query because the loop formats every veto the same way.
|
|
75
|
+
|
|
76
|
+
## Do not glob a playthrough blindly
|
|
77
|
+
|
|
78
|
+
A gateway conversation of N posts writes N files. `Agent.run` seeds from `history`, and `persist_session` appends all of `outcome.messages`, so each file is a superset of the previous exchange. Globbing `*.jsonl` double-counts. Take the newest file per label (names sort by timestamp prefix), or dedupe on `tool_call.id`, which stays stable when the same call is replayed into the next file.
|
|
79
|
+
|
|
80
|
+
Compression can rewrite a long run in place: when estimated tokens cross `compress_threshold` of `context_budget_tokens`, older messages become one summary and the 8 most recent non-system messages stay verbatim. Early rounds may survive only as that summary. The recipes see the file on disk, not the pre-compression transcript.
|
|
81
|
+
|
|
82
|
+
## Player modeling
|
|
83
|
+
|
|
84
|
+
Cross-run notes are tool arguments, not a second memory agent. `dungeon_memory_write` args (`note`) and `dungeon_memory_read` results are in the JSONL. The file on disk is `.lich/game/memory.jsonl`; Godot does not drain it as orders. `MEMORY.md` is never auto-loaded and is not this paper trail.
|
|
85
|
+
|
|
86
|
+
Replay `dungeon_memory_read` calls around a boss fight, then read `enemy_actions` `rationale` after them, to see which notes the commander actually used. The notes are reference data. A note in the tool result is not an instruction, and the session file does not promote it into the system prompt.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# (memory) dungeon notes
|
|
90
|
+
jq -r 'select(.kind=="message") | select(.message.role=="assistant")
|
|
91
|
+
| .message.tool_calls[]? | select(.name=="dungeon_memory_write")
|
|
92
|
+
| .args.note' .lich/sessions/*.jsonl
|
|
93
|
+
```
|
package/docs/user-guide/godot.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
lich is the AI brain in a separate process. Godot speaks HTTP. This page is the recipe; the file contract and tool list live in [`examples/game_bridge/README.md`](https://github.com/Moikapy/lich/blob/main/examples/game_bridge/README.md). Plugin authoring is the [plugins guide](plugins.md). Platform setup beyond the webhook is the [gateway guide](gateway.md). A TypeScript game backend that embeds the library uses the [library guide](library.md) — Godot itself does not.
|
|
6
6
|
|
|
7
|
+
Editor MCP and this page are opposite directions. An editor MCP is lich connecting to the editor to build the project — the [Redot guide](redot.md). This page is the game connecting to lich. Godot has no official MCP server. lich does not download a community addon and does not spawn one. A game master is another persona, not an editor tool. Play for both editors is still one `POST /message` per round, then drain `.lich/game/orders.jsonl`. Do not call lich per frame.
|
|
8
|
+
|
|
7
9
|
## Architecture
|
|
8
10
|
|
|
9
11
|
Two tiers:
|
|
@@ -24,7 +26,7 @@ flowchart LR
|
|
|
24
26
|
G --> H[resolve the round]
|
|
25
27
|
```
|
|
26
28
|
|
|
27
|
-
A game backend may instead call `run_agent`, which loads `config.plugins`. `create_agent` does not. Godot still reaches that backend over HTTP; it does not import the package.
|
|
29
|
+
A game backend may instead call `run_agent`, which loads `config.plugins`. `create_agent` does not. Godot still reaches that backend over HTTP; it does not import the package. Several personas means several agents behind that HTTP process — the pattern is [`examples/persona_orchestrator`](https://github.com/Moikapy/lich/blob/main/examples/persona_orchestrator/README.md), and the service is the game's. Session replay is the [games guide](games.md).
|
|
28
30
|
|
|
29
31
|
## Gateway contract
|
|
30
32
|
|
|
@@ -145,7 +145,9 @@ Listed providers form a failover chain tried in order: `rate_limit`/`network` er
|
|
|
145
145
|
|
|
146
146
|
## Custom tool filtering
|
|
147
147
|
|
|
148
|
-
`tools_enabled` accepts `"all"` (default) or an array of
|
|
148
|
+
`tools_enabled` accepts `"all"` (default) or an array of tool names to register; everything else stays unregistered and invisible to the model. MCP tools, when a named server is `enabled`, use the same allowlist and stay off when the list is `[]` (that empty list does not connect). The filter does not apply to plugin tools: they register afterward, including the gatekeeper's `git_commit` (fail-closed unless `LICH_ALLOW_SELF_COMMIT=1`). `[]` strips every builtin and every MCP tool and does not throw.
|
|
149
|
+
|
|
150
|
+
`mcp_servers` and `catalog_client_entry` are in this source (changelog 0.7.0, unreleased). The published 0.6.0 package does not include them. See the [Redot guide](redot.md).
|
|
149
151
|
|
|
150
152
|
```ts
|
|
151
153
|
const agent = create_agent({
|
|
@@ -182,6 +184,10 @@ try {
|
|
|
182
184
|
|
|
183
185
|
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.
|
|
184
186
|
|
|
187
|
+
## Games
|
|
188
|
+
|
|
189
|
+
A Godot client does not embed the library. A game backend that does is still one `Agent` per persona, not a second loop. The pattern — factory, history cap, per-conversation queue, `POST /message` → `{reply, usage}` — is [`examples/persona_orchestrator`](https://github.com/Moikapy/lich/blob/main/examples/persona_orchestrator/README.md). The service itself is game-repo work. Session files as a combat log: [games guide](games.md).
|
|
190
|
+
|
|
185
191
|
## Session access
|
|
186
192
|
|
|
187
|
-
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?}
|
|
193
|
+
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?}`. A completed run (`final`, `budget`, or `aborted` returned by the loop) closes with `{event: "run_end", stopped_reason, usage}` where `usage` equals `usage_total`. A budget stop also writes `{event: "budget_exhausted"}` before that. Provider throws do not persist. Read transcripts with `jq` (see the [games guide](games.md)) or `read_session_messages(path)` from a source checkout — it is not a package export. Persistence is best-effort: a write failure logs a warning, returns `session_path: undefined`, and never fails the run.
|
|
@@ -112,7 +112,7 @@ Failures are contained at every layer:
|
|
|
112
112
|
|
|
113
113
|
## Runtime notes
|
|
114
114
|
|
|
115
|
-
`package.json` `engines.node` is `>=20`.
|
|
115
|
+
`package.json` `engines.node` is `>=20`. Bare `lich`, one-shot, chat, tui, and gateway load `config.plugins` through `create_agent_with_plugins` before the run. `lich init`, `lich config`, `lich update`, and `lich mcp` do not start an agent. `create_agent` stays plugin-free; `run_agent` loads plugins. A broken entry logs one `plugin load errors` warning and the run continues without that plugin.
|
|
116
116
|
|
|
117
117
|
`.mjs` and other plain JS (no type syntax) are the form that matches `engines.node` `>=20`. They load on Node and on Bun with no plugin-load warning.
|
|
118
118
|
|
|
@@ -122,7 +122,7 @@ Checked with Node 26.8.2 (`node dist/cli.js`) and Bun 1.3.14 (`bun src/cli.ts`):
|
|
|
122
122
|
- On Node 20 and Node 22 before 22.18, a `.ts` entry still warns and the run continues without that plugin.
|
|
123
123
|
- Syntax Node cannot strip (for example `enum`) warns and continues. Bun runs that same file with no plugin-load warning. A syntax error warns on both and the run continues.
|
|
124
124
|
|
|
125
|
-
The combat-commander reference is `examples/game_bridge/game_bridge.plugin.mjs`. Point `config.plugins` at `./examples/game_bridge/game_bridge.plugin.mjs` (relative to `work_dir`). See `examples/game_bridge/README.md
|
|
125
|
+
The combat-commander reference is `examples/game_bridge/game_bridge.plugin.mjs`. Point `config.plugins` at `./examples/game_bridge/game_bridge.plugin.mjs` (relative to `work_dir`). See [`examples/game_bridge/README.md`](https://github.com/Moikapy/lich/blob/main/examples/game_bridge/README.md). Embedding it beside a Godot game: [Godot guide](godot.md).
|
|
126
126
|
|
|
127
127
|
Plain JS (matches `engines.node` `>=20`):
|
|
128
128
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Redot guide
|
|
2
|
+
|
|
3
|
+
> What you'll learn: how to add an editor MCP server, and why that is the opposite of in-game play.
|
|
4
|
+
|
|
5
|
+
Editor MCP and in-game play point opposite ways. This page is lich connecting **to** an editor. NPC control, vetoes, and the game master are the game connecting **to** lich: the webhook and [`examples/game_bridge`](https://github.com/Moikapy/lich/blob/main/examples/game_bridge/README.md). That contract is the [Godot guide](godot.md). A game master is another persona, not an editor tool.
|
|
6
|
+
|
|
7
|
+
The client is general, the same shape as Hermes and Claude: a named list you extend when you need a server. Redot is a catalog entry on that client, not a hard-wired spawner. lich does not ship the engine and does not download an addon.
|
|
8
|
+
|
|
9
|
+
## Client
|
|
10
|
+
|
|
11
|
+
`mcp_servers` is a closed record. Omit it and no MCP tools register. Each name is snake_case. Each value is either stdio or loopback HTTP. `enabled` defaults to `false`. Unknown keys are rejected.
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"mcp_servers": {
|
|
16
|
+
"notes": {
|
|
17
|
+
"enabled": true,
|
|
18
|
+
"command": "/usr/local/bin/notes-mcp",
|
|
19
|
+
"args": ["--stdio"]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
stdio is a local binary you named, plus `args`, plus optional `env`. The process is spawned with those argv, never a shell. `npx`, `npm`, `bunx`, `uvx`, `curl`, `wget`, a URL, and shell metacharacters are refused. lich does not run `npx -y` or fetch an addon. Values in `env` are passed to the process and are never logged. `lich mcp add` has no flag for `env`; set that key in the file if you need it.
|
|
26
|
+
|
|
27
|
+
HTTP is `{ "url": "http://127.0.0.1:9/mcp" }`. The host must be `127.0.0.1` or `localhost`. `0.0.0.0` and any other host are refused. There is no remote MCP in v1.
|
|
28
|
+
|
|
29
|
+
On connect the client sends `initialize`, then `notifications/initialized`, then `tools/list`. `tools/call` runs only when the model invokes a registered tool. Registered names are `mcp_<server>_<tool>`, so two servers cannot collide. They appear only when that server is `enabled` and `tools_enabled` is `"all"` or lists the prefixed name. `tools_enabled: []` drops them even when the server is enabled, and does not connect. Plugin tools still register, including the gatekeeper's `git_commit`. The commander persona keeps `tools_enabled: []` and the `game_bridge` plugin only — `persona_config` does not copy `mcp_servers`.
|
|
30
|
+
|
|
31
|
+
This client is in this source (changelog 0.7.0, unreleased). The published npm package is 0.6.0 and does not include `lich mcp` or `mcp_servers`. From a clone, use `bun src/cli.ts mcp ...`. `lich --version` still prints `0.6.0`.
|
|
32
|
+
|
|
33
|
+
## Add a server
|
|
34
|
+
|
|
35
|
+
Paste an entry, call `catalog_client_entry`, or use the CLI. None of these start a process. New entries stay disabled until you enable them.
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
lich mcp add redot --project-path /home/me/game
|
|
39
|
+
lich mcp enable redot
|
|
40
|
+
lich mcp list
|
|
41
|
+
lich mcp disable redot
|
|
42
|
+
lich mcp remove redot
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`lich mcp add redot --project-path /home/me/game` writes a disabled entry whose `command` is `redot` and whose args are `--headless --mcp-server --path` plus that directory. PATH lookup happens at connect, not at add.
|
|
46
|
+
|
|
47
|
+
`lich mcp add notes --command /usr/local/bin/notes-mcp --arg --stdio` is a custom stdio server. `--url` must be loopback. The command writes through the existing config writer and leaves providers, plugins, and gateway untouched. `lich mcp list` reads this work dir's `.lich/config.json`. A missing `mcp_servers` prints nothing and exits 0.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { catalog_client_entry } from "@moikapy/lich";
|
|
51
|
+
|
|
52
|
+
catalog_client_entry("redot", { project_path: "/home/me/game" });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That returns a disabled stdio entry. Unknown names and the Godot catalog return an explanation instead of a spawn plan. Catalog files live in `optional-mcps/<name>/manifest.json`. A new server is a new manifest plus a config entry. It is not a new client.
|
|
56
|
+
|
|
57
|
+
## Redot
|
|
58
|
+
|
|
59
|
+
Official Redot 26.1+ only. The catalog command is the local `redot` binary. The args are exactly:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
redot --headless --mcp-server --path <project>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Basename must be `redot`. A missing binary tells you to install official Redot 26.1+ from [redotengine.org](https://redotengine.org). lich does not download a build.
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcp_servers": {
|
|
70
|
+
"redot": {
|
|
71
|
+
"enabled": true,
|
|
72
|
+
"command": "/usr/local/bin/redot",
|
|
73
|
+
"args": ["--headless", "--mcp-server", "--path", "/home/me/game"]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`tools/list` is what registers. The official server lists five controllers. The client drops a listed tool named `execute` (`exclude_tools`). It does not hard-allowlist those five names: any other listed tool is registered. Existing `.gd` files stay normal file edits (`edit_file` / `write_file`). Scene edits go through the scene tool. Do not rewrite `.tscn` as text.
|
|
80
|
+
|
|
81
|
+
| Registered name | Use it for |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `mcp_redot_scene_action` | `.tscn` edits: nodes, properties, instances, signals |
|
|
84
|
+
| `mcp_redot_resource_action` | `.tres` files and asset import metadata |
|
|
85
|
+
| `mcp_redot_code_intel` | GDScript validation and engine docs |
|
|
86
|
+
| `mcp_redot_project_config` | Input map, autoloads, settings, run and stop |
|
|
87
|
+
| `mcp_redot_game_control` | Screenshots, clicks, and the live scene tree |
|
|
88
|
+
|
|
89
|
+
## Play is the other direction
|
|
90
|
+
|
|
91
|
+
These tools build and inspect the project. They do not queue combat orders. NPC and game-master play still use one `POST /message` per round, then drain `work_dir/.lich/game/orders.jsonl` and refresh `state.json`. Do not call lich per frame.
|
|
92
|
+
|
|
93
|
+
Godot has no official MCP server. lich does not spawn a community godot-mcp and does not download an Asset Library addon. If you later point a **different** server name at a local binary you already have, that is a normal stdio entry. The catalog name `godot` writes no spawn plan. Play for both editors stays the [Godot guide](godot.md).
|
package/docs/user-guide/tui.md
CHANGED
|
@@ -9,22 +9,22 @@ lich # front door: TUI, plus a first-run setup wizard when no config exi
|
|
|
9
9
|
lich tui # same TUI, no wizard. From a clone: bun src/cli.ts tui
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
The TUI needs a TTY and a resolvable provider (same resolution as every mode). On startup it prints a dim header
|
|
12
|
+
The TUI needs a TTY and a resolvable provider (same resolution as every mode). On startup it prints a dim header from the active theme welcome string, e.g. `⚱ lich v0.6.0 — the agent that will not stay dead · llama3.2 (ollama)`. `{version}` is `LICH_VERSION` from `package.json`. That banner is the only tagline placement. Quit with `/exit`, `/quit`, `/q`, or Ctrl+C.
|
|
13
13
|
|
|
14
14
|
## Anatomy
|
|
15
15
|
|
|
16
16
|
```
|
|
17
|
-
lich v0.
|
|
18
|
-
|
|
17
|
+
⚱ lich v0.6.0 — the agent that will not stay dead · llama3.2 (ollama)
|
|
18
|
+
mortal › list the files here <- your input, echoed into the transcript
|
|
19
19
|
⏺ list_dir({}) <- live tool-call row (name + args preview)
|
|
20
20
|
⏷ list_dir: ok (d src/ d test/ ...) <- result row (ok/error + output preview)
|
|
21
|
-
lich › Here is what I found ... <- the agent's reply
|
|
22
|
-
model llama3.2 · turns 2 · tokens 1,204 · [
|
|
21
|
+
lich › Here is what I found ... <- the agent's reply (`response_label`)
|
|
22
|
+
model llama3.2 · turns 2 · tokens 1,204 · [dormant] · /path/.lich/sessions/...jsonl
|
|
23
23
|
› ▌ <- input row (cursor block)
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
- **Header** —
|
|
27
|
-
- **Transcript** — user lines (`
|
|
26
|
+
- **Header** — theme welcome string (version, model, provider kind). The tagline appears only here.
|
|
27
|
+
- **Transcript** — user lines (`mortal ›` by default), replies (`lich ›`, or the theme `response_label`), tool rows (`⏺ name(args)` with a result line), and meta notices (`· context compressed — memories distilled ...`, `· error: ...`). The view keeps the newest 50 blocks; older lines scroll out of the transcript (session JSONLs still hold everything — see [limitations](#known-limitations)).
|
|
28
28
|
- **Input row** — `› ` when idle, `… ` while the agent works; Enter submits, Backspace edits, pasted newlines collapse to spaces.
|
|
29
29
|
- **Status bar** — see below.
|
|
30
30
|
|
|
@@ -36,7 +36,7 @@ model llama3.2 · turns 2 · tokens 1,204 · [idle] · /path/.lich/sessions/...j
|
|
|
36
36
|
| `/model` | Show the active model and provider kind (from `providers[0]`). |
|
|
37
37
|
| `/usage` | Show tokens used this session (cumulative across turns). |
|
|
38
38
|
| `/clear` | Wipe the on-screen transcript. Does **not** reset agent memory — the next message still sees prior turns. |
|
|
39
|
-
| `/sessions` | List the 10 newest `.jsonl` files in `session_dir` with sizes. |
|
|
39
|
+
| `/sessions` | List the 10 newest `.jsonl` files in `session_dir` with sizes. The heading uses the theme sessions label (`phylacteries (n):` by default). |
|
|
40
40
|
| `/exit`, `/quit`, `/q` | Exit the TUI. |
|
|
41
41
|
|
|
42
42
|
Unknown commands print `· unknown command: /x (try /help)`. Slash commands are handled client-side and never invoke the model.
|
|
@@ -54,14 +54,14 @@ The bottom line shows, left to right:
|
|
|
54
54
|
| `model <name>` | First provider's model from config. |
|
|
55
55
|
| `turns N` | Turns used by the most recent run (resets each message). |
|
|
56
56
|
| `tokens N` | Cumulative session token usage (prompt + completion, across all turns). |
|
|
57
|
-
| `[
|
|
57
|
+
| `[dormant]` / `[deliberating]` / `[casting]` | Current phase: waiting for input, calling the model, or executing a tool. Labels come from the theme. |
|
|
58
58
|
| `compressed N` | How many times context compression fired this session (hidden when 0). |
|
|
59
59
|
| `<session path>` | Path of the newest persisted transcript (appears after the first run). |
|
|
60
|
-
| `· budget exhausted` | Red notice when a run hit the turn cap. |
|
|
60
|
+
| `· budget exhausted — the ritual is spent (turn cap reached)` | Red notice when a run hit the turn cap. The keyword stays; the flavor comes from the theme. |
|
|
61
61
|
|
|
62
62
|
## Multi-turn memory
|
|
63
63
|
|
|
64
|
-
The TUI keeps one conversation: every submitted message is sent together with the full prior message history, so the agent remembers earlier turns for as long as the session stays open. When estimated tokens cross `compress_threshold` of `context_budget_tokens`, older turns are replaced by an LLM-generated summary (the 8 most recent messages always stay verbatim) and a `· context compressed` notice appears. There is deliberately no per-conversation reset command — `/clear` only clears the display; start a fresh `lich tui` process for an empty context.
|
|
64
|
+
The TUI keeps one conversation: every submitted message is sent together with the full prior message history, so the agent remembers earlier turns for as long as the session stays open. When estimated tokens cross `compress_threshold` of `context_budget_tokens`, older turns are replaced by an LLM-generated summary (the 8 most recent messages always stay verbatim) and a `· context compressed — memories distilled` notice appears. There is deliberately no per-conversation reset command — `/clear` only clears the display; start a fresh `lich tui` process for an empty context.
|
|
65
65
|
|
|
66
66
|
## Known limitations
|
|
67
67
|
|
|
@@ -66,3 +66,5 @@ A round that does not match `state.json` is still appended. The tool output then
|
|
|
66
66
|
## Failures
|
|
67
67
|
|
|
68
68
|
Tools return `{ok: false, error}` and do not throw. Reads skip malformed JSONL lines. The first call creates `.lich/game/` if it is missing. `memory.jsonl` grows forever; only the read is capped.
|
|
69
|
+
|
|
70
|
+
Editor MCP builds the project. This plugin is the opposite direction: the game connects to lich. NPC and game-master play still drain `.lich/game/orders.jsonl` and the webhook. A game master is another persona, not an editor tool. See the [Redot guide](../../docs/user-guide/redot.md).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "godot",
|
|
3
|
+
"description": "Godot has no official MCP server. Do not download a community addon.",
|
|
4
|
+
"transport": "none",
|
|
5
|
+
"note": "Godot has no official MCP server. lich does not spawn a community addon. Play stays the webhook and examples/game_bridge: the game connects to lich. That is the opposite direction from editor MCP."
|
|
6
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "redot",
|
|
3
|
+
"description": "Official Redot 26.1 MCP server. lich does not ship the engine.",
|
|
4
|
+
"transport": "stdio",
|
|
5
|
+
"command": "redot",
|
|
6
|
+
"args": ["--headless", "--mcp-server", "--path", "${project_path}"],
|
|
7
|
+
"command_basename": "redot",
|
|
8
|
+
"args_prefix": ["--headless", "--mcp-server", "--path"],
|
|
9
|
+
"exclude_tools": ["execute"],
|
|
10
|
+
"missing_hint": "redot binary not found. Install official Redot 26.1+ from https://redotengine.org",
|
|
11
|
+
"tools": [
|
|
12
|
+
"scene_action",
|
|
13
|
+
"resource_action",
|
|
14
|
+
"code_intel",
|
|
15
|
+
"project_config",
|
|
16
|
+
"game_control"
|
|
17
|
+
]
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moikapy/lich",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Lich — a TypeScript AI agent harness (library + CLI) inspired by Hermes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"!docs/.vitepress/dist",
|
|
23
23
|
"!docs/.vitepress/cache",
|
|
24
24
|
"examples/game_bridge",
|
|
25
|
+
"optional-mcps",
|
|
25
26
|
"README.md",
|
|
26
27
|
"CHANGELOG.md"
|
|
27
28
|
],
|
package/dist/chunk-6M6OAQGN.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// src/index.ts
|
|
2
|
-
import { readFileSync } from "fs";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
function read_package_version() {
|
|
5
|
-
const pkg_path = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
6
|
-
const pkg = JSON.parse(readFileSync(pkg_path, "utf8"));
|
|
7
|
-
if (typeof pkg.version !== "string" || pkg.version.length === 0) {
|
|
8
|
-
throw new Error("package.json is missing version");
|
|
9
|
-
}
|
|
10
|
-
return pkg.version;
|
|
11
|
-
}
|
|
12
|
-
var LICH_VERSION = read_package_version();
|
|
13
|
-
|
|
14
|
-
export {
|
|
15
|
-
LICH_VERSION
|
|
16
|
-
};
|
|
17
|
-
//# sourceMappingURL=chunk-6M6OAQGN.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Public surface of the lich agent harness. Pure re-exports, plus the package version.\n */\nimport { readFileSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\n\nfunction read_package_version(): string {\n const pkg_path = fileURLToPath(new URL(\"../package.json\", import.meta.url));\n const pkg = JSON.parse(readFileSync(pkg_path, \"utf8\")) as { version?: unknown };\n if (typeof pkg.version !== \"string\" || pkg.version.length === 0) {\n throw new Error(\"package.json is missing version\");\n }\n return pkg.version;\n}\n\nexport const LICH_VERSION = read_package_version();\n\nexport { Agent, create_agent, run_agent, create_agent_with_plugins } from \"./agent/agent.js\";\nexport type { AgentRunOptions, AgentRunResult } from \"./agent/agent.js\";\nexport type { AgentConfig } from \"./agent/config.js\";\nexport { parse_agent_config } from \"./agent/config.js\";\nexport { AgentEmitter } from \"./agent/events.js\";\nexport type { AgentEvent, AgentEventHandler, AgentEvents } from \"./agent/events.js\";\nexport type { LoopOutcome, LoopParams } from \"./agent/loop.js\";\nexport type {\n ChatOptions,\n ChatResult,\n Message,\n ProviderConfig,\n ToolCall,\n ToolDefinition,\n Usage,\n} from \"./providers/types.js\";\nexport { ProviderError } from \"./providers/types.js\";\nexport { ToolExecutor } from \"./tools/executor.js\";\nexport { ToolRegistry } from \"./tools/registry.js\";\nexport type { Tool, ToolContext, ToolResult } from \"./tools/types.js\";\nexport { register_builtin_tools } from \"./tools/builtin/index.js\";\nexport { HookedToolRunner } from \"./plugins/hooks.js\";\nexport { load_plugins, plugin_errors_summary } from \"./plugins/loader.js\";\nexport type { LoadedPlugin, PluginLoadError } from \"./plugins/loader.js\";\nexport type {\n AfterToolCallInfo,\n BeforeToolCallInfo,\n BeforeToolCallResult,\n HookContext,\n Plugin,\n PluginHooks,\n RunEndInfo,\n} from \"./plugins/types.js\";"],"mappings":";AAGA,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAE9B,SAAS,uBAA+B;AACtC,QAAM,WAAW,cAAc,IAAI,IAAI,mBAAmB,YAAY,GAAG,CAAC;AAC1E,QAAM,MAAM,KAAK,MAAM,aAAa,UAAU,MAAM,CAAC;AACrD,MAAI,OAAO,IAAI,YAAY,YAAY,IAAI,QAAQ,WAAW,GAAG;AAC/D,UAAM,IAAI,MAAM,iCAAiC;AAAA,EACnD;AACA,SAAO,IAAI;AACb;AAEO,IAAM,eAAe,qBAAqB;","names":[]}
|