@moikapy/lich 0.4.0 → 0.5.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 +24 -0
- package/dist/{chunk-MLFJW4JU.js → chunk-CV2YH3FH.js} +51 -2
- package/dist/chunk-CV2YH3FH.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +590 -25
- 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 +33 -0
- package/dist/index.js +1 -1
- package/dist/{tui-VYBJSGRV.js → tui-DT7XWDTX.js} +8 -5
- package/dist/tui-DT7XWDTX.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 +2 -1
- package/dist/chunk-MLFJW4JU.js.map +0 -1
- package/dist/gateway-XTYDYT67.js.map +0 -1
- package/dist/tui-VYBJSGRV.js.map +0 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { append_jsonl, ensure_game_dir, tool_failure } from "./bridge_io.mjs";
|
|
2
|
+
import { ORDERS_FILE, game_file } from "./bridge_paths.mjs";
|
|
3
|
+
import { read_snapshot_round } from "./snapshot.mjs";
|
|
4
|
+
import { enemy_actions_schema } from "./schemas.mjs";
|
|
5
|
+
import { normalize_actions, validate_enemy_actions } from "./validate_order.mjs";
|
|
6
|
+
|
|
7
|
+
export const enemy_actions_tool = {
|
|
8
|
+
name: "enemy_actions",
|
|
9
|
+
description: "Queue every enemy action for one combat round. Godot drains the order next tick.",
|
|
10
|
+
parameters: enemy_actions_schema,
|
|
11
|
+
execute: queue_enemy_actions,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
async function queue_enemy_actions(args, context) {
|
|
15
|
+
const problem = validate_enemy_actions(args);
|
|
16
|
+
if (problem !== undefined) {
|
|
17
|
+
return { ok: false, output: "", error: problem };
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
await ensure_game_dir(context.work_dir);
|
|
21
|
+
const actions = normalize_actions(args.actions);
|
|
22
|
+
const written = await append_jsonl(game_file(context.work_dir, ORDERS_FILE), {
|
|
23
|
+
ts: new Date().toISOString(),
|
|
24
|
+
round: args.round,
|
|
25
|
+
actions,
|
|
26
|
+
rationale: args.rationale,
|
|
27
|
+
});
|
|
28
|
+
if (written.ok === false) {
|
|
29
|
+
return { ok: false, output: "", error: written.error };
|
|
30
|
+
}
|
|
31
|
+
const snapshot_round = await read_snapshot_round(context.work_dir);
|
|
32
|
+
return { ok: true, output: format_count(actions.length, args.round, snapshot_round) };
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return tool_failure(error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function format_count(count, round, snapshot_round) {
|
|
39
|
+
const base = `appended ${count} orders for round ${round}`;
|
|
40
|
+
if (snapshot_round === undefined || snapshot_round === round) {
|
|
41
|
+
return base;
|
|
42
|
+
}
|
|
43
|
+
return `${base}; snapshot_round_mismatch: snapshot=${snapshot_round}`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combat-commander bridge. Tools write `.lich/game/` files; Godot drains them.
|
|
3
|
+
* Plain `.mjs` so Node (>=20) loads it from config.plugins with no type-strip.
|
|
4
|
+
*/
|
|
5
|
+
import { dungeon_memory_read_tool, dungeon_memory_write_tool } from "./dungeon_memory.mjs";
|
|
6
|
+
import { enemy_actions_tool } from "./enemy_actions.mjs";
|
|
7
|
+
import { meteor_veto } from "./meteor_veto.mjs";
|
|
8
|
+
|
|
9
|
+
const game_bridge = {
|
|
10
|
+
name: "game_bridge",
|
|
11
|
+
tools: [enemy_actions_tool, dungeon_memory_read_tool, dungeon_memory_write_tool],
|
|
12
|
+
hooks: {
|
|
13
|
+
before_tool_call: meteor_veto,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default game_bridge;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const METEOR_GATE_ROUND = 3;
|
|
2
|
+
const METEOR_REASON = "meteor_gates_closed_until_round_3";
|
|
3
|
+
|
|
4
|
+
export async function meteor_veto(info) {
|
|
5
|
+
if (info.tool_name !== "enemy_actions") {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const round = Number(info.args.round ?? 0);
|
|
9
|
+
const actions = Array.isArray(info.args.actions) ? info.args.actions : [];
|
|
10
|
+
if (round < METEOR_GATE_ROUND && uses_meteor(actions) === true) {
|
|
11
|
+
return { block: true, reason: METEOR_REASON };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function uses_meteor(actions) {
|
|
16
|
+
return actions.some((action) => {
|
|
17
|
+
if (typeof action !== "object" || action === null) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return String(action.action).includes("meteor");
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const enemy_actions_schema = {
|
|
2
|
+
type: "object",
|
|
3
|
+
properties: {
|
|
4
|
+
round: {
|
|
5
|
+
type: "number",
|
|
6
|
+
description: "Current combat round number (from the battle snapshot).",
|
|
7
|
+
},
|
|
8
|
+
actions: {
|
|
9
|
+
type: "array",
|
|
10
|
+
items: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
enemy_id: { type: "string", description: "Acting enemy's id." },
|
|
14
|
+
action: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Ability id from the enemy's kit, or the literal move: attack|defend|flee.",
|
|
17
|
+
},
|
|
18
|
+
target_ref: { type: "string", description: "Target reference: hero:<id> or enemy:<id>." },
|
|
19
|
+
},
|
|
20
|
+
required: ["enemy_id", "action", "target_ref"],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
rationale: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "One-line explanation of the round plan; doubles as the replayable combat log entry.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
required: ["round", "actions", "rationale"],
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const dungeon_memory_read_schema = {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {},
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const dungeon_memory_write_schema = {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
note: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Durable cross-run observation about the player; appended to dungeon memory.",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
required: ["note"],
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { STATE_FILE, game_file } from "./bridge_paths.mjs";
|
|
3
|
+
|
|
4
|
+
/** Godot may refresh state.json; a missing or partial snapshot is not an error. */
|
|
5
|
+
export async function read_snapshot_round(work_dir) {
|
|
6
|
+
try {
|
|
7
|
+
const raw = await readFile(game_file(work_dir, STATE_FILE), "utf8");
|
|
8
|
+
const parsed = JSON.parse(raw);
|
|
9
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
if (typeof parsed.round !== "number" || Number.isFinite(parsed.round) === false) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
return parsed.round;
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const TARGET_REF = /^(hero|enemy):.+$/;
|
|
2
|
+
|
|
3
|
+
export function validate_enemy_actions(args) {
|
|
4
|
+
if (typeof args.round !== "number" || Number.isFinite(args.round) === false) {
|
|
5
|
+
return "round_must_be_number";
|
|
6
|
+
}
|
|
7
|
+
if (typeof args.rationale !== "string" || args.rationale.length === 0) {
|
|
8
|
+
return "rationale_required";
|
|
9
|
+
}
|
|
10
|
+
if (Array.isArray(args.actions) === false) {
|
|
11
|
+
return "actions_must_be_array";
|
|
12
|
+
}
|
|
13
|
+
for (const action of args.actions) {
|
|
14
|
+
const problem = validate_action(action);
|
|
15
|
+
if (problem !== undefined) {
|
|
16
|
+
return problem;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function normalize_actions(actions) {
|
|
23
|
+
return actions.map((action) => ({
|
|
24
|
+
enemy_id: action.enemy_id,
|
|
25
|
+
action: action.action,
|
|
26
|
+
target_ref: action.target_ref,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validate_action(action) {
|
|
31
|
+
if (typeof action !== "object" || action === null || Array.isArray(action)) {
|
|
32
|
+
return "action_must_be_object";
|
|
33
|
+
}
|
|
34
|
+
if (typeof action.enemy_id !== "string" || action.enemy_id.length === 0) {
|
|
35
|
+
return "enemy_id_required";
|
|
36
|
+
}
|
|
37
|
+
if (typeof action.action !== "string" || action.action.length === 0) {
|
|
38
|
+
return "action_required";
|
|
39
|
+
}
|
|
40
|
+
if (typeof action.target_ref !== "string" || TARGET_REF.test(action.target_ref) === false) {
|
|
41
|
+
return "target_ref_must_be_hero_or_enemy";
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moikapy/lich",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Lich — a TypeScript AI agent harness (library + CLI) inspired by Hermes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"docs",
|
|
22
22
|
"!docs/.vitepress/dist",
|
|
23
23
|
"!docs/.vitepress/cache",
|
|
24
|
+
"examples/game_bridge",
|
|
24
25
|
"README.md",
|
|
25
26
|
"CHANGELOG.md"
|
|
26
27
|
],
|