@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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Plugins
|
|
2
|
+
|
|
3
|
+
> What you'll learn: how to extend lich with your own tools and lifecycle hooks — a quickstart, the full hook reference, tool authoring, blocking semantics, error isolation, naming rules, and security notes.
|
|
4
|
+
|
|
5
|
+
## What plugins are
|
|
6
|
+
|
|
7
|
+
A plugin is a plain TypeScript (or JavaScript) module you keep in your repo that exports a `Plugin` object: a unique `name`, optional `tools` to merge into the agent's registry, and optional `hooks` that observe (and can veto) tool calls and run lifecycle events. Plugins load at agent startup from explicit paths listed in your config — no installation step, no registry service, just files you control.
|
|
8
|
+
|
|
9
|
+
Plugins are inspired by [Hermes](https://github.com/NousResearch/Hermes-Function-calling) style customization: the harness stays small; your repo grows around it.
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
Create the plugin module (defaults to `.lich/plugins/`, but any path works):
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// .lich/plugins/my-plugin.ts
|
|
17
|
+
import type { Plugin } from "lich";
|
|
18
|
+
|
|
19
|
+
const my_plugin: Plugin = {
|
|
20
|
+
name: "my-plugin",
|
|
21
|
+
tools: [],
|
|
22
|
+
hooks: {
|
|
23
|
+
on_run_start: async (_info, ctx) => {
|
|
24
|
+
console.log(`run starting in ${ctx.work_dir}`);
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default my_plugin;
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Point your config at it and restart:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"providers": [ ... ],
|
|
37
|
+
"plugins": ["./.lich/plugins/my-plugin.ts"]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Entry paths are relative to `work_dir` (or absolute). Restarting the agent reloads plugins — there is no hot reload.
|
|
42
|
+
|
|
43
|
+
## Hook reference
|
|
44
|
+
|
|
45
|
+
All hooks are awaited. Hook errors are logged as warnings and skipped — a broken hook never breaks the run.
|
|
46
|
+
|
|
47
|
+
| Hook | Signature | Purpose |
|
|
48
|
+
| --- | --- | --- |
|
|
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. |
|
|
51
|
+
| `on_run_start` | `(info: {input_chars}, ctx) => void` | Runs once before the conversation loop starts. |
|
|
52
|
+
| `on_run_end` | `(info: {stopped_reason, turns_used}, ctx) => void` | Runs once after the loop ends with the outcome. |
|
|
53
|
+
|
|
54
|
+
`ctx` is `{work_dir: string}` — the agent's working directory.
|
|
55
|
+
|
|
56
|
+
## Tool authoring
|
|
57
|
+
|
|
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
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import type { Plugin, Tool } from "lich";
|
|
62
|
+
|
|
63
|
+
const upper_tool: Tool = {
|
|
64
|
+
name: "upper_case",
|
|
65
|
+
description: "Uppercase a short piece of text.",
|
|
66
|
+
parameters: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
text: { type: "string", description: "Text to uppercase" },
|
|
70
|
+
},
|
|
71
|
+
required: ["text"],
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
},
|
|
74
|
+
execute: async (args, context) => {
|
|
75
|
+
const text = typeof args.text === "string" ? args.text : "";
|
|
76
|
+
return { ok: true, output: text.toUpperCase() };
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const upper_case_plugin: Plugin = {
|
|
81
|
+
name: "upper-case",
|
|
82
|
+
tools: [upper_tool],
|
|
83
|
+
};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Guidelines:
|
|
87
|
+
|
|
88
|
+
- `name` must be unique across builtins and all plugins; duplicates are warned and skipped at registration.
|
|
89
|
+
- Keep `description` crisp — the model chooses tools from it.
|
|
90
|
+
- Tool failures return `{ok: false, error}` rather than throwing; the loop turns that into an error tool message the model can react to.
|
|
91
|
+
|
|
92
|
+
## before_tool_call blocking semantics
|
|
93
|
+
|
|
94
|
+
Hooks run in plugin registration order (config array order). The **first hook to return `{block: true}` wins**; later hooks do not run for that call and the real executor is never invoked. The model receives a tool message with error `blocked_by_plugin: <reason>` (or `blocked_by_plugin: plugin-less` when no reason is given) and can adapt, end the run, or try another approach. Non-blocking return values (including `undefined`) fall through to the next hook.
|
|
95
|
+
|
|
96
|
+
Blocking is observe-then-veto, not middleware: you cannot rewrite `args` or the result, only allow or veto. This keeps v1 semantics predictable.
|
|
97
|
+
|
|
98
|
+
## Error isolation
|
|
99
|
+
|
|
100
|
+
Failures are contained at every layer:
|
|
101
|
+
|
|
102
|
+
- **Broken import** (missing file, syntax error, missing export): the loader records the entry as an error, logs one warning with `plugin_errors_summary`, and starts the agent without that plugin.
|
|
103
|
+
- **Duplicate plugin names**: later duplicates become error entries; the first registration wins.
|
|
104
|
+
- **Throwing hook**: logged as a warning; the run continues as if the hook did not exist.
|
|
105
|
+
- **Throwing tool**: the executor captures it and returns `{ok: false, error}` to the loop.
|
|
106
|
+
|
|
107
|
+
## Naming rules
|
|
108
|
+
|
|
109
|
+
- `plugin.name` is required, must be a non-empty string, and must be unique per agent.
|
|
110
|
+
- Tool `name`s share one namespace with builtin tools — pick a prefix unique to your plugin (e.g. `mycorp_`).
|
|
111
|
+
- Entry modules must have a module extension (`.ts`, `.js`, `.mjs`, `.mts`, `.cts`, `.jsx`, `.tsx`).
|
|
112
|
+
|
|
113
|
+
## Runtime notes
|
|
114
|
+
|
|
115
|
+
- **Bun** runs TypeScript plugin files natively — `.ts` entries just work (`bun src/cli.ts ...`).
|
|
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
|
+
|
|
118
|
+
## Security note
|
|
119
|
+
|
|
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.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# TUI guide
|
|
2
|
+
|
|
3
|
+
> What you'll learn: how to launch the terminal UI, read its layout and status bar, use slash commands and input history, and how memory and session persistence work across turns.
|
|
4
|
+
|
|
5
|
+
## Launching
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun src/cli.ts tui # or: lich tui
|
|
9
|
+
```
|
|
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.
|
|
12
|
+
|
|
13
|
+
## Anatomy
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
lich v0.2.0 — llama3.2 (ollama) <- header: version, model, provider kind
|
|
17
|
+
you › list the files here <- your input, echoed into the transcript
|
|
18
|
+
⏺ list_dir({}) <- live tool-call row (name + args preview)
|
|
19
|
+
⏷ list_dir: ok (d src/ d test/ ...) <- result row (ok/error + output preview)
|
|
20
|
+
lich › Here is what I found ... <- the agent's reply
|
|
21
|
+
model llama3.2 · turns 2 · tokens 1,204 · [idle] · /path/.lich/sessions/...jsonl
|
|
22
|
+
› ▌ <- input row (cursor block)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- **Header** — static version/model/provider line.
|
|
26
|
+
- **Transcript** — user lines (`you ›`), replies (`lich ›`), tool rows (`⏺ name(args)` with a result line), and meta notices (`· context compressed ...`, `· error: ...`). The view keeps the newest 50 blocks; older lines scroll out of the transcript (session JSONLs still hold everything — see [limitations](#known-limitations)).
|
|
27
|
+
- **Input row** — `› ` when idle, `… ` while the agent works; Enter submits, Backspace edits, pasted newlines collapse to spaces.
|
|
28
|
+
- **Status bar** — see below.
|
|
29
|
+
|
|
30
|
+
## Slash commands
|
|
31
|
+
|
|
32
|
+
| Command | Effect |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `/help` | Print the command list and key hints. |
|
|
35
|
+
| `/model` | Show the active model and provider kind (from `providers[0]`). |
|
|
36
|
+
| `/usage` | Show tokens used this session (cumulative across turns). |
|
|
37
|
+
| `/clear` | Wipe the on-screen transcript. Does **not** reset agent memory — the next message still sees prior turns. |
|
|
38
|
+
| `/sessions` | List the 10 newest `.jsonl` files in `session_dir` with sizes. |
|
|
39
|
+
| `/exit`, `/quit`, `/q` | Exit the TUI. |
|
|
40
|
+
|
|
41
|
+
Unknown commands print `· unknown command: /x (try /help)`. Slash commands are handled client-side and never invoke the model.
|
|
42
|
+
|
|
43
|
+
## Input history
|
|
44
|
+
|
|
45
|
+
Up/Down arrow keys walk a 20-entry recall ring of previously submitted lines (newest first); submitted duplicates move to the top instead of repeating. Recall position resets when you submit.
|
|
46
|
+
|
|
47
|
+
## Status bar
|
|
48
|
+
|
|
49
|
+
The bottom line shows, left to right:
|
|
50
|
+
|
|
51
|
+
| Segment | Meaning |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `model <name>` | First provider's model from config. |
|
|
54
|
+
| `turns N` | Turns used by the most recent run (resets each message). |
|
|
55
|
+
| `tokens N` | Cumulative session token usage (prompt + completion, across all turns). |
|
|
56
|
+
| `[idle]` / `[thinking]` / `[tool]` | Current phase: waiting for input, calling the model, or executing a tool. |
|
|
57
|
+
| `compressed N` | How many times context compression fired this session (hidden when 0). |
|
|
58
|
+
| `<session path>` | Path of the newest persisted transcript (appears after the first run). |
|
|
59
|
+
| `· budget exhausted` | Red notice when a run hit the turn cap. |
|
|
60
|
+
|
|
61
|
+
## Multi-turn memory
|
|
62
|
+
|
|
63
|
+
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
|
+
|
|
65
|
+
## Known limitations
|
|
66
|
+
|
|
67
|
+
- The transcript keeps only the newest 50 blocks; older context scrolls away (it remains in the session file).
|
|
68
|
+
- No token streaming: replies appear once the model finishes the turn.
|
|
69
|
+
- No per-conversation reset — `/clear` is cosmetic; restart the TUI to reset memory.
|
|
70
|
+
- Tool output previews are truncated to 120 chars per row; read the session JSONL for full output.
|
|
71
|
+
|
|
72
|
+
## Tips
|
|
73
|
+
|
|
74
|
+
- Use `/clear` when the transcript is visually noisy and you want to keep reading from the top — memory is unaffected.
|
|
75
|
+
- After a long session, inspect what actually happened: `jq -r 'select(.kind=="message") | "\(.message.role): \(.message.content // "(tool call)")"' .lich/sessions/<newest>-tui.jsonl`.
|
|
76
|
+
- Watch `compressed N` in the status bar: if it climbs steadily in short sessions, lower `context_budget_tokens` or expect summarization of older details (file paths survive compression; long verbatim outputs do not).
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@moikapy/lich",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Lich — a TypeScript AI agent harness (library + CLI) inspired by Hermes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/moikapy/lich.git"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=20"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"bin": {
|
|
17
|
+
"lich": "./dist/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"docs",
|
|
22
|
+
"!docs/.vitepress/dist",
|
|
23
|
+
"!docs/.vitepress/cache",
|
|
24
|
+
"README.md",
|
|
25
|
+
"CHANGELOG.md"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "node node_modules/tsup/dist/cli-default.js src/index.ts src/cli.ts --format esm --dts --clean --sourcemap",
|
|
32
|
+
"typecheck": "bun x tsc --noEmit",
|
|
33
|
+
"test": "node node_modules/vitest/vitest.mjs run",
|
|
34
|
+
"test:watch": "node node_modules/vitest/vitest.mjs",
|
|
35
|
+
"cli": "bun src/cli.ts",
|
|
36
|
+
"docs:dev": "vitepress dev docs",
|
|
37
|
+
"docs:build": "vitepress build docs",
|
|
38
|
+
"docs:preview": "vitepress preview docs"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"ink": "^6.0.0",
|
|
42
|
+
"react": "^19.0.0",
|
|
43
|
+
"zod": "^3.25.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.10.0",
|
|
47
|
+
"@types/react": "^19.0.0",
|
|
48
|
+
"tsup": "^8.3.0",
|
|
49
|
+
"tsx": "^4.19.0",
|
|
50
|
+
"typescript": "^5.6.0",
|
|
51
|
+
"vitest": "^3.0.0",
|
|
52
|
+
"vitepress": "^1.6.4"
|
|
53
|
+
}
|
|
54
|
+
}
|