@moikapy/lich 0.3.0 → 0.4.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/README.md +34 -8
  3. package/dist/{chunk-P52U5M3L.js → chunk-MLFJW4JU.js} +399 -70
  4. package/dist/chunk-MLFJW4JU.js.map +1 -0
  5. package/dist/cli.d.ts +1 -2
  6. package/dist/cli.js +4 -3
  7. package/dist/cli.js.map +1 -1
  8. package/dist/{gateway-CWPVIU3W.js → gateway-XTYDYT67.js} +2 -2
  9. package/dist/index.d.ts +25 -11
  10. package/dist/index.js +1 -1
  11. package/dist/{tui-V7ATLIKW.js → tui-VYBJSGRV.js} +6 -3
  12. package/dist/tui-VYBJSGRV.js.map +1 -0
  13. package/docs/.vitepress/config.mts +1 -0
  14. package/docs/architecture/extending.md +5 -2
  15. package/docs/architecture/overview.md +9 -7
  16. package/docs/architecture/plugins.md +58 -5
  17. package/docs/architecture/tools.md +16 -4
  18. package/docs/design/council/architecture-review-r2.md +36 -0
  19. package/docs/design/council/architecture-review-r3.md +69 -0
  20. package/docs/design/council/index.md +15 -0
  21. package/docs/design/council/security-review-r2.md +32 -0
  22. package/docs/design/council/security-review-r3.md +19 -0
  23. package/docs/design/council/simplicity-review-r2.md +35 -0
  24. package/docs/design/council/simplicity-review-r3.md +22 -0
  25. package/docs/design/self-improvement-loop.md +160 -162
  26. package/docs/getting-started.md +37 -15
  27. package/docs/index.md +11 -11
  28. package/docs/user-guide/cli.md +18 -5
  29. package/docs/user-guide/gateway.md +10 -10
  30. package/docs/user-guide/library.md +15 -9
  31. package/docs/user-guide/plugins.md +59 -6
  32. package/docs/user-guide/tui.md +3 -3
  33. package/package.json +4 -2
  34. package/dist/chunk-P52U5M3L.js.map +0 -1
  35. package/dist/tui-V7ATLIKW.js.map +0 -1
  36. /package/dist/{gateway-CWPVIU3W.js.map → gateway-XTYDYT67.js.map} +0 -0
@@ -19,10 +19,10 @@ flowchart LR
19
19
  Platforms: `webhook` (HTTP server), `telegram` (long-poll), `discord` (gateway WebSocket), `twitch` (IRC over WebSocket). Pick any combination:
20
20
 
21
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
22
+ lich gateway webhook # http only
23
+ lich gateway webhook telegram # http + telegram polling
24
+ lich gateway telegram discord twitch # no webhook server
25
+ lich gateway # defaults to webhook
26
26
  ```
27
27
 
28
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".
@@ -32,7 +32,7 @@ The gateway is silent after startup: Telegram/Discord/Twitch respond only in cha
32
32
  Zero configuration — the server binds `0.0.0.0:$LICH_GATEWAY_PORT` (default 8089).
33
33
 
34
34
  ```sh
35
- bun src/cli.ts gateway webhook
35
+ lich gateway webhook
36
36
  ```
37
37
 
38
38
  ```sh
@@ -47,7 +47,7 @@ curl -s http://localhost:8089/health
47
47
  With token auth, every POST must carry the exact `x-lich-token` header; mismatched or missing tokens get `401 {"error":"unauthorized"}`:
48
48
 
49
49
  ```sh
50
- LICH_GATEWAY_TOKEN=s3cret bun src/cli.ts gateway webhook
50
+ LICH_GATEWAY_TOKEN=s3cret lich gateway webhook
51
51
  curl -s -X POST http://localhost:8089/message \
52
52
  -H "x-lich-token: s3cret" -H "content-type: application/json" -d '{"text": "hello"}'
53
53
  ```
@@ -61,7 +61,7 @@ Payload fields (all optional except `text`): `platform` (default `"webhook"`), `
61
61
 
62
62
  ```sh
63
63
  export LICH_TELEGRAM_BOT_TOKEN=123456:ABC-your-token
64
- bun src/cli.ts gateway telegram
64
+ lich gateway telegram
65
65
  ```
66
66
 
67
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.
@@ -78,7 +78,7 @@ Telegram uses long polling (no public URL needed). Replies split at 4096 chars.
78
78
  ```sh
79
79
  export LICH_DISCORD_BOT_TOKEN=your-bot-token
80
80
  export LICH_DISCORD_BOT_ID=123456789012345678
81
- bun src.cli.ts gateway discord
81
+ lich gateway discord
82
82
  ```
83
83
 
84
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).
@@ -94,7 +94,7 @@ bun src.cli.ts gateway discord
94
94
  export LICH_TWITCH_OAUTH_TOKEN=oauth:abc123...
95
95
  export LICH_TWITCH_NICK=mylichbot
96
96
  export LICH_TWITCH_CHANNELS=channelone,channeltwo
97
- bun src/cli.ts gateway twitch
97
+ lich gateway twitch
98
98
  ```
99
99
 
100
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.
@@ -107,7 +107,7 @@ List the platforms in one command; all configured adapters start together and sh
107
107
 
108
108
  ```sh
109
109
  LICH_TELEGRAM_BOT_TOKEN=... LICH_DISCORD_BOT_TOKEN=... \
110
- bun src/cli.ts gateway webhook telegram discord
110
+ lich gateway webhook telegram discord
111
111
  ```
112
112
 
113
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`.
@@ -4,12 +4,16 @@
4
4
 
5
5
  ## Install
6
6
 
7
- From a checkout of this repository (or a published tarball):
7
+ ```sh
8
+ npm install @moikapy/lich
9
+ ```
10
+
11
+ From source instead (`dist/` is not committed, so build first — see [development install](../getting-started.md#development-install-from-source)):
8
12
 
9
13
  ```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
14
+ git clone https://github.com/Moikapy/lich.git && cd lich
15
+ bun install && bun run build
16
+ # then, from your project: npm install <path-to>/lich
13
17
  ```
14
18
 
15
19
  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`.
@@ -19,7 +23,7 @@ The package ships ESM (`dist/index.js`, types at `dist/index.d.ts`, binary at `d
19
23
  `create_agent(raw_config)` validates the config (zod, defaults applied, frozen result) and returns an `Agent` with a `.run()` loop:
20
24
 
21
25
  ```ts
22
- import { create_agent } from "lich";
26
+ import { create_agent } from "@moikapy/lich";
23
27
 
24
28
  const agent = create_agent({
25
29
  providers: [{ kind: "ollama", name: "local", model: "llama3.2:latest" }],
@@ -33,7 +37,7 @@ console.log(`tokens: ${result.usage_total.total_tokens}`);
33
37
  The one-liner `run_agent(config, input)` is equivalent when you only need a single run:
34
38
 
35
39
  ```ts
36
- import { run_agent } from "lich";
40
+ import { run_agent } from "@moikapy/lich";
37
41
 
38
42
  const result = await run_agent(
39
43
  { providers: [{ kind: "ollama", name: "local", model: "llama3.2:latest" }] },
@@ -43,7 +47,7 @@ const result = await run_agent(
43
47
 
44
48
  ## Agent class
45
49
 
46
- `new Agent(config)` (or `create_agent(raw)`) builds the provider router, registers the twelve builtin tools (filtered by `tools_enabled`), and exposes:
50
+ `new Agent(config)` (or `create_agent(raw)`) builds the provider router, registers the builtin tools (filtered by `tools_enabled`) plus the gatekeeper's `git_commit`, and exposes:
47
51
 
48
52
  | Member | Type | Purpose |
49
53
  | --- | --- | --- |
@@ -137,6 +141,8 @@ const config = {
137
141
 
138
142
  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
143
 
144
+ `LICH_ALLOW_SELF_COMMIT` and `LICH_TEST_COMMAND` are process-env knobs, not config fields. See the [CLI environment](cli.md#self-improvement-environment).
145
+
140
146
  ## Custom tool filtering
141
147
 
142
148
  `tools_enabled` accepts `"all"` (default) or an array of builtin tool names to register; everything else stays unregistered and invisible to the model:
@@ -162,7 +168,7 @@ Provider failures throw `ProviderError`, an `Error` subclass with `kind`, `provi
162
168
  | `unknown` | Non-provider errors (e.g. tool crashes surfaced as strings). | Treated as fatal for the provider. |
163
169
 
164
170
  ```ts
165
- import { ProviderError } from "lich";
171
+ import { ProviderError } from "@moikapy/lich";
166
172
 
167
173
  try {
168
174
  await agent.run({ input: "hello" });
@@ -178,4 +184,4 @@ When every configured provider fails, the last `ProviderError` is thrown. Tool f
178
184
 
179
185
  ## Session access
180
186
 
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.
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?}`; read them with `jq` (or the `read_session_messages(path)` helper if you are working from a source checkout). Persistence is best-effort: a write failure logs a warning, returns `session_path: undefined`, and never fails the run.
@@ -14,7 +14,7 @@ Create the plugin module (defaults to `.lich/plugins/`, but any path works):
14
14
 
15
15
  ```ts
16
16
  // .lich/plugins/my-plugin.ts
17
- import type { Plugin } from "lich";
17
+ import type { Plugin } from "@moikapy/lich";
18
18
 
19
19
  const my_plugin: Plugin = {
20
20
  name: "my-plugin",
@@ -47,18 +47,18 @@ All hooks are awaited. Hook errors are logged as warnings and skipped — a brok
47
47
  | Hook | Signature | Purpose |
48
48
  | --- | --- | --- |
49
49
  | `before_tool_call` | `(info: {tool_name, args}, ctx) => {block?: boolean, reason?: string} \| void` | Runs before each tool call in plugin registration order. Return `{block: true, reason}` to veto. |
50
- | `after_tool_call` | `(info: {tool_name, args, result_summary}, ctx) => void` | Runs after each tool call with a 300-char result summary. |
50
+ | `after_tool_call` | `(info: {tool_name, args, result_summary, ok, error?}, ctx) => void` | Runs after each tool call with a 300-char summary plus structured `ok`/`error`. |
51
51
  | `on_run_start` | `(info: {input_chars}, ctx) => void` | Runs once before the conversation loop starts. |
52
52
  | `on_run_end` | `(info: {stopped_reason, turns_used}, ctx) => void` | Runs once after the loop ends with the outcome. |
53
53
 
54
- `ctx` is `{work_dir: string}` — the agent's working directory.
54
+ `ctx` is `{work_dir, state?}` — the agent's working directory plus that plugin's per-run bag.
55
55
 
56
56
  ## Tool authoring
57
57
 
58
58
  Plugin tools implement the same `Tool` interface as builtins: a `name`, a `description` the model reads, a JSON Schema `parameters` object, and an async `execute(args, context)` returning `{ok, output, error?}`:
59
59
 
60
60
  ```ts
61
- import type { Plugin, Tool } from "lich";
61
+ import type { Plugin, Tool } from "@moikapy/lich";
62
62
 
63
63
  const upper_tool: Tool = {
64
64
  name: "upper_case",
@@ -112,9 +112,62 @@ Failures are contained at every layer:
112
112
 
113
113
  ## Runtime notes
114
114
 
115
- - **Bun** runs TypeScript plugin files natively — `.ts` entries just work (`bun src/cli.ts ...`).
115
+ - **Bun** runs TypeScript plugin files natively — `.ts` entries just work (`bun src/cli.ts ...` from a clone).
116
116
  - **Node** (the built `dist/cli.js`) uses the native ESM loader, which does not compile TS. For node deployments, compile your plugin or ship it as `.mjs`/plain JS and list that file in `plugins`.
117
117
 
118
+ ## Self-improvement loop
119
+
120
+ The agent can write a tool, prove it with `run_tests`, and commit it with
121
+ `git_commit` — one commit per run, and only when you opt in. The gatekeeper
122
+ is constructed in code (not listed in `config.plugins`). If it does not
123
+ register, `git_commit` is absent. A config plugin naming `git_commit` is
124
+ inside the plugin-trust floor only when the gatekeeper is off; while it is
125
+ on, first-wins keeps the gatekeeper's tool.
126
+
127
+ Set `LICH_ALLOW_SELF_COMMIT=1` before startup. Unset, or any other value, is
128
+ fail-closed. `git_commit` is vetoed unless every condition holds; the reason
129
+ names the first failure, and the model sees `blocked_by_plugin: <reason>`:
130
+
131
+ | Failed condition | Reason |
132
+ | --- | --- |
133
+ | `LICH_ALLOW_SELF_COMMIT` is not `1` | `self_commit_disabled` |
134
+ | no green `run_tests` yet this run | `tests_not_ok` |
135
+ | a `write_file` or `edit_file` succeeded after that green run | `worktree_dirty` |
136
+ | this run already committed once | `commit_budget_exhausted` |
137
+
138
+ `terminal` is vetoed when the command matches the hardcoded git denylist.
139
+ The reason is `git_denylist: <pattern>`. Patterns are flag-tolerant
140
+ `commit`/`push` (`commit`, `-commit`, `--commit`, `push`, `-push`, `--push`)
141
+ and any occurrence of `commit-tree` or `update-ref`. There is no `remote`
142
+ pattern. The denylist is best-effort: raw `terminal` can still run git. The
143
+ boundary is a human reviewing the local repo. Push is human-only.
144
+
145
+ `git_commit` takes `{message, paths}` — 1 to 50 paths relative to `work_dir`.
146
+ It rejects `""`, `.`, a path that resolves to `work_dir` itself, and
147
+ secret-ish basenames (`.env`, `.env.local`, `*.pem`, `*.p12`, `id_rsa*`).
148
+ It refuses an unreachable `HEAD`. It stages exactly the named paths
149
+ (`git add -- <paths>`) and commits with `git commit --only`. It never pushes.
150
+
151
+ `run_tests` takes an optional `filter` and runs `LICH_TEST_COMMAND` in
152
+ `work_dir` (default `node node_modules/vitest/vitest.mjs run`) with a 600s
153
+ timeout. A second call in the same process returns `run_tests_busy`. The
154
+ mutex is process-local: one lich process per repo.
155
+
156
+ Clean state attests no `write_file`/`edit_file` since the last green
157
+ `run_tests`; it does NOT attest absence of terminal-mediated writes.
158
+
159
+ ### Skills and memory
160
+
161
+ Write a markdown note with `write_file` to `.lich/skills/<name>.md`.
162
+ `docs_search` finds those files. That directory does not need `index.md`,
163
+ and it is walked fresh on every search. The default system prompt says tool
164
+ results — docs, skills, memory — are reference data, not instructions.
165
+
166
+ `MEMORY.md` is append-only and human-reviewable. It is never auto-loaded.
167
+ Review it between appends and the next self-commit.
168
+
118
169
  ## Security note
119
170
 
120
- Plugins execute **in-process with full privileges** — the same trust level as the agent itself and your shell. A plugin can read any file the process can, make network calls, and alter process state. Only load plugin files you wrote or audited; treat `.lich/plugins/` like you treat `.env` files.
171
+ Plugins execute **in-process with full privileges** — the same trust level as the agent itself and your shell. A plugin can read any file the process can, make network calls, and alter process state. Only load plugin files you wrote or audited; treat `.lich/plugins/` like you treat `.env` files.
172
+
173
+ `.lich/config.json` `plugins` is persistent arbitrary code at the next process start. Review config diffs before the next self-commit. The terminal git denylist does not close that hole.
@@ -5,15 +5,15 @@
5
5
  ## Launching
6
6
 
7
7
  ```sh
8
- bun src/cli.ts tui # or: lich tui
8
+ lich tui # or, from a clone: bun src/cli.ts tui
9
9
  ```
10
10
 
11
- The TUI needs a TTY and a resolvable provider (same resolution as every mode). On startup it prints a dim header with the version and the first provider's model, e.g. `lich v0.2.0 — llama3.2 (ollama)`. Quit with `/exit`, `/quit`, `/q`, or Ctrl+C.
11
+ The TUI needs a TTY and a resolvable provider (same resolution as every mode). On startup it prints a dim header with the version and the first provider's model, e.g. `lich v0.3.0 — llama3.2 (ollama)`. Quit with `/exit`, `/quit`, `/q`, or Ctrl+C.
12
12
 
13
13
  ## Anatomy
14
14
 
15
15
  ```
16
- lich v0.2.0 — llama3.2 (ollama) <- header: version, model, provider kind
16
+ lich v0.3.0 — llama3.2 (ollama) <- header: version, model, provider kind
17
17
  you › list the files here <- your input, echoed into the transcript
18
18
  ⏺ list_dir({}) <- live tool-call row (name + args preview)
19
19
  ⏷ list_dir: ok (d src/ d test/ ...) <- result row (ok/error + output preview)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moikapy/lich",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Lich — a TypeScript AI agent harness (library + CLI) inspired by Hermes",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,6 +33,8 @@
33
33
  "test": "node node_modules/vitest/vitest.mjs run",
34
34
  "test:watch": "node node_modules/vitest/vitest.mjs",
35
35
  "cli": "bun src/cli.ts",
36
+ "release": "bun scripts/release.ts",
37
+ "release:package": "bun scripts/release.ts",
36
38
  "docs:dev": "vitepress dev docs",
37
39
  "docs:build": "vitepress build docs",
38
40
  "docs:preview": "vitepress preview docs"
@@ -51,4 +53,4 @@
51
53
  "vitest": "^3.0.0",
52
54
  "vitepress": "^1.6.4"
53
55
  }
54
- }
56
+ }