@moikapy/lich 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/chunk-6M6OAQGN.js +17 -0
- package/dist/chunk-6M6OAQGN.js.map +1 -0
- package/dist/{chunk-MLFJW4JU.js → chunk-CV2YH3FH.js} +51 -2
- package/dist/chunk-CV2YH3FH.js.map +1 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +666 -29
- package/dist/cli.js.map +1 -1
- package/dist/{gateway-XTYDYT67.js → gateway-W6S43ETE.js} +27 -18
- package/dist/gateway-W6S43ETE.js.map +1 -0
- package/dist/index.d.ts +56 -26
- package/dist/index.js +2 -2
- package/dist/{tui-VYBJSGRV.js → tui-2VO6LAFM.js} +9 -6
- package/dist/tui-2VO6LAFM.js.map +1 -0
- package/docs/getting-started.md +15 -4
- package/docs/index.md +1 -0
- package/docs/user-guide/cli.md +11 -3
- package/docs/user-guide/godot.md +160 -0
- package/docs/user-guide/library.md +1 -1
- package/docs/user-guide/plugins.md +23 -2
- package/docs/user-guide/tui.md +4 -3
- package/examples/game_bridge/README.md +68 -0
- package/examples/game_bridge/bridge_io.mjs +60 -0
- package/examples/game_bridge/bridge_paths.mjs +11 -0
- package/examples/game_bridge/dungeon_memory.mjs +56 -0
- package/examples/game_bridge/enemy_actions.mjs +44 -0
- package/examples/game_bridge/game_bridge.plugin.mjs +17 -0
- package/examples/game_bridge/meteor_veto.mjs +22 -0
- package/examples/game_bridge/schemas.mjs +48 -0
- package/examples/game_bridge/snapshot.mjs +19 -0
- package/examples/game_bridge/validate_order.mjs +44 -0
- package/package.json +3 -1
- package/dist/chunk-MLFJW4JU.js.map +0 -1
- package/dist/chunk-ZVK3MUPC.js +0 -7
- package/dist/chunk-ZVK3MUPC.js.map +0 -1
- package/dist/gateway-XTYDYT67.js.map +0 -1
- package/dist/tui-VYBJSGRV.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
LICH_VERSION
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-6M6OAQGN.js";
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
DEFAULT_GATEWAY_TOKEN_ENVS,
|
|
7
|
+
create_agent_with_plugins,
|
|
8
|
+
is_env_var_name,
|
|
7
9
|
parse_agent_config,
|
|
8
10
|
safe_json_parse
|
|
9
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-CV2YH3FH.js";
|
|
10
12
|
|
|
11
13
|
// src/cli.ts
|
|
12
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
14
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, realpathSync } from "fs";
|
|
13
15
|
import { createInterface } from "readline";
|
|
14
|
-
import
|
|
15
|
-
import { pathToFileURL } from "url";
|
|
16
|
+
import path4 from "path";
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
16
18
|
|
|
17
19
|
// src/cli_config.ts
|
|
18
|
-
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
19
21
|
import { homedir } from "os";
|
|
20
22
|
import path from "path";
|
|
21
23
|
var LICH_DIRNAME = ".lich";
|
|
@@ -34,12 +36,7 @@ function find_config_file(explicit) {
|
|
|
34
36
|
}
|
|
35
37
|
return explicit;
|
|
36
38
|
}
|
|
37
|
-
|
|
38
|
-
if (existsSync(candidate) === true) {
|
|
39
|
-
return candidate;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return void 0;
|
|
39
|
+
return existing_config_path(process.cwd());
|
|
43
40
|
}
|
|
44
41
|
function load_config(explicit) {
|
|
45
42
|
const found = find_config_file(explicit);
|
|
@@ -65,6 +62,509 @@ function config_template() {
|
|
|
65
62
|
}
|
|
66
63
|
return JSON.stringify({ providers: [provider], max_turns: 25 }, null, 2);
|
|
67
64
|
}
|
|
65
|
+
function ensure_lich_config_dir(work_dir) {
|
|
66
|
+
const dir = path.resolve(work_dir, LICH_DIRNAME);
|
|
67
|
+
mkdirSync(dir, { recursive: true });
|
|
68
|
+
return dir;
|
|
69
|
+
}
|
|
70
|
+
function project_config_path(work_dir) {
|
|
71
|
+
return path.resolve(work_dir, CONFIG_RELPATH);
|
|
72
|
+
}
|
|
73
|
+
function existing_config_path(work_dir) {
|
|
74
|
+
for (const candidate of config_search_paths(work_dir)) {
|
|
75
|
+
if (existsSync(candidate) === true) {
|
|
76
|
+
return candidate;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
function starter_config_object() {
|
|
82
|
+
const parsed = JSON.parse(config_template());
|
|
83
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
84
|
+
throw new Error("config template must be a JSON object");
|
|
85
|
+
}
|
|
86
|
+
return parsed;
|
|
87
|
+
}
|
|
88
|
+
function write_lich_config(work_dir, config) {
|
|
89
|
+
ensure_lich_config_dir(work_dir);
|
|
90
|
+
const file = project_config_path(work_dir);
|
|
91
|
+
if (existsSync(file) === true) {
|
|
92
|
+
return already_exists(file);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}
|
|
96
|
+
`, { encoding: "utf8", flag: "wx" });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (is_eexist(error) === true) {
|
|
99
|
+
return already_exists(file);
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
return { path: file, written: true, message: `wrote ${file}` };
|
|
104
|
+
}
|
|
105
|
+
function already_exists(file) {
|
|
106
|
+
return {
|
|
107
|
+
path: file,
|
|
108
|
+
written: false,
|
|
109
|
+
message: `lich: .lich/config.json already exists at ${file}; not overwriting`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function is_eexist(error) {
|
|
113
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/cli_update.ts
|
|
117
|
+
import { spawn } from "child_process";
|
|
118
|
+
import { existsSync as existsSync2 } from "fs";
|
|
119
|
+
import path2 from "path";
|
|
120
|
+
var PACKAGE_NAME = "@moikapy/lich";
|
|
121
|
+
var VIEW_ARGS = ["view", PACKAGE_NAME, "version"];
|
|
122
|
+
var INSTALL_ARGS = ["install", "-g", `${PACKAGE_NAME}@latest`];
|
|
123
|
+
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";
|
|
124
|
+
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";
|
|
125
|
+
function parse_semver(version) {
|
|
126
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(version.trim());
|
|
127
|
+
if (match === null || match[1] === void 0 || match[2] === void 0 || match[3] === void 0) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return [Number(match[1]), Number(match[2]), Number(match[3]), match[4] ?? ""];
|
|
131
|
+
}
|
|
132
|
+
function version_relation(installed, registry) {
|
|
133
|
+
const left = parse_semver(installed);
|
|
134
|
+
const right = parse_semver(registry);
|
|
135
|
+
if (left === null || right === null) {
|
|
136
|
+
return "invalid";
|
|
137
|
+
}
|
|
138
|
+
for (let index = 0; index < 3; index += 1) {
|
|
139
|
+
const installed_part = left[index];
|
|
140
|
+
const registry_part = right[index];
|
|
141
|
+
if (typeof installed_part !== "number" || typeof registry_part !== "number") {
|
|
142
|
+
return "invalid";
|
|
143
|
+
}
|
|
144
|
+
if (installed_part < registry_part) {
|
|
145
|
+
return "update";
|
|
146
|
+
}
|
|
147
|
+
if (installed_part > registry_part) {
|
|
148
|
+
return "current";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (left[3] !== "" && right[3] === "") {
|
|
152
|
+
return "update";
|
|
153
|
+
}
|
|
154
|
+
return "current";
|
|
155
|
+
}
|
|
156
|
+
function detect_install_kind(module_path, env, has_git) {
|
|
157
|
+
if (env.npm_command === "exec" || module_path.includes(`${path2.sep}_npx${path2.sep}`)) {
|
|
158
|
+
return "npx";
|
|
159
|
+
}
|
|
160
|
+
let dir = path2.dirname(path2.resolve(module_path));
|
|
161
|
+
const root = path2.parse(dir).root;
|
|
162
|
+
while (dir !== root) {
|
|
163
|
+
if (path2.basename(dir) === "node_modules") {
|
|
164
|
+
return "npm";
|
|
165
|
+
}
|
|
166
|
+
if (has_git(dir) === true) {
|
|
167
|
+
return "git";
|
|
168
|
+
}
|
|
169
|
+
const parent = path2.dirname(dir);
|
|
170
|
+
if (parent === dir) {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
dir = parent;
|
|
174
|
+
}
|
|
175
|
+
return "npm";
|
|
176
|
+
}
|
|
177
|
+
function blocked_install_message(kind) {
|
|
178
|
+
if (kind === "git") {
|
|
179
|
+
return "lich: this copy is a git clone, not an npm install. Update with `git pull` in the repository.\n";
|
|
180
|
+
}
|
|
181
|
+
if (kind === "npx") {
|
|
182
|
+
return "lich: npx runs a temporary copy and cannot persist an update. Install with `npm install -g @moikapy/lich`.\n";
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
function registry_version(viewed, request) {
|
|
187
|
+
if (viewed.error_code === "ENOENT") {
|
|
188
|
+
request.write_stderr(NPM_MISSING);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
if (viewed.exit_code !== 0) {
|
|
192
|
+
const detail = viewed.stderr.trim() || viewed.stdout.trim() || `exit ${viewed.exit_code}`;
|
|
193
|
+
request.write_stderr(`lich: could not read the registry version of ${PACKAGE_NAME}: ${detail}
|
|
194
|
+
`);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const registry = viewed.stdout.trim();
|
|
198
|
+
if (parse_semver(registry) === null) {
|
|
199
|
+
request.write_stderr(`lich: could not read the registry version of ${PACKAGE_NAME}: unexpected version "${registry}"
|
|
200
|
+
`);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return registry;
|
|
204
|
+
}
|
|
205
|
+
function is_permission_failure(installed) {
|
|
206
|
+
if (installed.error_code === "EACCES" || installed.error_code === "EPERM") {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
const text = `${installed.stderr}
|
|
210
|
+
${installed.stdout}`.toLowerCase();
|
|
211
|
+
return text.includes("eacces") || text.includes("eperm") || text.includes("permission denied");
|
|
212
|
+
}
|
|
213
|
+
function report_install(request, installed) {
|
|
214
|
+
if (installed.exit_code === 0) {
|
|
215
|
+
request.write_stdout(`updated ${PACKAGE_NAME}. Run \`lich --version\` in a new shell to confirm.
|
|
216
|
+
`);
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
if (installed.error_code === "ENOENT") {
|
|
220
|
+
request.write_stderr(NPM_MISSING);
|
|
221
|
+
return 1;
|
|
222
|
+
}
|
|
223
|
+
if (is_permission_failure(installed) === true) {
|
|
224
|
+
request.write_stderr("lich: npm install failed (permission denied). Use a writable npm prefix, or run `npm install -g @moikapy/lich@latest` yourself.\n");
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
const detail = installed.stderr.trim() || `exit ${installed.exit_code}`;
|
|
228
|
+
request.write_stderr(`lich: npm install failed: ${detail}
|
|
229
|
+
`);
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
async function install_if_newer(request, registry) {
|
|
233
|
+
const relation = version_relation(request.installed_version, registry);
|
|
234
|
+
if (relation === "invalid") {
|
|
235
|
+
request.write_stderr(`lich: could not compare versions (installed ${request.installed_version}, registry ${registry})
|
|
236
|
+
`);
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
if (relation !== "update") {
|
|
240
|
+
request.write_stdout(`lich ${request.installed_version} is up to date
|
|
241
|
+
`);
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
request.write_stdout(`lich ${request.installed_version} -> ${registry}
|
|
245
|
+
`);
|
|
246
|
+
request.write_stdout(EXIT_FIRST_HINT);
|
|
247
|
+
const installed = await request.run_command("npm", INSTALL_ARGS);
|
|
248
|
+
return report_install(request, installed);
|
|
249
|
+
}
|
|
250
|
+
async function run_lich_update(request) {
|
|
251
|
+
const blocked = blocked_install_message(request.install_kind);
|
|
252
|
+
if (blocked !== null) {
|
|
253
|
+
request.write_stderr(blocked);
|
|
254
|
+
return 1;
|
|
255
|
+
}
|
|
256
|
+
const viewed = await request.run_command("npm", VIEW_ARGS);
|
|
257
|
+
const registry = registry_version(viewed, request);
|
|
258
|
+
if (registry === null) {
|
|
259
|
+
return 1;
|
|
260
|
+
}
|
|
261
|
+
return install_if_newer(request, registry);
|
|
262
|
+
}
|
|
263
|
+
function running_under_bun() {
|
|
264
|
+
return process.versions.bun !== void 0;
|
|
265
|
+
}
|
|
266
|
+
function default_command_runner(command, args) {
|
|
267
|
+
if (running_under_bun() === true) {
|
|
268
|
+
return bun_command_runner(command, args);
|
|
269
|
+
}
|
|
270
|
+
return node_command_runner(command, args);
|
|
271
|
+
}
|
|
272
|
+
function node_command_runner(command, args) {
|
|
273
|
+
return new Promise((resolve) => {
|
|
274
|
+
let stdout = "";
|
|
275
|
+
let stderr = "";
|
|
276
|
+
let settled = false;
|
|
277
|
+
const finish = (result) => {
|
|
278
|
+
if (settled === true) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
settled = true;
|
|
282
|
+
resolve(result);
|
|
283
|
+
};
|
|
284
|
+
const forward = args[0] === "install";
|
|
285
|
+
const child = spawn(command, [...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
286
|
+
child.stdout?.on("data", (chunk) => {
|
|
287
|
+
const text = chunk.toString();
|
|
288
|
+
stdout += text;
|
|
289
|
+
if (forward === true) {
|
|
290
|
+
process.stdout.write(text);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
child.stderr?.on("data", (chunk) => {
|
|
294
|
+
const text = chunk.toString();
|
|
295
|
+
stderr += text;
|
|
296
|
+
if (forward === true) {
|
|
297
|
+
process.stderr.write(text);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
child.on("error", (error) => {
|
|
301
|
+
finish({ exit_code: 127, stdout, stderr: error.message, error_code: error.code });
|
|
302
|
+
});
|
|
303
|
+
child.on("close", (code) => {
|
|
304
|
+
finish({ exit_code: code ?? 1, stdout, stderr });
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
async function read_piped_text(stream, forward, write) {
|
|
309
|
+
const reader = stream.getReader();
|
|
310
|
+
const decoder = new TextDecoder();
|
|
311
|
+
let text = "";
|
|
312
|
+
for (; ; ) {
|
|
313
|
+
const next = await reader.read();
|
|
314
|
+
if (next.done === true) {
|
|
315
|
+
return text;
|
|
316
|
+
}
|
|
317
|
+
const chunk = decoder.decode(next.value, { stream: true });
|
|
318
|
+
text += chunk;
|
|
319
|
+
if (forward === true) {
|
|
320
|
+
write(chunk);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function bun_command_runner(command, args) {
|
|
325
|
+
const forward = args[0] === "install";
|
|
326
|
+
try {
|
|
327
|
+
const child = Bun.spawn([command, ...args], {
|
|
328
|
+
stdin: "ignore",
|
|
329
|
+
stdout: "pipe",
|
|
330
|
+
stderr: "pipe"
|
|
331
|
+
});
|
|
332
|
+
return Promise.all([
|
|
333
|
+
read_piped_text(child.stdout, forward, (text) => {
|
|
334
|
+
process.stdout.write(text);
|
|
335
|
+
}),
|
|
336
|
+
read_piped_text(child.stderr, forward, (text) => {
|
|
337
|
+
process.stderr.write(text);
|
|
338
|
+
}),
|
|
339
|
+
child.exited
|
|
340
|
+
]).then(([stdout, stderr, exit_code]) => ({ exit_code, stdout, stderr }));
|
|
341
|
+
} catch (error) {
|
|
342
|
+
const coded = error;
|
|
343
|
+
const stderr = typeof coded.message === "string" ? coded.message : "spawn failed";
|
|
344
|
+
return Promise.resolve({ exit_code: 127, stdout: "", stderr, error_code: coded.code });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async function run_update(module_path) {
|
|
348
|
+
return run_lich_update({
|
|
349
|
+
installed_version: LICH_VERSION,
|
|
350
|
+
install_kind: detect_install_kind(module_path, process.env, (dir) => existsSync2(path2.join(dir, ".git"))),
|
|
351
|
+
run_command: default_command_runner,
|
|
352
|
+
write_stdout: (text) => {
|
|
353
|
+
process.stdout.write(text);
|
|
354
|
+
},
|
|
355
|
+
write_stderr: (text) => {
|
|
356
|
+
process.stderr.write(text);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/setup_wizard.ts
|
|
362
|
+
import { readdirSync } from "fs";
|
|
363
|
+
import path3 from "path";
|
|
364
|
+
var PLATFORMS = ["webhook", "telegram", "discord", "twitch"];
|
|
365
|
+
var PLUGIN_EXT = /\.(mjs|js|ts|mts|cts|jsx|tsx)$/;
|
|
366
|
+
var PROVIDER_DEFAULTS = {
|
|
367
|
+
ollama: { model: "llama3.2", base_url: "http://localhost:11434" },
|
|
368
|
+
openai_compat: { model: "gpt-4.1-mini", base_url: "https://api.openai.com/v1", api_key_env: "OPENAI_API_KEY" },
|
|
369
|
+
anthropic: { model: "claude-sonnet-4", base_url: "https://api.anthropic.com", api_key_env: "ANTHROPIC_API_KEY" }
|
|
370
|
+
};
|
|
371
|
+
function discover_plugin_entries(work_dir) {
|
|
372
|
+
const dir = path3.resolve(work_dir, ".lich", "plugins");
|
|
373
|
+
let names;
|
|
374
|
+
try {
|
|
375
|
+
names = readdirSync(dir);
|
|
376
|
+
} catch {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
const entries = [];
|
|
380
|
+
for (const name of names) {
|
|
381
|
+
if (PLUGIN_EXT.test(name) === true) {
|
|
382
|
+
entries.push(path3.posix.join(".lich/plugins", name));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return entries;
|
|
386
|
+
}
|
|
387
|
+
function build_setup_config(answers) {
|
|
388
|
+
const provider = {
|
|
389
|
+
kind: answers.provider_kind,
|
|
390
|
+
name: "main",
|
|
391
|
+
model: answers.model,
|
|
392
|
+
base_url: answers.base_url
|
|
393
|
+
};
|
|
394
|
+
if (answers.api_key_env !== void 0 && answers.api_key_env.length > 0) {
|
|
395
|
+
provider["api_key_env"] = answers.api_key_env;
|
|
396
|
+
}
|
|
397
|
+
const config = {
|
|
398
|
+
agent_name: answers.agent_name,
|
|
399
|
+
providers: [provider],
|
|
400
|
+
max_turns: 25,
|
|
401
|
+
plugins: answers.plugins
|
|
402
|
+
};
|
|
403
|
+
if (answers.platforms.length > 0) {
|
|
404
|
+
config["gateway"] = { platforms: answers.platforms, token_envs: answers.token_envs };
|
|
405
|
+
}
|
|
406
|
+
return config;
|
|
407
|
+
}
|
|
408
|
+
function blank_as_default(raw, fallback) {
|
|
409
|
+
const trimmed = raw.trim();
|
|
410
|
+
return trimmed.length === 0 ? fallback : trimmed;
|
|
411
|
+
}
|
|
412
|
+
function parse_kind(raw) {
|
|
413
|
+
const trimmed = raw.trim();
|
|
414
|
+
const kind = trimmed.length === 0 ? "ollama" : trimmed;
|
|
415
|
+
if (kind === "ollama" || kind === "openai_compat" || kind === "anthropic") {
|
|
416
|
+
return kind;
|
|
417
|
+
}
|
|
418
|
+
return void 0;
|
|
419
|
+
}
|
|
420
|
+
function parse_platforms(raw) {
|
|
421
|
+
const seen = /* @__PURE__ */ new Set();
|
|
422
|
+
const platforms = [];
|
|
423
|
+
for (const part of raw.split(",")) {
|
|
424
|
+
const name = part.trim().toLowerCase();
|
|
425
|
+
if (PLATFORMS.includes(name) === false || seen.has(name) === true) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
seen.add(name);
|
|
429
|
+
platforms.push(name);
|
|
430
|
+
}
|
|
431
|
+
return platforms;
|
|
432
|
+
}
|
|
433
|
+
function ask_line(rl, prompt) {
|
|
434
|
+
return new Promise((resolve) => {
|
|
435
|
+
let settled = false;
|
|
436
|
+
const finish = (value) => {
|
|
437
|
+
if (settled === true) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
settled = true;
|
|
441
|
+
rl.removeListener("SIGINT", on_sigint);
|
|
442
|
+
rl.removeListener("close", on_close);
|
|
443
|
+
resolve(value);
|
|
444
|
+
};
|
|
445
|
+
const on_sigint = () => finish(void 0);
|
|
446
|
+
const on_close = () => finish(void 0);
|
|
447
|
+
rl.once("SIGINT", on_sigint);
|
|
448
|
+
rl.once("close", on_close);
|
|
449
|
+
rl.question(prompt, (answer) => finish(answer));
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
async function ask_name(ask) {
|
|
453
|
+
const raw = await ask("Agent name [lich]: ");
|
|
454
|
+
if (raw === void 0) {
|
|
455
|
+
return void 0;
|
|
456
|
+
}
|
|
457
|
+
return blank_as_default(raw, "lich");
|
|
458
|
+
}
|
|
459
|
+
async function ask_provider(ask, model_hint2) {
|
|
460
|
+
const kind_raw = await ask("Provider kind (ollama|openai_compat|anthropic) [ollama]: ");
|
|
461
|
+
if (kind_raw === void 0) {
|
|
462
|
+
return void 0;
|
|
463
|
+
}
|
|
464
|
+
let kind = parse_kind(kind_raw);
|
|
465
|
+
if (kind === void 0) {
|
|
466
|
+
const retry = await ask("Unknown kind. Provider kind (ollama|openai_compat|anthropic) [ollama]: ");
|
|
467
|
+
if (retry === void 0) {
|
|
468
|
+
return void 0;
|
|
469
|
+
}
|
|
470
|
+
kind = parse_kind(retry) ?? "ollama";
|
|
471
|
+
}
|
|
472
|
+
const defaults = PROVIDER_DEFAULTS[kind];
|
|
473
|
+
const model_default = model_hint2 !== void 0 && model_hint2.length > 0 ? model_hint2 : defaults.model;
|
|
474
|
+
const model_raw = await ask(`Model [${model_default}]: `);
|
|
475
|
+
if (model_raw === void 0) {
|
|
476
|
+
return void 0;
|
|
477
|
+
}
|
|
478
|
+
const url_raw = await ask(`Base URL [${defaults.base_url}]: `);
|
|
479
|
+
if (url_raw === void 0) {
|
|
480
|
+
return void 0;
|
|
481
|
+
}
|
|
482
|
+
const key_default = defaults.api_key_env ?? "";
|
|
483
|
+
const key_prompt = key_default.length === 0 ? "API key env var name (empty to skip): " : `API key env var name [${key_default}]: `;
|
|
484
|
+
const key_raw = await ask(key_prompt);
|
|
485
|
+
if (key_raw === void 0) {
|
|
486
|
+
return void 0;
|
|
487
|
+
}
|
|
488
|
+
const api_key_env = blank_as_default(key_raw, key_default);
|
|
489
|
+
return {
|
|
490
|
+
provider_kind: kind,
|
|
491
|
+
model: blank_as_default(model_raw, model_default),
|
|
492
|
+
base_url: blank_as_default(url_raw, defaults.base_url),
|
|
493
|
+
api_key_env: api_key_env.length === 0 ? void 0 : api_key_env
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function token_env_name(raw, fallback) {
|
|
497
|
+
const name = blank_as_default(raw, fallback);
|
|
498
|
+
return is_env_var_name(name) === true ? name : void 0;
|
|
499
|
+
}
|
|
500
|
+
async function ask_token_env(ask, platform, fallback) {
|
|
501
|
+
const first = await ask(`${platform} token env var [${fallback}] (name only, not the secret): `);
|
|
502
|
+
if (first === void 0) {
|
|
503
|
+
return void 0;
|
|
504
|
+
}
|
|
505
|
+
const named = token_env_name(first, fallback);
|
|
506
|
+
if (named !== void 0) {
|
|
507
|
+
return named;
|
|
508
|
+
}
|
|
509
|
+
const retry = await ask(`Env var name must match [A-Za-z_][A-Za-z0-9_]* [${fallback}]: `);
|
|
510
|
+
if (retry === void 0) {
|
|
511
|
+
return void 0;
|
|
512
|
+
}
|
|
513
|
+
return token_env_name(retry, fallback);
|
|
514
|
+
}
|
|
515
|
+
async function ask_gateway(ask) {
|
|
516
|
+
const raw = await ask("Gateway platforms (webhook,telegram,discord,twitch; empty to skip): ");
|
|
517
|
+
if (raw === void 0) {
|
|
518
|
+
return void 0;
|
|
519
|
+
}
|
|
520
|
+
const platforms = parse_platforms(raw);
|
|
521
|
+
const token_envs = {};
|
|
522
|
+
for (const platform of platforms) {
|
|
523
|
+
const name = await ask_token_env(ask, platform, DEFAULT_GATEWAY_TOKEN_ENVS[platform] ?? "");
|
|
524
|
+
if (name === void 0) {
|
|
525
|
+
return void 0;
|
|
526
|
+
}
|
|
527
|
+
token_envs[platform] = name;
|
|
528
|
+
}
|
|
529
|
+
return { platforms, token_envs };
|
|
530
|
+
}
|
|
531
|
+
async function ask_plugins(work_dir, ask) {
|
|
532
|
+
const found = discover_plugin_entries(work_dir);
|
|
533
|
+
if (found.length === 0) {
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
const enabled = [];
|
|
537
|
+
for (const entry of found) {
|
|
538
|
+
const answer = await ask(`Enable plugin ${entry}? [y/N]: `);
|
|
539
|
+
if (answer === void 0) {
|
|
540
|
+
return void 0;
|
|
541
|
+
}
|
|
542
|
+
const yes = answer.trim().toLowerCase();
|
|
543
|
+
if (yes === "y" || yes === "yes") {
|
|
544
|
+
enabled.push(entry);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return enabled;
|
|
548
|
+
}
|
|
549
|
+
async function collect_setup_answers(work_dir, ask, model_hint2) {
|
|
550
|
+
const agent_name = await ask_name(ask);
|
|
551
|
+
if (agent_name === void 0) {
|
|
552
|
+
return void 0;
|
|
553
|
+
}
|
|
554
|
+
const provider = await ask_provider(ask, model_hint2);
|
|
555
|
+
if (provider === void 0) {
|
|
556
|
+
return void 0;
|
|
557
|
+
}
|
|
558
|
+
const gateway = await ask_gateway(ask);
|
|
559
|
+
if (gateway === void 0) {
|
|
560
|
+
return void 0;
|
|
561
|
+
}
|
|
562
|
+
const plugins = await ask_plugins(work_dir, ask);
|
|
563
|
+
if (plugins === void 0) {
|
|
564
|
+
return void 0;
|
|
565
|
+
}
|
|
566
|
+
return { agent_name, ...provider, platforms: gateway.platforms, token_envs: gateway.token_envs, plugins };
|
|
567
|
+
}
|
|
68
568
|
|
|
69
569
|
// src/cli.ts
|
|
70
570
|
var FLAG_KEYS = {
|
|
@@ -94,11 +594,14 @@ function usage_text() {
|
|
|
94
594
|
"lich \u2014 a TypeScript AI agent harness",
|
|
95
595
|
"",
|
|
96
596
|
"Usage:",
|
|
597
|
+
" lich open the TUI (first run: setup wizard, then TUI)",
|
|
598
|
+
" lich init write .lich/config.json without the wizard (flags apply; never overwrites)",
|
|
97
599
|
' lich "one shot task" run a single task and print the reply',
|
|
98
600
|
" lich chat interactive chat (commands: /exit, /quit)",
|
|
99
601
|
" lich tui interactive terminal UI (ink)",
|
|
100
602
|
" lich gateway <plat..> messaging gateway (webhook|telegram|discord|twitch)",
|
|
101
603
|
" lich config print a starter config template (save as .lich/config.json)",
|
|
604
|
+
" lich update install a newer @moikapy/lich from npm, if one exists",
|
|
102
605
|
" lich --help show this help",
|
|
103
606
|
" lich --version print version",
|
|
104
607
|
"",
|
|
@@ -239,8 +742,15 @@ function apply_overrides(config, overrides) {
|
|
|
239
742
|
}
|
|
240
743
|
apply_provider_override(config, overrides);
|
|
241
744
|
}
|
|
745
|
+
function load_discovered_config(work_dir) {
|
|
746
|
+
const found = existing_config_path(work_dir);
|
|
747
|
+
if (found === void 0) {
|
|
748
|
+
return void 0;
|
|
749
|
+
}
|
|
750
|
+
return load_config(found);
|
|
751
|
+
}
|
|
242
752
|
function build_config(options) {
|
|
243
|
-
const base = options.config_path === void 0 ?
|
|
753
|
+
const base = options.config_path === void 0 ? load_discovered_config(work_dir_of(options)) ?? { providers: [env_provider(options.overrides)] } : load_config_file(options.config_path);
|
|
244
754
|
apply_overrides(base, options.overrides);
|
|
245
755
|
const providers = base["providers"];
|
|
246
756
|
if (Array.isArray(providers) === false || providers.length === 0) {
|
|
@@ -274,7 +784,7 @@ function attach_progress(emitter) {
|
|
|
274
784
|
});
|
|
275
785
|
}
|
|
276
786
|
async function run_one_shot(config, input) {
|
|
277
|
-
const agent =
|
|
787
|
+
const agent = await create_agent_with_plugins(config);
|
|
278
788
|
const stop_progress = attach_progress(agent.events);
|
|
279
789
|
let result;
|
|
280
790
|
try {
|
|
@@ -316,7 +826,7 @@ async function run_chat_turn(agent, input) {
|
|
|
316
826
|
stop_progress();
|
|
317
827
|
}
|
|
318
828
|
}
|
|
319
|
-
function
|
|
829
|
+
function ask_line2(rl) {
|
|
320
830
|
return new Promise((resolve) => {
|
|
321
831
|
const on_close = () => resolve("");
|
|
322
832
|
rl.question("> ", (answer) => {
|
|
@@ -327,11 +837,11 @@ function ask_line(rl) {
|
|
|
327
837
|
});
|
|
328
838
|
}
|
|
329
839
|
async function run_chat(config) {
|
|
330
|
-
const agent =
|
|
840
|
+
const agent = await create_agent_with_plugins(config);
|
|
331
841
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
332
842
|
try {
|
|
333
843
|
for (; ; ) {
|
|
334
|
-
const line = await
|
|
844
|
+
const line = await ask_line2(rl);
|
|
335
845
|
if (line.trim() === "") {
|
|
336
846
|
return 0;
|
|
337
847
|
}
|
|
@@ -345,22 +855,136 @@ async function run_chat(config) {
|
|
|
345
855
|
rl.close();
|
|
346
856
|
}
|
|
347
857
|
}
|
|
858
|
+
function model_hint(options) {
|
|
859
|
+
const model = options.overrides["provider_model"] ?? process.env.LICH_MODEL;
|
|
860
|
+
if (model === void 0 || model.length === 0) {
|
|
861
|
+
return void 0;
|
|
862
|
+
}
|
|
863
|
+
return model;
|
|
864
|
+
}
|
|
865
|
+
function skip_setup_message(existing) {
|
|
866
|
+
if (existing.endsWith(`${path4.sep}.lich${path4.sep}config.json`) === true) {
|
|
867
|
+
return `lich: .lich/config.json already exists at ${existing}; skipping setup`;
|
|
868
|
+
}
|
|
869
|
+
return `lich: config already exists at ${existing}; skipping setup`;
|
|
870
|
+
}
|
|
871
|
+
function work_dir_of(options) {
|
|
872
|
+
return options.overrides["work_dir"] ?? process.cwd();
|
|
873
|
+
}
|
|
874
|
+
async function offer_wizard(work_dir, hint) {
|
|
875
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
876
|
+
try {
|
|
877
|
+
const answers = await collect_setup_answers(work_dir, (prompt) => ask_line(rl, prompt), hint);
|
|
878
|
+
if (answers === void 0) {
|
|
879
|
+
return "cancelled";
|
|
880
|
+
}
|
|
881
|
+
const result = write_lich_config(work_dir, build_setup_config(answers));
|
|
882
|
+
process.stdout.write(`${result.message}
|
|
883
|
+
`);
|
|
884
|
+
return result.path;
|
|
885
|
+
} finally {
|
|
886
|
+
rl.close();
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
async function maybe_first_run(options, work_dir) {
|
|
890
|
+
if (options.config_path !== void 0) {
|
|
891
|
+
return void 0;
|
|
892
|
+
}
|
|
893
|
+
const existing = existing_config_path(work_dir);
|
|
894
|
+
if (existing !== void 0) {
|
|
895
|
+
process.stdout.write(`${skip_setup_message(existing)}
|
|
896
|
+
`);
|
|
897
|
+
options.config_path = existing;
|
|
898
|
+
return void 0;
|
|
899
|
+
}
|
|
900
|
+
const wrote = await offer_wizard(work_dir, model_hint(options));
|
|
901
|
+
if (wrote === "cancelled") {
|
|
902
|
+
process.stderr.write("lich: setup cancelled; nothing written\n");
|
|
903
|
+
return 1;
|
|
904
|
+
}
|
|
905
|
+
options.config_path = wrote;
|
|
906
|
+
return void 0;
|
|
907
|
+
}
|
|
908
|
+
async function run_bare(options) {
|
|
909
|
+
if (process.stdin.isTTY !== true) {
|
|
910
|
+
process.stderr.write("lich: stdin is not a TTY; skipping setup wizard. Run `lich init` to write .lich/config.json, or `lich --help` for usage.\n");
|
|
911
|
+
return 1;
|
|
912
|
+
}
|
|
913
|
+
const work_dir = work_dir_of(options);
|
|
914
|
+
const stopped = await maybe_first_run(options, work_dir);
|
|
915
|
+
if (stopped !== void 0) {
|
|
916
|
+
return stopped;
|
|
917
|
+
}
|
|
918
|
+
return run_tui_entry(build_config_for(options, "tui"));
|
|
919
|
+
}
|
|
920
|
+
function model_still_placeholder(config) {
|
|
921
|
+
const providers = config["providers"];
|
|
922
|
+
const first = Array.isArray(providers) === true ? providers[0] : void 0;
|
|
923
|
+
if (typeof first !== "object" || first === null) {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
return first["model"] === "<model-name>";
|
|
927
|
+
}
|
|
928
|
+
function run_init(options) {
|
|
929
|
+
if (options.positionals.length > 1) {
|
|
930
|
+
throw new Error("init takes no extra arguments");
|
|
931
|
+
}
|
|
932
|
+
const config = starter_config_object();
|
|
933
|
+
apply_overrides(config, options.overrides);
|
|
934
|
+
const result = write_lich_config(work_dir_of(options), config);
|
|
935
|
+
process.stdout.write(`${result.message}
|
|
936
|
+
`);
|
|
937
|
+
if (result.written === true && model_still_placeholder(config) === true) {
|
|
938
|
+
process.stdout.write("next: edit the model in .lich/config.json if needed, then run `lich`\n");
|
|
939
|
+
}
|
|
940
|
+
return 0;
|
|
941
|
+
}
|
|
348
942
|
async function run_tui_entry(config) {
|
|
349
|
-
const { run_tui } = await import("./tui-
|
|
943
|
+
const { run_tui } = await import("./tui-2VO6LAFM.js");
|
|
350
944
|
return run_tui(config);
|
|
351
945
|
}
|
|
946
|
+
function gateway_platforms(config, cli_platforms) {
|
|
947
|
+
if (cli_platforms.length > 0) {
|
|
948
|
+
return cli_platforms;
|
|
949
|
+
}
|
|
950
|
+
const configured = config.gateway?.platforms ?? [];
|
|
951
|
+
return configured.length === 0 ? ["webhook"] : configured;
|
|
952
|
+
}
|
|
352
953
|
async function run_gateway_entry(config, platforms) {
|
|
353
|
-
const { run_gateway } = await import("./gateway-
|
|
354
|
-
return run_gateway(config,
|
|
954
|
+
const { run_gateway } = await import("./gateway-W6S43ETE.js");
|
|
955
|
+
return run_gateway(config, gateway_platforms(config, platforms));
|
|
956
|
+
}
|
|
957
|
+
function is_cli_entry(input) {
|
|
958
|
+
if (input.bun === true) {
|
|
959
|
+
return input.import_meta_main === true;
|
|
960
|
+
}
|
|
961
|
+
return node_entry_matches(input.module_url, input.argv1);
|
|
355
962
|
}
|
|
356
963
|
function is_main_module() {
|
|
357
|
-
|
|
358
|
-
|
|
964
|
+
return is_cli_entry({
|
|
965
|
+
bun: process.versions.bun !== void 0,
|
|
966
|
+
import_meta_main: import.meta.main,
|
|
967
|
+
module_url: import.meta.url,
|
|
968
|
+
argv1: process.argv[1]
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
function node_entry_matches(module_url, entry) {
|
|
972
|
+
if (entry === void 0 || entry === "") {
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
const resolved = path4.resolve(entry);
|
|
976
|
+
if (module_url === pathToFileURL(resolved).href) {
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
return same_real_file(fileURLToPath(module_url), resolved);
|
|
980
|
+
}
|
|
981
|
+
function same_real_file(left, right) {
|
|
982
|
+
if (existsSync3(left) === false || existsSync3(right) === false) {
|
|
359
983
|
return false;
|
|
360
984
|
}
|
|
361
|
-
return
|
|
985
|
+
return realpathSync(left) === realpathSync(right);
|
|
362
986
|
}
|
|
363
|
-
async function
|
|
987
|
+
async function run_cli(argv) {
|
|
364
988
|
const options = parse_args(argv);
|
|
365
989
|
if (options.help === true) {
|
|
366
990
|
process.stdout.write(`${usage_text()}
|
|
@@ -374,15 +998,22 @@ async function main(argv) {
|
|
|
374
998
|
}
|
|
375
999
|
const [first] = options.positionals;
|
|
376
1000
|
if (first === void 0) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
1001
|
+
return run_bare(options);
|
|
1002
|
+
}
|
|
1003
|
+
if (first === "init") {
|
|
1004
|
+
return run_init(options);
|
|
380
1005
|
}
|
|
381
1006
|
if (first === "config") {
|
|
382
1007
|
process.stdout.write(`${config_template()}
|
|
383
1008
|
`);
|
|
384
1009
|
return 0;
|
|
385
1010
|
}
|
|
1011
|
+
if (first === "update") {
|
|
1012
|
+
if (options.positionals.length > 1) {
|
|
1013
|
+
throw new Error("update takes no arguments");
|
|
1014
|
+
}
|
|
1015
|
+
return run_update(fileURLToPath(import.meta.url));
|
|
1016
|
+
}
|
|
386
1017
|
if (first === "chat") {
|
|
387
1018
|
if (options.positionals.length > 1) {
|
|
388
1019
|
throw new Error("chat mode takes no task argument");
|
|
@@ -398,7 +1029,7 @@ async function main(argv) {
|
|
|
398
1029
|
return run_one_shot(build_config_for(options, "one-shot"), options.positionals.join(" "));
|
|
399
1030
|
}
|
|
400
1031
|
if (is_main_module() === true) {
|
|
401
|
-
|
|
1032
|
+
run_cli(process.argv.slice(2)).then((code) => {
|
|
402
1033
|
process.exitCode = code;
|
|
403
1034
|
}).catch((error) => {
|
|
404
1035
|
process.stderr.write(`lich: ${error_message(error)}
|
|
@@ -407,4 +1038,10 @@ if (is_main_module() === true) {
|
|
|
407
1038
|
process.exitCode = 1;
|
|
408
1039
|
});
|
|
409
1040
|
}
|
|
1041
|
+
export {
|
|
1042
|
+
is_cli_entry,
|
|
1043
|
+
run_chat,
|
|
1044
|
+
run_cli,
|
|
1045
|
+
run_one_shot
|
|
1046
|
+
};
|
|
410
1047
|
//# sourceMappingURL=cli.js.map
|