@moikapy/lich 0.8.0 → 0.9.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 (35) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/README.md +38 -19
  3. package/dist/{chunk-QOTECFCN.js → chunk-4N3S6ACI.js} +2 -2
  4. package/dist/{chunk-EDRUZF22.js → chunk-KOEZQUIX.js} +1207 -512
  5. package/dist/chunk-KOEZQUIX.js.map +1 -0
  6. package/dist/{chunk-W6JXZBUE.js → chunk-P2YEHNM4.js} +2 -2
  7. package/dist/chunk-P2YEHNM4.js.map +1 -0
  8. package/dist/{chunk-VKEOHUCB.js → chunk-TYRKVBWQ.js} +26 -8
  9. package/dist/chunk-TYRKVBWQ.js.map +1 -0
  10. package/dist/cli.js +82 -44
  11. package/dist/cli.js.map +1 -1
  12. package/dist/{gateway-WU5G4ODO.js → gateway-OSB7254F.js} +209 -47
  13. package/dist/gateway-OSB7254F.js.map +1 -0
  14. package/dist/index.d.ts +105 -60
  15. package/dist/index.js +12 -5
  16. package/dist/{store-COOLBAHB.js → store-YYLEB7JG.js} +2 -2
  17. package/dist/{tui-4BP3TI7J.js → tui-XXIU4W7K.js} +17 -5
  18. package/dist/tui-XXIU4W7K.js.map +1 -0
  19. package/docs/architecture/overview.md +2 -2
  20. package/docs/architecture/tools.md +3 -1
  21. package/docs/getting-started.md +6 -6
  22. package/docs/index.md +1 -1
  23. package/docs/user-guide/cli.md +9 -7
  24. package/docs/user-guide/games.md +2 -1
  25. package/docs/user-guide/library.md +1 -1
  26. package/docs/user-guide/redot.md +2 -2
  27. package/docs/user-guide/tui.md +2 -2
  28. package/package.json +1 -1
  29. package/dist/chunk-EDRUZF22.js.map +0 -1
  30. package/dist/chunk-VKEOHUCB.js.map +0 -1
  31. package/dist/chunk-W6JXZBUE.js.map +0 -1
  32. package/dist/gateway-WU5G4ODO.js.map +0 -1
  33. package/dist/tui-4BP3TI7J.js.map +0 -1
  34. /package/dist/{chunk-QOTECFCN.js.map → chunk-4N3S6ACI.js.map} +0 -0
  35. /package/dist/{store-COOLBAHB.js.map → store-YYLEB7JG.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  catalog_by_name
3
- } from "./chunk-EDRUZF22.js";
3
+ } from "./chunk-KOEZQUIX.js";
4
4
 
5
5
  // src/index.ts
6
6
  import { readFileSync } from "fs";
@@ -55,4 +55,4 @@ export {
55
55
  catalog_client_entry,
56
56
  LICH_VERSION
57
57
  };
58
- //# sourceMappingURL=chunk-W6JXZBUE.js.map
58
+ //# sourceMappingURL=chunk-P2YEHNM4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/mcp/mcp_catalog_entry.ts"],"sourcesContent":["/**\n * Public surface of the lich agent harness. Pure re-exports, plus the package version.\n */\nimport { readFileSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\n\nfunction read_package_version(): string {\n const pkg_path = fileURLToPath(new URL(\"../package.json\", import.meta.url));\n const pkg = JSON.parse(readFileSync(pkg_path, \"utf8\")) as { version?: unknown };\n if (typeof pkg.version !== \"string\" || pkg.version.length === 0) {\n throw new Error(\"package.json is missing version\");\n }\n return pkg.version;\n}\n\nexport const LICH_VERSION = read_package_version();\n\nexport { Agent, create_agent, run_agent, create_agent_with_plugins } from \"./agent/agent.js\";\nexport type { AgentRunOptions, AgentRunResult } from \"./agent/agent.js\";\nexport type { AgentConfig } from \"./agent/config.js\";\nexport { parse_agent_config } from \"./agent/config.js\";\nexport { catalog_client_entry } from \"./mcp/mcp_catalog_entry.js\";\nexport { AgentEmitter } from \"./agent/events.js\";\nexport type { AgentEvent, AgentEventHandler, AgentEvents } from \"./agent/events.js\";\nexport { run_conversation } from \"./agent/loop.js\";\nexport type { LoopDeps, LoopOutcome, LoopParams, ToolRunner } from \"./agent/loop.js\";\nexport { open_session, read_session_messages } from \"./session/store.js\";\nexport type {\n AssistantMessage,\n ChatOptions,\n ChatResult,\n Message,\n ProviderConfig,\n ToolCall,\n ToolDefinition,\n Usage,\n} from \"./providers/types.js\";\nexport { ProviderError } from \"./providers/types.js\";\nexport type { JsonSchemaObject } from \"./util/json_schema.js\";\nexport { ToolExecutor } from \"./tools/executor.js\";\nexport { ToolRegistry } from \"./tools/registry.js\";\nexport type { Tool, ToolContext, ToolResult } from \"./tools/types.js\";\nexport { register_builtin_tools } from \"./tools/builtin/index.js\";\nexport { HookedToolRunner } from \"./plugins/hooks.js\";\nexport { load_plugins, plugin_errors_summary } from \"./plugins/loader.js\";\nexport type { LoadedPlugin, PluginLoadError } from \"./plugins/loader.js\";\nexport type {\n AfterToolCallInfo,\n BeforeToolCallInfo,\n BeforeToolCallResult,\n HookContext,\n Plugin,\n PluginHooks,\n RunEndInfo,\n} from \"./plugins/types.js\";","/**\n * Install shape: turn a catalog manifest into a disabled mcp_servers entry.\n * Does not spawn, download, or copy secrets.\n */\nimport { catalog_by_name } from \"./mcp_catalog.js\";\n\nconst PLACEHOLDER = /\\$\\{([a-z_]+)\\}/g;\n\nfunction expand(value: string, substitutes: Record<string, string>): string | undefined {\n const tokens = value.match(PLACEHOLDER) ?? [];\n let next = value;\n for (const token of tokens) {\n const key = token.slice(2, -1);\n const found = substitutes[key];\n if (found === undefined || found.length === 0) {\n return undefined;\n }\n next = next.replace(token, found);\n }\n return next;\n}\n\n/** Write the client entry. The user sets enabled. Godot has nothing to write. */\nexport function catalog_client_entry(\n name: string,\n substitutes: Record<string, string> = {},\n): Record<string, unknown> | string {\n const manifest = catalog_by_name(name);\n if (manifest === undefined) {\n return `unknown mcp catalog entry '${name}'`;\n }\n if (manifest.transport !== \"stdio\" || manifest.command === undefined) {\n return manifest.note ?? `${name} has no official mcp server to enable`;\n }\n const args: string[] = [];\n for (const arg of manifest.args ?? []) {\n const expanded = expand(arg, substitutes);\n if (expanded === undefined) {\n return `mcp catalog '${name}' needs a substitute for ${arg}`;\n }\n args.push(expanded);\n }\n return { enabled: false, command: manifest.command, args };\n}\n"],"mappings":";;;;;AAGA,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;;;ACE9B,IAAM,cAAc;AAEpB,SAAS,OAAO,OAAe,aAAyD;AACtF,QAAM,SAAS,MAAM,MAAM,WAAW,KAAK,CAAC;AAC5C,MAAI,OAAO;AACX,aAAW,SAAS,QAAQ;AAC1B,UAAM,MAAM,MAAM,MAAM,GAAG,EAAE;AAC7B,UAAM,QAAQ,YAAY,GAAG;AAC7B,QAAI,UAAU,UAAa,MAAM,WAAW,GAAG;AAC7C,aAAO;AAAA,IACT;AACA,WAAO,KAAK,QAAQ,OAAO,KAAK;AAAA,EAClC;AACA,SAAO;AACT;AAGO,SAAS,qBACd,MACA,cAAsC,CAAC,GACL;AAClC,QAAM,WAAW,gBAAgB,IAAI;AACrC,MAAI,aAAa,QAAW;AAC1B,WAAO,8BAA8B,IAAI;AAAA,EAC3C;AACA,MAAI,SAAS,cAAc,WAAW,SAAS,YAAY,QAAW;AACpE,WAAO,SAAS,QAAQ,GAAG,IAAI;AAAA,EACjC;AACA,QAAM,OAAiB,CAAC;AACxB,aAAW,OAAO,SAAS,QAAQ,CAAC,GAAG;AACrC,UAAM,WAAW,OAAO,KAAK,WAAW;AACxC,QAAI,aAAa,QAAW;AAC1B,aAAO,gBAAgB,IAAI,4BAA4B,GAAG;AAAA,IAC5D;AACA,SAAK,KAAK,QAAQ;AAAA,EACpB;AACA,SAAO,EAAE,SAAS,OAAO,SAAS,SAAS,SAAS,KAAK;AAC3D;;;ADrCA,SAAS,uBAA+B;AACtC,QAAM,WAAW,cAAc,IAAI,IAAI,mBAAmB,YAAY,GAAG,CAAC;AAC1E,QAAM,MAAM,KAAK,MAAM,aAAa,UAAU,MAAM,CAAC;AACrD,MAAI,OAAO,IAAI,YAAY,YAAY,IAAI,QAAQ,WAAW,GAAG;AAC/D,UAAM,IAAI,MAAM,iCAAiC;AAAA,EACnD;AACA,SAAO,IAAI;AACb;AAEO,IAAM,eAAe,qBAAqB;","names":[]}
@@ -1,5 +1,6 @@
1
1
  // src/session/store.ts
2
- import { appendFile, mkdir, readFile } from "fs/promises";
2
+ import { randomBytes } from "crypto";
3
+ import { access, appendFile, mkdir, readFile } from "fs/promises";
3
4
  import path from "path";
4
5
 
5
6
  // src/util/json.ts
@@ -29,21 +30,38 @@ function truncate_text(text, max_chars) {
29
30
  // src/session/store.ts
30
31
  var counter_state = { value: 0 };
31
32
  var MESSAGE_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
33
+ var CREATE_ATTEMPTS = 8;
32
34
  function slugify_label(label) {
33
35
  const slug = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
34
36
  return slug.length > 0 ? `-${slug}` : "";
35
37
  }
38
+ function next_session_id(label_part) {
39
+ counter_state.value += 1;
40
+ const rand = randomBytes(3).toString("hex");
41
+ return `${Date.now().toString(36)}-${process.pid.toString(36)}-${rand}-${counter_state.value}${label_part}`;
42
+ }
43
+ async function create_unique_session_path(dir, label_part) {
44
+ for (let attempt = 0; attempt < CREATE_ATTEMPTS; attempt += 1) {
45
+ const id = next_session_id(label_part);
46
+ const file_path = path.join(dir, `${id}.jsonl`);
47
+ try {
48
+ await access(file_path);
49
+ continue;
50
+ } catch {
51
+ return { id, path: file_path };
52
+ }
53
+ }
54
+ throw new Error(`could not create unique session file in ${dir}`);
55
+ }
36
56
  async function open_session(dir, label) {
37
57
  await mkdir(dir, { recursive: true });
38
- counter_state.value += 1;
39
58
  const label_part = label === void 0 ? "" : slugify_label(label);
40
- const id = `${Date.now().toString(36)}-${counter_state.value}${label_part}`;
41
- const file_path = path.join(dir, `${id}.jsonl`);
59
+ const created = await create_unique_session_path(dir, label_part);
42
60
  return {
43
- id,
44
- path: file_path,
61
+ id: created.id,
62
+ path: created.path,
45
63
  append: async (record) => {
46
- await appendFile(file_path, `${safe_stringify(record)}
64
+ await appendFile(created.path, `${safe_stringify(record)}
47
65
  `, "utf8");
48
66
  }
49
67
  };
@@ -83,4 +101,4 @@ export {
83
101
  open_session,
84
102
  read_session_messages
85
103
  };
86
- //# sourceMappingURL=chunk-VKEOHUCB.js.map
104
+ //# sourceMappingURL=chunk-TYRKVBWQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/session/store.ts","../src/util/json.ts"],"sourcesContent":["/**\n * JSONL transcript persistence for agent sessions. Each session is one\n * append-only .jsonl file; records carry either a message or arbitrary meta.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { access, appendFile, mkdir, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { Message } from \"../providers/types.js\";\nimport { safe_json_parse, safe_stringify } from \"../util/json.js\";\n\nexport interface SessionRecord {\n ts: string;\n kind: \"message\" | \"meta\";\n message?: Message;\n meta?: Record<string, unknown>;\n}\n\nexport interface SessionHandle {\n id: string;\n path: string;\n append(record: SessionRecord): Promise<void>;\n}\n\n/** Mutable via holder object: conventions require const bindings. */\nconst counter_state = { value: 0 };\n\nconst MESSAGE_ROLES: ReadonlySet<string> = new Set([\"system\", \"user\", \"assistant\", \"tool\"]);\nconst CREATE_ATTEMPTS = 8;\n\nfunction slugify_label(label: string): string {\n const slug = label\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 40);\n return slug.length > 0 ? `-${slug}` : \"\";\n}\n\nfunction next_session_id(label_part: string): string {\n counter_state.value += 1;\n const rand = randomBytes(3).toString(\"hex\");\n return `${Date.now().toString(36)}-${process.pid.toString(36)}-${rand}-${counter_state.value}${label_part}`;\n}\n\nasync function create_unique_session_path(dir: string, label_part: string): Promise<{ id: string; path: string }> {\n for (let attempt = 0; attempt < CREATE_ATTEMPTS; attempt += 1) {\n const id = next_session_id(label_part);\n const file_path = path.join(dir, `${id}.jsonl`);\n try {\n await access(file_path);\n // Already taken; try another id.\n continue;\n } catch {\n // Path is free — create lazily on first append so empty TUI launches leave no file.\n return { id, path: file_path };\n }\n }\n throw new Error(`could not create unique session file in ${dir}`);\n}\n\nexport async function open_session(dir: string, label?: string): Promise<SessionHandle> {\n await mkdir(dir, { recursive: true });\n const label_part = label === undefined ? \"\" : slugify_label(label);\n const created = await create_unique_session_path(dir, label_part);\n return {\n id: created.id,\n path: created.path,\n append: async (record: SessionRecord): Promise<void> => {\n await appendFile(created.path, `${safe_stringify(record)}\\n`, \"utf8\");\n },\n };\n}\n\nfunction is_message(value: unknown): value is Message {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n const role = (value as { role?: unknown }).role;\n return typeof role === \"string\" && MESSAGE_ROLES.has(role);\n}\n\nexport async function read_session_messages(file_path: string): Promise<Message[]> {\n let raw: string;\n try {\n raw = await readFile(file_path, \"utf8\");\n } catch {\n return [];\n }\n const messages: Message[] = [];\n for (const line of raw.split(\"\\n\")) {\n const record = safe_json_parse<SessionRecord>(line);\n if (record?.message !== undefined && is_message(record.message)) {\n messages.push(record.message);\n }\n }\n // Provider throw / abort-before-turn can leave a dangling user seed with no\n // assistant reply. Drop it so resume does not start with two consecutive users.\n const last = messages.at(-1);\n if (last?.role === \"user\") {\n messages.pop();\n }\n return messages;\n}\n","export function safe_json_parse<T>(raw: string): T | undefined {\n try {\n return JSON.parse(raw) as T;\n } catch {\n return undefined;\n }\n}\n\nexport function safe_stringify(value: unknown, space?: number): string {\n try {\n return JSON.stringify(value, null, space) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\nexport function truncate_text(text: string, max_chars: number): string {\n if (text.length <= max_chars) {\n return text;\n }\n const omitted = text.length - max_chars;\n return `${text.slice(0, max_chars)}\\n[... truncated, ${omitted} chars omitted ...]`;\n}"],"mappings":"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","names":[]}
package/dist/cli.js CHANGED
@@ -2,21 +2,21 @@
2
2
  import {
3
3
  load_theme,
4
4
  notice_flavor
5
- } from "./chunk-QOTECFCN.js";
5
+ } from "./chunk-4N3S6ACI.js";
6
6
  import {
7
7
  LICH_VERSION,
8
8
  catalog_client_entry
9
- } from "./chunk-W6JXZBUE.js";
9
+ } from "./chunk-P2YEHNM4.js";
10
10
  import {
11
11
  DEFAULT_GATEWAY_TOKEN_ENVS,
12
12
  create_agent_with_plugins,
13
13
  is_env_var_name,
14
14
  parse_agent_config,
15
15
  refuse_mcp_entry
16
- } from "./chunk-EDRUZF22.js";
16
+ } from "./chunk-KOEZQUIX.js";
17
17
  import {
18
18
  safe_json_parse
19
- } from "./chunk-VKEOHUCB.js";
19
+ } from "./chunk-TYRKVBWQ.js";
20
20
 
21
21
  // src/cli.ts
22
22
  import { existsSync as existsSync4, readFileSync as readFileSync2, realpathSync } from "fs";
@@ -31,6 +31,20 @@ import path from "path";
31
31
  var LICH_DIRNAME = ".lich";
32
32
  var CONFIG_RELPATH = `${LICH_DIRNAME}/config.json`;
33
33
  var DEFAULT_USER_CONFIG = ".config/lich/config.json";
34
+ var KIND_DEFAULTS = {
35
+ openai_compat: { base_url: "https://api.openai.com/v1", api_key_env: "OPENAI_API_KEY" },
36
+ anthropic: { base_url: "https://api.anthropic.com", api_key_env: "ANTHROPIC_API_KEY" },
37
+ ollama: { base_url: "http://localhost:11434" }
38
+ };
39
+ function provider_kind_defaults(kind) {
40
+ return KIND_DEFAULTS[kind];
41
+ }
42
+ function parse_template_kind(raw) {
43
+ if (raw === "openai_compat" || raw === "anthropic" || raw === "ollama") {
44
+ return raw;
45
+ }
46
+ return "ollama";
47
+ }
34
48
  function config_search_paths(work_dir) {
35
49
  const root = work_dir ?? process.cwd();
36
50
  const first = path.resolve(root, CONFIG_RELPATH);
@@ -59,14 +73,16 @@ function load_config(explicit) {
59
73
  return parsed;
60
74
  }
61
75
  function config_template() {
62
- const kind = process.env.LICH_PROVIDER_KIND ?? "ollama";
76
+ const kind = parse_template_kind(process.env.LICH_PROVIDER_KIND ?? "ollama");
77
+ const defaults = provider_kind_defaults(kind);
63
78
  const provider = {
64
79
  kind,
65
80
  name: "main",
66
- model: process.env.LICH_MODEL ?? "<model-name>"
81
+ model: process.env.LICH_MODEL ?? "<model-name>",
82
+ base_url: defaults.base_url
67
83
  };
68
- if (kind === "ollama") {
69
- provider["base_url"] = "http://localhost:11434";
84
+ if (defaults.api_key_env !== void 0) {
85
+ provider["api_key_env"] = defaults.api_key_env;
70
86
  }
71
87
  return JSON.stringify({ providers: [provider], max_turns: 25, theme: "lich" }, null, 2);
72
88
  }
@@ -336,6 +352,7 @@ var VIEW_ARGS = ["view", PACKAGE_NAME, "version"];
336
352
  var INSTALL_ARGS = ["install", "-g", `${PACKAGE_NAME}@latest`];
337
353
  var NPM_MISSING = "lich: npm is not on PATH. Install Node.js, or run `npm install -g @moikapy/lich@latest` once npm is available.\n";
338
354
  var EXIT_FIRST_HINT = "Exit any running `lich tui` or `lich gateway` first \u2014 npm cannot replace the package while those processes are running.\n";
355
+ var LOCAL_INSTALL_HINT = "lich: this copy is not an npm global install. Update via your package manager, or install with `npm install -g @moikapy/lich`.\n";
339
356
  function parse_semver(version) {
340
357
  const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(version.trim());
341
358
  if (match === null || match[1] === void 0 || match[2] === void 0 || match[3] === void 0) {
@@ -367,6 +384,13 @@ function version_relation(installed, registry) {
367
384
  }
368
385
  return "current";
369
386
  }
387
+ function is_npm_global_node_modules(node_modules_dir) {
388
+ const parent = path2.basename(path2.dirname(node_modules_dir));
389
+ if (parent === "lib") {
390
+ return true;
391
+ }
392
+ return process.platform === "win32" && parent === "npm";
393
+ }
370
394
  function detect_install_kind(module_path, env, has_git) {
371
395
  if (env.npm_command === "exec" || module_path.includes(`${path2.sep}_npx${path2.sep}`)) {
372
396
  return "npx";
@@ -375,7 +399,10 @@ function detect_install_kind(module_path, env, has_git) {
375
399
  const root = path2.parse(dir).root;
376
400
  while (dir !== root) {
377
401
  if (path2.basename(dir) === "node_modules") {
378
- return "npm";
402
+ if (is_npm_global_node_modules(dir) === true) {
403
+ return "npm";
404
+ }
405
+ return "local";
379
406
  }
380
407
  if (has_git(dir) === true) {
381
408
  return "git";
@@ -395,6 +422,9 @@ function blocked_install_message(kind) {
395
422
  if (kind === "npx") {
396
423
  return "lich: npx runs a temporary copy and cannot persist an update. Install with `npm install -g @moikapy/lich`.\n";
397
424
  }
425
+ if (kind === "local") {
426
+ return LOCAL_INSTALL_HINT;
427
+ }
398
428
  return null;
399
429
  }
400
430
  function registry_version(viewed, request) {
@@ -793,17 +823,6 @@ var FLAG_KEYS = {
793
823
  "--log-level": "log_level",
794
824
  "--theme": "theme"
795
825
  };
796
- var DEFAULT_BASE_URLS = {
797
- openai_compat: "https://api.openai.com/v1",
798
- anthropic: "https://api.anthropic.com",
799
- ollama: "http://localhost:11434"
800
- };
801
- var DEFAULT_ENV_API_KEYS = {
802
- openai_compat: "OPENAI_API_KEY",
803
- anthropic: "ANTHROPIC_API_KEY",
804
- ollama: void 0
805
- // ollama needs no api key
806
- };
807
826
  function usage_text() {
808
827
  return [
809
828
  "lich \u2014 the undead agent harness",
@@ -930,22 +949,25 @@ function parse_args(argv) {
930
949
  }
931
950
  return options;
932
951
  }
933
- function env_provider(overrides) {
934
- const kind_value = overrides["provider_kind"] ?? process.env.LICH_PROVIDER_KIND ?? "openai_compat";
952
+ function parse_provider_kind(kind_value) {
935
953
  if (kind_value !== "openai_compat" && kind_value !== "anthropic" && kind_value !== "ollama") {
936
954
  throw new Error(`unknown provider kind "${kind_value}" (expected openai_compat, anthropic, or ollama)`);
937
955
  }
938
- const kind = kind_value;
956
+ return kind_value;
957
+ }
958
+ function env_provider(overrides) {
959
+ const kind = parse_provider_kind(overrides["provider_kind"] ?? process.env.LICH_PROVIDER_KIND ?? "openai_compat");
939
960
  const model = overrides["provider_model"] ?? process.env.LICH_MODEL;
940
961
  if (model === void 0 || model.length === 0) {
941
962
  throw new Error("no model configured: set LICH_MODEL, pass --model, or create .lich/config.json (`lich config` prints a template)");
942
963
  }
964
+ const defaults = provider_kind_defaults(kind);
943
965
  return {
944
966
  kind,
945
967
  name: "default",
946
968
  model,
947
- base_url: overrides["provider_base_url"] ?? process.env.LICH_BASE_URL ?? DEFAULT_BASE_URLS[kind],
948
- api_key_env: overrides["provider_api_key_env"] ?? process.env.LICH_API_KEY_ENV ?? DEFAULT_ENV_API_KEYS[kind]
969
+ base_url: overrides["provider_base_url"] ?? process.env.LICH_BASE_URL ?? defaults.base_url,
970
+ api_key_env: overrides["provider_api_key_env"] ?? process.env.LICH_API_KEY_ENV ?? defaults.api_key_env
949
971
  };
950
972
  }
951
973
  function load_config_file(config_path) {
@@ -960,6 +982,19 @@ function load_config_file(config_path) {
960
982
  throw new Error(`cannot use config file ${config_path}: ${error_message(error)}`);
961
983
  }
962
984
  }
985
+ function apply_kind_defaults(provider, kind, overrides) {
986
+ const defaults = provider_kind_defaults(kind);
987
+ if (overrides["provider_base_url"] === void 0) {
988
+ provider["base_url"] = defaults.base_url;
989
+ }
990
+ if (overrides["provider_api_key_env"] === void 0) {
991
+ if (defaults.api_key_env === void 0) {
992
+ delete provider["api_key_env"];
993
+ } else {
994
+ provider["api_key_env"] = defaults.api_key_env;
995
+ }
996
+ }
997
+ }
963
998
  function apply_provider_override(config, overrides) {
964
999
  const keys = ["provider_kind", "provider_model", "provider_base_url", "provider_api_key_env"];
965
1000
  const any_present = keys.some((key) => overrides[key] !== void 0);
@@ -976,12 +1011,19 @@ function apply_provider_override(config, overrides) {
976
1011
  throw new Error("config providers[0] must be an object");
977
1012
  }
978
1013
  const provider = first;
1014
+ const previous_kind = provider["kind"];
979
1015
  for (const key of keys) {
980
1016
  const value = overrides[key];
981
1017
  if (value !== void 0) {
982
1018
  provider[key.replace("provider_", "")] = value;
983
1019
  }
984
1020
  }
1021
+ if (overrides["provider_kind"] !== void 0) {
1022
+ const new_kind = parse_provider_kind(overrides["provider_kind"]);
1023
+ if (previous_kind !== new_kind) {
1024
+ apply_kind_defaults(provider, new_kind, overrides);
1025
+ }
1026
+ }
985
1027
  }
986
1028
  function apply_overrides(config, overrides) {
987
1029
  for (const key of ["work_dir", "system_prompt", "session_dir", "log_level", "theme"]) {
@@ -1065,10 +1107,10 @@ async function run_one_shot(config, input) {
1065
1107
  }
1066
1108
  return 0;
1067
1109
  }
1068
- async function run_chat_turn(agent, input) {
1110
+ async function run_chat_turn(agent, input, history) {
1069
1111
  const stop_progress = attach_progress(agent.events);
1070
1112
  try {
1071
- const result = await agent.run({ input });
1113
+ const result = await agent.run({ input, history });
1072
1114
  const final = result.outcome.final;
1073
1115
  if (final !== void 0 && final.content.length > 0) {
1074
1116
  process.stdout.write(`${final.content}
@@ -1076,23 +1118,15 @@ async function run_chat_turn(agent, input) {
1076
1118
  }
1077
1119
  process.stdout.write(`[turns ${result.outcome.turns_used} | tokens ${result.usage_total.total_tokens}]
1078
1120
  `);
1121
+ return result.messages;
1079
1122
  } catch (error) {
1080
1123
  process.stderr.write(`[lich] ${error_message(error)}
1081
1124
  `);
1125
+ return [...history];
1082
1126
  } finally {
1083
1127
  stop_progress();
1084
1128
  }
1085
1129
  }
1086
- function ask_line2(rl) {
1087
- return new Promise((resolve) => {
1088
- const on_close = () => resolve("");
1089
- rl.question("> ", (answer) => {
1090
- rl.removeListener("close", on_close);
1091
- resolve(answer);
1092
- });
1093
- rl.once("close", on_close);
1094
- });
1095
- }
1096
1130
  function budget_stderr_line(turns, notice) {
1097
1131
  const flavor = notice_flavor(notice);
1098
1132
  const suffix = flavor.length === 0 ? "" : ` \u2014 ${flavor}`;
@@ -1103,11 +1137,13 @@ async function run_chat(config) {
1103
1137
  const agent = await create_agent_with_plugins(config);
1104
1138
  const theme = load_theme(agent.config.theme);
1105
1139
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1140
+ let history = [];
1106
1141
  try {
1107
- for (; ; ) {
1108
- const line = await ask_line2(rl);
1142
+ process.stdout.write("> ");
1143
+ for await (const line of rl) {
1109
1144
  if (line.trim() === "") {
1110
- return 0;
1145
+ process.stdout.write("> ");
1146
+ continue;
1111
1147
  }
1112
1148
  const command = line.trim();
1113
1149
  if (command === "/exit" || command === "/quit") {
@@ -1115,8 +1151,10 @@ async function run_chat(config) {
1115
1151
  `);
1116
1152
  return 0;
1117
1153
  }
1118
- await run_chat_turn(agent, command);
1154
+ history = await run_chat_turn(agent, command, history);
1155
+ process.stdout.write("> ");
1119
1156
  }
1157
+ return 0;
1120
1158
  } finally {
1121
1159
  rl.close();
1122
1160
  agent.close();
@@ -1207,12 +1245,12 @@ function run_init(options) {
1207
1245
  return 0;
1208
1246
  }
1209
1247
  async function run_tui_entry(config, resume) {
1210
- const { run_tui } = await import("./tui-4BP3TI7J.js");
1248
+ const { run_tui } = await import("./tui-XXIU4W7K.js");
1211
1249
  if (resume === void 0) {
1212
1250
  return run_tui(config);
1213
1251
  }
1214
1252
  const { resolve_session_path } = await import("./resolve-H22TOVB5.js");
1215
- const { read_session_messages } = await import("./store-COOLBAHB.js");
1253
+ const { read_session_messages } = await import("./store-YYLEB7JG.js");
1216
1254
  const transcript = await resolve_session_path(config.session_dir, resume);
1217
1255
  const initial_history = await read_session_messages(transcript);
1218
1256
  const resumed_id = path4.basename(transcript, ".jsonl");
@@ -1226,7 +1264,7 @@ function gateway_platforms(config, cli_platforms) {
1226
1264
  return configured.length === 0 ? ["webhook"] : configured;
1227
1265
  }
1228
1266
  async function run_gateway_entry(config, platforms) {
1229
- const { run_gateway } = await import("./gateway-WU5G4ODO.js");
1267
+ const { run_gateway } = await import("./gateway-OSB7254F.js");
1230
1268
  return run_gateway(config, gateway_platforms(config, platforms));
1231
1269
  }
1232
1270
  function is_cli_entry(input) {