hoi4-agent-tools 2.1.1 → 2.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 +9 -0
- package/README.md +7 -2
- package/dist/bin/stdio.js +6 -1
- package/dist/hoi4_agent_tools/core/engine.js +9 -0
- package/dist/hoi4_agent_tools/core/index.js +292 -1
- package/dist/hoi4_agent_tools/focus/render.js +37 -13
- package/dist/hoi4_agent_tools/mcp/server/create.js +3 -1
- package/dist/hoi4_agent_tools/mcp/server/result.js +4 -4
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +530 -0
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +39 -2
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +368 -0
- package/dist/hoi4_agent_tools/schemas/technology.js +77 -0
- package/dist/hoi4_agent_tools/technology/compare.js +240 -0
- package/dist/hoi4_agent_tools/technology/graph.js +1196 -0
- package/dist/hoi4_agent_tools/technology/index.js +7 -0
- package/dist/hoi4_agent_tools/technology/model.js +2 -0
- package/dist/hoi4_agent_tools/technology/queries.js +300 -0
- package/dist/hoi4_agent_tools/technology/render.js +641 -0
- package/dist/hoi4_agent_tools/technology/service.js +686 -0
- package/dist/hoi4_agent_tools/technology/source-analysis.js +967 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/development.md +5 -1
- package/docs/setup.md +1 -1
- package/docs/technology.md +90 -0
- package/package.json +12 -3
- package/server.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0 - 2026-07-22
|
|
4
|
+
|
|
5
|
+
- Added a read-only Technology Tree Viewer for classic technologies, legacy and current doctrines, source folder layouts, prerequisites, exclusive choices, categories, tags, unlocks, bonuses, grants, metadata, localisation, icons, and cross-file references.
|
|
6
|
+
- Added `hoi4.tech_inspect`, `hoi4.tech_render`, and `hoi4.tech_compare`, with complete graph resources, bounded large-tree overviews, focused folder renders, in-memory source comparison, and rename or removal impact analysis.
|
|
7
|
+
- Added source-linked diagnostic classification for structural, placement, reference, unlock, bonus, doctrine, AI, localisation, icon, and unresolved static-analysis findings.
|
|
8
|
+
- Added a deterministic 1,040-technology acceptance workspace with 13 folders, expected graph and reference manifests, stable SVG and PNG evidence, cancellation, incremental indexing, resource retrieval, and stdio, secured Streamable HTTP, package, and agent-workflow coverage.
|
|
9
|
+
- Pinned patched transitive HTTP, URI, and shell parsing dependencies; the release dependency audit reports no known vulnerabilities.
|
|
10
|
+
- 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.
|
|
11
|
+
|
|
3
12
|
## 2.1.1 - 2026-07-18
|
|
4
13
|
|
|
5
14
|
- Updated container attestation verification for current in-toto `Statement/v1` documents emitted by BuildKit.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# HOI4 Agent Tools
|
|
2
2
|
|
|
3
|
-
HOI4 Agent Tools is an MCP server for coding agents to
|
|
3
|
+
HOI4 Agent Tools is an MCP server for coding agents to understand Hearts of Iron IV event chains and technology systems and to inspect, create, or clean up focus trees, scripted GUIs, and maps. Start it in a mod folder and it works there immediately, with offline renders for review.
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
@@ -8,6 +8,7 @@ HOI4 Agent Tools is an MCP server for coding agents to analyze Hearts of Iron IV
|
|
|
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
10
|
- Event chains: scan definitions and call sites, trace routes and state flow, lint references, render graphs, and compare revisions without editing event source.
|
|
11
|
+
- Technology trees: reconstruct technology and doctrine paths, folder layouts, unlocks, bonuses, grants, metadata, assets, and structural changes.
|
|
11
12
|
|
|
12
13
|
## Setup
|
|
13
14
|
|
|
@@ -44,6 +45,9 @@ Paste the printed Codex or generic global-install entry into your MCP client, th
|
|
|
44
45
|
| `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
|
|
45
46
|
| `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
|
|
46
47
|
| `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
|
|
48
|
+
| `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems. |
|
|
49
|
+
| `hoi4.tech_render` | Render source layouts, dependency paths, unlocks, grants, metadata, and asset coverage. |
|
|
50
|
+
| `hoi4.tech_compare` | Compare technology graphs, placements, references, diagnostics, and source overlays. |
|
|
47
51
|
|
|
48
52
|
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>`.
|
|
49
53
|
|
|
@@ -51,7 +55,7 @@ Large outputs are linked `hoi4-agent://` resources. For resources over 1 MiB, fo
|
|
|
51
55
|
|
|
52
56
|
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.
|
|
53
57
|
|
|
54
|
-
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.
|
|
58
|
+
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. Event and technology tools analyze source without editing it; only `hoi4.*_rewrite` calls edit mod source.
|
|
55
59
|
|
|
56
60
|
## Create or clean content
|
|
57
61
|
|
|
@@ -61,6 +65,7 @@ Ask your agent in normal task language. A typical workflow is inspect, render, r
|
|
|
61
65
|
- 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).
|
|
62
66
|
- 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).
|
|
63
67
|
- 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).
|
|
68
|
+
- Technology trees: "Explain everything this technology requires and unlocks," or "Compare this technology patch and render every affected folder and doctrine branch." See [Technology trees](docs/technology.md).
|
|
64
69
|
|
|
65
70
|
## HTTP
|
|
66
71
|
|
package/dist/bin/stdio.js
CHANGED
|
@@ -6,7 +6,12 @@ import { FinalProtocolTransport } from '../hoi4_agent_tools/mcp/transports/proto
|
|
|
6
6
|
import { createEngine } from '../hoi4_agent_tools/runtime.js';
|
|
7
7
|
async function main() {
|
|
8
8
|
const engine = await createEngine();
|
|
9
|
-
const
|
|
9
|
+
const context = {};
|
|
10
|
+
const server = createMcpServer(engine, context);
|
|
11
|
+
if (process.env.HOI4_AGENT_TOOLS_CHAOSX === '1') {
|
|
12
|
+
const { registerChaosxTools } = await import('../hoi4_agent_tools/mcp/tools/chaosx.js');
|
|
13
|
+
registerChaosxTools(server, engine, context);
|
|
14
|
+
}
|
|
10
15
|
server.server.onerror = (error) => {
|
|
11
16
|
const fatalInputError = error instanceof StdioFrameLimitError || error instanceof StdioInvalidUtf8Error;
|
|
12
17
|
const rejectedMessage = error instanceof StdioInvalidMessageError;
|
|
@@ -39,6 +39,15 @@ function defaultPatterns(workspace) {
|
|
|
39
39
|
'common/bop/**/*.txt',
|
|
40
40
|
'common/resistance_compliance_modifiers/**/*.txt',
|
|
41
41
|
'common/special_projects/**/*.txt',
|
|
42
|
+
'common/technologies/**/*.txt',
|
|
43
|
+
'common/technology_tags/**/*.txt',
|
|
44
|
+
'common/technology_sharing/**/*.txt',
|
|
45
|
+
'common/doctrines/**/*.txt',
|
|
46
|
+
'common/units/**/*.txt',
|
|
47
|
+
'common/unit_tags/**/*.txt',
|
|
48
|
+
'common/buildings/**/*.txt',
|
|
49
|
+
'common/abilities/**/*.txt',
|
|
50
|
+
'common/combat_tactics.txt',
|
|
42
51
|
'events/**/*.txt',
|
|
43
52
|
'history/countries/**/*.txt',
|
|
44
53
|
'history/states/**/*.txt',
|
|
@@ -67,6 +67,36 @@ function possibleSymbolKinds(file, definitionFiles) {
|
|
|
67
67
|
if (sourcePath.endsWith('.gui'))
|
|
68
68
|
kinds.add('gui_element');
|
|
69
69
|
if (sourcePath.endsWith('.txt')) {
|
|
70
|
+
if (sourcePath.startsWith('common/technologies/'))
|
|
71
|
+
kinds.add('technology');
|
|
72
|
+
if (sourcePath.startsWith('common/technology_tags/')) {
|
|
73
|
+
kinds.add('technology_folder');
|
|
74
|
+
kinds.add('technology_category');
|
|
75
|
+
kinds.add('technology_tag');
|
|
76
|
+
}
|
|
77
|
+
if (sourcePath.startsWith('common/units/equipment/modules/'))
|
|
78
|
+
kinds.add('equipment_module');
|
|
79
|
+
else if (sourcePath.startsWith('common/units/equipment/'))
|
|
80
|
+
kinds.add('equipment');
|
|
81
|
+
else if (sourcePath.startsWith('common/units/'))
|
|
82
|
+
kinds.add('sub_unit');
|
|
83
|
+
if (sourcePath.startsWith('common/unit_tags/'))
|
|
84
|
+
kinds.add('unit_category');
|
|
85
|
+
if (sourcePath.startsWith('common/buildings/'))
|
|
86
|
+
kinds.add('building');
|
|
87
|
+
if (sourcePath.startsWith('common/abilities/'))
|
|
88
|
+
kinds.add('ability');
|
|
89
|
+
if (sourcePath === 'common/combat_tactics.txt' ||
|
|
90
|
+
sourcePath.startsWith('common/combat_tactics/'))
|
|
91
|
+
kinds.add('combat_tactic');
|
|
92
|
+
if (sourcePath.startsWith('common/doctrines/folders/'))
|
|
93
|
+
kinds.add('doctrine_folder');
|
|
94
|
+
if (sourcePath.startsWith('common/doctrines/grand_doctrines/'))
|
|
95
|
+
kinds.add('grand_doctrine');
|
|
96
|
+
if (sourcePath.startsWith('common/doctrines/tracks/'))
|
|
97
|
+
kinds.add('doctrine_track');
|
|
98
|
+
if (sourcePath.startsWith('common/doctrines/subdoctrines/'))
|
|
99
|
+
kinds.add('subdoctrine');
|
|
70
100
|
if (/(?:^|\/)focus(?:es)?(?:\/|$)|national_focus|continuous_focus/u.test(sourcePath)) {
|
|
71
101
|
kinds.add('focus_tree');
|
|
72
102
|
kinds.add('focus');
|
|
@@ -463,6 +493,267 @@ export class SymbolIndex {
|
|
|
463
493
|
metadata: {},
|
|
464
494
|
});
|
|
465
495
|
}
|
|
496
|
+
else if (sourcePath.startsWith('common/technologies/') &&
|
|
497
|
+
ancestors.length === 1 &&
|
|
498
|
+
ancestors[0] === 'technologies' &&
|
|
499
|
+
!key.startsWith('@')) {
|
|
500
|
+
this.addSymbol({
|
|
501
|
+
kind: 'technology',
|
|
502
|
+
id: key,
|
|
503
|
+
path: file.displayPath,
|
|
504
|
+
rootKind: file.rootKind,
|
|
505
|
+
loadOrder: file.loadOrder,
|
|
506
|
+
location: nodeLocation(document, assignment, key),
|
|
507
|
+
metadata: {
|
|
508
|
+
startYear: firstScalar(child, 'start_year')?.value,
|
|
509
|
+
researchCost: firstScalar(child, 'research_cost')?.value,
|
|
510
|
+
doctrine: firstScalar(child, 'doctrine')?.value === 'yes',
|
|
511
|
+
icon: `GFX_${key}_medium`,
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
for (const pathBlock of childBlocks(child, 'path')) {
|
|
515
|
+
const target = firstScalar(pathBlock, 'leads_to_tech');
|
|
516
|
+
if (target !== undefined)
|
|
517
|
+
this.addReference({
|
|
518
|
+
kind: 'technology_prerequisite',
|
|
519
|
+
from: key,
|
|
520
|
+
toKind: 'technology',
|
|
521
|
+
to: target.value,
|
|
522
|
+
path: file.displayPath,
|
|
523
|
+
location: nodeLocation(document, target, key),
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
for (const xor of childBlocks(child, 'xor'))
|
|
527
|
+
for (const target of scalars(xor))
|
|
528
|
+
this.addReference({
|
|
529
|
+
kind: 'technology_exclusive',
|
|
530
|
+
from: key,
|
|
531
|
+
toKind: 'technology',
|
|
532
|
+
to: target,
|
|
533
|
+
path: file.displayPath,
|
|
534
|
+
location: nodeLocation(document, xor, key),
|
|
535
|
+
});
|
|
536
|
+
for (const folder of childBlocks(child, 'folder')) {
|
|
537
|
+
const target = firstScalar(folder, 'name');
|
|
538
|
+
if (target !== undefined)
|
|
539
|
+
this.addReference({
|
|
540
|
+
kind: 'technology_folder',
|
|
541
|
+
from: key,
|
|
542
|
+
toKind: 'technology_folder',
|
|
543
|
+
to: target.value,
|
|
544
|
+
path: file.displayPath,
|
|
545
|
+
location: nodeLocation(document, target, key),
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
for (const categories of childBlocks(child, 'categories'))
|
|
549
|
+
for (const target of scalars(categories))
|
|
550
|
+
this.addReference({
|
|
551
|
+
kind: 'technology_category',
|
|
552
|
+
from: key,
|
|
553
|
+
toKind: 'technology_category',
|
|
554
|
+
to: target,
|
|
555
|
+
path: file.displayPath,
|
|
556
|
+
location: nodeLocation(document, categories, key),
|
|
557
|
+
});
|
|
558
|
+
for (const tags of childBlocks(child, 'tags'))
|
|
559
|
+
for (const target of scalars(tags))
|
|
560
|
+
this.addReference({
|
|
561
|
+
kind: 'technology_tag',
|
|
562
|
+
from: key,
|
|
563
|
+
toKind: 'technology_tag',
|
|
564
|
+
to: target,
|
|
565
|
+
path: file.displayPath,
|
|
566
|
+
location: nodeLocation(document, tags, key),
|
|
567
|
+
});
|
|
568
|
+
for (const [field, targetKind] of [
|
|
569
|
+
['enable_equipments', 'equipment'],
|
|
570
|
+
['enable_equipment_modules', 'equipment_module'],
|
|
571
|
+
['enable_subunits', 'sub_unit'],
|
|
572
|
+
])
|
|
573
|
+
for (const unlockBlock of childBlocks(child, field))
|
|
574
|
+
for (const target of scalars(unlockBlock))
|
|
575
|
+
this.addReference({
|
|
576
|
+
kind: `technology_${field}`,
|
|
577
|
+
from: key,
|
|
578
|
+
toKind: targetKind,
|
|
579
|
+
to: target,
|
|
580
|
+
path: file.displayPath,
|
|
581
|
+
location: nodeLocation(document, unlockBlock, key),
|
|
582
|
+
});
|
|
583
|
+
for (const building of childBlocks(child, 'enable_building')) {
|
|
584
|
+
const target = firstScalar(building, 'building');
|
|
585
|
+
if (target !== undefined)
|
|
586
|
+
this.addReference({
|
|
587
|
+
kind: 'technology_enable_building',
|
|
588
|
+
from: key,
|
|
589
|
+
toKind: 'building',
|
|
590
|
+
to: target.value,
|
|
591
|
+
path: file.displayPath,
|
|
592
|
+
location: nodeLocation(document, target, key),
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
else if (sourcePath.startsWith('common/technology_tags/') &&
|
|
597
|
+
key === 'technology_categories') {
|
|
598
|
+
for (const category of child.entries.filter((entry) => entry.type === 'scalar'))
|
|
599
|
+
this.addSymbol({
|
|
600
|
+
kind: 'technology_category',
|
|
601
|
+
id: category.value,
|
|
602
|
+
path: file.displayPath,
|
|
603
|
+
rootKind: file.rootKind,
|
|
604
|
+
loadOrder: file.loadOrder,
|
|
605
|
+
location: nodeLocation(document, category, category.value),
|
|
606
|
+
metadata: {},
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
else if (sourcePath.startsWith('common/technology_tags/') && key === 'technology_tags') {
|
|
610
|
+
for (const tag of child.entries.filter((entry) => entry.type === 'scalar'))
|
|
611
|
+
this.addSymbol({
|
|
612
|
+
kind: 'technology_tag',
|
|
613
|
+
id: tag.value,
|
|
614
|
+
path: file.displayPath,
|
|
615
|
+
rootKind: file.rootKind,
|
|
616
|
+
loadOrder: file.loadOrder,
|
|
617
|
+
location: nodeLocation(document, tag, tag.value),
|
|
618
|
+
metadata: {},
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
else if (sourcePath.startsWith('common/technology_tags/') &&
|
|
622
|
+
ancestors.at(-1) === 'technology_folders') {
|
|
623
|
+
this.addSymbol({
|
|
624
|
+
kind: 'technology_folder',
|
|
625
|
+
id: key,
|
|
626
|
+
path: file.displayPath,
|
|
627
|
+
rootKind: file.rootKind,
|
|
628
|
+
loadOrder: file.loadOrder,
|
|
629
|
+
location: nodeLocation(document, assignment, key),
|
|
630
|
+
metadata: {
|
|
631
|
+
doctrine: firstScalar(child, 'doctrine')?.value === 'yes',
|
|
632
|
+
ledger: firstScalar(child, 'ledger')?.value,
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
else if (sourcePath.startsWith('common/units/equipment/modules/') &&
|
|
637
|
+
ancestors.at(-1) === 'equipment_modules' &&
|
|
638
|
+
key !== 'limit') {
|
|
639
|
+
this.addSymbol({
|
|
640
|
+
kind: 'equipment_module',
|
|
641
|
+
id: key,
|
|
642
|
+
path: file.displayPath,
|
|
643
|
+
rootKind: file.rootKind,
|
|
644
|
+
loadOrder: file.loadOrder,
|
|
645
|
+
location: nodeLocation(document, assignment, key),
|
|
646
|
+
metadata: {},
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
else if (sourcePath.startsWith('common/units/equipment/') &&
|
|
650
|
+
!sourcePath.startsWith('common/units/equipment/modules/') &&
|
|
651
|
+
ancestors.at(-1) === 'equipments' &&
|
|
652
|
+
key !== 'limit') {
|
|
653
|
+
this.addSymbol({
|
|
654
|
+
kind: 'equipment',
|
|
655
|
+
id: key,
|
|
656
|
+
path: file.displayPath,
|
|
657
|
+
rootKind: file.rootKind,
|
|
658
|
+
loadOrder: file.loadOrder,
|
|
659
|
+
location: nodeLocation(document, assignment, key),
|
|
660
|
+
metadata: {},
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
else if (sourcePath.startsWith('common/units/') &&
|
|
664
|
+
!sourcePath.startsWith('common/units/equipment/') &&
|
|
665
|
+
ancestors.at(-1) === 'sub_units') {
|
|
666
|
+
this.addSymbol({
|
|
667
|
+
kind: 'sub_unit',
|
|
668
|
+
id: key,
|
|
669
|
+
path: file.displayPath,
|
|
670
|
+
rootKind: file.rootKind,
|
|
671
|
+
loadOrder: file.loadOrder,
|
|
672
|
+
location: nodeLocation(document, assignment, key),
|
|
673
|
+
metadata: {},
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
else if (sourcePath.startsWith('common/buildings/') && ancestors.at(-1) === 'buildings') {
|
|
677
|
+
this.addSymbol({
|
|
678
|
+
kind: 'building',
|
|
679
|
+
id: key,
|
|
680
|
+
path: file.displayPath,
|
|
681
|
+
rootKind: file.rootKind,
|
|
682
|
+
loadOrder: file.loadOrder,
|
|
683
|
+
location: nodeLocation(document, assignment, key),
|
|
684
|
+
metadata: {},
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
else if (sourcePath.startsWith('common/abilities/') && ancestors.at(-1) === 'ability') {
|
|
688
|
+
this.addSymbol({
|
|
689
|
+
kind: 'ability',
|
|
690
|
+
id: key,
|
|
691
|
+
path: file.displayPath,
|
|
692
|
+
rootKind: file.rootKind,
|
|
693
|
+
loadOrder: file.loadOrder,
|
|
694
|
+
location: nodeLocation(document, assignment, key),
|
|
695
|
+
metadata: {},
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
else if ((sourcePath === 'common/combat_tactics.txt' ||
|
|
699
|
+
sourcePath.startsWith('common/combat_tactics/')) &&
|
|
700
|
+
ancestors.at(-1) === 'combat_tactics') {
|
|
701
|
+
this.addSymbol({
|
|
702
|
+
kind: 'combat_tactic',
|
|
703
|
+
id: key,
|
|
704
|
+
path: file.displayPath,
|
|
705
|
+
rootKind: file.rootKind,
|
|
706
|
+
loadOrder: file.loadOrder,
|
|
707
|
+
location: nodeLocation(document, assignment, key),
|
|
708
|
+
metadata: {},
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
else if (sourcePath.startsWith('common/doctrines/folders/') && ancestors.length === 0) {
|
|
712
|
+
this.addSymbol({
|
|
713
|
+
kind: 'doctrine_folder',
|
|
714
|
+
id: key,
|
|
715
|
+
path: file.displayPath,
|
|
716
|
+
rootKind: file.rootKind,
|
|
717
|
+
loadOrder: file.loadOrder,
|
|
718
|
+
location: nodeLocation(document, assignment, key),
|
|
719
|
+
metadata: {},
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
else if (sourcePath.startsWith('common/doctrines/grand_doctrines/') &&
|
|
723
|
+
ancestors.length === 0) {
|
|
724
|
+
this.addSymbol({
|
|
725
|
+
kind: 'grand_doctrine',
|
|
726
|
+
id: key,
|
|
727
|
+
path: file.displayPath,
|
|
728
|
+
rootKind: file.rootKind,
|
|
729
|
+
loadOrder: file.loadOrder,
|
|
730
|
+
location: nodeLocation(document, assignment, key),
|
|
731
|
+
metadata: { folder: firstScalar(child, 'folder')?.value },
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
else if (sourcePath.startsWith('common/doctrines/tracks/') && ancestors.length === 0) {
|
|
735
|
+
this.addSymbol({
|
|
736
|
+
kind: 'doctrine_track',
|
|
737
|
+
id: key,
|
|
738
|
+
path: file.displayPath,
|
|
739
|
+
rootKind: file.rootKind,
|
|
740
|
+
loadOrder: file.loadOrder,
|
|
741
|
+
location: nodeLocation(document, assignment, key),
|
|
742
|
+
metadata: {},
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
else if (sourcePath.startsWith('common/doctrines/subdoctrines/') &&
|
|
746
|
+
ancestors.length === 0) {
|
|
747
|
+
this.addSymbol({
|
|
748
|
+
kind: 'subdoctrine',
|
|
749
|
+
id: key,
|
|
750
|
+
path: file.displayPath,
|
|
751
|
+
rootKind: file.rootKind,
|
|
752
|
+
loadOrder: file.loadOrder,
|
|
753
|
+
location: nodeLocation(document, assignment, key),
|
|
754
|
+
metadata: {},
|
|
755
|
+
});
|
|
756
|
+
}
|
|
466
757
|
else if (key === 'focus' &&
|
|
467
758
|
id !== undefined &&
|
|
468
759
|
ancestors.at(-1) === 'continuous_focus_palette') {
|
|
@@ -936,7 +1227,7 @@ export class SymbolIndex {
|
|
|
936
1227
|
continue;
|
|
937
1228
|
const active = candidates[0];
|
|
938
1229
|
this.#active.set(key, active);
|
|
939
|
-
const additiveCategory = active.kind === 'decision_category';
|
|
1230
|
+
const additiveCategory = active.kind === 'decision_category' || active.kind === 'technology_category';
|
|
940
1231
|
for (const candidate of group) {
|
|
941
1232
|
candidate.overridden = additiveCategory ? candidate.sourceShadowed : candidate !== active;
|
|
942
1233
|
}
|
|
@@ -143,6 +143,22 @@ function focusIconDataUri(focus, options) {
|
|
|
143
143
|
? undefined
|
|
144
144
|
: (options.presentation?.icons[icon.sprite]?.dataUri ?? options.iconDataUris?.[icon.sprite]);
|
|
145
145
|
}
|
|
146
|
+
function focusTitleLayout(renderer, value, maximumWidth) {
|
|
147
|
+
const title = value.trim().replace(/\s+/gu, ' ');
|
|
148
|
+
if (renderer.measure(title, 11) <= maximumWidth)
|
|
149
|
+
return { lines: [title], fontSize: 11 };
|
|
150
|
+
const words = title.split(' ');
|
|
151
|
+
if (words.length < 2)
|
|
152
|
+
return { lines: [title], fontSize: 10 };
|
|
153
|
+
let best = { lines: [title], width: renderer.measure(title, 10) };
|
|
154
|
+
for (let index = 1; index < words.length; index += 1) {
|
|
155
|
+
const lines = [words.slice(0, index).join(' '), words.slice(index).join(' ')];
|
|
156
|
+
const width = Math.max(...lines.map((line) => renderer.measure(line, 10)));
|
|
157
|
+
if (width < best.width)
|
|
158
|
+
best = { lines, width };
|
|
159
|
+
}
|
|
160
|
+
return { lines: best.lines, fontSize: 10 };
|
|
161
|
+
}
|
|
146
162
|
function svgDocument(plan, layout, diagnostics, options) {
|
|
147
163
|
const horizontal = options.horizontalSpacing ?? FOCUS_HORIZONTAL_GRID_PIXELS;
|
|
148
164
|
const vertical = options.verticalSpacing ?? FOCUS_VERTICAL_GRID_PIXELS;
|
|
@@ -160,10 +176,10 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
160
176
|
const nodeWidth = FOCUS_NODE_WIDTH_PIXELS;
|
|
161
177
|
const nodeHeight = FOCUS_NODE_HEIGHT_PIXELS;
|
|
162
178
|
const nodes = [...layout.nodes].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
163
|
-
const minimumX = Math.min(
|
|
164
|
-
const minimumY = Math.min(
|
|
165
|
-
const maximumX = Math.max(
|
|
166
|
-
const maximumY = Math.max(
|
|
179
|
+
const minimumX = nodes.length === 0 ? 0 : Math.min(...nodes.map(({ x }) => x));
|
|
180
|
+
const minimumY = nodes.length === 0 ? 0 : Math.min(...nodes.map(({ y }) => y));
|
|
181
|
+
const maximumX = nodes.length === 0 ? 0 : Math.max(...nodes.map(({ x }) => x));
|
|
182
|
+
const maximumY = nodes.length === 0 ? 0 : Math.max(...nodes.map(({ y }) => y));
|
|
167
183
|
const logicalWidth = Math.max(320, (maximumX - minimumX) * horizontal + nodeWidth + padding * 2);
|
|
168
184
|
const logicalHeight = Math.max(240, (maximumY - minimumY) * vertical + nodeHeight + padding * 2);
|
|
169
185
|
const width = Math.max(1, Math.round(logicalWidth * outputScale));
|
|
@@ -268,6 +284,8 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
268
284
|
.map(({ code }) => code)
|
|
269
285
|
.sort()
|
|
270
286
|
.join(' ');
|
|
287
|
+
const titleWidth = nodeWidth - 72;
|
|
288
|
+
const titleLayout = focusTitleLayout(toolText, title, titleWidth);
|
|
271
289
|
parts.push(`<g class="focus-node ${escapeXml(focus.visibility)} ${severity}" id="focus-${escapeXml(node.id)}" data-focus-id="${escapeXml(node.id)}" data-branch="${escapeXml(focus.branchId ?? '')}" data-lane="${escapeXml(node.laneId)}" data-diagnostics="${escapeXml(diagnosticCodes)}" transform="translate(${point.x} ${point.y})" filter="url(#shadow)">`, `<title>${escapeXml(`${title} (${node.id}) at ${node.x},${node.y}`)}</title>`, `<rect width="${nodeWidth}" height="${nodeHeight}" rx="8" fill="${fill}" stroke="${stroke}" stroke-width="${severity === 'clean' ? 1.5 : 3}"/>`);
|
|
272
290
|
if (iconUri !== undefined) {
|
|
273
291
|
parts.push(`<image href="${escapeXml(iconUri)}" x="8" y="10" width="48" height="48" preserveAspectRatio="xMidYMid meet"/>`);
|
|
@@ -281,20 +299,26 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
281
299
|
fill: '#91a0b4',
|
|
282
300
|
}));
|
|
283
301
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
302
|
+
const titleY = titleLayout.lines.length === 1 ? 25 : 20;
|
|
303
|
+
for (const [index, line] of titleLayout.lines.entries()) {
|
|
304
|
+
const measuredWidth = toolText.measure(line, titleLayout.fontSize);
|
|
305
|
+
parts.push(toolText.render(line, {
|
|
306
|
+
x: 64,
|
|
307
|
+
y: titleY + index * 13,
|
|
308
|
+
fontSize: titleLayout.fontSize,
|
|
309
|
+
weight: 700,
|
|
310
|
+
fill: '#f2f5fa',
|
|
311
|
+
...(measuredWidth <= titleWidth ? {} : { targetWidth: titleWidth }),
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
parts.push(toolText.render(node.id.slice(0, 22), {
|
|
291
315
|
x: 64,
|
|
292
|
-
y:
|
|
316
|
+
y: 49,
|
|
293
317
|
fontSize: 9,
|
|
294
318
|
fill: '#aeb9c8',
|
|
295
319
|
}), toolText.render(`(${node.x}, ${node.y}) · ${node.laneId}`, {
|
|
296
320
|
x: 64,
|
|
297
|
-
y:
|
|
321
|
+
y: 64,
|
|
298
322
|
fontSize: 9,
|
|
299
323
|
fill: '#8391a5',
|
|
300
324
|
}), '</g>');
|
|
@@ -4,8 +4,9 @@ import { registerFocusTools } from '../tools/focus.js';
|
|
|
4
4
|
import { registerGuiTools } from '../tools/gui.js';
|
|
5
5
|
import { registerMapTools } from '../tools/map.js';
|
|
6
6
|
import { registerEventTools } from '../tools/event.js';
|
|
7
|
+
import { registerTechnologyTools } from '../tools/technology.js';
|
|
7
8
|
import { registerMcpResources } from '../resources/register.js';
|
|
8
|
-
export const SERVER_INSTRUCTIONS = 'Use
|
|
9
|
+
export const SERVER_INSTRUCTIONS = 'Use focus tools for focus trees, GUI tools for interfaces, and map tools for map data. Use hoi4.event_inspect, hoi4.event_render, and hoi4.event_compare for event chains. Use hoi4.tech_inspect, hoi4.tech_render, and hoi4.tech_compare for technology and doctrines. Event tools are read-only. Technology tools are read-only. Large evidence is linked as resources.';
|
|
9
10
|
export function createMcpServer(engine, context = {}) {
|
|
10
11
|
const server = new McpServer({
|
|
11
12
|
name: PACKAGE_NAME,
|
|
@@ -19,6 +20,7 @@ export function createMcpServer(engine, context = {}) {
|
|
|
19
20
|
registerGuiTools(server, engine, context);
|
|
20
21
|
registerMapTools(server, engine, context);
|
|
21
22
|
registerEventTools(server, engine, context);
|
|
23
|
+
registerTechnologyTools(server, engine, context);
|
|
22
24
|
registerMcpResources(server, engine, context);
|
|
23
25
|
return server;
|
|
24
26
|
}
|
|
@@ -44,13 +44,13 @@ const compactPublicOperationResultSchema = z
|
|
|
44
44
|
status: z.enum(['ok', 'blocked', 'error']),
|
|
45
45
|
code: z.string(),
|
|
46
46
|
workspaceId: z.string(),
|
|
47
|
-
filesScanned: z.array(z.unknown())
|
|
48
|
-
proposedFiles: z.array(z.unknown())
|
|
49
|
-
changedFiles: z.array(z.unknown())
|
|
47
|
+
filesScanned: z.array(z.unknown()),
|
|
48
|
+
proposedFiles: z.array(z.unknown()),
|
|
49
|
+
changedFiles: z.array(z.unknown()),
|
|
50
50
|
diagnostics: z.array(z.unknown()).max(MAX_INLINE_DIAGNOSTICS),
|
|
51
51
|
artifacts: z.array(z.unknown()).max(MAX_INLINE_ARTIFACT_LINKS),
|
|
52
52
|
validation: z.unknown(),
|
|
53
|
-
blockers: z.array(z.unknown())
|
|
53
|
+
blockers: z.array(z.unknown()),
|
|
54
54
|
data: z.object({}).catchall(z.unknown()),
|
|
55
55
|
})
|
|
56
56
|
.strict();
|