@moikapy/lich 0.7.1 → 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.
- package/CHANGELOG.md +63 -0
- package/README.md +38 -19
- package/dist/{chunk-CVX7LZWC.js → chunk-4N3S6ACI.js} +2 -2
- package/dist/{chunk-WNFBIX4E.js → chunk-KOEZQUIX.js} +1312 -600
- package/dist/chunk-KOEZQUIX.js.map +1 -0
- package/dist/{chunk-PZNYVGD4.js → chunk-P2YEHNM4.js} +2 -2
- package/dist/chunk-P2YEHNM4.js.map +1 -0
- package/dist/chunk-PL6MKRKE.js +75 -0
- package/dist/chunk-PL6MKRKE.js.map +1 -0
- package/dist/chunk-TYRKVBWQ.js +104 -0
- package/dist/chunk-TYRKVBWQ.js.map +1 -0
- package/dist/cli.d.ts +19 -1
- package/dist/cli.js +125 -47
- package/dist/cli.js.map +1 -1
- package/dist/{gateway-44QTIJTJ.js → gateway-OSB7254F.js} +209 -46
- package/dist/gateway-OSB7254F.js.map +1 -0
- package/dist/index.d.ts +122 -62
- package/dist/index.js +12 -4
- package/dist/resolve-H22TOVB5.js +7 -0
- package/dist/resolve-H22TOVB5.js.map +1 -0
- package/dist/store-YYLEB7JG.js +9 -0
- package/dist/store-YYLEB7JG.js.map +1 -0
- package/dist/{tui-MEJGEILU.js → tui-XXIU4W7K.js} +216 -25
- package/dist/tui-XXIU4W7K.js.map +1 -0
- package/docs/architecture/agent-loop.md +28 -9
- package/docs/architecture/overview.md +10 -9
- package/docs/architecture/tools.md +3 -1
- package/docs/getting-started.md +6 -6
- package/docs/index.md +1 -1
- package/docs/user-guide/cli.md +17 -8
- package/docs/user-guide/games.md +5 -4
- package/docs/user-guide/library.md +6 -3
- package/docs/user-guide/redot.md +2 -2
- package/docs/user-guide/tui.md +3 -2
- package/package.json +1 -1
- package/dist/chunk-PZNYVGD4.js.map +0 -1
- package/dist/chunk-WNFBIX4E.js.map +0 -1
- package/dist/gateway-44QTIJTJ.js.map +0 -1
- package/dist/tui-MEJGEILU.js.map +0 -1
- /package/dist/{chunk-CVX7LZWC.js.map → chunk-4N3S6ACI.js.map} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
catalog_by_name
|
|
3
|
-
} from "./chunk-
|
|
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-
|
|
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":[]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/session/resolve.ts
|
|
2
|
+
import { readdir, stat } from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
var CANDIDATE_CAP = 5;
|
|
5
|
+
function is_enoent(error) {
|
|
6
|
+
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
7
|
+
}
|
|
8
|
+
async function list_session_files(dir) {
|
|
9
|
+
let names;
|
|
10
|
+
try {
|
|
11
|
+
names = await readdir(dir);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (is_enoent(error) === true) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
const entries = [];
|
|
19
|
+
for (const name of names) {
|
|
20
|
+
if (name.endsWith(".jsonl") === false) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const info = await stat(path.join(dir, name));
|
|
25
|
+
if (info.isFile() === false) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
entries.push({ name, id: name.slice(0, -".jsonl".length), mtime_ms: info.mtimeMs });
|
|
29
|
+
} catch {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return entries.sort((a, b) => b.mtime_ms - a.mtime_ms);
|
|
34
|
+
}
|
|
35
|
+
function candidate_ids(entries) {
|
|
36
|
+
if (entries.length === 0) {
|
|
37
|
+
return "(none)";
|
|
38
|
+
}
|
|
39
|
+
return entries.slice(0, CANDIDATE_CAP).map((entry) => entry.id).join(", ");
|
|
40
|
+
}
|
|
41
|
+
function missing_error(dir, value, entries) {
|
|
42
|
+
return new Error(`session not found: "${value}" in ${dir} (candidates: ${candidate_ids(entries)})`);
|
|
43
|
+
}
|
|
44
|
+
function ambiguous_error(dir, value, matches) {
|
|
45
|
+
const sorted = [...matches].sort((a, b) => b.mtime_ms - a.mtime_ms);
|
|
46
|
+
return new Error(`ambiguous session prefix: "${value}" in ${dir} matches: ${candidate_ids(sorted)}`);
|
|
47
|
+
}
|
|
48
|
+
async function resolve_session_path(dir, value) {
|
|
49
|
+
const entries = await list_session_files(dir);
|
|
50
|
+
if (value === "latest") {
|
|
51
|
+
const newest = entries[0];
|
|
52
|
+
if (newest === void 0) {
|
|
53
|
+
throw missing_error(dir, value, entries);
|
|
54
|
+
}
|
|
55
|
+
return path.join(dir, newest.name);
|
|
56
|
+
}
|
|
57
|
+
const exact_name = `${value}.jsonl`;
|
|
58
|
+
const exact = entries.find((entry) => entry.name === exact_name);
|
|
59
|
+
if (exact !== void 0) {
|
|
60
|
+
return path.join(dir, exact.name);
|
|
61
|
+
}
|
|
62
|
+
const prefix_matches = entries.filter((entry) => entry.id.startsWith(value));
|
|
63
|
+
if (prefix_matches.length === 1) {
|
|
64
|
+
return path.join(dir, prefix_matches[0].name);
|
|
65
|
+
}
|
|
66
|
+
if (prefix_matches.length > 1) {
|
|
67
|
+
throw ambiguous_error(dir, value, prefix_matches);
|
|
68
|
+
}
|
|
69
|
+
throw missing_error(dir, value, entries);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
resolve_session_path
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=chunk-PL6MKRKE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/session/resolve.ts"],"sourcesContent":["/**\n * Resolve a `--resume` value to an existing session transcript path.\n * Pure filesystem lookup: latest by mtime, exact id, or unique prefix.\n */\nimport { readdir, stat } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport interface SessionFileInfo {\n readonly name: string;\n readonly id: string;\n readonly mtime_ms: number;\n}\n\nconst CANDIDATE_CAP = 5;\n\nfunction is_enoent(error: unknown): boolean {\n return typeof error === \"object\" && error !== null && (error as { code?: unknown }).code === \"ENOENT\";\n}\n\nasync function list_session_files(dir: string): Promise<SessionFileInfo[]> {\n let names: string[];\n try {\n names = await readdir(dir);\n } catch (error) {\n if (is_enoent(error) === true) {\n return [];\n }\n throw error;\n }\n const entries: SessionFileInfo[] = [];\n for (const name of names) {\n if (name.endsWith(\".jsonl\") === false) {\n continue;\n }\n try {\n const info = await stat(path.join(dir, name));\n if (info.isFile() === false) {\n continue;\n }\n entries.push({ name, id: name.slice(0, -\".jsonl\".length), mtime_ms: info.mtimeMs });\n } catch {\n continue;\n }\n }\n return entries.sort((a, b) => b.mtime_ms - a.mtime_ms);\n}\n\nfunction candidate_ids(entries: readonly SessionFileInfo[]): string {\n if (entries.length === 0) {\n return \"(none)\";\n }\n return entries\n .slice(0, CANDIDATE_CAP)\n .map((entry) => entry.id)\n .join(\", \");\n}\n\nfunction missing_error(dir: string, value: string, entries: readonly SessionFileInfo[]): Error {\n return new Error(`session not found: \"${value}\" in ${dir} (candidates: ${candidate_ids(entries)})`);\n}\n\nfunction ambiguous_error(dir: string, value: string, matches: readonly SessionFileInfo[]): Error {\n const sorted = [...matches].sort((a, b) => b.mtime_ms - a.mtime_ms);\n return new Error(`ambiguous session prefix: \"${value}\" in ${dir} matches: ${candidate_ids(sorted)}`);\n}\n\n/** Map `(dir, value)` to an absolute transcript path, or throw with candidates. */\nexport async function resolve_session_path(dir: string, value: string): Promise<string> {\n const entries = await list_session_files(dir);\n if (value === \"latest\") {\n const newest = entries[0];\n if (newest === undefined) {\n throw missing_error(dir, value, entries);\n }\n return path.join(dir, newest.name);\n }\n const exact_name = `${value}.jsonl`;\n const exact = entries.find((entry) => entry.name === exact_name);\n if (exact !== undefined) {\n return path.join(dir, exact.name);\n }\n const prefix_matches = entries.filter((entry) => entry.id.startsWith(value));\n if (prefix_matches.length === 1) {\n return path.join(dir, prefix_matches[0]!.name);\n }\n if (prefix_matches.length > 1) {\n throw ambiguous_error(dir, value, prefix_matches);\n }\n throw missing_error(dir, value, entries);\n}\n"],"mappings":";AAIA,SAAS,SAAS,YAAY;AAC9B,OAAO,UAAU;AAQjB,IAAM,gBAAgB;AAEtB,SAAS,UAAU,OAAyB;AAC1C,SAAO,OAAO,UAAU,YAAY,UAAU,QAAS,MAA6B,SAAS;AAC/F;AAEA,eAAe,mBAAmB,KAAyC;AACzE,MAAI;AACJ,MAAI;AACF,YAAQ,MAAM,QAAQ,GAAG;AAAA,EAC3B,SAAS,OAAO;AACd,QAAI,UAAU,KAAK,MAAM,MAAM;AAC7B,aAAO,CAAC;AAAA,IACV;AACA,UAAM;AAAA,EACR;AACA,QAAM,UAA6B,CAAC;AACpC,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,SAAS,QAAQ,MAAM,OAAO;AACrC;AAAA,IACF;AACA,QAAI;AACF,YAAM,OAAO,MAAM,KAAK,KAAK,KAAK,KAAK,IAAI,CAAC;AAC5C,UAAI,KAAK,OAAO,MAAM,OAAO;AAC3B;AAAA,MACF;AACA,cAAQ,KAAK,EAAE,MAAM,IAAI,KAAK,MAAM,GAAG,CAAC,SAAS,MAAM,GAAG,UAAU,KAAK,QAAQ,CAAC;AAAA,IACpF,QAAQ;AACN;AAAA,IACF;AAAA,EACF;AACA,SAAO,QAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AACvD;AAEA,SAAS,cAAc,SAA6C;AAClE,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO;AAAA,EACT;AACA,SAAO,QACJ,MAAM,GAAG,aAAa,EACtB,IAAI,CAAC,UAAU,MAAM,EAAE,EACvB,KAAK,IAAI;AACd;AAEA,SAAS,cAAc,KAAa,OAAe,SAA4C;AAC7F,SAAO,IAAI,MAAM,uBAAuB,KAAK,QAAQ,GAAG,iBAAiB,cAAc,OAAO,CAAC,GAAG;AACpG;AAEA,SAAS,gBAAgB,KAAa,OAAe,SAA4C;AAC/F,QAAM,SAAS,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AAClE,SAAO,IAAI,MAAM,8BAA8B,KAAK,QAAQ,GAAG,aAAa,cAAc,MAAM,CAAC,EAAE;AACrG;AAGA,eAAsB,qBAAqB,KAAa,OAAgC;AACtF,QAAM,UAAU,MAAM,mBAAmB,GAAG;AAC5C,MAAI,UAAU,UAAU;AACtB,UAAM,SAAS,QAAQ,CAAC;AACxB,QAAI,WAAW,QAAW;AACxB,YAAM,cAAc,KAAK,OAAO,OAAO;AAAA,IACzC;AACA,WAAO,KAAK,KAAK,KAAK,OAAO,IAAI;AAAA,EACnC;AACA,QAAM,aAAa,GAAG,KAAK;AAC3B,QAAM,QAAQ,QAAQ,KAAK,CAAC,UAAU,MAAM,SAAS,UAAU;AAC/D,MAAI,UAAU,QAAW;AACvB,WAAO,KAAK,KAAK,KAAK,MAAM,IAAI;AAAA,EAClC;AACA,QAAM,iBAAiB,QAAQ,OAAO,CAAC,UAAU,MAAM,GAAG,WAAW,KAAK,CAAC;AAC3E,MAAI,eAAe,WAAW,GAAG;AAC/B,WAAO,KAAK,KAAK,KAAK,eAAe,CAAC,EAAG,IAAI;AAAA,EAC/C;AACA,MAAI,eAAe,SAAS,GAAG;AAC7B,UAAM,gBAAgB,KAAK,OAAO,cAAc;AAAA,EAClD;AACA,QAAM,cAAc,KAAK,OAAO,OAAO;AACzC;","names":[]}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// src/session/store.ts
|
|
2
|
+
import { randomBytes } from "crypto";
|
|
3
|
+
import { access, appendFile, mkdir, readFile } from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
// src/util/json.ts
|
|
7
|
+
function safe_json_parse(raw) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(raw);
|
|
10
|
+
} catch {
|
|
11
|
+
return void 0;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function safe_stringify(value, space) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.stringify(value, null, space) ?? String(value);
|
|
17
|
+
} catch {
|
|
18
|
+
return String(value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function truncate_text(text, max_chars) {
|
|
22
|
+
if (text.length <= max_chars) {
|
|
23
|
+
return text;
|
|
24
|
+
}
|
|
25
|
+
const omitted = text.length - max_chars;
|
|
26
|
+
return `${text.slice(0, max_chars)}
|
|
27
|
+
[... truncated, ${omitted} chars omitted ...]`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/session/store.ts
|
|
31
|
+
var counter_state = { value: 0 };
|
|
32
|
+
var MESSAGE_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
|
|
33
|
+
var CREATE_ATTEMPTS = 8;
|
|
34
|
+
function slugify_label(label) {
|
|
35
|
+
const slug = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
36
|
+
return slug.length > 0 ? `-${slug}` : "";
|
|
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
|
+
}
|
|
56
|
+
async function open_session(dir, label) {
|
|
57
|
+
await mkdir(dir, { recursive: true });
|
|
58
|
+
const label_part = label === void 0 ? "" : slugify_label(label);
|
|
59
|
+
const created = await create_unique_session_path(dir, label_part);
|
|
60
|
+
return {
|
|
61
|
+
id: created.id,
|
|
62
|
+
path: created.path,
|
|
63
|
+
append: async (record) => {
|
|
64
|
+
await appendFile(created.path, `${safe_stringify(record)}
|
|
65
|
+
`, "utf8");
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function is_message(value) {
|
|
70
|
+
if (typeof value !== "object" || value === null) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const role = value.role;
|
|
74
|
+
return typeof role === "string" && MESSAGE_ROLES.has(role);
|
|
75
|
+
}
|
|
76
|
+
async function read_session_messages(file_path) {
|
|
77
|
+
let raw;
|
|
78
|
+
try {
|
|
79
|
+
raw = await readFile(file_path, "utf8");
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const messages = [];
|
|
84
|
+
for (const line of raw.split("\n")) {
|
|
85
|
+
const record = safe_json_parse(line);
|
|
86
|
+
if (record?.message !== void 0 && is_message(record.message)) {
|
|
87
|
+
messages.push(record.message);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const last = messages.at(-1);
|
|
91
|
+
if (last?.role === "user") {
|
|
92
|
+
messages.pop();
|
|
93
|
+
}
|
|
94
|
+
return messages;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
safe_json_parse,
|
|
99
|
+
safe_stringify,
|
|
100
|
+
truncate_text,
|
|
101
|
+
open_session,
|
|
102
|
+
read_session_messages
|
|
103
|
+
};
|
|
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.d.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/** MCP-only flags. Values are not logged. */
|
|
3
|
+
interface McpCliFlags {
|
|
4
|
+
command?: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
url?: string;
|
|
7
|
+
project_path?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface CliOptions {
|
|
11
|
+
config_path?: string;
|
|
12
|
+
resume?: string;
|
|
13
|
+
help?: boolean;
|
|
14
|
+
version?: boolean;
|
|
15
|
+
overrides: Record<string, string>;
|
|
16
|
+
positionals: string[];
|
|
17
|
+
mcp_flags: McpCliFlags;
|
|
18
|
+
}
|
|
19
|
+
declare function parse_args(argv: string[]): CliOptions;
|
|
2
20
|
declare function run_one_shot(config: unknown, input: string): Promise<number>;
|
|
3
21
|
declare function run_chat(config: unknown): Promise<number>;
|
|
4
22
|
interface CliEntryInput {
|
|
@@ -11,4 +29,4 @@ interface CliEntryInput {
|
|
|
11
29
|
declare function is_cli_entry(input: CliEntryInput): boolean;
|
|
12
30
|
declare function run_cli(argv: string[]): Promise<number>;
|
|
13
31
|
|
|
14
|
-
export { type CliEntryInput, is_cli_entry, run_chat, run_cli, run_one_shot };
|
|
32
|
+
export { type CliEntryInput, is_cli_entry, parse_args, run_chat, run_cli, run_one_shot };
|
package/dist/cli.js
CHANGED
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
import {
|
|
3
3
|
load_theme,
|
|
4
4
|
notice_flavor
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-4N3S6ACI.js";
|
|
6
6
|
import {
|
|
7
7
|
LICH_VERSION,
|
|
8
8
|
catalog_client_entry
|
|
9
|
-
} from "./chunk-
|
|
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
|
-
refuse_mcp_entry
|
|
15
|
+
refuse_mcp_entry
|
|
16
|
+
} from "./chunk-KOEZQUIX.js";
|
|
17
|
+
import {
|
|
16
18
|
safe_json_parse
|
|
17
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-TYRKVBWQ.js";
|
|
18
20
|
|
|
19
21
|
// src/cli.ts
|
|
20
22
|
import { existsSync as existsSync4, readFileSync as readFileSync2, realpathSync } from "fs";
|
|
@@ -29,6 +31,20 @@ import path from "path";
|
|
|
29
31
|
var LICH_DIRNAME = ".lich";
|
|
30
32
|
var CONFIG_RELPATH = `${LICH_DIRNAME}/config.json`;
|
|
31
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
|
+
}
|
|
32
48
|
function config_search_paths(work_dir) {
|
|
33
49
|
const root = work_dir ?? process.cwd();
|
|
34
50
|
const first = path.resolve(root, CONFIG_RELPATH);
|
|
@@ -57,14 +73,16 @@ function load_config(explicit) {
|
|
|
57
73
|
return parsed;
|
|
58
74
|
}
|
|
59
75
|
function config_template() {
|
|
60
|
-
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);
|
|
61
78
|
const provider = {
|
|
62
79
|
kind,
|
|
63
80
|
name: "main",
|
|
64
|
-
model: process.env.LICH_MODEL ?? "<model-name>"
|
|
81
|
+
model: process.env.LICH_MODEL ?? "<model-name>",
|
|
82
|
+
base_url: defaults.base_url
|
|
65
83
|
};
|
|
66
|
-
if (
|
|
67
|
-
provider["
|
|
84
|
+
if (defaults.api_key_env !== void 0) {
|
|
85
|
+
provider["api_key_env"] = defaults.api_key_env;
|
|
68
86
|
}
|
|
69
87
|
return JSON.stringify({ providers: [provider], max_turns: 25, theme: "lich" }, null, 2);
|
|
70
88
|
}
|
|
@@ -334,6 +352,7 @@ var VIEW_ARGS = ["view", PACKAGE_NAME, "version"];
|
|
|
334
352
|
var INSTALL_ARGS = ["install", "-g", `${PACKAGE_NAME}@latest`];
|
|
335
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";
|
|
336
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";
|
|
337
356
|
function parse_semver(version) {
|
|
338
357
|
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(version.trim());
|
|
339
358
|
if (match === null || match[1] === void 0 || match[2] === void 0 || match[3] === void 0) {
|
|
@@ -365,6 +384,13 @@ function version_relation(installed, registry) {
|
|
|
365
384
|
}
|
|
366
385
|
return "current";
|
|
367
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
|
+
}
|
|
368
394
|
function detect_install_kind(module_path, env, has_git) {
|
|
369
395
|
if (env.npm_command === "exec" || module_path.includes(`${path2.sep}_npx${path2.sep}`)) {
|
|
370
396
|
return "npx";
|
|
@@ -373,7 +399,10 @@ function detect_install_kind(module_path, env, has_git) {
|
|
|
373
399
|
const root = path2.parse(dir).root;
|
|
374
400
|
while (dir !== root) {
|
|
375
401
|
if (path2.basename(dir) === "node_modules") {
|
|
376
|
-
|
|
402
|
+
if (is_npm_global_node_modules(dir) === true) {
|
|
403
|
+
return "npm";
|
|
404
|
+
}
|
|
405
|
+
return "local";
|
|
377
406
|
}
|
|
378
407
|
if (has_git(dir) === true) {
|
|
379
408
|
return "git";
|
|
@@ -393,6 +422,9 @@ function blocked_install_message(kind) {
|
|
|
393
422
|
if (kind === "npx") {
|
|
394
423
|
return "lich: npx runs a temporary copy and cannot persist an update. Install with `npm install -g @moikapy/lich`.\n";
|
|
395
424
|
}
|
|
425
|
+
if (kind === "local") {
|
|
426
|
+
return LOCAL_INSTALL_HINT;
|
|
427
|
+
}
|
|
396
428
|
return null;
|
|
397
429
|
}
|
|
398
430
|
function registry_version(viewed, request) {
|
|
@@ -791,17 +823,6 @@ var FLAG_KEYS = {
|
|
|
791
823
|
"--log-level": "log_level",
|
|
792
824
|
"--theme": "theme"
|
|
793
825
|
};
|
|
794
|
-
var DEFAULT_BASE_URLS = {
|
|
795
|
-
openai_compat: "https://api.openai.com/v1",
|
|
796
|
-
anthropic: "https://api.anthropic.com",
|
|
797
|
-
ollama: "http://localhost:11434"
|
|
798
|
-
};
|
|
799
|
-
var DEFAULT_ENV_API_KEYS = {
|
|
800
|
-
openai_compat: "OPENAI_API_KEY",
|
|
801
|
-
anthropic: "ANTHROPIC_API_KEY",
|
|
802
|
-
ollama: void 0
|
|
803
|
-
// ollama needs no api key
|
|
804
|
-
};
|
|
805
826
|
function usage_text() {
|
|
806
827
|
return [
|
|
807
828
|
"lich \u2014 the undead agent harness",
|
|
@@ -831,6 +852,7 @@ function usage_text() {
|
|
|
831
852
|
" --api-key-env <NAME> env var holding the api key (default LICH_API_KEY_ENV; unused by ollama)",
|
|
832
853
|
" --system-prompt <s> system prompt override",
|
|
833
854
|
" --session-dir <path> session transcript directory",
|
|
855
|
+
" --resume <id|latest> TUI only: load an existing session transcript",
|
|
834
856
|
" --log-level <level> debug | info | warn | error",
|
|
835
857
|
" --theme <name> display theme (default lich; files in ~/.lich/themes)",
|
|
836
858
|
" --command <bin> mcp add: local stdio binary",
|
|
@@ -842,6 +864,21 @@ function usage_text() {
|
|
|
842
864
|
function error_message(error) {
|
|
843
865
|
return error instanceof Error ? error.message : String(error);
|
|
844
866
|
}
|
|
867
|
+
function reject_resume_outside_tui(resume, mode) {
|
|
868
|
+
if (resume === void 0) {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
throw new Error(`--resume is only supported in TUI mode (not ${mode})`);
|
|
872
|
+
}
|
|
873
|
+
function non_tui_resume_mode(first) {
|
|
874
|
+
if (first === void 0 || first === "tui") {
|
|
875
|
+
return void 0;
|
|
876
|
+
}
|
|
877
|
+
if (first === "init" || first === "config" || first === "mcp" || first === "update" || first === "chat" || first === "gateway") {
|
|
878
|
+
return first;
|
|
879
|
+
}
|
|
880
|
+
return "one-shot";
|
|
881
|
+
}
|
|
845
882
|
function error_for_mode(mode, base_message) {
|
|
846
883
|
if (mode === "tui") {
|
|
847
884
|
return "lich tui: no model configured \u2014 set LICH_MODEL (e.g. glm-5.3-flash:cloud), pass --model, or create .lich/config.json (`lich config` prints a template)";
|
|
@@ -879,6 +916,15 @@ function parse_args(argv) {
|
|
|
879
916
|
index += 1;
|
|
880
917
|
continue;
|
|
881
918
|
}
|
|
919
|
+
if (arg === "--resume") {
|
|
920
|
+
const value2 = argv[index + 1];
|
|
921
|
+
if (value2 === void 0) {
|
|
922
|
+
throw new Error("--resume requires a value");
|
|
923
|
+
}
|
|
924
|
+
options.resume = value2;
|
|
925
|
+
index += 1;
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
882
928
|
if (mcp === true) {
|
|
883
929
|
const consumed = take_mcp_flag(argv, index, options.mcp_flags);
|
|
884
930
|
if (consumed !== void 0) {
|
|
@@ -903,22 +949,25 @@ function parse_args(argv) {
|
|
|
903
949
|
}
|
|
904
950
|
return options;
|
|
905
951
|
}
|
|
906
|
-
function
|
|
907
|
-
const kind_value = overrides["provider_kind"] ?? process.env.LICH_PROVIDER_KIND ?? "openai_compat";
|
|
952
|
+
function parse_provider_kind(kind_value) {
|
|
908
953
|
if (kind_value !== "openai_compat" && kind_value !== "anthropic" && kind_value !== "ollama") {
|
|
909
954
|
throw new Error(`unknown provider kind "${kind_value}" (expected openai_compat, anthropic, or ollama)`);
|
|
910
955
|
}
|
|
911
|
-
|
|
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");
|
|
912
960
|
const model = overrides["provider_model"] ?? process.env.LICH_MODEL;
|
|
913
961
|
if (model === void 0 || model.length === 0) {
|
|
914
962
|
throw new Error("no model configured: set LICH_MODEL, pass --model, or create .lich/config.json (`lich config` prints a template)");
|
|
915
963
|
}
|
|
964
|
+
const defaults = provider_kind_defaults(kind);
|
|
916
965
|
return {
|
|
917
966
|
kind,
|
|
918
967
|
name: "default",
|
|
919
968
|
model,
|
|
920
|
-
base_url: overrides["provider_base_url"] ?? process.env.LICH_BASE_URL ??
|
|
921
|
-
api_key_env: overrides["provider_api_key_env"] ?? process.env.LICH_API_KEY_ENV ??
|
|
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
|
|
922
971
|
};
|
|
923
972
|
}
|
|
924
973
|
function load_config_file(config_path) {
|
|
@@ -933,6 +982,19 @@ function load_config_file(config_path) {
|
|
|
933
982
|
throw new Error(`cannot use config file ${config_path}: ${error_message(error)}`);
|
|
934
983
|
}
|
|
935
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
|
+
}
|
|
936
998
|
function apply_provider_override(config, overrides) {
|
|
937
999
|
const keys = ["provider_kind", "provider_model", "provider_base_url", "provider_api_key_env"];
|
|
938
1000
|
const any_present = keys.some((key) => overrides[key] !== void 0);
|
|
@@ -949,12 +1011,19 @@ function apply_provider_override(config, overrides) {
|
|
|
949
1011
|
throw new Error("config providers[0] must be an object");
|
|
950
1012
|
}
|
|
951
1013
|
const provider = first;
|
|
1014
|
+
const previous_kind = provider["kind"];
|
|
952
1015
|
for (const key of keys) {
|
|
953
1016
|
const value = overrides[key];
|
|
954
1017
|
if (value !== void 0) {
|
|
955
1018
|
provider[key.replace("provider_", "")] = value;
|
|
956
1019
|
}
|
|
957
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
|
+
}
|
|
958
1027
|
}
|
|
959
1028
|
function apply_overrides(config, overrides) {
|
|
960
1029
|
for (const key of ["work_dir", "system_prompt", "session_dir", "log_level", "theme"]) {
|
|
@@ -1038,10 +1107,10 @@ async function run_one_shot(config, input) {
|
|
|
1038
1107
|
}
|
|
1039
1108
|
return 0;
|
|
1040
1109
|
}
|
|
1041
|
-
async function run_chat_turn(agent, input) {
|
|
1110
|
+
async function run_chat_turn(agent, input, history) {
|
|
1042
1111
|
const stop_progress = attach_progress(agent.events);
|
|
1043
1112
|
try {
|
|
1044
|
-
const result = await agent.run({ input });
|
|
1113
|
+
const result = await agent.run({ input, history });
|
|
1045
1114
|
const final = result.outcome.final;
|
|
1046
1115
|
if (final !== void 0 && final.content.length > 0) {
|
|
1047
1116
|
process.stdout.write(`${final.content}
|
|
@@ -1049,23 +1118,15 @@ async function run_chat_turn(agent, input) {
|
|
|
1049
1118
|
}
|
|
1050
1119
|
process.stdout.write(`[turns ${result.outcome.turns_used} | tokens ${result.usage_total.total_tokens}]
|
|
1051
1120
|
`);
|
|
1121
|
+
return result.messages;
|
|
1052
1122
|
} catch (error) {
|
|
1053
1123
|
process.stderr.write(`[lich] ${error_message(error)}
|
|
1054
1124
|
`);
|
|
1125
|
+
return [...history];
|
|
1055
1126
|
} finally {
|
|
1056
1127
|
stop_progress();
|
|
1057
1128
|
}
|
|
1058
1129
|
}
|
|
1059
|
-
function ask_line2(rl) {
|
|
1060
|
-
return new Promise((resolve) => {
|
|
1061
|
-
const on_close = () => resolve("");
|
|
1062
|
-
rl.question("> ", (answer) => {
|
|
1063
|
-
rl.removeListener("close", on_close);
|
|
1064
|
-
resolve(answer);
|
|
1065
|
-
});
|
|
1066
|
-
rl.once("close", on_close);
|
|
1067
|
-
});
|
|
1068
|
-
}
|
|
1069
1130
|
function budget_stderr_line(turns, notice) {
|
|
1070
1131
|
const flavor = notice_flavor(notice);
|
|
1071
1132
|
const suffix = flavor.length === 0 ? "" : ` \u2014 ${flavor}`;
|
|
@@ -1076,11 +1137,13 @@ async function run_chat(config) {
|
|
|
1076
1137
|
const agent = await create_agent_with_plugins(config);
|
|
1077
1138
|
const theme = load_theme(agent.config.theme);
|
|
1078
1139
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1140
|
+
let history = [];
|
|
1079
1141
|
try {
|
|
1080
|
-
|
|
1081
|
-
|
|
1142
|
+
process.stdout.write("> ");
|
|
1143
|
+
for await (const line of rl) {
|
|
1082
1144
|
if (line.trim() === "") {
|
|
1083
|
-
|
|
1145
|
+
process.stdout.write("> ");
|
|
1146
|
+
continue;
|
|
1084
1147
|
}
|
|
1085
1148
|
const command = line.trim();
|
|
1086
1149
|
if (command === "/exit" || command === "/quit") {
|
|
@@ -1088,8 +1151,10 @@ async function run_chat(config) {
|
|
|
1088
1151
|
`);
|
|
1089
1152
|
return 0;
|
|
1090
1153
|
}
|
|
1091
|
-
await run_chat_turn(agent, command);
|
|
1154
|
+
history = await run_chat_turn(agent, command, history);
|
|
1155
|
+
process.stdout.write("> ");
|
|
1092
1156
|
}
|
|
1157
|
+
return 0;
|
|
1093
1158
|
} finally {
|
|
1094
1159
|
rl.close();
|
|
1095
1160
|
agent.close();
|
|
@@ -1155,7 +1220,7 @@ async function run_bare(options) {
|
|
|
1155
1220
|
if (stopped !== void 0) {
|
|
1156
1221
|
return stopped;
|
|
1157
1222
|
}
|
|
1158
|
-
return run_tui_entry(build_config_for(options, "tui"));
|
|
1223
|
+
return run_tui_entry(build_config_for(options, "tui"), options.resume);
|
|
1159
1224
|
}
|
|
1160
1225
|
function model_still_placeholder(config) {
|
|
1161
1226
|
const providers = config["providers"];
|
|
@@ -1179,9 +1244,17 @@ function run_init(options) {
|
|
|
1179
1244
|
}
|
|
1180
1245
|
return 0;
|
|
1181
1246
|
}
|
|
1182
|
-
async function run_tui_entry(config) {
|
|
1183
|
-
const { run_tui } = await import("./tui-
|
|
1184
|
-
|
|
1247
|
+
async function run_tui_entry(config, resume) {
|
|
1248
|
+
const { run_tui } = await import("./tui-XXIU4W7K.js");
|
|
1249
|
+
if (resume === void 0) {
|
|
1250
|
+
return run_tui(config);
|
|
1251
|
+
}
|
|
1252
|
+
const { resolve_session_path } = await import("./resolve-H22TOVB5.js");
|
|
1253
|
+
const { read_session_messages } = await import("./store-YYLEB7JG.js");
|
|
1254
|
+
const transcript = await resolve_session_path(config.session_dir, resume);
|
|
1255
|
+
const initial_history = await read_session_messages(transcript);
|
|
1256
|
+
const resumed_id = path4.basename(transcript, ".jsonl");
|
|
1257
|
+
return run_tui(config, { initial_history, resumed_id });
|
|
1185
1258
|
}
|
|
1186
1259
|
function gateway_platforms(config, cli_platforms) {
|
|
1187
1260
|
if (cli_platforms.length > 0) {
|
|
@@ -1191,7 +1264,7 @@ function gateway_platforms(config, cli_platforms) {
|
|
|
1191
1264
|
return configured.length === 0 ? ["webhook"] : configured;
|
|
1192
1265
|
}
|
|
1193
1266
|
async function run_gateway_entry(config, platforms) {
|
|
1194
|
-
const { run_gateway } = await import("./gateway-
|
|
1267
|
+
const { run_gateway } = await import("./gateway-OSB7254F.js");
|
|
1195
1268
|
return run_gateway(config, gateway_platforms(config, platforms));
|
|
1196
1269
|
}
|
|
1197
1270
|
function is_cli_entry(input) {
|
|
@@ -1237,6 +1310,10 @@ async function run_cli(argv) {
|
|
|
1237
1310
|
return 0;
|
|
1238
1311
|
}
|
|
1239
1312
|
const [first] = options.positionals;
|
|
1313
|
+
const blocked_resume_mode = non_tui_resume_mode(first);
|
|
1314
|
+
if (blocked_resume_mode !== void 0) {
|
|
1315
|
+
reject_resume_outside_tui(options.resume, blocked_resume_mode);
|
|
1316
|
+
}
|
|
1240
1317
|
if (first === void 0) {
|
|
1241
1318
|
return run_bare(options);
|
|
1242
1319
|
}
|
|
@@ -1264,7 +1341,7 @@ async function run_cli(argv) {
|
|
|
1264
1341
|
return run_chat(build_config_for(options, first));
|
|
1265
1342
|
}
|
|
1266
1343
|
if (first === "tui") {
|
|
1267
|
-
return run_tui_entry(build_config_for(options, first));
|
|
1344
|
+
return run_tui_entry(build_config_for(options, first), options.resume);
|
|
1268
1345
|
}
|
|
1269
1346
|
if (first === "gateway") {
|
|
1270
1347
|
return run_gateway_entry(build_config_for(options, first), options.positionals.slice(1));
|
|
@@ -1283,6 +1360,7 @@ if (is_main_module() === true) {
|
|
|
1283
1360
|
}
|
|
1284
1361
|
export {
|
|
1285
1362
|
is_cli_entry,
|
|
1363
|
+
parse_args,
|
|
1286
1364
|
run_chat,
|
|
1287
1365
|
run_cli,
|
|
1288
1366
|
run_one_shot
|