@moikapy/lich 0.5.0 → 0.6.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.
@@ -0,0 +1,92 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-7HLVKVIG.js";
4
+
5
+ // src/util/theme.ts
6
+ import { readFileSync } from "fs";
7
+ import { homedir } from "os";
8
+ import path from "path";
9
+ import { z } from "zod";
10
+
11
+ // src/util/lore.ts
12
+ var phase_labels = Object.freeze({
13
+ idle: "dormant",
14
+ thinking: "deliberating",
15
+ tool: "casting"
16
+ });
17
+ var notices = Object.freeze({
18
+ budget_exhausted: "budget exhausted \u2014 the ritual is spent (turn cap reached)",
19
+ compressed: "context compressed \u2014 memories distilled (summary {chars} chars)",
20
+ sessions: "phylacteries ({count}):"
21
+ });
22
+ var LICH_THEME = Object.freeze({
23
+ name: "lich",
24
+ agent_name: "lich",
25
+ glyph: "\u26B1",
26
+ tagline: "the agent that will not stay dead",
27
+ welcome: "\u26B1 lich v{version} \u2014 the agent that will not stay dead \xB7 {model} ({kind})",
28
+ goodbye: "the lich endures",
29
+ response_label: "lich",
30
+ user_label: "mortal",
31
+ phase_labels,
32
+ notices
33
+ });
34
+
35
+ // src/util/theme.ts
36
+ var text = z.string().min(1);
37
+ var theme_schema = z.object({
38
+ name: text,
39
+ agent_name: text,
40
+ glyph: text,
41
+ tagline: text,
42
+ welcome: text,
43
+ goodbye: text,
44
+ response_label: text,
45
+ user_label: text,
46
+ phase_labels: z.object({ idle: text, thinking: text, tool: text }),
47
+ notices: z.object({ budget_exhausted: text, compressed: text, sessions: text })
48
+ });
49
+ function fill_template(template, vars) {
50
+ let filled = template;
51
+ for (const [key, value] of Object.entries(vars)) {
52
+ filled = filled.replaceAll(`{${key}}`, String(value));
53
+ }
54
+ return filled;
55
+ }
56
+ function notice_flavor(notice) {
57
+ const start = notice.indexOf(" \u2014 ");
58
+ if (start === -1) {
59
+ return "";
60
+ }
61
+ const rest = notice.slice(start + 3);
62
+ const end = rest.indexOf(" (");
63
+ return end === -1 ? rest : rest.slice(0, end);
64
+ }
65
+ function load_theme(name, themes_dir) {
66
+ if (name === "lich" || name.length === 0) {
67
+ return LICH_THEME;
68
+ }
69
+ try {
70
+ return read_theme_file(name, themes_dir);
71
+ } catch (error) {
72
+ logger.warn(`theme '${name}' unavailable; using default lich theme`, error);
73
+ return LICH_THEME;
74
+ }
75
+ }
76
+ function read_theme_file(name, themes_dir) {
77
+ if (path.basename(name) !== name) {
78
+ throw new Error("invalid theme name");
79
+ }
80
+ const root = themes_dir ?? path.join(homedir(), ".lich", "themes");
81
+ const theme = theme_schema.parse(JSON.parse(readFileSync(path.join(root, `${name}.json`), "utf8")));
82
+ Object.freeze(theme.phase_labels);
83
+ Object.freeze(theme.notices);
84
+ return Object.freeze(theme);
85
+ }
86
+
87
+ export {
88
+ fill_template,
89
+ notice_flavor,
90
+ load_theme
91
+ };
92
+ //# sourceMappingURL=chunk-JC2G3XH2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util/theme.ts","../src/util/lore.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { z } from \"zod\";\nimport { logger } from \"./log.js\";\nimport { LICH_THEME, type ThemeSpec } from \"./lore.js\";\n\nconst text = z.string().min(1);\n\nexport const theme_schema = z.object({\n name: text, agent_name: text, glyph: text, tagline: text,\n welcome: text, goodbye: text, response_label: text, user_label: text,\n phase_labels: z.object({ idle: text, thinking: text, tool: text }),\n notices: z.object({ budget_exhausted: text, compressed: text, sessions: text }),\n});\n\nexport function fill_template(template: string, vars: Record<string, string | number>): string {\n let filled = template;\n for (const [key, value] of Object.entries(vars)) {\n filled = filled.replaceAll(`{${key}}`, String(value));\n }\n return filled;\n}\n\nexport function notice_flavor(notice: string): string {\n const start = notice.indexOf(\" — \");\n if (start === -1) {\n return \"\";\n }\n const rest = notice.slice(start + 3);\n const end = rest.indexOf(\" (\");\n return end === -1 ? rest : rest.slice(0, end);\n}\n\nexport function load_theme(name: string, themes_dir?: string): ThemeSpec {\n if (name === \"lich\" || name.length === 0) {\n return LICH_THEME;\n }\n try {\n return read_theme_file(name, themes_dir);\n } catch (error) {\n logger.warn(`theme '${name}' unavailable; using default lich theme`, error);\n return LICH_THEME;\n }\n}\n\nfunction read_theme_file(name: string, themes_dir?: string): ThemeSpec {\n if (path.basename(name) !== name) {\n throw new Error(\"invalid theme name\");\n }\n const root = themes_dir ?? path.join(homedir(), \".lich\", \"themes\");\n const theme: ThemeSpec = theme_schema.parse(JSON.parse(readFileSync(path.join(root, `${name}.json`), \"utf8\")));\n Object.freeze(theme.phase_labels);\n Object.freeze(theme.notices);\n return Object.freeze(theme);\n}\n","/**\n * Default lich theme as frozen display data. No loader logic here.\n * Mythology stays in these strings; the system prompt stays myth-free.\n */\nexport interface ThemeSpec {\n readonly name: string;\n readonly agent_name: string;\n readonly glyph: string;\n readonly tagline: string;\n readonly welcome: string;\n readonly goodbye: string;\n readonly response_label: string;\n readonly user_label: string;\n readonly phase_labels: {\n readonly idle: string;\n readonly thinking: string;\n readonly tool: string;\n };\n readonly notices: {\n readonly budget_exhausted: string;\n readonly compressed: string;\n readonly sessions: string;\n };\n}\n\nconst phase_labels = Object.freeze({\n idle: \"dormant\",\n thinking: \"deliberating\",\n tool: \"casting\",\n});\n\nconst notices = Object.freeze({\n budget_exhausted: \"budget exhausted — the ritual is spent (turn cap reached)\",\n compressed: \"context compressed — memories distilled (summary {chars} chars)\",\n sessions: \"phylacteries ({count}):\",\n});\n\nexport const LICH_THEME: ThemeSpec = Object.freeze({\n name: \"lich\",\n agent_name: \"lich\",\n glyph: \"⚱\",\n tagline: \"the agent that will not stay dead\",\n welcome: \"⚱ lich v{version} — the agent that will not stay dead · {model} ({kind})\",\n goodbye: \"the lich endures\",\n response_label: \"lich\",\n user_label: \"mortal\",\n phase_labels,\n notices,\n});\n"],"mappings":";;;;;AAAA,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AACxB,OAAO,UAAU;AACjB,SAAS,SAAS;;;ACsBlB,IAAM,eAAe,OAAO,OAAO;AAAA,EACjC,MAAM;AAAA,EACN,UAAU;AAAA,EACV,MAAM;AACR,CAAC;AAED,IAAM,UAAU,OAAO,OAAO;AAAA,EAC5B,kBAAkB;AAAA,EAClB,YAAY;AAAA,EACZ,UAAU;AACZ,CAAC;AAEM,IAAM,aAAwB,OAAO,OAAO;AAAA,EACjD,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,YAAY;AAAA,EACZ;AAAA,EACA;AACF,CAAC;;;ADzCD,IAAM,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAEtB,IAAM,eAAe,EAAE,OAAO;AAAA,EACnC,MAAM;AAAA,EAAM,YAAY;AAAA,EAAM,OAAO;AAAA,EAAM,SAAS;AAAA,EACpD,SAAS;AAAA,EAAM,SAAS;AAAA,EAAM,gBAAgB;AAAA,EAAM,YAAY;AAAA,EAChE,cAAc,EAAE,OAAO,EAAE,MAAM,MAAM,UAAU,MAAM,MAAM,KAAK,CAAC;AAAA,EACjE,SAAS,EAAE,OAAO,EAAE,kBAAkB,MAAM,YAAY,MAAM,UAAU,KAAK,CAAC;AAChF,CAAC;AAEM,SAAS,cAAc,UAAkB,MAA+C;AAC7F,MAAI,SAAS;AACb,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC/C,aAAS,OAAO,WAAW,IAAI,GAAG,KAAK,OAAO,KAAK,CAAC;AAAA,EACtD;AACA,SAAO;AACT;AAEO,SAAS,cAAc,QAAwB;AACpD,QAAM,QAAQ,OAAO,QAAQ,UAAK;AAClC,MAAI,UAAU,IAAI;AAChB,WAAO;AAAA,EACT;AACA,QAAM,OAAO,OAAO,MAAM,QAAQ,CAAC;AACnC,QAAM,MAAM,KAAK,QAAQ,IAAI;AAC7B,SAAO,QAAQ,KAAK,OAAO,KAAK,MAAM,GAAG,GAAG;AAC9C;AAEO,SAAS,WAAW,MAAc,YAAgC;AACvE,MAAI,SAAS,UAAU,KAAK,WAAW,GAAG;AACxC,WAAO;AAAA,EACT;AACA,MAAI;AACF,WAAO,gBAAgB,MAAM,UAAU;AAAA,EACzC,SAAS,OAAO;AACd,WAAO,KAAK,UAAU,IAAI,2CAA2C,KAAK;AAC1E,WAAO;AAAA,EACT;AACF;AAEA,SAAS,gBAAgB,MAAc,YAAgC;AACrE,MAAI,KAAK,SAAS,IAAI,MAAM,MAAM;AAChC,UAAM,IAAI,MAAM,oBAAoB;AAAA,EACtC;AACA,QAAM,OAAO,cAAc,KAAK,KAAK,QAAQ,GAAG,SAAS,QAAQ;AACjE,QAAM,QAAmB,aAAa,MAAM,KAAK,MAAM,aAAa,KAAK,KAAK,MAAM,GAAG,IAAI,OAAO,GAAG,MAAM,CAAC,CAAC;AAC7G,SAAO,OAAO,MAAM,YAAY;AAChC,SAAO,OAAO,MAAM,OAAO;AAC3B,SAAO,OAAO,OAAO,KAAK;AAC5B;","names":[]}
package/dist/cli.d.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  declare function run_one_shot(config: unknown, input: string): Promise<number>;
3
3
  declare function run_chat(config: unknown): Promise<number>;
4
+ interface CliEntryInput {
5
+ bun: boolean;
6
+ import_meta_main: boolean | undefined;
7
+ module_url: string;
8
+ argv1: string | undefined;
9
+ }
10
+ /** Bun uses `import.meta.main`. Node compares argv to this file, including a bin symlink. */
11
+ declare function is_cli_entry(input: CliEntryInput): boolean;
4
12
  declare function run_cli(argv: string[]): Promise<number>;
5
13
 
6
- export { run_chat, run_cli, run_one_shot };
14
+ export { type CliEntryInput, is_cli_entry, run_chat, run_cli, run_one_shot };
package/dist/cli.js CHANGED
@@ -1,17 +1,21 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ load_theme,
4
+ notice_flavor
5
+ } from "./chunk-JC2G3XH2.js";
2
6
  import {
3
7
  LICH_VERSION
4
- } from "./chunk-ZVK3MUPC.js";
8
+ } from "./chunk-6M6OAQGN.js";
5
9
  import {
6
10
  DEFAULT_GATEWAY_TOKEN_ENVS,
7
11
  create_agent_with_plugins,
8
12
  is_env_var_name,
9
13
  parse_agent_config,
10
14
  safe_json_parse
11
- } from "./chunk-CV2YH3FH.js";
15
+ } from "./chunk-7HLVKVIG.js";
12
16
 
13
17
  // src/cli.ts
14
- import { readFileSync as readFileSync2 } from "fs";
18
+ import { existsSync as existsSync3, readFileSync as readFileSync2, realpathSync } from "fs";
15
19
  import { createInterface } from "readline";
16
20
  import path4 from "path";
17
21
  import { fileURLToPath, pathToFileURL } from "url";
@@ -60,7 +64,7 @@ function config_template() {
60
64
  if (kind === "ollama") {
61
65
  provider["base_url"] = "http://localhost:11434";
62
66
  }
63
- return JSON.stringify({ providers: [provider], max_turns: 25 }, null, 2);
67
+ return JSON.stringify({ providers: [provider], max_turns: 25, theme: "lich" }, null, 2);
64
68
  }
65
69
  function ensure_lich_config_dir(work_dir) {
66
70
  const dir = path.resolve(work_dir, LICH_DIRNAME);
@@ -260,7 +264,16 @@ async function run_lich_update(request) {
260
264
  }
261
265
  return install_if_newer(request, registry);
262
266
  }
267
+ function running_under_bun() {
268
+ return process.versions.bun !== void 0;
269
+ }
263
270
  function default_command_runner(command, args) {
271
+ if (running_under_bun() === true) {
272
+ return bun_command_runner(command, args);
273
+ }
274
+ return node_command_runner(command, args);
275
+ }
276
+ function node_command_runner(command, args) {
264
277
  return new Promise((resolve) => {
265
278
  let stdout = "";
266
279
  let stderr = "";
@@ -296,6 +309,45 @@ function default_command_runner(command, args) {
296
309
  });
297
310
  });
298
311
  }
312
+ async function read_piped_text(stream, forward, write) {
313
+ const reader = stream.getReader();
314
+ const decoder = new TextDecoder();
315
+ let text = "";
316
+ for (; ; ) {
317
+ const next = await reader.read();
318
+ if (next.done === true) {
319
+ return text;
320
+ }
321
+ const chunk = decoder.decode(next.value, { stream: true });
322
+ text += chunk;
323
+ if (forward === true) {
324
+ write(chunk);
325
+ }
326
+ }
327
+ }
328
+ function bun_command_runner(command, args) {
329
+ const forward = args[0] === "install";
330
+ try {
331
+ const child = Bun.spawn([command, ...args], {
332
+ stdin: "ignore",
333
+ stdout: "pipe",
334
+ stderr: "pipe"
335
+ });
336
+ return Promise.all([
337
+ read_piped_text(child.stdout, forward, (text) => {
338
+ process.stdout.write(text);
339
+ }),
340
+ read_piped_text(child.stderr, forward, (text) => {
341
+ process.stderr.write(text);
342
+ }),
343
+ child.exited
344
+ ]).then(([stdout, stderr, exit_code]) => ({ exit_code, stdout, stderr }));
345
+ } catch (error) {
346
+ const coded = error;
347
+ const stderr = typeof coded.message === "string" ? coded.message : "spawn failed";
348
+ return Promise.resolve({ exit_code: 127, stdout: "", stderr, error_code: coded.code });
349
+ }
350
+ }
299
351
  async function run_update(module_path) {
300
352
  return run_lich_update({
301
353
  installed_version: LICH_VERSION,
@@ -528,7 +580,8 @@ var FLAG_KEYS = {
528
580
  "--api-key-env": "provider_api_key_env",
529
581
  "--system-prompt": "system_prompt",
530
582
  "--session-dir": "session_dir",
531
- "--log-level": "log_level"
583
+ "--log-level": "log_level",
584
+ "--theme": "theme"
532
585
  };
533
586
  var DEFAULT_BASE_URLS = {
534
587
  openai_compat: "https://api.openai.com/v1",
@@ -543,7 +596,7 @@ var DEFAULT_ENV_API_KEYS = {
543
596
  };
544
597
  function usage_text() {
545
598
  return [
546
- "lich \u2014 a TypeScript AI agent harness",
599
+ "lich \u2014 the undead agent harness",
547
600
  "",
548
601
  "Usage:",
549
602
  " lich open the TUI (first run: setup wizard, then TUI)",
@@ -567,7 +620,8 @@ function usage_text() {
567
620
  " --api-key-env <NAME> env var holding the api key (default LICH_API_KEY_ENV; unused by ollama)",
568
621
  " --system-prompt <s> system prompt override",
569
622
  " --session-dir <path> session transcript directory",
570
- " --log-level <level> debug | info | warn | error"
623
+ " --log-level <level> debug | info | warn | error",
624
+ " --theme <name> display theme (default lich; files in ~/.lich/themes)"
571
625
  ].join("\n");
572
626
  }
573
627
  function error_message(error) {
@@ -680,7 +734,7 @@ function apply_provider_override(config, overrides) {
680
734
  }
681
735
  }
682
736
  function apply_overrides(config, overrides) {
683
- for (const key of ["work_dir", "system_prompt", "session_dir", "log_level"]) {
737
+ for (const key of ["work_dir", "system_prompt", "session_dir", "log_level", "theme"]) {
684
738
  if (overrides[key] !== void 0) {
685
739
  config[key] = overrides[key];
686
740
  }
@@ -737,6 +791,7 @@ function attach_progress(emitter) {
737
791
  }
738
792
  async function run_one_shot(config, input) {
739
793
  const agent = await create_agent_with_plugins(config);
794
+ const theme = load_theme(agent.config.theme);
740
795
  const stop_progress = attach_progress(agent.events);
741
796
  let result;
742
797
  try {
@@ -750,8 +805,7 @@ async function run_one_shot(config, input) {
750
805
  `);
751
806
  }
752
807
  if (result.outcome.stopped_reason === "budget") {
753
- process.stderr.write(`[lich] budget exhausted after ${result.outcome.turns_used} turns
754
- `);
808
+ process.stderr.write(budget_stderr_line(result.outcome.turns_used, theme.notices.budget_exhausted));
755
809
  return 1;
756
810
  }
757
811
  if (result.outcome.stopped_reason === "aborted") {
@@ -788,8 +842,15 @@ function ask_line2(rl) {
788
842
  rl.once("close", on_close);
789
843
  });
790
844
  }
845
+ function budget_stderr_line(turns, notice) {
846
+ const flavor = notice_flavor(notice);
847
+ const suffix = flavor.length === 0 ? "" : ` \u2014 ${flavor}`;
848
+ return `[lich] budget exhausted after ${turns} turns${suffix}
849
+ `;
850
+ }
791
851
  async function run_chat(config) {
792
852
  const agent = await create_agent_with_plugins(config);
853
+ const theme = load_theme(agent.config.theme);
793
854
  const rl = createInterface({ input: process.stdin, output: process.stdout });
794
855
  try {
795
856
  for (; ; ) {
@@ -799,6 +860,8 @@ async function run_chat(config) {
799
860
  }
800
861
  const command = line.trim();
801
862
  if (command === "/exit" || command === "/quit") {
863
+ process.stdout.write(`${theme.glyph} ${theme.goodbye}
864
+ `);
802
865
  return 0;
803
866
  }
804
867
  await run_chat_turn(agent, command);
@@ -892,7 +955,7 @@ function run_init(options) {
892
955
  return 0;
893
956
  }
894
957
  async function run_tui_entry(config) {
895
- const { run_tui } = await import("./tui-DT7XWDTX.js");
958
+ const { run_tui } = await import("./tui-LOUJVZ6A.js");
896
959
  return run_tui(config);
897
960
  }
898
961
  function gateway_platforms(config, cli_platforms) {
@@ -903,15 +966,38 @@ function gateway_platforms(config, cli_platforms) {
903
966
  return configured.length === 0 ? ["webhook"] : configured;
904
967
  }
905
968
  async function run_gateway_entry(config, platforms) {
906
- const { run_gateway } = await import("./gateway-W6S43ETE.js");
969
+ const { run_gateway } = await import("./gateway-RJFZJEUZ.js");
907
970
  return run_gateway(config, gateway_platforms(config, platforms));
908
971
  }
972
+ function is_cli_entry(input) {
973
+ if (input.bun === true) {
974
+ return input.import_meta_main === true;
975
+ }
976
+ return node_entry_matches(input.module_url, input.argv1);
977
+ }
909
978
  function is_main_module() {
910
- const entry = process.argv[1];
911
- if (entry === void 0) {
979
+ return is_cli_entry({
980
+ bun: process.versions.bun !== void 0,
981
+ import_meta_main: import.meta.main,
982
+ module_url: import.meta.url,
983
+ argv1: process.argv[1]
984
+ });
985
+ }
986
+ function node_entry_matches(module_url, entry) {
987
+ if (entry === void 0 || entry === "") {
988
+ return false;
989
+ }
990
+ const resolved = path4.resolve(entry);
991
+ if (module_url === pathToFileURL(resolved).href) {
992
+ return true;
993
+ }
994
+ return same_real_file(fileURLToPath(module_url), resolved);
995
+ }
996
+ function same_real_file(left, right) {
997
+ if (existsSync3(left) === false || existsSync3(right) === false) {
912
998
  return false;
913
999
  }
914
- return import.meta.url === pathToFileURL(path4.resolve(entry)).href;
1000
+ return realpathSync(left) === realpathSync(right);
915
1001
  }
916
1002
  async function run_cli(argv) {
917
1003
  const options = parse_args(argv);
@@ -968,6 +1054,7 @@ if (is_main_module() === true) {
968
1054
  });
969
1055
  }
970
1056
  export {
1057
+ is_cli_entry,
971
1058
  run_chat,
972
1059
  run_cli,
973
1060
  run_one_shot