hoi4-agent-tools 1.1.0 → 1.2.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 +12 -0
- package/README.md +21 -16
- package/dist/hoi4_agent_tools/core/engine.js +12 -0
- package/dist/hoi4_agent_tools/event/algorithms.js +208 -0
- package/dist/hoi4_agent_tools/event/artifact-validation.js +572 -0
- package/dist/hoi4_agent_tools/event/compare.js +413 -0
- package/dist/hoi4_agent_tools/event/graph.js +1586 -0
- package/dist/hoi4_agent_tools/event/index.js +10 -0
- package/dist/hoi4_agent_tools/event/layout.js +218 -0
- package/dist/hoi4_agent_tools/event/limits.js +41 -0
- package/dist/hoi4_agent_tools/event/model.js +2 -0
- package/dist/hoi4_agent_tools/event/queries.js +643 -0
- package/dist/hoi4_agent_tools/event/render.js +443 -0
- package/dist/hoi4_agent_tools/event/scc.js +72 -0
- package/dist/hoi4_agent_tools/event/service.js +1147 -0
- package/dist/hoi4_agent_tools/event/source-analysis.js +1458 -0
- package/dist/hoi4_agent_tools/focus/native-effects.js +3 -1
- package/dist/hoi4_agent_tools/mcp/resources/register.js +1 -1
- package/dist/hoi4_agent_tools/mcp/server/create.js +4 -0
- package/dist/hoi4_agent_tools/mcp/server/result.js +8 -14
- package/dist/hoi4_agent_tools/mcp/tools/event.js +459 -0
- package/dist/hoi4_agent_tools/schemas/event.js +95 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/development.md +10 -0
- package/docs/events.md +113 -0
- package/docs/setup.md +3 -3
- package/package.json +7 -3
- package/server.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.0 - 2026-07-13
|
|
4
|
+
|
|
5
|
+
- Added a read-only Event Chain Viewer for scanning, tracing, explaining, linting, rendering, and comparing large HOI4 event chains.
|
|
6
|
+
- Kept the event surface to three tools with seven inspect modes and linked authoritative graph artifacts, bringing the public surface to thirteen tools within the fixed discovery budget.
|
|
7
|
+
- Added event workflow documentation, package and Registry metadata, and project-owned acceptance coverage for more than 300 event definitions.
|
|
8
|
+
- Reconfirmed MCP protocol `2025-11-25`, TypeScript SDK 1.29.0, Registry schema `2025-12-11`, and Registry publisher 1.7.9 for this release.
|
|
9
|
+
|
|
10
|
+
## 1.1.1 - 2026-07-13
|
|
11
|
+
|
|
12
|
+
- Replaced the repository About text with a direct description of the focus-tree, GUI, and map tools.
|
|
13
|
+
- Simplified the README introduction and HTTP summary so operational safeguards remain in their dedicated documentation.
|
|
14
|
+
|
|
3
15
|
## 1.1.0 - 2026-07-12
|
|
4
16
|
|
|
5
17
|
- Added plan-free `compact` rewrites for existing national focus trees while retaining authored mode for complete plans and new trees.
|
package/README.md
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
# HOI4 Agent Tools
|
|
2
2
|
|
|
3
|
-
HOI4 Agent Tools is an MCP server
|
|
3
|
+
HOI4 Agent Tools is an MCP server for coding agents to analyze Hearts of Iron IV event chains and inspect, create, or clean up focus trees, scripted GUIs, and maps. It works directly with configured mod folders and produces offline renders for review.
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
7
|
- Focus trees: inspect structure and references, render layouts, create trees, and reorganize existing branches.
|
|
8
8
|
- Scripted GUIs: trace GUI, GFX, scripted-GUI, and localisation links; render states; create or repair interface source.
|
|
9
9
|
- Maps: inspect provinces, states, regions, adjacency, supply, and railways; render layers; create and repair exact map data.
|
|
10
|
+
- Event chains: scan definitions and call sites, trace routes and state flow, lint references, render graphs, and compare revisions without editing event source.
|
|
10
11
|
|
|
11
12
|
## Setup
|
|
12
13
|
|
|
13
14
|
Requires Node.js 22 or 24.
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
|
-
npm install --global hoi4-agent-tools@1.
|
|
17
|
+
npm install --global hoi4-agent-tools@1.2.0
|
|
17
18
|
hoi4-agent-tools-setup --init
|
|
18
19
|
```
|
|
19
20
|
|
|
@@ -29,18 +30,21 @@ Paste the printed Codex or generic global-install entry into your MCP client, th
|
|
|
29
30
|
|
|
30
31
|
## Tools
|
|
31
32
|
|
|
32
|
-
| Tool | Purpose
|
|
33
|
-
| -------------------- |
|
|
34
|
-
| `hoi4.mods` | List the writable mods available to the coding agent.
|
|
35
|
-
| `hoi4.focus_inspect` | Read focus trees and report structural or reference problems.
|
|
36
|
-
| `hoi4.focus_render` | Render an existing focus tree or continuous palette.
|
|
37
|
-
| `hoi4.focus_rewrite` | Create or update a focus tree.
|
|
38
|
-
| `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic.
|
|
39
|
-
| `hoi4.gui_render` | Render GUI states and resolutions offline.
|
|
40
|
-
| `hoi4.gui_rewrite` | Create or update a GUI source package.
|
|
41
|
-
| `hoi4.map_inspect` | Read map, state, province, region, supply, and railway data.
|
|
42
|
-
| `hoi4.map_render` | Render map layers and overlays.
|
|
43
|
-
| `hoi4.map_rewrite` | Create or update map data from an ordered list of exact changes.
|
|
33
|
+
| Tool | Purpose |
|
|
34
|
+
| -------------------- | --------------------------------------------------------------------------------------- |
|
|
35
|
+
| `hoi4.mods` | List the writable mods available to the coding agent. |
|
|
36
|
+
| `hoi4.focus_inspect` | Read focus trees and report structural or reference problems. |
|
|
37
|
+
| `hoi4.focus_render` | Render an existing focus tree or continuous palette. |
|
|
38
|
+
| `hoi4.focus_rewrite` | Create or update a focus tree. |
|
|
39
|
+
| `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic. |
|
|
40
|
+
| `hoi4.gui_render` | Render GUI states and resolutions offline. |
|
|
41
|
+
| `hoi4.gui_rewrite` | Create or update a GUI source package. |
|
|
42
|
+
| `hoi4.map_inspect` | Read map, state, province, region, supply, and railway data. |
|
|
43
|
+
| `hoi4.map_render` | Render map layers and overlays. |
|
|
44
|
+
| `hoi4.map_rewrite` | Create or update map data from an ordered list of exact changes. |
|
|
45
|
+
| `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
|
|
46
|
+
| `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
|
|
47
|
+
| `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
|
|
44
48
|
|
|
45
49
|
Large outputs are linked `hoi4-agent://` resources. For resources over 1 MiB, follow the `continuationUri` returned in `_meta` until it is `null`; clients may also request byte ranges with `?offset=<bytes>&length=<bytes>`.
|
|
46
50
|
|
|
@@ -48,7 +52,7 @@ Large outputs are linked `hoi4-agent://` resources. For resources over 1 MiB, fo
|
|
|
48
52
|
|
|
49
53
|
HOI4 Agent Tools provides HOI4 domain operations; it does not register MCP prompts, replace repository instructions such as `AGENTS.md`, manage skills or plans, or start subagents. The coding agent decides when to call it as part of its existing workflow.
|
|
50
54
|
|
|
51
|
-
Connecting and listing tools does not scan mod source. Compact tool schemas and linked resources keep large diagnostics, renders, and diffs out of the agent's working context until needed.
|
|
55
|
+
Connecting and listing tools does not scan mod source. Compact tool schemas and linked resources keep large diagnostics, renders, and diffs out of the agent's working context until needed. The event tools are read-only; only `hoi4.*_rewrite` calls edit mod source.
|
|
52
56
|
|
|
53
57
|
## Create or clean content
|
|
54
58
|
|
|
@@ -57,10 +61,11 @@ Ask your agent in normal task language. A typical workflow is inspect, render, r
|
|
|
57
61
|
- Focus trees: "Create a complete national focus tree for this route specification," or "Compact this existing tree into a balanced, readable layout." Existing trees can use a plan-free compact reflow; new trees use a complete plan. See [Focus trees](docs/focus.md).
|
|
58
62
|
- Scripted GUIs: "Create a scripted GUI for this mechanic," or "Render this window at common resolutions and fix clipping, missing assets, and click-region conflicts." See [Scripted GUIs](docs/gui.md).
|
|
59
63
|
- Maps: "Create a state from these exact provinces," or "Inspect this state and split these provinces while keeping supply and railway references valid." See [Maps](docs/map.md).
|
|
64
|
+
- Event chains: "Trace every route from this event and explain where its flags and variables change," or "Compare the workspace event graph with its previous revision and render the affected routes." See [Event chains](docs/events.md).
|
|
60
65
|
|
|
61
66
|
## HTTP
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
Use stdio for local MCP clients. For shared or remote deployments, see [HTTP](docs/http.md).
|
|
64
69
|
|
|
65
70
|
## Development
|
|
66
71
|
|
|
@@ -33,7 +33,15 @@ function defaultPatterns(workspace) {
|
|
|
33
33
|
'common/characters/**/*.txt',
|
|
34
34
|
'common/scripted_effects/**/*.txt',
|
|
35
35
|
'common/scripted_triggers/**/*.txt',
|
|
36
|
+
'common/on_actions/**/*.txt',
|
|
37
|
+
'common/operations/**/*.txt',
|
|
38
|
+
'common/raids/**/*.txt',
|
|
39
|
+
'common/bop/**/*.txt',
|
|
40
|
+
'common/resistance_compliance_modifiers/**/*.txt',
|
|
41
|
+
'common/special_projects/**/*.txt',
|
|
36
42
|
'events/**/*.txt',
|
|
43
|
+
'history/countries/**/*.txt',
|
|
44
|
+
'history/states/**/*.txt',
|
|
37
45
|
...under(roots.states, '**/*.txt'),
|
|
38
46
|
// Binary rasters and positional data tables are domain-selected by Agent
|
|
39
47
|
// Nudger. The shared symbol scan retains only map sources it can index.
|
|
@@ -183,6 +191,10 @@ export class CoreEngine {
|
|
|
183
191
|
flight.controller.abort();
|
|
184
192
|
}
|
|
185
193
|
}
|
|
194
|
+
/** Monotonic cache generation used by domain services to invalidate derived snapshots. */
|
|
195
|
+
generation(workspaceId) {
|
|
196
|
+
return this.#scanGenerations.get(workspaceId) ?? 0;
|
|
197
|
+
}
|
|
186
198
|
awaitScanFlight(requestKey, flight, signal) {
|
|
187
199
|
signal?.throwIfAborted();
|
|
188
200
|
flight.waiters += 1;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { compareCodeUnits } from '../core/canonical.js';
|
|
2
|
+
import { EventAnalysisBudget } from './limits.js';
|
|
3
|
+
import { iterativeStronglyConnectedComponents } from './scc.js';
|
|
4
|
+
function sortedUnique(values) {
|
|
5
|
+
return [...new Set(values)].sort(compareCodeUnits);
|
|
6
|
+
}
|
|
7
|
+
function nodeMap(graph) {
|
|
8
|
+
return new Map(graph.nodes.map((node) => [node.id, node]));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Select either the structural helper graph or its event-to-event projection.
|
|
12
|
+
* Direct, option, entry, unresolved, and terminal edges remain visible in both.
|
|
13
|
+
*/
|
|
14
|
+
export function eventFlowEdges(graph, expandHelpers) {
|
|
15
|
+
const nodes = nodeMap(graph);
|
|
16
|
+
return graph.edges
|
|
17
|
+
.filter((edge) => {
|
|
18
|
+
if (expandHelpers)
|
|
19
|
+
return !edge.derived;
|
|
20
|
+
if (edge.derived)
|
|
21
|
+
return true;
|
|
22
|
+
const from = nodes.get(edge.from);
|
|
23
|
+
const to = nodes.get(edge.to);
|
|
24
|
+
return from?.kind !== 'helper' && to?.kind !== 'helper';
|
|
25
|
+
})
|
|
26
|
+
.sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
27
|
+
}
|
|
28
|
+
function edgeMaps(edges) {
|
|
29
|
+
const outgoing = new Map();
|
|
30
|
+
const incoming = new Map();
|
|
31
|
+
for (const edge of edges) {
|
|
32
|
+
const from = outgoing.get(edge.from) ?? [];
|
|
33
|
+
from.push(edge);
|
|
34
|
+
outgoing.set(edge.from, from);
|
|
35
|
+
const to = incoming.get(edge.to) ?? [];
|
|
36
|
+
to.push(edge);
|
|
37
|
+
incoming.set(edge.to, to);
|
|
38
|
+
}
|
|
39
|
+
for (const values of [...outgoing.values(), ...incoming.values()]) {
|
|
40
|
+
values.sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
41
|
+
}
|
|
42
|
+
return { outgoing, incoming };
|
|
43
|
+
}
|
|
44
|
+
function edgesForDirection(nodeId, direction, maps) {
|
|
45
|
+
const results = [];
|
|
46
|
+
if (direction !== 'upstream') {
|
|
47
|
+
for (const edge of maps.outgoing.get(nodeId) ?? [])
|
|
48
|
+
results.push({ edge, next: edge.to });
|
|
49
|
+
}
|
|
50
|
+
if (direction !== 'downstream') {
|
|
51
|
+
for (const edge of maps.incoming.get(nodeId) ?? [])
|
|
52
|
+
results.push({ edge, next: edge.from });
|
|
53
|
+
}
|
|
54
|
+
return results.sort((left, right) => compareCodeUnits(left.next, right.next) || compareCodeUnits(left.edge.id, right.edge.id));
|
|
55
|
+
}
|
|
56
|
+
export function traceEventGraph(graph, startNodeIds, boundary, signal) {
|
|
57
|
+
signal?.throwIfAborted();
|
|
58
|
+
const byId = nodeMap(graph);
|
|
59
|
+
const resolvedStarts = sortedUnique(startNodeIds).filter((id) => byId.has(id));
|
|
60
|
+
const starts = resolvedStarts.slice(0, Math.max(0, boundary.maxNodes));
|
|
61
|
+
const edges = eventFlowEdges(graph, boundary.expandHelpers);
|
|
62
|
+
const maps = edgeMaps(edges);
|
|
63
|
+
const depthByNode = new Map(starts.map((id) => [id, 0]));
|
|
64
|
+
const queue = [...starts];
|
|
65
|
+
const retainedEdges = new Map();
|
|
66
|
+
const boundaryNodes = new Set(resolvedStarts.length > starts.length ? starts : []);
|
|
67
|
+
let truncated = resolvedStarts.length > starts.length;
|
|
68
|
+
let cursor = 0;
|
|
69
|
+
while (cursor < queue.length) {
|
|
70
|
+
if ((cursor & 255) === 0)
|
|
71
|
+
signal?.throwIfAborted();
|
|
72
|
+
const current = queue[cursor++];
|
|
73
|
+
const depth = depthByNode.get(current) ?? 0;
|
|
74
|
+
const nextEdges = edgesForDirection(current, boundary.direction, maps);
|
|
75
|
+
if (depth >= boundary.maxDepth) {
|
|
76
|
+
if (nextEdges.length > 0) {
|
|
77
|
+
boundaryNodes.add(current);
|
|
78
|
+
truncated = true;
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
for (const candidate of nextEdges) {
|
|
83
|
+
if (retainedEdges.size >= boundary.maxEdges) {
|
|
84
|
+
boundaryNodes.add(current);
|
|
85
|
+
truncated = true;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
const known = depthByNode.has(candidate.next);
|
|
89
|
+
if (!known && depthByNode.size >= boundary.maxNodes) {
|
|
90
|
+
boundaryNodes.add(current);
|
|
91
|
+
truncated = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
retainedEdges.set(candidate.edge.id, candidate.edge);
|
|
95
|
+
if (!known) {
|
|
96
|
+
depthByNode.set(candidate.next, depth + 1);
|
|
97
|
+
queue.push(candidate.next);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const retainedNodes = [...depthByNode.keys()]
|
|
102
|
+
.flatMap((id) => {
|
|
103
|
+
const node = byId.get(id);
|
|
104
|
+
return node === undefined ? [] : [node];
|
|
105
|
+
})
|
|
106
|
+
.sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
107
|
+
const layers = [...new Set(depthByNode.values())]
|
|
108
|
+
.sort((left, right) => left - right)
|
|
109
|
+
.map((depth) => ({
|
|
110
|
+
depth,
|
|
111
|
+
nodeIds: [...depthByNode.entries()]
|
|
112
|
+
.filter(([, value]) => value === depth)
|
|
113
|
+
.map(([id]) => id)
|
|
114
|
+
.sort(compareCodeUnits),
|
|
115
|
+
}));
|
|
116
|
+
return {
|
|
117
|
+
startNodeIds: starts,
|
|
118
|
+
nodes: retainedNodes,
|
|
119
|
+
edges: [...retainedEdges.values()].sort((left, right) => compareCodeUnits(left.id, right.id)),
|
|
120
|
+
layers,
|
|
121
|
+
boundary,
|
|
122
|
+
boundaryNodeIds: [...boundaryNodes].sort(compareCodeUnits),
|
|
123
|
+
truncated,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export function shortestEventPath(graph, fromNodeIds, toNodeIds, options, signal) {
|
|
127
|
+
signal?.throwIfAborted();
|
|
128
|
+
const nodes = nodeMap(graph);
|
|
129
|
+
const starts = sortedUnique(fromNodeIds).filter((id) => nodes.has(id));
|
|
130
|
+
const targets = new Set(toNodeIds.filter((id) => nodes.has(id)));
|
|
131
|
+
const maps = edgeMaps(eventFlowEdges(graph, options.expandHelpers));
|
|
132
|
+
const queue = starts.map((nodeId) => ({ nodeId, depth: 0 }));
|
|
133
|
+
const visited = new Set(starts);
|
|
134
|
+
const predecessor = new Map();
|
|
135
|
+
let cursor = 0;
|
|
136
|
+
let found = starts.find((id) => targets.has(id));
|
|
137
|
+
while (found === undefined && cursor < queue.length) {
|
|
138
|
+
if ((cursor & 255) === 0)
|
|
139
|
+
signal?.throwIfAborted();
|
|
140
|
+
const current = queue[cursor++];
|
|
141
|
+
if (current.depth >= options.maxDepth)
|
|
142
|
+
continue;
|
|
143
|
+
for (const edge of maps.outgoing.get(current.nodeId) ?? []) {
|
|
144
|
+
if (visited.has(edge.to))
|
|
145
|
+
continue;
|
|
146
|
+
if (visited.size >= options.maxNodes)
|
|
147
|
+
return undefined;
|
|
148
|
+
visited.add(edge.to);
|
|
149
|
+
predecessor.set(edge.to, { nodeId: current.nodeId, edge });
|
|
150
|
+
if (targets.has(edge.to)) {
|
|
151
|
+
found = edge.to;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
queue.push({ nodeId: edge.to, depth: current.depth + 1 });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (found === undefined)
|
|
158
|
+
return undefined;
|
|
159
|
+
const nodeIds = [found];
|
|
160
|
+
const pathEdges = [];
|
|
161
|
+
let cursorNode = found;
|
|
162
|
+
while (!starts.includes(cursorNode)) {
|
|
163
|
+
const previous = predecessor.get(cursorNode);
|
|
164
|
+
if (previous === undefined)
|
|
165
|
+
return undefined;
|
|
166
|
+
pathEdges.push(previous.edge);
|
|
167
|
+
cursorNode = previous.nodeId;
|
|
168
|
+
nodeIds.push(cursorNode);
|
|
169
|
+
}
|
|
170
|
+
nodeIds.reverse();
|
|
171
|
+
pathEdges.reverse();
|
|
172
|
+
return { nodeIds, edges: pathEdges };
|
|
173
|
+
}
|
|
174
|
+
/** Stable, bounded iterative Tarjan SCCs over the selected flow projection. */
|
|
175
|
+
export function eventStronglyConnectedComponents(graph, expandHelpers = false, selectedNodeIds, signal) {
|
|
176
|
+
signal?.throwIfAborted();
|
|
177
|
+
const retainedNodes = graph.nodes
|
|
178
|
+
.filter((node) => selectedNodeIds === undefined || selectedNodeIds.has(node.id))
|
|
179
|
+
.map(({ id }) => id)
|
|
180
|
+
.sort(compareCodeUnits);
|
|
181
|
+
const retained = new Set(retainedNodes);
|
|
182
|
+
const edges = eventFlowEdges(graph, expandHelpers).filter(({ from, to }) => retained.has(from) && retained.has(to));
|
|
183
|
+
const outgoing = edgeMaps(edges).outgoing;
|
|
184
|
+
const adjacency = new Map([...outgoing].map(([nodeId, values]) => [nodeId, values.map(({ to }) => to)]));
|
|
185
|
+
const budget = new EventAnalysisBudget(signal);
|
|
186
|
+
const components = iterativeStronglyConnectedComponents(retainedNodes, adjacency, () => budget.spend('strongly_connected_components'));
|
|
187
|
+
budget.check();
|
|
188
|
+
return components
|
|
189
|
+
.map((nodeIds) => {
|
|
190
|
+
const members = new Set(nodeIds);
|
|
191
|
+
const selfLoop = edges.some(({ from, to }) => from === to && members.has(from));
|
|
192
|
+
return {
|
|
193
|
+
id: `scc:${nodeIds.join('|')}`,
|
|
194
|
+
nodeIds,
|
|
195
|
+
cyclic: nodeIds.length > 1 || selfLoop,
|
|
196
|
+
};
|
|
197
|
+
})
|
|
198
|
+
.sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
199
|
+
}
|
|
200
|
+
export function reachableEventNodeIds(graph, startNodeIds, direction, expandHelpers = false, signal) {
|
|
201
|
+
return traceEventGraph(graph, startNodeIds, {
|
|
202
|
+
maxDepth: Number.MAX_SAFE_INTEGER,
|
|
203
|
+
maxNodes: Math.max(1, graph.nodes.length),
|
|
204
|
+
maxEdges: Math.max(1, graph.edges.length),
|
|
205
|
+
direction,
|
|
206
|
+
expandHelpers,
|
|
207
|
+
}, signal).nodes.map(({ id }) => id);
|
|
208
|
+
}
|