@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.
- package/CHANGELOG.md +35 -1
- package/README.md +34 -8
- package/dist/{chunk-P52U5M3L.js → chunk-MLFJW4JU.js} +399 -70
- package/dist/chunk-MLFJW4JU.js.map +1 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +4 -3
- package/dist/cli.js.map +1 -1
- package/dist/{gateway-CWPVIU3W.js → gateway-XTYDYT67.js} +2 -2
- package/dist/index.d.ts +25 -11
- package/dist/index.js +1 -1
- package/dist/{tui-V7ATLIKW.js → tui-VYBJSGRV.js} +6 -3
- package/dist/tui-VYBJSGRV.js.map +1 -0
- package/docs/.vitepress/config.mts +1 -0
- package/docs/architecture/extending.md +5 -2
- package/docs/architecture/overview.md +9 -7
- package/docs/architecture/plugins.md +58 -5
- package/docs/architecture/tools.md +16 -4
- package/docs/design/council/architecture-review-r2.md +36 -0
- package/docs/design/council/architecture-review-r3.md +69 -0
- package/docs/design/council/index.md +15 -0
- package/docs/design/council/security-review-r2.md +32 -0
- package/docs/design/council/security-review-r3.md +19 -0
- package/docs/design/council/simplicity-review-r2.md +35 -0
- package/docs/design/council/simplicity-review-r3.md +22 -0
- package/docs/design/self-improvement-loop.md +160 -162
- package/docs/getting-started.md +37 -15
- package/docs/index.md +11 -11
- package/docs/user-guide/cli.md +18 -5
- package/docs/user-guide/gateway.md +10 -10
- package/docs/user-guide/library.md +15 -9
- package/docs/user-guide/plugins.md +59 -6
- package/docs/user-guide/tui.md +3 -3
- package/package.json +4 -2
- package/dist/chunk-P52U5M3L.js.map +0 -1
- package/dist/tui-V7ATLIKW.js.map +0 -1
- /package/dist/{gateway-CWPVIU3W.js.map → gateway-XTYDYT67.js.map} +0 -0
package/dist/cli.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { }
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import {
|
|
2
3
|
LICH_VERSION
|
|
3
4
|
} from "./chunk-ZVK3MUPC.js";
|
|
@@ -5,7 +6,7 @@ import {
|
|
|
5
6
|
create_agent,
|
|
6
7
|
parse_agent_config,
|
|
7
8
|
safe_json_parse
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-MLFJW4JU.js";
|
|
9
10
|
|
|
10
11
|
// src/cli.ts
|
|
11
12
|
import { readFileSync as readFileSync2 } from "fs";
|
|
@@ -345,11 +346,11 @@ async function run_chat(config) {
|
|
|
345
346
|
}
|
|
346
347
|
}
|
|
347
348
|
async function run_tui_entry(config) {
|
|
348
|
-
const { run_tui } = await import("./tui-
|
|
349
|
+
const { run_tui } = await import("./tui-VYBJSGRV.js");
|
|
349
350
|
return run_tui(config);
|
|
350
351
|
}
|
|
351
352
|
async function run_gateway_entry(config, platforms) {
|
|
352
|
-
const { run_gateway } = await import("./gateway-
|
|
353
|
+
const { run_gateway } = await import("./gateway-XTYDYT67.js");
|
|
353
354
|
return run_gateway(config, platforms.length === 0 ? ["webhook"] : platforms);
|
|
354
355
|
}
|
|
355
356
|
function is_main_module() {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/cli_config.ts"],"sourcesContent":["/**\n * Thin zero-dependency CLI for lich: one-shot tasks, interactive chat,\n * config files, and environment-based provider resolution.\n */\nimport { readFileSync } from \"node:fs\";\nimport { createInterface, type Interface } from \"node:readline\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { create_agent, type Agent, type AgentRunResult } from \"./agent/agent.js\";\nimport { parse_agent_config, type AgentConfig } from \"./agent/config.js\";\nimport { AgentEmitter } from \"./agent/events.js\";\nimport { LICH_VERSION } from \"./index.js\";\nimport { load_config, config_template } from \"./cli_config.js\";\n\ntype ProviderKind = \"openai_compat\" | \"anthropic\" | \"ollama\";\n\ninterface CliOptions {\n config_path?: string;\n help?: boolean;\n version?: boolean;\n overrides: Record<string, string>;\n positionals: string[];\n}\n\nconst FLAG_KEYS: Record<string, string> = {\n \"--work-dir\": \"work_dir\",\n \"--max-turns\": \"max_turns\",\n \"--model\": \"provider_model\",\n \"--provider-kind\": \"provider_kind\",\n \"--base-url\": \"provider_base_url\",\n \"--api-key-env\": \"provider_api_key_env\",\n \"--system-prompt\": \"system_prompt\",\n \"--session-dir\": \"session_dir\",\n \"--log-level\": \"log_level\",\n};\n\nconst DEFAULT_BASE_URLS: Record<ProviderKind, string> = {\n openai_compat: \"https://api.openai.com/v1\",\n anthropic: \"https://api.anthropic.com\",\n ollama: \"http://localhost:11434\",\n};\n\nconst DEFAULT_ENV_API_KEYS: Record<ProviderKind, string | undefined> = {\n openai_compat: \"OPENAI_API_KEY\",\n anthropic: \"ANTHROPIC_API_KEY\",\n ollama: undefined, // ollama needs no api key\n};\n\nfunction usage_text(): string {\n return [\n \"lich — a TypeScript AI agent harness\",\n \"\",\n \"Usage:\",\n ' lich \"one shot task\" run a single task and print the reply',\n \" lich chat interactive chat (commands: /exit, /quit)\",\n \" lich tui interactive terminal UI (ink)\",\n \" lich gateway <plat..> messaging gateway (webhook|telegram|discord|twitch)\",\n \" lich config print a starter config template (save as .lich/config.json)\",\n \" lich --help show this help\",\n \" lich --version print version\",\n \"\",\n \"Flags (before or after the subcommand):\",\n \" --config <path> JSON config file parsed by parse_agent_config\",\n \" --work-dir <path> working directory for tools\",\n \" --max-turns <n> loop turn budget (default 25)\",\n \" --model <m> model name (default from LICH_MODEL)\",\n \" --provider-kind <k> openai_compat | anthropic | ollama (default LICH_PROVIDER_KIND)\",\n \" --base-url <u> provider base url (default LICH_BASE_URL)\",\n \" --api-key-env <NAME> env var holding the api key (default LICH_API_KEY_ENV; unused by ollama)\",\n \" --system-prompt <s> system prompt override\",\n \" --session-dir <path> session transcript directory\",\n \" --log-level <level> debug | info | warn | error\",\n ].join(\"\\n\");\n}\n\nfunction error_message(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Mode-aware config failure message; one-shot keeps the generic variant. */\nfunction error_for_mode(mode: string, base_message: string): string {\n if (mode === \"tui\") {\n return \"lich tui: no model configured — set LICH_MODEL (e.g. glm-5.3-flash:cloud), pass --model, or create .lich/config.json (`lich config` prints a template)\";\n }\n if (mode === \"gateway\") {\n return \"lich gateway: no model configured — set LICH_MODEL (e.g. glm-5.3-flash:cloud), pass --model, or create .lich/config.json (`lich config` prints a template)\";\n }\n if (mode === \"chat\") {\n return \"lich chat: no model configured — set LICH_MODEL (e.g. glm-5.3-flash:cloud), pass --model, or create .lich/config.json (`lich config` prints a template)\";\n }\n return base_message;\n}\n\nfunction parse_args(argv: string[]): CliOptions {\n const options: CliOptions = { overrides: {}, positionals: [] };\n for (let index = 0; index < argv.length; index += 1) {\n const arg = argv[index];\n if (arg === undefined) {\n break;\n }\n if (arg === \"--help\") {\n options.help = true;\n continue;\n }\n if (arg === \"--version\") {\n options.version = true;\n continue;\n }\n if (arg === \"--config\") {\n const value = argv[index + 1];\n if (value === undefined) {\n throw new Error(\"--config requires a path\");\n }\n options.config_path = value;\n index += 1;\n continue;\n }\n const override_key = FLAG_KEYS[arg];\n if (override_key === undefined) {\n if (arg.startsWith(\"--\") === true) {\n throw new Error(`unknown flag: ${arg}`);\n }\n options.positionals.push(arg);\n continue;\n }\n const value = argv[index + 1];\n if (value === undefined || value.startsWith(\"--\") === true) {\n throw new Error(`${arg} requires a value`);\n }\n options.overrides[override_key] = value;\n index += 1;\n }\n return options;\n}\n\nfunction env_provider(overrides: Record<string, string>): Record<string, unknown> {\n const kind_value = overrides[\"provider_kind\"] ?? process.env.LICH_PROVIDER_KIND ?? \"openai_compat\";\n if (kind_value !== \"openai_compat\" && kind_value !== \"anthropic\" && kind_value !== \"ollama\") {\n throw new Error(`unknown provider kind \"${kind_value}\" (expected openai_compat, anthropic, or ollama)`);\n }\n const kind: ProviderKind = kind_value;\n const model = overrides[\"provider_model\"] ?? process.env.LICH_MODEL;\n if (model === undefined || model.length === 0) {\n throw new Error(\"no model configured: set LICH_MODEL, pass --model, or create .lich/config.json (`lich config` prints a template)\");\n }\n return {\n kind,\n name: \"default\",\n model,\n base_url: overrides[\"provider_base_url\"] ?? process.env.LICH_BASE_URL ?? DEFAULT_BASE_URLS[kind],\n api_key_env: overrides[\"provider_api_key_env\"] ?? process.env.LICH_API_KEY_ENV ?? DEFAULT_ENV_API_KEYS[kind],\n };\n}\n\nfunction load_config_file(config_path: string): Record<string, unknown> {\n try {\n const raw = readFileSync(config_path, \"utf8\");\n const parsed = JSON.parse(raw) as unknown;\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(\"config root must be a JSON object\");\n }\n return parsed as Record<string, unknown>;\n } catch (error) {\n throw new Error(`cannot use config file ${config_path}: ${error_message(error)}`);\n }\n}\n\nfunction apply_provider_override(config: Record<string, unknown>, overrides: Record<string, string>): void {\n const keys = [\"provider_kind\", \"provider_model\", \"provider_base_url\", \"provider_api_key_env\"] as const;\n const any_present = keys.some((key) => overrides[key] !== undefined);\n if (any_present === false) {\n return;\n }\n const providers = config[\"providers\"];\n if (Array.isArray(providers) === false || providers.length === 0) {\n config[\"providers\"] = [env_provider(overrides)];\n return;\n }\n const first = providers[0];\n if (typeof first !== \"object\" || first === null) {\n throw new Error(\"config providers[0] must be an object\");\n }\n const provider = first as Record<string, unknown>;\n for (const key of keys) {\n const value = overrides[key];\n if (value !== undefined) {\n provider[key.replace(\"provider_\", \"\")] = value;\n }\n }\n}\n\nfunction apply_overrides(config: Record<string, unknown>, overrides: Record<string, string>): void {\n for (const key of [\"work_dir\", \"system_prompt\", \"session_dir\", \"log_level\"]) {\n if (overrides[key] !== undefined) {\n config[key] = overrides[key];\n }\n }\n if (overrides[\"max_turns\"] !== undefined) {\n const turns = Number(overrides[\"max_turns\"]);\n if (Number.isInteger(turns) === false || turns < 1) {\n throw new Error(\"--max-turns must be a positive integer\");\n }\n config[\"max_turns\"] = turns;\n }\n apply_provider_override(config, overrides);\n}\n\nfunction build_config(options: CliOptions): AgentConfig {\n const base =\n options.config_path === undefined ? load_config() ?? { providers: [env_provider(options.overrides)] } : load_config_file(options.config_path);\n apply_overrides(base, options.overrides);\n const providers = base[\"providers\"];\n if (Array.isArray(providers) === false || providers.length === 0) {\n throw new Error(\"no model configured: set LICH_MODEL, pass --model, or create .lich/config.json (`lich config` prints a template)\");\n }\n return parse_agent_config(base);\n}\n\n/** build_config with the failure message prefixed for the invoking subcommand. */\nfunction build_config_for(options: CliOptions, mode: string): AgentConfig {\n try {\n return build_config(options);\n } catch (error) {\n throw new Error(error_for_mode(mode, error_message(error)));\n }\n}\n\nfunction attach_progress(emitter: AgentEmitter): () => void {\n return emitter.on((event) => {\n if (event.type === \"turn_start\") {\n process.stderr.write(`\\n[lich] turn ${event.turn}`);\n return;\n }\n if (event.type === \"tool_call_end\") {\n const status = event.result.ok === true ? \"ok\" : `error: ${event.result.error ?? \"\"}`;\n process.stderr.write(`\\n[lich] ${event.call.name}: ${status}`);\n return;\n }\n if (event.type === \"final\") {\n process.stderr.write(\"\\n\");\n }\n });\n}\n\nasync function run_one_shot(config: unknown, input: string): Promise<number> {\n const agent = create_agent(config);\n const stop_progress = attach_progress(agent.events);\n let result: AgentRunResult;\n try {\n result = await agent.run({ input });\n } finally {\n stop_progress();\n }\n const final = result.outcome.final;\n if (final !== undefined && final.content.length > 0) {\n process.stdout.write(`${final.content}\\n`);\n }\n if (result.outcome.stopped_reason === \"budget\") {\n process.stderr.write(`[lich] budget exhausted after ${result.outcome.turns_used} turns\\n`);\n return 1;\n }\n if (result.outcome.stopped_reason === \"aborted\") {\n process.stderr.write(\"[lich] run aborted\\n\");\n return 1;\n }\n return 0;\n}\n\nasync function run_chat_turn(agent: Agent, input: string): Promise<void> {\n const stop_progress = attach_progress(agent.events);\n try {\n const result = await agent.run({ input });\n const final = result.outcome.final;\n if (final !== undefined && final.content.length > 0) {\n process.stdout.write(`${final.content}\\n`);\n }\n process.stdout.write(`[turns ${result.outcome.turns_used} | tokens ${result.usage_total.total_tokens}]\\n`);\n } catch (error) {\n process.stderr.write(`[lich] ${error_message(error)}\\n`);\n } finally {\n stop_progress();\n }\n}\n\nfunction ask_line(rl: Interface): Promise<string> {\n return new Promise((resolve) => {\n const on_close = (): void => resolve(\"\");\n rl.question(\"> \", (answer) => {\n rl.removeListener(\"close\", on_close);\n resolve(answer);\n });\n rl.once(\"close\", on_close);\n });\n}\n\nasync function run_chat(config: unknown): Promise<number> {\n const agent = create_agent(config);\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n try {\n for (;;) {\n const line = await ask_line(rl);\n if (line.trim() === \"\") {\n return 0;\n }\n const command = line.trim();\n if (command === \"/exit\" || command === \"/quit\") {\n return 0;\n }\n await run_chat_turn(agent, command);\n }\n } finally {\n rl.close();\n }\n}\n\nasync function run_tui_entry(config: ReturnType<typeof build_config>): Promise<number> {\n const { run_tui } = await import(\"./tui.js\");\n return run_tui(config);\n}\n\nasync function run_gateway_entry(\n config: ReturnType<typeof build_config>,\n platforms: string[],\n): Promise<number> {\n const { run_gateway } = await import(\"./gateway.js\");\n return run_gateway(config, platforms.length === 0 ? [\"webhook\"] : platforms);\n}\n\nfunction is_main_module(): boolean {\n const entry = process.argv[1];\n if (entry === undefined) {\n return false;\n }\n return import.meta.url === pathToFileURL(path.resolve(entry)).href;\n}\n\nasync function main(argv: string[]): Promise<number> {\n const options = parse_args(argv);\n if (options.help === true) {\n process.stdout.write(`${usage_text()}\\n`);\n return 0;\n }\n if (options.version === true) {\n process.stdout.write(`${LICH_VERSION}\\n`);\n return 0;\n }\n const [first] = options.positionals;\n if (first === undefined) {\n process.stderr.write(`${usage_text()}\\n`);\n return 1;\n }\n if (first === \"config\") {\n process.stdout.write(`${config_template()}\\n`);\n return 0;\n }\n if (first === \"chat\") {\n if (options.positionals.length > 1) {\n throw new Error(\"chat mode takes no task argument\");\n }\n return run_chat(build_config_for(options, first));\n }\n if (first === \"tui\") {\n return run_tui_entry(build_config_for(options, first));\n }\n if (first === \"gateway\") {\n return run_gateway_entry(build_config_for(options, first), options.positionals.slice(1));\n }\n return run_one_shot(build_config_for(options, \"one-shot\"), options.positionals.join(\" \"));\n}\n\nif (is_main_module() === true) {\n main(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code;\n })\n .catch((error) => {\n process.stderr.write(`lich: ${error_message(error)}\\n`);\n process.stderr.write(\"run `lich --help` for usage\\n\");\n process.exitCode = 1;\n });\n}","/**\n * Config-file discovery for the CLI: an ordered search chain, safe loading,\n * and a starter template so `lich tui` works with zero environment setup.\n */\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { safe_json_parse } from \"./util/json.js\";\n\nconst LICH_DIRNAME = \".lich\";\nconst CONFIG_RELPATH = `${LICH_DIRNAME}/config.json`;\nconst DEFAULT_USER_CONFIG = \".config/lich/config.json\";\n\n/** Ordered absolute chain: work_dir (default cwd) first, then the user config home. */\nexport function config_search_paths(work_dir?: string): string[] {\n const root = work_dir ?? process.cwd();\n const first = path.resolve(root, CONFIG_RELPATH);\n const second = path.resolve(homedir(), DEFAULT_USER_CONFIG);\n return first === second ? [first] : [first, second];\n}\n\n/** Explicit path must exist (else throw); otherwise walk the chain, undefined when absent. */\nexport function find_config_file(explicit?: string): string | undefined {\n if (explicit !== undefined) {\n if (existsSync(explicit) === false) {\n throw new Error(`config not found: ${explicit}`);\n }\n return explicit;\n }\n for (const candidate of config_search_paths()) {\n if (existsSync(candidate) === true) {\n return candidate;\n }\n }\n return undefined;\n}\n\n/** Load the first config found as an object; undefined when absent, throw on bad json. */\nexport function load_config(explicit?: string): Record<string, unknown> | undefined {\n const found = find_config_file(explicit);\n if (found === undefined) {\n return undefined;\n }\n const raw = readFileSync(found, \"utf8\");\n const parsed = safe_json_parse<unknown>(raw);\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(`invalid config json: ${found}`);\n }\n return parsed as Record<string, unknown>;\n}\n\n/** Starter config honoring LICH_* env hints; `lich config` prints it. */\nexport function config_template(): string {\n const kind = process.env.LICH_PROVIDER_KIND ?? \"ollama\";\n const provider: Record<string, unknown> = {\n kind,\n name: \"main\",\n model: process.env.LICH_MODEL ?? \"<model-name>\",\n };\n if (kind === \"ollama\") {\n provider[\"base_url\"] = \"http://localhost:11434\";\n }\n return JSON.stringify({ providers: [provider], max_turns: 25 }, null, 2);\n}\n\n/** mkdir -p `${work_dir}/.lich` and return the directory path. */\nexport function ensure_lich_config_dir(work_dir: string): string {\n const dir = path.resolve(work_dir, LICH_DIRNAME);\n mkdirSync(dir, { recursive: true });\n return dir;\n}"],"mappings":"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","names":["readFileSync","path","value","readFileSync","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/cli_config.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Thin zero-dependency CLI for lich: one-shot tasks, interactive chat,\n * config files, and environment-based provider resolution.\n */\nimport { readFileSync } from \"node:fs\";\nimport { createInterface, type Interface } from \"node:readline\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { create_agent, type Agent, type AgentRunResult } from \"./agent/agent.js\";\nimport { parse_agent_config, type AgentConfig } from \"./agent/config.js\";\nimport { AgentEmitter } from \"./agent/events.js\";\nimport { LICH_VERSION } from \"./index.js\";\nimport { load_config, config_template } from \"./cli_config.js\";\n\ntype ProviderKind = \"openai_compat\" | \"anthropic\" | \"ollama\";\n\ninterface CliOptions {\n config_path?: string;\n help?: boolean;\n version?: boolean;\n overrides: Record<string, string>;\n positionals: string[];\n}\n\nconst FLAG_KEYS: Record<string, string> = {\n \"--work-dir\": \"work_dir\",\n \"--max-turns\": \"max_turns\",\n \"--model\": \"provider_model\",\n \"--provider-kind\": \"provider_kind\",\n \"--base-url\": \"provider_base_url\",\n \"--api-key-env\": \"provider_api_key_env\",\n \"--system-prompt\": \"system_prompt\",\n \"--session-dir\": \"session_dir\",\n \"--log-level\": \"log_level\",\n};\n\nconst DEFAULT_BASE_URLS: Record<ProviderKind, string> = {\n openai_compat: \"https://api.openai.com/v1\",\n anthropic: \"https://api.anthropic.com\",\n ollama: \"http://localhost:11434\",\n};\n\nconst DEFAULT_ENV_API_KEYS: Record<ProviderKind, string | undefined> = {\n openai_compat: \"OPENAI_API_KEY\",\n anthropic: \"ANTHROPIC_API_KEY\",\n ollama: undefined, // ollama needs no api key\n};\n\nfunction usage_text(): string {\n return [\n \"lich — a TypeScript AI agent harness\",\n \"\",\n \"Usage:\",\n ' lich \"one shot task\" run a single task and print the reply',\n \" lich chat interactive chat (commands: /exit, /quit)\",\n \" lich tui interactive terminal UI (ink)\",\n \" lich gateway <plat..> messaging gateway (webhook|telegram|discord|twitch)\",\n \" lich config print a starter config template (save as .lich/config.json)\",\n \" lich --help show this help\",\n \" lich --version print version\",\n \"\",\n \"Flags (before or after the subcommand):\",\n \" --config <path> JSON config file parsed by parse_agent_config\",\n \" --work-dir <path> working directory for tools\",\n \" --max-turns <n> loop turn budget (default 25)\",\n \" --model <m> model name (default from LICH_MODEL)\",\n \" --provider-kind <k> openai_compat | anthropic | ollama (default LICH_PROVIDER_KIND)\",\n \" --base-url <u> provider base url (default LICH_BASE_URL)\",\n \" --api-key-env <NAME> env var holding the api key (default LICH_API_KEY_ENV; unused by ollama)\",\n \" --system-prompt <s> system prompt override\",\n \" --session-dir <path> session transcript directory\",\n \" --log-level <level> debug | info | warn | error\",\n ].join(\"\\n\");\n}\n\nfunction error_message(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Mode-aware config failure message; one-shot keeps the generic variant. */\nfunction error_for_mode(mode: string, base_message: string): string {\n if (mode === \"tui\") {\n return \"lich tui: no model configured — set LICH_MODEL (e.g. glm-5.3-flash:cloud), pass --model, or create .lich/config.json (`lich config` prints a template)\";\n }\n if (mode === \"gateway\") {\n return \"lich gateway: no model configured — set LICH_MODEL (e.g. glm-5.3-flash:cloud), pass --model, or create .lich/config.json (`lich config` prints a template)\";\n }\n if (mode === \"chat\") {\n return \"lich chat: no model configured — set LICH_MODEL (e.g. glm-5.3-flash:cloud), pass --model, or create .lich/config.json (`lich config` prints a template)\";\n }\n return base_message;\n}\n\nfunction parse_args(argv: string[]): CliOptions {\n const options: CliOptions = { overrides: {}, positionals: [] };\n for (let index = 0; index < argv.length; index += 1) {\n const arg = argv[index];\n if (arg === undefined) {\n break;\n }\n if (arg === \"--help\") {\n options.help = true;\n continue;\n }\n if (arg === \"--version\") {\n options.version = true;\n continue;\n }\n if (arg === \"--config\") {\n const value = argv[index + 1];\n if (value === undefined) {\n throw new Error(\"--config requires a path\");\n }\n options.config_path = value;\n index += 1;\n continue;\n }\n const override_key = FLAG_KEYS[arg];\n if (override_key === undefined) {\n if (arg.startsWith(\"--\") === true) {\n throw new Error(`unknown flag: ${arg}`);\n }\n options.positionals.push(arg);\n continue;\n }\n const value = argv[index + 1];\n if (value === undefined || value.startsWith(\"--\") === true) {\n throw new Error(`${arg} requires a value`);\n }\n options.overrides[override_key] = value;\n index += 1;\n }\n return options;\n}\n\nfunction env_provider(overrides: Record<string, string>): Record<string, unknown> {\n const kind_value = overrides[\"provider_kind\"] ?? process.env.LICH_PROVIDER_KIND ?? \"openai_compat\";\n if (kind_value !== \"openai_compat\" && kind_value !== \"anthropic\" && kind_value !== \"ollama\") {\n throw new Error(`unknown provider kind \"${kind_value}\" (expected openai_compat, anthropic, or ollama)`);\n }\n const kind: ProviderKind = kind_value;\n const model = overrides[\"provider_model\"] ?? process.env.LICH_MODEL;\n if (model === undefined || model.length === 0) {\n throw new Error(\"no model configured: set LICH_MODEL, pass --model, or create .lich/config.json (`lich config` prints a template)\");\n }\n return {\n kind,\n name: \"default\",\n model,\n base_url: overrides[\"provider_base_url\"] ?? process.env.LICH_BASE_URL ?? DEFAULT_BASE_URLS[kind],\n api_key_env: overrides[\"provider_api_key_env\"] ?? process.env.LICH_API_KEY_ENV ?? DEFAULT_ENV_API_KEYS[kind],\n };\n}\n\nfunction load_config_file(config_path: string): Record<string, unknown> {\n try {\n const raw = readFileSync(config_path, \"utf8\");\n const parsed = JSON.parse(raw) as unknown;\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(\"config root must be a JSON object\");\n }\n return parsed as Record<string, unknown>;\n } catch (error) {\n throw new Error(`cannot use config file ${config_path}: ${error_message(error)}`);\n }\n}\n\nfunction apply_provider_override(config: Record<string, unknown>, overrides: Record<string, string>): void {\n const keys = [\"provider_kind\", \"provider_model\", \"provider_base_url\", \"provider_api_key_env\"] as const;\n const any_present = keys.some((key) => overrides[key] !== undefined);\n if (any_present === false) {\n return;\n }\n const providers = config[\"providers\"];\n if (Array.isArray(providers) === false || providers.length === 0) {\n config[\"providers\"] = [env_provider(overrides)];\n return;\n }\n const first = providers[0];\n if (typeof first !== \"object\" || first === null) {\n throw new Error(\"config providers[0] must be an object\");\n }\n const provider = first as Record<string, unknown>;\n for (const key of keys) {\n const value = overrides[key];\n if (value !== undefined) {\n provider[key.replace(\"provider_\", \"\")] = value;\n }\n }\n}\n\nfunction apply_overrides(config: Record<string, unknown>, overrides: Record<string, string>): void {\n for (const key of [\"work_dir\", \"system_prompt\", \"session_dir\", \"log_level\"]) {\n if (overrides[key] !== undefined) {\n config[key] = overrides[key];\n }\n }\n if (overrides[\"max_turns\"] !== undefined) {\n const turns = Number(overrides[\"max_turns\"]);\n if (Number.isInteger(turns) === false || turns < 1) {\n throw new Error(\"--max-turns must be a positive integer\");\n }\n config[\"max_turns\"] = turns;\n }\n apply_provider_override(config, overrides);\n}\n\nfunction build_config(options: CliOptions): AgentConfig {\n const base =\n options.config_path === undefined ? load_config() ?? { providers: [env_provider(options.overrides)] } : load_config_file(options.config_path);\n apply_overrides(base, options.overrides);\n const providers = base[\"providers\"];\n if (Array.isArray(providers) === false || providers.length === 0) {\n throw new Error(\"no model configured: set LICH_MODEL, pass --model, or create .lich/config.json (`lich config` prints a template)\");\n }\n return parse_agent_config(base);\n}\n\n/** build_config with the failure message prefixed for the invoking subcommand. */\nfunction build_config_for(options: CliOptions, mode: string): AgentConfig {\n try {\n return build_config(options);\n } catch (error) {\n throw new Error(error_for_mode(mode, error_message(error)));\n }\n}\n\nfunction attach_progress(emitter: AgentEmitter): () => void {\n return emitter.on((event) => {\n if (event.type === \"turn_start\") {\n process.stderr.write(`\\n[lich] turn ${event.turn}`);\n return;\n }\n if (event.type === \"tool_call_end\") {\n const status = event.result.ok === true ? \"ok\" : `error: ${event.result.error ?? \"\"}`;\n process.stderr.write(`\\n[lich] ${event.call.name}: ${status}`);\n return;\n }\n if (event.type === \"final\") {\n process.stderr.write(\"\\n\");\n }\n });\n}\n\nasync function run_one_shot(config: unknown, input: string): Promise<number> {\n const agent = create_agent(config);\n const stop_progress = attach_progress(agent.events);\n let result: AgentRunResult;\n try {\n result = await agent.run({ input });\n } finally {\n stop_progress();\n }\n const final = result.outcome.final;\n if (final !== undefined && final.content.length > 0) {\n process.stdout.write(`${final.content}\\n`);\n }\n if (result.outcome.stopped_reason === \"budget\") {\n process.stderr.write(`[lich] budget exhausted after ${result.outcome.turns_used} turns\\n`);\n return 1;\n }\n if (result.outcome.stopped_reason === \"aborted\") {\n process.stderr.write(\"[lich] run aborted\\n\");\n return 1;\n }\n return 0;\n}\n\nasync function run_chat_turn(agent: Agent, input: string): Promise<void> {\n const stop_progress = attach_progress(agent.events);\n try {\n const result = await agent.run({ input });\n const final = result.outcome.final;\n if (final !== undefined && final.content.length > 0) {\n process.stdout.write(`${final.content}\\n`);\n }\n process.stdout.write(`[turns ${result.outcome.turns_used} | tokens ${result.usage_total.total_tokens}]\\n`);\n } catch (error) {\n process.stderr.write(`[lich] ${error_message(error)}\\n`);\n } finally {\n stop_progress();\n }\n}\n\nfunction ask_line(rl: Interface): Promise<string> {\n return new Promise((resolve) => {\n const on_close = (): void => resolve(\"\");\n rl.question(\"> \", (answer) => {\n rl.removeListener(\"close\", on_close);\n resolve(answer);\n });\n rl.once(\"close\", on_close);\n });\n}\n\nasync function run_chat(config: unknown): Promise<number> {\n const agent = create_agent(config);\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n try {\n for (;;) {\n const line = await ask_line(rl);\n if (line.trim() === \"\") {\n return 0;\n }\n const command = line.trim();\n if (command === \"/exit\" || command === \"/quit\") {\n return 0;\n }\n await run_chat_turn(agent, command);\n }\n } finally {\n rl.close();\n }\n}\n\nasync function run_tui_entry(config: ReturnType<typeof build_config>): Promise<number> {\n const { run_tui } = await import(\"./tui.js\");\n return run_tui(config);\n}\n\nasync function run_gateway_entry(\n config: ReturnType<typeof build_config>,\n platforms: string[],\n): Promise<number> {\n const { run_gateway } = await import(\"./gateway.js\");\n return run_gateway(config, platforms.length === 0 ? [\"webhook\"] : platforms);\n}\n\nfunction is_main_module(): boolean {\n const entry = process.argv[1];\n if (entry === undefined) {\n return false;\n }\n return import.meta.url === pathToFileURL(path.resolve(entry)).href;\n}\n\nasync function main(argv: string[]): Promise<number> {\n const options = parse_args(argv);\n if (options.help === true) {\n process.stdout.write(`${usage_text()}\\n`);\n return 0;\n }\n if (options.version === true) {\n process.stdout.write(`${LICH_VERSION}\\n`);\n return 0;\n }\n const [first] = options.positionals;\n if (first === undefined) {\n process.stderr.write(`${usage_text()}\\n`);\n return 1;\n }\n if (first === \"config\") {\n process.stdout.write(`${config_template()}\\n`);\n return 0;\n }\n if (first === \"chat\") {\n if (options.positionals.length > 1) {\n throw new Error(\"chat mode takes no task argument\");\n }\n return run_chat(build_config_for(options, first));\n }\n if (first === \"tui\") {\n return run_tui_entry(build_config_for(options, first));\n }\n if (first === \"gateway\") {\n return run_gateway_entry(build_config_for(options, first), options.positionals.slice(1));\n }\n return run_one_shot(build_config_for(options, \"one-shot\"), options.positionals.join(\" \"));\n}\n\nif (is_main_module() === true) {\n main(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code;\n })\n .catch((error) => {\n process.stderr.write(`lich: ${error_message(error)}\\n`);\n process.stderr.write(\"run `lich --help` for usage\\n\");\n process.exitCode = 1;\n });\n}","/**\n * Config-file discovery for the CLI: an ordered search chain, safe loading,\n * and a starter template so `lich tui` works with zero environment setup.\n */\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { safe_json_parse } from \"./util/json.js\";\n\nconst LICH_DIRNAME = \".lich\";\nconst CONFIG_RELPATH = `${LICH_DIRNAME}/config.json`;\nconst DEFAULT_USER_CONFIG = \".config/lich/config.json\";\n\n/** Ordered absolute chain: work_dir (default cwd) first, then the user config home. */\nexport function config_search_paths(work_dir?: string): string[] {\n const root = work_dir ?? process.cwd();\n const first = path.resolve(root, CONFIG_RELPATH);\n const second = path.resolve(homedir(), DEFAULT_USER_CONFIG);\n return first === second ? [first] : [first, second];\n}\n\n/** Explicit path must exist (else throw); otherwise walk the chain, undefined when absent. */\nexport function find_config_file(explicit?: string): string | undefined {\n if (explicit !== undefined) {\n if (existsSync(explicit) === false) {\n throw new Error(`config not found: ${explicit}`);\n }\n return explicit;\n }\n for (const candidate of config_search_paths()) {\n if (existsSync(candidate) === true) {\n return candidate;\n }\n }\n return undefined;\n}\n\n/** Load the first config found as an object; undefined when absent, throw on bad json. */\nexport function load_config(explicit?: string): Record<string, unknown> | undefined {\n const found = find_config_file(explicit);\n if (found === undefined) {\n return undefined;\n }\n const raw = readFileSync(found, \"utf8\");\n const parsed = safe_json_parse<unknown>(raw);\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(`invalid config json: ${found}`);\n }\n return parsed as Record<string, unknown>;\n}\n\n/** Starter config honoring LICH_* env hints; `lich config` prints it. */\nexport function config_template(): string {\n const kind = process.env.LICH_PROVIDER_KIND ?? \"ollama\";\n const provider: Record<string, unknown> = {\n kind,\n name: \"main\",\n model: process.env.LICH_MODEL ?? \"<model-name>\",\n };\n if (kind === \"ollama\") {\n provider[\"base_url\"] = \"http://localhost:11434\";\n }\n return JSON.stringify({ providers: [provider], max_turns: 25 }, null, 2);\n}\n\n/** mkdir -p `${work_dir}/.lich` and return the directory path. */\nexport function ensure_lich_config_dir(work_dir: string): string {\n const dir = path.resolve(work_dir, LICH_DIRNAME);\n mkdirSync(dir, { recursive: true });\n return dir;\n}"],"mappings":"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","names":["readFileSync","path","value","readFileSync","path"]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
create_agent,
|
|
3
3
|
logger,
|
|
4
4
|
sleep
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-MLFJW4JU.js";
|
|
6
6
|
|
|
7
7
|
// src/gateway/types.ts
|
|
8
8
|
var ERROR_SNIPPET_CHARS = 300;
|
|
@@ -749,4 +749,4 @@ function install_signal_handlers(bus, adapters) {
|
|
|
749
749
|
export {
|
|
750
750
|
run_gateway
|
|
751
751
|
};
|
|
752
|
-
//# sourceMappingURL=gateway-
|
|
752
|
+
//# sourceMappingURL=gateway-XTYDYT67.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ interface Tool {
|
|
|
37
37
|
name: string;
|
|
38
38
|
description: string;
|
|
39
39
|
parameters: JsonSchemaObject;
|
|
40
|
+
/** Per-tool executor timeout override in ms; unset tools get the 30s default. */
|
|
41
|
+
timeout_ms?: number;
|
|
40
42
|
execute(args: Record<string, unknown>, context: ToolContext): Promise<ToolResult>;
|
|
41
43
|
}
|
|
42
44
|
interface Toolset {
|
|
@@ -55,6 +57,12 @@ interface Toolset {
|
|
|
55
57
|
/** Runtime info handed to every hook call. */
|
|
56
58
|
interface HookContext {
|
|
57
59
|
work_dir: string;
|
|
60
|
+
/**
|
|
61
|
+
* This plugin's own per-run state sub-map. Every hook invocation receives
|
|
62
|
+
* a ctx exposing only the invoking plugin's bag; the bag is swapped fresh
|
|
63
|
+
* at each run start. Absent only on hand-built contexts outside the runner.
|
|
64
|
+
*/
|
|
65
|
+
state?: Map<string, unknown>;
|
|
58
66
|
}
|
|
59
67
|
/** Argument passed to before_tool_call hooks. */
|
|
60
68
|
interface BeforeToolCallInfo {
|
|
@@ -69,6 +77,9 @@ interface BeforeToolCallResult {
|
|
|
69
77
|
/** Argument passed to after_tool_call hooks. */
|
|
70
78
|
interface AfterToolCallInfo extends BeforeToolCallInfo {
|
|
71
79
|
result_summary: string;
|
|
80
|
+
/** Structured executor outcome; gate on this, never parse result_summary. */
|
|
81
|
+
ok: boolean;
|
|
82
|
+
error?: string;
|
|
72
83
|
}
|
|
73
84
|
interface RunEndInfo {
|
|
74
85
|
stopped_reason: string;
|
|
@@ -445,6 +456,7 @@ declare class Agent {
|
|
|
445
456
|
private readonly hook_runner;
|
|
446
457
|
constructor(config: AgentConfig, plugins?: readonly LoadedPlugin[]);
|
|
447
458
|
run(options: AgentRunOptions): Promise<AgentRunResult>;
|
|
459
|
+
/** Per-run deps: the built-once ToolContext threads through every tool execution. */
|
|
448
460
|
private loop_deps;
|
|
449
461
|
/** Best-effort on_run_start fan-out; hook errors are logged, never fatal. */
|
|
450
462
|
private call_plugin_run_start;
|
|
@@ -503,7 +515,11 @@ declare function register_builtin_tools(registry: ToolRegistry, context?: ToolCo
|
|
|
503
515
|
*
|
|
504
516
|
* before_tool_call hooks run in registration order and may veto a call (first
|
|
505
517
|
* blocker wins; the wrapped executor is never called). Hook errors are warned
|
|
506
|
-
* and skipped, never fatal. after_tool_call hooks observe the result summary
|
|
518
|
+
* and skipped, never fatal. after_tool_call hooks observe the result summary
|
|
519
|
+
* plus the executor's structured ok/error fields. Every hook invocation
|
|
520
|
+
* receives a ctx exposing only its own plugin's state sub-map: the bag is a
|
|
521
|
+
* module-internal WeakMap keyed on the plugin object, swapped fresh at each
|
|
522
|
+
* call_run_start and shared by the lifecycle and per-tool ctx build sites.
|
|
507
523
|
*/
|
|
508
524
|
|
|
509
525
|
/** Structural ToolRunner shape accepted from the wrapped executor. */
|
|
@@ -511,27 +527,25 @@ interface WrappedToolRunner {
|
|
|
511
527
|
execute(name: string, args: Record<string, unknown>, context?: ToolContext): Promise<ToolResult>;
|
|
512
528
|
}
|
|
513
529
|
/**
|
|
514
|
-
*
|
|
515
|
-
*
|
|
530
|
+
* Hooks stay attached to their plugin (no flattening) so each invocation can
|
|
531
|
+
* be handed a ctx exposing only that plugin's sub-map; hooks always run in
|
|
532
|
+
* plugin registration order.
|
|
516
533
|
*/
|
|
517
534
|
declare class HookedToolRunner {
|
|
518
535
|
private readonly wrapped;
|
|
519
|
-
private readonly
|
|
520
|
-
|
|
521
|
-
private readonly run_start_hooks;
|
|
522
|
-
private readonly run_end_hooks;
|
|
523
|
-
constructor(wrapped: WrappedToolRunner, hooks: readonly PluginHooks[]);
|
|
536
|
+
private readonly hooked_plugins;
|
|
537
|
+
constructor(wrapped: WrappedToolRunner, plugins: readonly Plugin[]);
|
|
524
538
|
/** Run before hooks in order; the first {block: true} verdict wins. */
|
|
525
539
|
private run_before_hooks;
|
|
526
540
|
/** Fire-and-forget in spirit but awaited here so runs settle cleanly. */
|
|
527
541
|
private run_after_hooks;
|
|
528
542
|
execute(name: string, args: Record<string, unknown>, context?: ToolContext): Promise<ToolResult>;
|
|
529
|
-
/** Best-effort on_run_start fan-out used by Agent.run; never throws. */
|
|
543
|
+
/** Best-effort on_run_start fan-out used by Agent.run; never throws. Swaps in a fresh state sub-map per hooked plugin first. */
|
|
530
544
|
call_run_start(info: {
|
|
531
545
|
input_chars: number;
|
|
532
|
-
},
|
|
546
|
+
}, base: HookContext): Promise<void>;
|
|
533
547
|
/** Best-effort on_run_end fan-out used by Agent.run; never throws. */
|
|
534
|
-
call_run_end(info: RunEndInfo,
|
|
548
|
+
call_run_end(info: RunEndInfo, base: HookContext): Promise<void>;
|
|
535
549
|
}
|
|
536
550
|
|
|
537
551
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LICH_VERSION
|
|
3
|
+
} from "./chunk-ZVK3MUPC.js";
|
|
1
4
|
import {
|
|
2
5
|
create_agent,
|
|
3
6
|
truncate_text
|
|
4
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-MLFJW4JU.js";
|
|
5
8
|
|
|
6
9
|
// src/tui.tsx
|
|
7
10
|
import { render } from "ink";
|
|
@@ -409,7 +412,7 @@ function TuiApp({ agent }) {
|
|
|
409
412
|
);
|
|
410
413
|
const provider = agent.config.providers[0];
|
|
411
414
|
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", minHeight: 8, children: [
|
|
412
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `lich
|
|
415
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `lich v${LICH_VERSION} \u2014 ${provider?.model ?? "unknown"} (${provider?.kind ?? "unknown"})` }),
|
|
413
416
|
/* @__PURE__ */ jsx4(MessageView, { blocks, state }),
|
|
414
417
|
/* @__PURE__ */ jsx4(StatusBar, { state, model: provider?.model ?? "unknown" }),
|
|
415
418
|
/* @__PURE__ */ jsx4(CommandBar, { busy: state.phase !== "idle", on_submit: submit })
|
|
@@ -427,4 +430,4 @@ async function run_tui(config) {
|
|
|
427
430
|
export {
|
|
428
431
|
run_tui
|
|
429
432
|
};
|
|
430
|
-
//# sourceMappingURL=tui-
|
|
433
|
+
//# sourceMappingURL=tui-VYBJSGRV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tui.tsx","../src/tui/app.tsx","../src/tui/state.ts","../src/tui/message_view.tsx","../src/tui/status_bar.tsx","../src/tui/command_bar.tsx"],"sourcesContent":["/**\n * TUI entry: builds the agent from the parsed config and renders the ink app\n * until exit. The CLI dynamic-imports this module for `lich tui`.\n */\nimport { render } from \"ink\";\nimport { create_agent } from \"./agent/agent.js\";\nimport type { AgentConfig } from \"./agent/config.js\";\nimport { TuiApp } from \"./tui/app.js\";\n\nexport async function run_tui(config: AgentConfig): Promise<number> {\n const agent = create_agent(config);\n const instance = render(<TuiApp agent={agent} />);\n await instance.waitUntilExit();\n return 0;\n}","/**\n * Root ink component for the lich TUI: wires agent events into the UI state\n * machine, drives agent.run with history continuity, and lays out header,\n * transcript, status bar, and the command input row.\n */\nimport { useCallback, useEffect, useRef, useState, type Dispatch, type SetStateAction } from \"react\";\nimport { Box, Text } from \"ink\";\nimport type { Agent, AgentRunResult } from \"../agent/agent.js\";\nimport type { AgentEvent } from \"../agent/events.js\";\nimport type { AgentConfig } from \"../agent/config.js\";\nimport type { Message } from \"../providers/types.js\";\nimport { LICH_VERSION } from \"../index.js\";\nimport { readdir, stat } from \"node:fs/promises\";\nimport {\n apply_event,\n apply_run_result,\n compress_notice_block,\n error_notice_block,\n help_block,\n HISTORY_CAP,\n INITIAL_UI_STATE,\n model_label_block,\n parse_command,\n run_notice_blocks,\n session_list_block,\n tool_result_block,\n unknown_command_block,\n usage_notice_block,\n type HistoryBlock,\n type ParsedInput,\n type SessionEntryInfo,\n type UiState,\n} from \"./state.js\";\nimport { MessageView } from \"./message_view.js\";\nimport { StatusBar } from \"./status_bar.js\";\nimport { CommandBar } from \"./command_bar.js\";\n\nconst SESSION_LIST_CAP = 10;\n\ntype SlashInput = Extract<ParsedInput, { kind: \"slash\" }>;\ntype AddBlocks = (added: readonly HistoryBlock[]) => void;\ntype SetUiState = Dispatch<SetStateAction<UiState>>;\ntype SetBlocks = Dispatch<SetStateAction<readonly HistoryBlock[]>>;\n\n/** Map one agent event to optional transcript blocks (tool rows, notices). */\nfunction event_blocks(event: AgentEvent): readonly HistoryBlock[] {\n if (event.type === \"tool_call_end\") {\n return [tool_result_block(event.call, event.result.ok === true, event.result.output)];\n }\n if (event.type === \"compress_end\") {\n return [compress_notice_block(event.summary_chars)];\n }\n if (event.type === \"error\") {\n return [error_notice_block(event.error instanceof Error ? event.error.message : String(event.error))];\n }\n return [];\n}\n\n/** One agent turn: subscribe to events, run, unsubscribe in finally. */\nasync function run_agent_turn(\n agent: Agent,\n history: readonly Message[],\n input: string,\n on_event: (event: AgentEvent) => void,\n on_done: (result: AgentRunResult) => void,\n signal: AbortSignal,\n): Promise<void> {\n const stop_listening = agent.events.on(on_event);\n try {\n const result = await agent.run({ input, history, signal, label: \"tui\" });\n on_done(result);\n } finally {\n stop_listening();\n }\n}\n\n/** Async /sessions listing as a meta block (never throws). */\nasync function sessions_block(config: AgentConfig): Promise<HistoryBlock> {\n try {\n const dir_entries = await readdir(config.session_dir, { withFileTypes: true });\n const entries: SessionEntryInfo[] = [];\n for (const entry of dir_entries) {\n if (entry.isFile() === false || entry.name.endsWith(\".jsonl\") === false) {\n continue;\n }\n const info = await stat(`${config.session_dir}/${entry.name}`);\n entries.push({ name: entry.name, size_bytes: info.size, mtime_ms: info.mtimeMs });\n }\n return session_list_block(entries, SESSION_LIST_CAP);\n } catch {\n return { role: \"meta\", lines: [\"· no session files yet\"] };\n }\n}\n\nfunction run_error_text(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Runs one message exchange; owns history continuity and abort wiring. */\nfunction use_agent_run(\n agent: Agent,\n add_blocks: AddBlocks,\n set_state: SetUiState,\n set_blocks: SetBlocks,\n): (text: string) => void {\n const history_ref = useRef<readonly Message[]>([]);\n const controller_ref = useRef<AbortController | undefined>(undefined);\n\n const finish_run = useCallback((result: AgentRunResult): void => {\n history_ref.current = result.messages;\n set_state((current) => apply_run_result(current, result));\n add_blocks(run_notice_blocks(result));\n }, [add_blocks, set_state]);\n\n const start_message_run = useCallback(\n (text: string): void => {\n add_blocks([{ role: \"user\", lines: [`you › ${text}`] }]);\n set_state((current) => ({ ...current, phase: \"thinking\", active_tool: undefined }));\n const controller = new AbortController();\n controller_ref.current = controller;\n const on_event = (event: AgentEvent): void => {\n set_state((current) => apply_event(current, event));\n set_blocks((current) => [...current, ...event_blocks(event)].slice(-HISTORY_CAP));\n };\n void run_agent_turn(agent, history_ref.current, text, on_event, finish_run, controller.signal)\n .catch((error: unknown) => {\n add_blocks([error_notice_block(run_error_text(error))]);\n set_state((current) => ({ ...current, phase: \"idle\" }));\n })\n .finally(() => {\n if (controller_ref.current === controller) {\n controller_ref.current = undefined;\n }\n });\n },\n [agent, add_blocks, finish_run, set_blocks, set_state],\n );\n\n useEffect(() => () => controller_ref.current?.abort(), []);\n\n return start_message_run;\n}\n\n/** Slash-command dispatch: pure client-side actions, never hits the agent. */\nfunction use_slash_commands(agent: Agent, add_blocks: AddBlocks, set_blocks: SetBlocks, total_tokens: number): (parsed: SlashInput) => void {\n const handle = useCallback(\n (parsed: SlashInput): void => {\n if (parsed.name === \"exit\" || parsed.name === \"quit\" || parsed.name === \"q\") {\n process.exit(0);\n return;\n }\n if (parsed.name === \"help\") {\n add_blocks([help_block()]);\n } else if (parsed.name === \"model\") {\n add_blocks([model_label_block(agent.config)]);\n } else if (parsed.name === \"usage\") {\n add_blocks([usage_notice_block(total_tokens)]);\n } else if (parsed.name === \"clear\") {\n set_blocks([]);\n } else if (parsed.name === \"sessions\") {\n void sessions_block(agent.config).then((block) => add_blocks([block]));\n } else {\n add_blocks([unknown_command_block(parsed.name)]);\n }\n },\n [agent, add_blocks, set_blocks, total_tokens],\n );\n return handle;\n}\n\ninterface TuiAppProps {\n readonly agent: Agent;\n}\n\nexport function TuiApp({ agent }: TuiAppProps): React.JSX.Element {\n const [blocks, set_blocks] = useState<readonly HistoryBlock[]>([]);\n const [state, set_state] = useState(INITIAL_UI_STATE);\n\n const add_blocks = useCallback<AddBlocks>((added: readonly HistoryBlock[]): void => {\n if (added.length === 0) {\n return;\n }\n set_blocks((current) => [...current, ...added].slice(-HISTORY_CAP));\n }, []);\n\n const start_message_run = use_agent_run(agent, add_blocks, set_state, set_blocks);\n const handle_slash = use_slash_commands(agent, add_blocks, set_blocks, state.usage.total_tokens);\n\n const submit = useCallback(\n (text: string): void => {\n const parsed = parse_command(text);\n if (parsed.kind === \"message\") {\n if (parsed.text.length > 0) {\n start_message_run(parsed.text);\n }\n return;\n }\n handle_slash(parsed);\n },\n [handle_slash, start_message_run],\n );\n\n const provider = agent.config.providers[0];\n return (\n <Box flexDirection=\"column\" minHeight={8}>\n <Text dimColor>{`lich v${LICH_VERSION} — ${provider?.model ?? \"unknown\"} (${provider?.kind ?? \"unknown\"})`}</Text>\n <MessageView blocks={blocks} state={state} />\n <StatusBar state={state} model={provider?.model ?? \"unknown\"} />\n <CommandBar busy={state.phase !== \"idle\"} on_submit={submit} />\n </Box>\n );\n}","/**\n * Pure state logic for the ink TUI: UI-state transitions from agent events,\n * slash-command parsing, transcript block mapping, and display formatters.\n * No ink/react imports here — this module is unit-tested without a TTY.\n */\nimport type { AgentEvent } from \"../agent/events.js\";\nimport type { AgentRunResult } from \"../agent/agent.js\";\nimport type { AssistantMessage, Message, ToolCall, Usage } from \"../providers/types.js\";\nimport { safe_json_parse, truncate_text } from \"../util/json.js\";\n\nexport type UiPhase = \"idle\" | \"thinking\" | \"tool\";\n\nexport interface UiState {\n readonly phase: UiPhase;\n readonly turns_used: number;\n readonly usage: Usage;\n readonly session_path: string | undefined;\n readonly compress_count: number;\n readonly budget_exhausted: boolean;\n readonly last_error: string | undefined;\n readonly active_tool: ToolCall | undefined;\n}\n\nexport const INITIAL_UI_STATE: UiState = {\n phase: \"idle\",\n turns_used: 0,\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n session_path: undefined,\n compress_count: 0,\n budget_exhausted: false,\n last_error: undefined,\n active_tool: undefined,\n};\n\nexport const HISTORY_CAP = 50;\nconst TOOL_ARGS_PREVIEW_CHARS = 80;\nconst TOOL_OUTPUT_PREVIEW_CHARS = 120;\nconst ERROR_PREVIEW_CHARS = 300;\n\nfunction error_text(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n return String(error);\n}\n\n/** Reducer over UiState; one pure mapping per AgentEvent variant. */\nexport function apply_event(state: UiState, event: AgentEvent): UiState {\n switch (event.type) {\n case \"llm_start\":\n return { ...state, phase: \"thinking\", active_tool: undefined };\n case \"llm_end\":\n return {\n ...state,\n usage: {\n prompt_tokens: state.usage.prompt_tokens + event.result.usage.prompt_tokens,\n completion_tokens: state.usage.completion_tokens + event.result.usage.completion_tokens,\n total_tokens: state.usage.total_tokens + event.result.usage.total_tokens,\n },\n };\n case \"tool_call_start\":\n return { ...state, phase: \"tool\", active_tool: event.call };\n case \"tool_call_end\":\n return {\n ...state,\n phase: \"thinking\",\n active_tool: undefined,\n last_error: event.result.ok === true ? state.last_error : (event.result.error ?? \"tool failed\"),\n };\n case \"turn_end\":\n return { ...state, turns_used: event.turn };\n case \"compress_start\":\n return { ...state, compress_count: state.compress_count + 1 };\n case \"budget_exhausted\":\n return { ...state, budget_exhausted: true };\n case \"error\":\n return { ...state, last_error: error_text(event.error) };\n default:\n return state;\n }\n}\n\n/** Fold an AgentRunResult back into UiState after the run promise resolves. */\nexport function apply_run_result(state: UiState, result: AgentRunResult): UiState {\n return {\n ...state,\n phase: \"idle\",\n turns_used: result.outcome.turns_used,\n session_path: result.session_path ?? state.session_path,\n last_error: result.outcome.stopped_reason === \"aborted\" ? \"run aborted\" : state.last_error,\n };\n}\n\nexport type ParsedInput = { kind: \"slash\"; name: string; args: string } | { kind: \"message\"; text: string };\n\n/** Split trimmed input into slash command vs plain message (empty input = message). */\nexport function parse_command(raw_input: string): ParsedInput {\n const text = raw_input.trim();\n if (text.startsWith(\"/\") === false) {\n return { kind: \"message\", text };\n }\n const body = text.slice(1);\n const space_index = body.indexOf(\" \");\n if (space_index === -1) {\n return { kind: \"slash\", name: body, args: \"\" };\n }\n return { kind: \"slash\", name: body.slice(0, space_index), args: body.slice(space_index + 1).trim() };\n}\n\n/** 1234567 -> \"1,234,567\" (US grouping, matching the status bar style). */\nexport function format_usage(total_tokens: number): string {\n return total_tokens.toLocaleString(\"en-US\");\n}\n\nexport type BlockRole = \"user\" | \"lich\" | \"tool\" | \"meta\" | \"error\";\n\nexport interface HistoryBlock {\n readonly role: BlockRole;\n readonly lines: readonly string[];\n}\n\nfunction assistant_tool_line(call: ToolCall): string {\n const args_json = JSON.stringify(call.args) ?? \"{}\";\n return ` \\u23bf ${truncate_text(args_json, TOOL_ARGS_PREVIEW_CHARS)}`;\n}\n\nfunction format_message_lines(message: Message): string[] {\n if (message.role === \"user\") {\n return [`you \\u203a ${message.content}`];\n }\n if (message.role === \"assistant\") {\n const lines = [`lich \\u203a ${message.content}`];\n for (const call of message.tool_calls ?? []) {\n lines.push(assistant_tool_line(call));\n }\n return lines;\n }\n if (message.role === \"tool\") {\n const flag = message.is_error === true ? \"error\" : \"ok\";\n return [` \\u23bf ${message.name}: ${flag} (${truncate_text(message.content, TOOL_OUTPUT_PREVIEW_CHARS)})`];\n }\n return [`\\u00b7 system: ${message.content}`];\n}\n\n/** Map one transcript Message to display lines with its role tag. */\nexport function format_message_block(message: Message): HistoryBlock {\n if (message.role === \"tool\") {\n const ok = message.is_error !== true;\n return { role: ok === true ? \"tool\" : \"error\", lines: format_message_lines(message) };\n }\n const roles: Record<Exclude<Message[\"role\"], \"tool\">, BlockRole> = {\n system: \"meta\",\n user: \"user\",\n assistant: \"lich\",\n };\n return { role: roles[message.role], lines: format_message_lines(message) };\n}\n\n/** Keep the newest `cap` non-system messages as renderable blocks. */\nexport function split_history_blocks(messages: readonly Message[], cap: number): HistoryBlock[] {\n const visible = messages.filter((message) => message.role !== \"system\");\n const start = Math.max(0, visible.length - cap);\n return visible.slice(start).map(format_message_block);\n}\n\nfunction assistant_result_block(message: AssistantMessage): HistoryBlock | undefined {\n if (message.content.length === 0) {\n return undefined;\n }\n return { role: \"lich\", lines: [`lich \\u203a ${message.content}`] };\n}\n\n/** Post-run meta blocks: compression notices, budget, errors, final answer. */\nexport function run_notice_blocks(result: AgentRunResult): HistoryBlock[] {\n const blocks: HistoryBlock[] = [];\n if (result.outcome.stopped_reason === \"budget\") {\n blocks.push({ role: \"error\", lines: [\"\\u00b7 budget exhausted (turn cap reached)\"] });\n }\n const final_block = result.outcome.final === undefined ? undefined : assistant_result_block(result.outcome.final);\n if (final_block !== undefined) {\n blocks.push(final_block);\n }\n return blocks;\n}\n\nfunction truncate_tool_preview(result_content: string, ok: boolean): string {\n return ` \\u23bf ${ok === true ? \"ok\" : \"error\"} (${truncate_text(result_content, TOOL_OUTPUT_PREVIEW_CHARS)})`;\n}\n\n/** One finalized live tool row; falls back to the transcript copy when absent. */\nexport function tool_result_block(call: ToolCall, ok: boolean, result_content: string): HistoryBlock {\n const args_json = JSON.stringify(call.args) ?? \"{}\";\n const lines = [\n `\\u23fa ${call.name}(${truncate_text(args_json, TOOL_ARGS_PREVIEW_CHARS)})`,\n truncate_tool_preview(result_content, ok),\n ];\n return { role: ok === true ? \"tool\" : \"error\", lines };\n}\n\nexport function parse_tool_message_content(content: string): { ok: boolean; output: string } {\n const parsed = safe_json_parse<{ ok?: unknown; output?: unknown }>(content);\n if (parsed !== undefined && typeof parsed.ok === \"boolean\" && typeof parsed.output === \"string\") {\n return { ok: parsed.ok, output: parsed.output };\n }\n return { ok: true, output: content };\n}\n\nexport function compress_notice_block(summary_chars: number): HistoryBlock {\n return { role: \"meta\", lines: [`\\u00b7 context compressed (summary ${summary_chars} chars)`] };\n}\n\nexport function error_notice_block(message: string): HistoryBlock {\n return { role: \"error\", lines: [`\\u00b7 error: ${truncate_text(message, ERROR_PREVIEW_CHARS)}`] };\n}\n\nexport function tool_args_preview(args: Record<string, unknown>): string {\n const args_json = JSON.stringify(args) ?? \"{}\";\n return truncate_text(args_json, TOOL_ARGS_PREVIEW_CHARS);\n}\n\nexport function help_block(): HistoryBlock {\n return { role: \"meta\", lines: [...HELP_LINES] };\n}\n\nexport function model_label_block(config: { providers: readonly { model: string; kind: string }[] }): HistoryBlock {\n const provider = config.providers[0];\n return {\n role: \"meta\",\n lines: [`\\u00b7 model: ${provider?.model ?? \"unknown\"} \\u00b7 provider: ${provider?.kind ?? \"unknown\"}`],\n };\n}\n\nexport function usage_notice_block(total_tokens: number): HistoryBlock {\n return { role: \"meta\", lines: [`\\u00b7 tokens used this session: ${format_usage(total_tokens)}`] };\n}\n\nexport function unknown_command_block(name: string): HistoryBlock {\n return { role: \"error\", lines: [`\\u00b7 unknown command: /${name} (try /help)`] };\n}\n\nexport interface SessionEntryInfo {\n readonly name: string;\n readonly size_bytes: number;\n readonly mtime_ms: number;\n}\n\n/** Newest-first session listing, capped at `cap` entries. */\nexport function session_list_block(entries: readonly SessionEntryInfo[], cap: number = 10): HistoryBlock {\n const sorted = [...entries].sort((a, b) => b.mtime_ms - a.mtime_ms).slice(0, cap);\n if (sorted.length === 0) {\n return { role: \"meta\", lines: [\"\\u00b7 no session files yet\"] };\n }\n const lines: string[] = [`\\u00b7 sessions (${sorted.length}):`];\n for (const entry of sorted) {\n lines.push(` ${entry.name} (${format_usage(entry.size_bytes)} bytes)`);\n }\n return { role: \"meta\", lines };\n}\n\nexport const SLASH_COMMAND_NAMES: readonly string[] = [\n \"exit\",\n \"quit\",\n \"q\",\n \"help\",\n \"model\",\n \"usage\",\n \"clear\",\n \"sessions\",\n];\n\nexport const HELP_LINES: readonly string[] = [\n \"commands: /help /model /usage /clear /sessions /exit (aliases: /quit /q)\",\n \"enter submits \\u00b7 backspace deletes \\u00b7 up/down recalls history \\u00b7 pasted newlines become spaces\",\n];","/**\n * Transcript rendering: maps HistoryBlock descriptors to ink elements with\n * role-based colors, and shows an animated braille spinner while thinking.\n * Blocks are pre-capped by the app, so a plain flex column is sufficient.\n */\nimport { useEffect, useState } from \"react\";\nimport { Box, Text } from \"ink\";\nimport { tool_args_preview, type HistoryBlock, type UiState } from \"./state.js\";\n\nconst SPINNER_FRAMES: readonly string[] = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\nconst SPINNER_INTERVAL_MS = 80;\n\n/** Braille spinner frames on an 80ms interval; clears on unmount. */\nfunction use_spinner(): string {\n const [frame, set_frame] = useState(SPINNER_FRAMES[0] ?? \"⠋\");\n useEffect(() => {\n const timer = setInterval(() => {\n const next = SPINNER_FRAMES[(SPINNER_FRAMES.indexOf(frame) + 1) % SPINNER_FRAMES.length];\n set_frame(next ?? \"⠋\");\n }, SPINNER_INTERVAL_MS);\n return () => {\n clearInterval(timer);\n };\n }, [frame]);\n return frame;\n}\n\nfunction ThinkingLine(): React.JSX.Element {\n const frame = use_spinner();\n return <Text dimColor>{`${frame} thinking…`}</Text>;\n}\n\nconst ROLE_COLORS: Record<HistoryBlock[\"role\"], string | undefined> = {\n user: \"white\",\n lich: \"green\",\n tool: \"cyan\",\n meta: undefined,\n error: \"red\",\n};\n\nfunction BlockLines({ block }: { block: HistoryBlock }): React.JSX.Element {\n const color = ROLE_COLORS[block.role];\n return (\n <>\n {block.lines.map((line, index) => (\n <Text key={index} color={color} dimColor={color === undefined}>{line}</Text>\n ))}\n </>\n );\n}\n\ninterface MessageViewProps {\n readonly blocks: readonly HistoryBlock[];\n readonly state: UiState;\n}\n\n/** Transcript column plus the live phase line (spinner / running tool row). */\nexport function MessageView({ blocks, state }: MessageViewProps): React.JSX.Element {\n return (\n <Box flexDirection=\"column\" flexGrow={1}>\n {blocks.map((block, index) => (\n <Box key={index} flexDirection=\"column\">\n <BlockLines block={block} />\n </Box>\n ))}\n {state.phase === \"thinking\" ? <ThinkingLine /> : null}\n {state.phase === \"tool\" && state.active_tool !== undefined ? (\n <Text color=\"cyan\">{`⏺ ${state.active_tool.name}(${tool_args_preview(state.active_tool.args)})`}</Text>\n ) : null}\n </Box>\n );\n}","/**\n * Bottom status line: model, turns, token totals, phase tag, compression\n * count, and the session path once the agent has persisted a transcript.\n */\nimport { Box, Text } from \"ink\";\nimport { format_usage, type UiState } from \"./state.js\";\n\nconst PHASE_LABELS: Record<UiState[\"phase\"], string> = {\n idle: \"idle\",\n thinking: \"thinking\",\n tool: \"tool\",\n};\n\ninterface StatusBarProps {\n readonly state: UiState;\n readonly model: string;\n}\n\nexport function StatusBar({ state, model }: StatusBarProps): React.JSX.Element {\n const phase = PHASE_LABELS[state.phase];\n return (\n <Box>\n <Text dimColor>\n {`model ${model} · turns ${state.turns_used} · tokens ${format_usage(state.usage.total_tokens)} · [${phase}]`}\n {state.compress_count > 0 ? ` · compressed ${state.compress_count}` : \"\"}\n {state.session_path !== undefined ? ` · ${state.session_path}` : \"\"}\n </Text>\n {state.budget_exhausted ? <Text color=\"red\"> · budget exhausted</Text> : null}\n </Box>\n );\n}","/**\n * Input row: printable characters accumulate in a buffer, Enter submits,\n * Backspace/Delete edits, Up/Down walk a 20-entry recall ring, and pasted\n * newlines collapse to spaces. Ctrl+C is left to ink's default handling.\n */\nimport { useState } from \"react\";\nimport { Box, Text, useInput } from \"ink\";\n\nconst INPUT_HISTORY_CAP = 20;\n\ninterface CommandBarProps {\n readonly busy: boolean;\n readonly on_submit: (text: string) => void;\n}\n\n/** Push onto a capped ring (newest first) without mutating the source. */\nfunction push_history(ring: readonly string[], entry: string): readonly string[] {\n return [entry, ...ring.filter((item) => item !== entry)].slice(0, INPUT_HISTORY_CAP);\n}\n\nexport function CommandBar({ busy, on_submit }: CommandBarProps): React.JSX.Element {\n const [buffer, set_buffer] = useState(\"\");\n const [recall_ring, set_recall_ring] = useState<readonly string[]>([]);\n const [recall_index, set_recall_index] = useState<number | undefined>(undefined);\n\n const submit_buffer = (): void => {\n const text = buffer.trim();\n set_buffer(\"\");\n set_recall_index(undefined);\n if (text.length > 0) {\n set_recall_ring((current) => push_history(current, text));\n on_submit(text);\n }\n };\n\n const walk_recall = (direction: 1 | -1): void => {\n if (recall_ring.length === 0) {\n return;\n }\n const current = recall_index ?? -direction;\n const next = Math.min(Math.max(current + direction, 0), recall_ring.length - 1);\n set_recall_index(next);\n set_buffer(recall_ring[next] ?? \"\");\n };\n\n useInput((input, key) => {\n if (key.return === true) {\n submit_buffer();\n return;\n }\n if (key.upArrow === true) {\n walk_recall(-1);\n return;\n }\n if (key.downArrow === true) {\n walk_recall(1);\n return;\n }\n if (key.backspace === true || key.delete === true) {\n set_buffer((current) => current.slice(0, -1));\n return;\n }\n if (key.ctrl === true || key.escape === true || key.tab === true || key.meta === true) {\n return;\n }\n if (input.length > 0) {\n set_buffer((current) => current + input.replaceAll(\"\\n\", \" \").replaceAll(\"\\r\", \" \"));\n }\n });\n\n return (\n <Box>\n <Text dimColor>{busy ? \" … \" : \"› \"}</Text>\n <Text>{buffer}</Text>\n <Text dimColor>▌</Text>\n </Box>\n );\n}"],"mappings":"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","names":["useEffect","useState","Box","Text","Box","Text","jsx","jsxs","Box","Text","useState","Box","Text","jsx","jsxs","jsx","jsxs","useEffect","useState","Box","Text","jsx"]}
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Hands-on recipes for the three extension axes: builtin tools, providers, and
|
|
4
4
|
gateway platforms - plus the TUI internals, the conventions that hold the
|
|
5
|
-
codebase together, and how the test suite is organized.
|
|
6
|
-
|
|
5
|
+
codebase together, and how the test suite is organized. Everything on this
|
|
6
|
+
page assumes a clone of the repository — the `@moikapy/lich` npm package is
|
|
7
|
+
the artifact this source builds (see
|
|
8
|
+
[development install](../getting-started.md#development-install-from-source)).
|
|
9
|
+
Read [tools](./tools.md) and [providers](./providers.md) first for the contracts.
|
|
7
10
|
|
|
8
11
|
## Add a builtin tool
|
|
9
12
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Architecture Overview
|
|
2
2
|
|
|
3
|
-
Lich v0.
|
|
3
|
+
Lich v0.3.0 is a small TypeScript AI agent harness: it drives a chat model in a
|
|
4
4
|
think-act-observe loop, lets the model call tools, compresses history when the
|
|
5
5
|
context budget demands it, and persists transcripts. It runs on Bun, is ESM
|
|
6
6
|
with NodeNext resolution, and its only runtime dependencies are `zod` (config
|
|
@@ -28,7 +28,7 @@ flowchart TB
|
|
|
28
28
|
CLIENTS["openai_compat / anthropic / ollama<br/>HTTP clients"]
|
|
29
29
|
EXEC["ToolExecutor<br/>(src/tools/executor.ts)"]
|
|
30
30
|
REG["ToolRegistry<br/>(src/tools/registry.ts)"]
|
|
31
|
-
|
|
31
|
+
BUILTIN["builtin tools<br/>(src/tools/builtin/*)"]
|
|
32
32
|
COMP["ContextCompressor<br/>(src/context/compressor.ts)"]
|
|
33
33
|
SESSION["SessionStore<br/>(src/session/store.ts)"]
|
|
34
34
|
|
|
@@ -59,18 +59,20 @@ structural interfaces ([`src/agent/loop.ts`](../../src/agent/loop.ts)):
|
|
|
59
59
|
|
|
60
60
|
- `ChatFn` - `(messages, tools, options?) => Promise<ChatResult>` (declared in
|
|
61
61
|
`src/context/compressor.ts`, since compression needs the same shape).
|
|
62
|
-
- `ToolRunner` - `{ execute(name, args) => Promise<ToolResult> }`.
|
|
62
|
+
- `ToolRunner` - `{ execute(name, args, context?) => Promise<ToolResult> }`.
|
|
63
63
|
|
|
64
64
|
`run_conversation` receives a `LoopDeps` object holding a `ChatFn`, a
|
|
65
|
-
`ToolRunner`, a `definitions()` callback for tool schemas,
|
|
66
|
-
emitter
|
|
67
|
-
`
|
|
65
|
+
`ToolRunner`, a `definitions()` callback for tool schemas, an optional
|
|
66
|
+
emitter, and an optional per-run `tool_context` threaded to every tool
|
|
67
|
+
execution. The `Agent` class (`src/agent/agent.ts`) is the composition root:
|
|
68
|
+
its `loop_deps()` method wires the real implementations -
|
|
68
69
|
|
|
69
70
|
```ts
|
|
70
71
|
chat: (messages, tools, chat_options) => this.router.chat_with_failover(messages, tools, chat_options),
|
|
71
72
|
tools: this.executor,
|
|
72
73
|
definitions: () => this.registry.definitions(),
|
|
73
74
|
emitter: this.events,
|
|
75
|
+
tool_context,
|
|
74
76
|
```
|
|
75
77
|
|
|
76
78
|
(src/agent/agent.ts, `loop_deps()`)
|
|
@@ -166,7 +168,7 @@ Walkthrough of a single `Agent.run({ input })` call
|
|
|
166
168
|
| `src/tools/guard.ts` | Path confinement, timeouts, clamping, arg coercion. |
|
|
167
169
|
| `src/tools/registry.ts` | Name-keyed tool registry; duplicate rejection. |
|
|
168
170
|
| `src/tools/executor.ts` | Never-throw execution with timeout and abort. |
|
|
169
|
-
| `src/tools/builtin/*` |
|
|
171
|
+
| `src/tools/builtin/*` | Builtin tools, including `run_tests` (see [tools](./tools.md)). |
|
|
170
172
|
| `src/gateway/bus.ts` | Conversation-keyed runner over one shared `Agent`. |
|
|
171
173
|
| `src/gateway/runner.ts` | Adapter construction, signal handling, process lifetime. |
|
|
172
174
|
| `src/gateway/{telegram,discord,twitch,webhook}.ts` | Platform adapters. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Plugin architecture
|
|
2
2
|
|
|
3
|
-
How the plugin system loads user modules, merges tools into the registry, and intercepts tool calls with hooks.
|
|
3
|
+
How the plugin system loads user modules, merges tools into the registry, and intercepts tool calls with hooks. Sources: [`loader.ts`](../../src/plugins/loader.ts), [`hooks.ts`](../../src/plugins/hooks.ts), [`types.ts`](../../src/plugins/types.ts).
|
|
4
4
|
|
|
5
5
|
## Loader flow
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ flowchart LR
|
|
|
23
23
|
K -- yes --> M["LoadedPlugin collected"]
|
|
24
24
|
M --> N["Agent constructor"]
|
|
25
25
|
N --> O["registry merge:\nplugin tools appended\n(dup tool name → warn+skip)"]
|
|
26
|
-
N --> P["
|
|
26
|
+
N --> P["hooked plugins kept\nwhole (per-plugin state channel)"]
|
|
27
27
|
P --> Q["HookedToolRunner wraps\nToolExecutor when hooks exist"]
|
|
28
28
|
E --> R["warn + continue"]
|
|
29
29
|
H --> R
|
|
@@ -53,14 +53,66 @@ sequenceDiagram
|
|
|
53
53
|
else no blocker
|
|
54
54
|
H->>E: execute(name, args, context?)
|
|
55
55
|
E-->>H: ToolResult
|
|
56
|
-
H->>A: await hook({...info, result_summary}, ctx)
|
|
57
|
-
note over A: summary = 300 chars of output/error
|
|
56
|
+
H->>A: await hook({...info, result_summary, ok, error?}, ctx)
|
|
57
|
+
note over A: summary = 300 chars of output/error; ok/error are structured
|
|
58
58
|
H-->>L: ToolResult unchanged
|
|
59
59
|
end
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
Lifecycle fan-outs live on the same wrapper: `Agent.run` calls `call_run_start({input_chars})` before `run_conversation` and `call_run_end({stopped_reason, turns_used})` after it (including the abort/throw path, via `finally`). Both are best-effort: hook throws are logged at `warn` and the run proceeds.
|
|
63
63
|
|
|
64
|
+
## Builtin gatekeeper
|
|
65
|
+
|
|
66
|
+
`Agent` constructs `gatekeeper_plugin` in code, before config plugins, and
|
|
67
|
+
pushes it as a synthetic `LoadedPlugin` through tool registration and the
|
|
68
|
+
hook runner. The config loader never sees it. Construction failure means
|
|
69
|
+
`git_commit` is not in the registry at all. "No gatekeeper → no `git_commit`"
|
|
70
|
+
is that trusted path: with the gatekeeper off, a config plugin may still name
|
|
71
|
+
a tool `git_commit` (the documented plugin-trust floor). While the gatekeeper
|
|
72
|
+
is registered, first-wins keeps its tool.
|
|
73
|
+
|
|
74
|
+
`LICH_ALLOW_SELF_COMMIT` is read from process env at construction. The spec
|
|
75
|
+
value is `1`. Unset or any other value is fail-closed.
|
|
76
|
+
|
|
77
|
+
Per-run state starts `tests_ok=false`, `dirty=true`, `commits=0` (swapped at
|
|
78
|
+
`call_run_start`). `after_tool_call` updates only on structured `ok`:
|
|
79
|
+
|
|
80
|
+
- `write_file` / `edit_file` success → `dirty=true`
|
|
81
|
+
- `run_tests` success → `tests_ok=true`, `dirty=false`
|
|
82
|
+
- `git_commit` success → `commits++`
|
|
83
|
+
|
|
84
|
+
`before_tool_call` vetoes `git_commit` unless
|
|
85
|
+
`allow_self_commit && tests_ok && !dirty && commits < 1`. The reason names
|
|
86
|
+
the failed condition: `self_commit_disabled`, `tests_not_ok`,
|
|
87
|
+
`worktree_dirty`, `commit_budget_exhausted`. The model sees
|
|
88
|
+
`blocked_by_plugin: <reason>`.
|
|
89
|
+
|
|
90
|
+
`terminal` is vetoed on a hardcoded denylist match; the reason is
|
|
91
|
+
`git_denylist: <pattern>`. Patterns: flag-tolerant `commit` and `push`
|
|
92
|
+
(`commit`, `-commit`, `--commit`, `push`, `-push`, `--push`) plus
|
|
93
|
+
any-occurrence `commit-tree` and `update-ref`. No `remote` pattern. One
|
|
94
|
+
commit per run, hardcoded.
|
|
95
|
+
|
|
96
|
+
`git_commit` args are `{message, paths}` with 1–50 paths relative to
|
|
97
|
+
`work_dir`. It rejects `""`, `.`, anything resolving to `work_dir`, and
|
|
98
|
+
secret-ish basenames (`.env`, `.env.local`, `*.pem`, `*.p12`, `id_rsa*`).
|
|
99
|
+
Unreachable `HEAD` is fail-closed. Recipe: scoped `git add -- <paths>`, then
|
|
100
|
+
`git commit --only` with explicit identity `-c` flags. `timeout_ms` is 60000.
|
|
101
|
+
It never pushes.
|
|
102
|
+
|
|
103
|
+
**Attestation:** clean state attests no `write_file`/`edit_file` since the
|
|
104
|
+
last green `run_tests`; it does NOT attest absence of terminal-mediated
|
|
105
|
+
writes — that sits with the documented terminal floor.
|
|
106
|
+
|
|
107
|
+
Floors, stated not closed:
|
|
108
|
+
|
|
109
|
+
- Terminal floor: raw `terminal` can run arbitrary git; the denylist is
|
|
110
|
+
best-effort. The boundary is human review of the local repo; push is
|
|
111
|
+
human-only.
|
|
112
|
+
- Plugin-trust floor: `.lich/config.json` `plugins` is persistent arbitrary
|
|
113
|
+
code at next process start. Review config diffs.
|
|
114
|
+
- One lich process per repo (the `run_tests` mutex is process-local).
|
|
115
|
+
|
|
64
116
|
## Design decisions
|
|
65
117
|
|
|
66
118
|
- **Explicit entries, no directory scan.** v1 loads only the files you list in `config.plugins`. Directory scanning would make runs depend on whatever happens to sit in a folder — non-reproducible, and a footgun for tools that write into `.lich/`. Explicit entries make the agent's tool surface a function of the config alone.
|
|
@@ -74,7 +126,8 @@ Lifecycle fan-outs live on the same wrapper: `Agent.run` calls `call_run_start({
|
|
|
74
126
|
| --- | --- | --- |
|
|
75
127
|
| `Plugin` | type | `{name, version?, tools?, hooks?}` — what a plugin module exports. |
|
|
76
128
|
| `PluginHooks` | type | The four optional lifecycle hooks with their signatures. |
|
|
77
|
-
| `HookContext` | type | `{work_dir}` passed to every hook. |
|
|
129
|
+
| `HookContext` | type | `{work_dir, state?}` passed to every hook; `state` is the invoking plugin's own per-run bag. |
|
|
130
|
+
| `AfterToolCallInfo` | type | Tool name/args plus the 300-char `result_summary` and structured `ok`/`error` fields. |
|
|
78
131
|
| `LoadedPlugin` | type | `{plugin, entry}` — a loaded plugin and its source path. |
|
|
79
132
|
| `load_plugins` | function | `(entries, base_dir) => {plugins, errors}` — dynamic import + shape validation. |
|
|
80
133
|
| `plugin_errors_summary` | function | Joins error entries into one warn-able string. |
|
|
@@ -118,9 +118,10 @@ args, context?)`:
|
|
|
118
118
|
defaults (falling back to `process.cwd()`) plus the configured `env`.
|
|
119
119
|
3. **Cancelled fast path**: if the context signal is already aborted, return
|
|
120
120
|
`{ ok: false, output: "", error: "cancelled" }` without running the tool.
|
|
121
|
-
4. **Signal merge +
|
|
121
|
+
4. **Signal merge + timeout**: a fresh `AbortController` is aborted by
|
|
122
122
|
the external signal (an `abort` listener), by the `with_timeout` deadline
|
|
123
|
-
(
|
|
123
|
+
(the tool's own `timeout_ms` when declared, else
|
|
124
|
+
`DEFAULT_TOOL_TIMEOUT_MS = 30000`), and the merged signal is what the tool
|
|
124
125
|
receives. The external listener is removed in a `finally`.
|
|
125
126
|
5. **Output clamping**: successful results pass through `clamp_result`
|
|
126
127
|
(`clamp_output`, 20 000 chars).
|
|
@@ -137,8 +138,9 @@ form back with `parse_tool_message_content` (src/tui/state.ts).
|
|
|
137
138
|
|
|
138
139
|
## Builtin catalog
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
([`src/tools/builtin/index.ts`](../../src/tools/builtin/index.ts))
|
|
141
|
+
Registered by `register_builtin_tools`
|
|
142
|
+
([`src/tools/builtin/index.ts`](../../src/tools/builtin/index.ts)).
|
|
143
|
+
Docs tools join the list only when a docs root resolves.
|
|
142
144
|
|
|
143
145
|
| Tool | Key args | Implementation insight |
|
|
144
146
|
| --- | --- | --- |
|
|
@@ -154,6 +156,7 @@ Twelve tools, registered by `register_builtin_tools`
|
|
|
154
156
|
| `process_list` | `filter?`, `max_results?` | Reads `/proc` synchronously: numeric dirs are pids, `cmdline` is NUL-separated; missing entries (process died mid-scan) read as empty. |
|
|
155
157
|
| `disk_usage` | `path?`, `max_entries?` | One `du -sb` subprocess per depth-1 entry with a 10 s timeout; sorted desc with a `TOTAL` row; `du` missing yields `du_unavailable`. |
|
|
156
158
|
| `env_get` | `keys?`, `prefix?`, `reveal?` | Values are hidden unless `reveal`; names matching `/(secret\|token\|password\|key\|credential\|auth)/i` are **always** masked as `<redacted: N chars>`. |
|
|
159
|
+
| `run_tests` | `filter?` | Runs `LICH_TEST_COMMAND` (default `node node_modules/vitest/vitest.mjs run`) in `work_dir` via `bash -lc`. `timeout_ms` is 600000. A module mutex makes a concurrent call return `{ok:false, error:"run_tests_busy"}`. `ok` is the structured pass/fail the gatekeeper reads; output is clamped to 2000 chars. One lich process per repo — a second process is fail-closed busy or failed. |
|
|
157
160
|
|
|
158
161
|
The three HTTP tools (`fetch_url`, `web_search`, `http_request`) share
|
|
159
162
|
helpers from `fetch_url.ts`: `valid_http_url` (URL parse + protocol
|
|
@@ -161,6 +164,15 @@ allowlist), `compose_abort_signal` (per-call `AbortSignal.timeout` merged
|
|
|
161
164
|
with the executor's cancellation via `AbortSignal.any`), and `clamp_int_arg`
|
|
162
165
|
(floored, bounded to `[1, max]`).
|
|
163
166
|
|
|
167
|
+
## Docs search and skills
|
|
168
|
+
|
|
169
|
+
`docs_search` scores sections under the resolved docs root (memoized) and, when
|
|
170
|
+
`<work_dir>/.lich/skills/` exists, also walks that directory. The skills
|
|
171
|
+
candidate is existence-only: it does not need `index.md`. The walk is fresh
|
|
172
|
+
on every call — user-writable skill files are not memoized into the package
|
|
173
|
+
docs cache. Skills are reference data, written with `write_file`, not
|
|
174
|
+
instructions. See the [plugins guide](../user-guide/plugins.md#skills-and-memory).
|
|
175
|
+
|
|
164
176
|
## Registry
|
|
165
177
|
|
|
166
178
|
[`ToolRegistry`](../../src/tools/registry.ts) is a name-keyed `Map`:
|