@moikapy/lich 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/README.md +34 -8
  3. package/dist/{chunk-P52U5M3L.js → chunk-MLFJW4JU.js} +399 -70
  4. package/dist/chunk-MLFJW4JU.js.map +1 -0
  5. package/dist/cli.d.ts +1 -2
  6. package/dist/cli.js +4 -3
  7. package/dist/cli.js.map +1 -1
  8. package/dist/{gateway-CWPVIU3W.js → gateway-XTYDYT67.js} +2 -2
  9. package/dist/index.d.ts +25 -11
  10. package/dist/index.js +1 -1
  11. package/dist/{tui-V7ATLIKW.js → tui-VYBJSGRV.js} +6 -3
  12. package/dist/tui-VYBJSGRV.js.map +1 -0
  13. package/docs/.vitepress/config.mts +1 -0
  14. package/docs/architecture/extending.md +5 -2
  15. package/docs/architecture/overview.md +9 -7
  16. package/docs/architecture/plugins.md +58 -5
  17. package/docs/architecture/tools.md +16 -4
  18. package/docs/design/council/architecture-review-r2.md +36 -0
  19. package/docs/design/council/architecture-review-r3.md +69 -0
  20. package/docs/design/council/index.md +15 -0
  21. package/docs/design/council/security-review-r2.md +32 -0
  22. package/docs/design/council/security-review-r3.md +19 -0
  23. package/docs/design/council/simplicity-review-r2.md +35 -0
  24. package/docs/design/council/simplicity-review-r3.md +22 -0
  25. package/docs/design/self-improvement-loop.md +160 -162
  26. package/docs/getting-started.md +37 -15
  27. package/docs/index.md +11 -11
  28. package/docs/user-guide/cli.md +18 -5
  29. package/docs/user-guide/gateway.md +10 -10
  30. package/docs/user-guide/library.md +15 -9
  31. package/docs/user-guide/plugins.md +59 -6
  32. package/docs/user-guide/tui.md +3 -3
  33. package/package.json +4 -2
  34. package/dist/chunk-P52U5M3L.js.map +0 -1
  35. package/dist/tui-V7ATLIKW.js.map +0 -1
  36. /package/dist/{gateway-CWPVIU3W.js.map → gateway-XTYDYT67.js.map} +0 -0
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-P52U5M3L.js";
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-V7ATLIKW.js");
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-CWPVIU3W.js");
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-P52U5M3L.js";
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-CWPVIU3W.js.map
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
- * All hook arrays are pre-flattened at construction so the per-call hot path
515
- * does no concat; hooks always run in plugin registration order.
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 before_hooks;
520
- private readonly after_hooks;
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
- }, ctx: HookContext): Promise<void>;
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, ctx: HookContext): Promise<void>;
548
+ call_run_end(info: RunEndInfo, base: HookContext): Promise<void>;
535
549
  }
536
550
 
537
551
  /**
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  plugin_errors_summary,
16
16
  register_builtin_tools,
17
17
  run_agent
18
- } from "./chunk-P52U5M3L.js";
18
+ } from "./chunk-MLFJW4JU.js";
19
19
  export {
20
20
  Agent,
21
21
  AgentEmitter,
@@ -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-P52U5M3L.js";
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 v0.2.0 \u2014 ${provider?.model ?? "unknown"} (${provider?.kind ?? "unknown"})` }),
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-V7ATLIKW.js.map
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"]}
@@ -1,6 +1,7 @@
1
1
  import { defineConfig } from 'vitepress'
2
2
 
3
3
  export default defineConfig({
4
+ base: '/lich/',
4
5
  title: 'Lich',
5
6
  description:
6
7
  'A TypeScript AI agent harness — Think-Act-Observe loop, provider failover, tools, gateway, TUI.',
@@ -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. Read
6
- [tools](./tools.md) and [providers](./providers.md) first for the contracts.
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.2.0 is a small TypeScript AI agent harness: it drives a chat model in a
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
- BUILTIN["12 builtin tools<br/>(src/tools/builtin/*)"]
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, and an optional
66
- emitter. The `Agent` class (`src/agent/agent.ts`) is the composition root: its
67
- `loop_deps()` method wires the real implementations -
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/*` | The 12 builtin tools (see [tools](./tools.md)). |
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. Source: [`src/plugins/`](../../src/plugins).
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["hook concat:\nPluginHooks[] in config order"]
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 + 30 s timeout**: a fresh `AbortController` is aborted by
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
- (`DEFAULT_TOOL_TIMEOUT_MS = 30000`), and the merged signal is what the tool
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
- Twelve tools, registered by `register_builtin_tools`
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`: