hoi4-agent-tools 1.2.0 → 2.1.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 +21 -1
- package/README.md +6 -7
- package/dist/hoi4_agent_tools/core/artifacts.js +109 -4
- package/dist/hoi4_agent_tools/core/diagnostic-collector.js +28 -8
- package/dist/hoi4_agent_tools/core/diff.js +162 -4
- package/dist/hoi4_agent_tools/core/engine.js +2 -0
- package/dist/hoi4_agent_tools/core/index.js +17 -6
- package/dist/hoi4_agent_tools/core/render-budget.js +5 -3
- package/dist/hoi4_agent_tools/core/scanner.js +3 -2
- package/dist/hoi4_agent_tools/core/source/limits.js +4 -1
- package/dist/hoi4_agent_tools/core/workspace.js +31 -3
- package/dist/hoi4_agent_tools/event/graph.js +28 -6
- package/dist/hoi4_agent_tools/event/limits.js +1 -1
- package/dist/hoi4_agent_tools/event/service.js +103 -7
- package/dist/hoi4_agent_tools/focus/continuous-render.js +17 -8
- package/dist/hoi4_agent_tools/focus/presentation.js +96 -41
- package/dist/hoi4_agent_tools/focus/render.js +27 -16
- package/dist/hoi4_agent_tools/gui/assets.js +9 -2
- package/dist/hoi4_agent_tools/gui/inspection-artifact.js +99 -0
- package/dist/hoi4_agent_tools/gui/layout.js +126 -27
- package/dist/hoi4_agent_tools/gui/renderer.js +16 -0
- package/dist/hoi4_agent_tools/gui/source-graph.js +61 -28
- package/dist/hoi4_agent_tools/gui/studio.js +23 -8
- package/dist/hoi4_agent_tools/map/diagnostic-limit.js +90 -9
- package/dist/hoi4_agent_tools/map/model.js +22 -4
- package/dist/hoi4_agent_tools/map/service.js +4 -3
- package/dist/hoi4_agent_tools/map/validation.js +3 -2
- package/dist/hoi4_agent_tools/mcp/server/create.js +1 -3
- package/dist/hoi4_agent_tools/mcp/server/output-schemas.js +1 -1
- package/dist/hoi4_agent_tools/mcp/tools/event.js +6 -6
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +149 -224
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +35 -25
- package/dist/hoi4_agent_tools/mcp/tools/map.js +6 -3
- package/dist/hoi4_agent_tools/runtime.js +112 -2
- package/dist/hoi4_agent_tools/schemas/common.js +5 -1
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/events.md +4 -5
- package/docs/focus.md +5 -4
- package/docs/gui.md +5 -1
- package/docs/map.md +1 -1
- package/docs/setup.md +9 -6
- package/package.json +1 -1
- package/server.json +2 -2
- package/dist/hoi4_agent_tools/mcp/tools/mods.js +0 -54
|
@@ -1,18 +1,99 @@
|
|
|
1
1
|
export const MAP_DIAGNOSTIC_LIMIT = 2_000;
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
function isHardDiagnostic(diagnostic) {
|
|
3
|
+
return diagnostic.severity === 'error' || diagnostic.severity === 'blocker';
|
|
4
|
+
}
|
|
5
|
+
function isWorkspacePath(path) {
|
|
6
|
+
return path.startsWith('mod:') || path.startsWith('fixture:');
|
|
7
|
+
}
|
|
8
|
+
function isBaselineOnlyDiagnostic(diagnostic) {
|
|
9
|
+
if (diagnostic.location === undefined || isWorkspacePath(diagnostic.location.path))
|
|
10
|
+
return false;
|
|
11
|
+
return !(diagnostic.related ?? []).some(({ path }) => isWorkspacePath(path));
|
|
12
|
+
}
|
|
13
|
+
function addOmittedDiagnostic(details, diagnostic) {
|
|
14
|
+
details.omitted += 1;
|
|
15
|
+
details.omittedBySeverity[diagnostic.severity] =
|
|
16
|
+
(details.omittedBySeverity[diagnostic.severity] ?? 0) + 1;
|
|
17
|
+
details.omittedByCode[diagnostic.code] = (details.omittedByCode[diagnostic.code] ?? 0) + 1;
|
|
18
|
+
}
|
|
19
|
+
function truncationDiagnostic(omitted) {
|
|
20
|
+
const details = {
|
|
21
|
+
limit: MAP_DIAGNOSTIC_LIMIT,
|
|
22
|
+
retained: MAP_DIAGNOSTIC_LIMIT - 1,
|
|
23
|
+
omitted: 0,
|
|
24
|
+
omittedBySeverity: {},
|
|
25
|
+
omittedByCode: {},
|
|
26
|
+
};
|
|
27
|
+
for (const diagnostic of omitted)
|
|
28
|
+
addOmittedDiagnostic(details, diagnostic);
|
|
29
|
+
const hardOmitted = omitted.some(isHardDiagnostic);
|
|
30
|
+
return {
|
|
31
|
+
code: 'MAP_DIAGNOSTICS_TRUNCATED',
|
|
32
|
+
severity: hardOmitted ? 'blocker' : 'info',
|
|
33
|
+
category: 'map',
|
|
34
|
+
message: hardOmitted
|
|
35
|
+
? 'Map diagnostic detail exceeded the result ceiling and omitted an error or blocker'
|
|
36
|
+
: 'Map diagnostic detail exceeded the result ceiling; deterministic samples and omission counts were retained',
|
|
37
|
+
details,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function existingTruncationDiagnostic(diagnostics) {
|
|
41
|
+
const marker = diagnostics.at(-1);
|
|
42
|
+
return marker?.code === 'MAP_DIAGNOSTICS_TRUNCATED'
|
|
43
|
+
? marker
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
46
|
+
function addBoundedDiagnostic(diagnostics, diagnostic, includeBaseline) {
|
|
47
|
+
if (!includeBaseline && isBaselineOnlyDiagnostic(diagnostic))
|
|
48
|
+
return;
|
|
49
|
+
const marker = existingTruncationDiagnostic(diagnostics);
|
|
50
|
+
if (marker !== undefined) {
|
|
51
|
+
if (isHardDiagnostic(diagnostic)) {
|
|
52
|
+
const replaceIndex = diagnostics
|
|
53
|
+
.slice(0, -1)
|
|
54
|
+
.findLastIndex((retained) => !isHardDiagnostic(retained));
|
|
55
|
+
if (replaceIndex >= 0) {
|
|
56
|
+
const replaced = diagnostics[replaceIndex];
|
|
57
|
+
diagnostics[replaceIndex] = diagnostic;
|
|
58
|
+
addOmittedDiagnostic(marker.details, replaced);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
marker.severity = 'blocker';
|
|
62
|
+
marker.message =
|
|
63
|
+
'Map diagnostic detail exceeded the result ceiling and omitted an error or blocker';
|
|
64
|
+
}
|
|
65
|
+
addOmittedDiagnostic(marker.details, diagnostic);
|
|
4
66
|
return;
|
|
67
|
+
}
|
|
5
68
|
if (diagnostics.length < MAP_DIAGNOSTIC_LIMIT) {
|
|
6
69
|
diagnostics.push(diagnostic);
|
|
7
70
|
return;
|
|
8
71
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
72
|
+
const candidates = [...diagnostics, diagnostic];
|
|
73
|
+
const hard = candidates.filter(isHardDiagnostic);
|
|
74
|
+
const soft = candidates.filter((candidate) => !isHardDiagnostic(candidate));
|
|
75
|
+
const retained = hard.length >= MAP_DIAGNOSTIC_LIMIT - 1
|
|
76
|
+
? hard.slice(0, MAP_DIAGNOSTIC_LIMIT - 1)
|
|
77
|
+
: [...hard, ...soft.slice(0, MAP_DIAGNOSTIC_LIMIT - 1 - hard.length)];
|
|
78
|
+
const retainedCounts = new Map();
|
|
79
|
+
for (const item of retained)
|
|
80
|
+
retainedCounts.set(item, (retainedCounts.get(item) ?? 0) + 1);
|
|
81
|
+
const omitted = candidates.filter((candidate) => {
|
|
82
|
+
const count = retainedCounts.get(candidate) ?? 0;
|
|
83
|
+
if (count === 0)
|
|
84
|
+
return true;
|
|
85
|
+
retainedCounts.set(candidate, count - 1);
|
|
86
|
+
return false;
|
|
87
|
+
});
|
|
88
|
+
diagnostics.splice(0, diagnostics.length, ...retained, truncationDiagnostic(omitted));
|
|
89
|
+
}
|
|
90
|
+
export function addMapDiagnostic(diagnostics, diagnostic) {
|
|
91
|
+
addBoundedDiagnostic(diagnostics, diagnostic, false);
|
|
92
|
+
}
|
|
93
|
+
/** Active topology validation must report failures in whichever source wins
|
|
94
|
+
* precedence, including inherited game and dependency files. */
|
|
95
|
+
export function addActiveMapDiagnostic(diagnostics, diagnostic) {
|
|
96
|
+
addBoundedDiagnostic(diagnostics, diagnostic, true);
|
|
16
97
|
}
|
|
17
98
|
export function addMapDiagnostics(diagnostics, additions) {
|
|
18
99
|
for (const diagnostic of additions)
|
|
@@ -24,11 +24,13 @@ const MAP_TEXT_FIELD_LIMIT = 10_000;
|
|
|
24
24
|
const MAP_MODEL_MAX_SCRIPT_FILES = 5_000;
|
|
25
25
|
const MAP_MODEL_MAX_SOURCE_TOKENS = 1_000_000;
|
|
26
26
|
const MAP_MODEL_MAX_RECORDS = 500_000;
|
|
27
|
+
const MAP_MODEL_MAX_LOCALISATION_RECORDS = 2_000_000;
|
|
27
28
|
const MAP_MODEL_MAX_MEMBERSHIP_EDGES = 1_000_000;
|
|
28
29
|
class MapModelBudget {
|
|
29
30
|
#documents = new Set();
|
|
30
31
|
#tokens = 0;
|
|
31
32
|
#records = 0;
|
|
33
|
+
#localisationRecords = 0;
|
|
32
34
|
#membershipEdges = 0;
|
|
33
35
|
assertScriptFiles(count) {
|
|
34
36
|
if (count > MAP_MODEL_MAX_SCRIPT_FILES) {
|
|
@@ -50,6 +52,13 @@ class MapModelBudget {
|
|
|
50
52
|
this.block('domain records', this.#records, MAP_MODEL_MAX_RECORDS);
|
|
51
53
|
}
|
|
52
54
|
}
|
|
55
|
+
addLocalisationRecords(count) {
|
|
56
|
+
this.#localisationRecords += count;
|
|
57
|
+
if (!Number.isSafeInteger(this.#localisationRecords) ||
|
|
58
|
+
this.#localisationRecords > MAP_MODEL_MAX_LOCALISATION_RECORDS) {
|
|
59
|
+
this.block('localisation records', this.#localisationRecords, MAP_MODEL_MAX_LOCALISATION_RECORDS);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
53
62
|
addMembershipEdges(count) {
|
|
54
63
|
this.#membershipEdges += count;
|
|
55
64
|
if (!Number.isSafeInteger(this.#membershipEdges) ||
|
|
@@ -869,7 +878,9 @@ export class MapWorkspaceIndex {
|
|
|
869
878
|
const modelBudget = new MapModelBudget();
|
|
870
879
|
modelBudget.assertScriptFiles(this.sourceFiles.filter((file) => inConfiguredRoot(file.relativePath, this.sourceRoots.states) ||
|
|
871
880
|
this.sourceRoots.map.some((root) => normalizedPath(file.relativePath).startsWith(`${root}/strategicregions/`)) ||
|
|
872
|
-
|
|
881
|
+
((file.rootKind === 'mod' || file.rootKind === 'fixture') &&
|
|
882
|
+
normalizedPath(file.relativePath).endsWith('.asset') &&
|
|
883
|
+
file.bytes.includes('locator'))).length);
|
|
873
884
|
this.defaultMapFile = fileInRoots(this.activeFiles, this.sourceRoots.map, 'default.map');
|
|
874
885
|
const definitionsName = defaultMapSelectorValue(this.defaultMapFile, 'definitions', 'definition.csv');
|
|
875
886
|
const provincesName = defaultMapSelectorValue(this.defaultMapFile, 'provinces', 'provinces.bmp');
|
|
@@ -1031,7 +1042,9 @@ export class MapWorkspaceIndex {
|
|
|
1031
1042
|
weatherFile === undefined ? [] : parseWeatherPositions(weatherFile, this.diagnostics);
|
|
1032
1043
|
modelBudget.addRecords(this.weatherPositions.length);
|
|
1033
1044
|
this.entityLocators = this.activeFiles.all
|
|
1034
|
-
.filter((file) =>
|
|
1045
|
+
.filter((file) => (file.rootKind === 'mod' || file.rootKind === 'fixture') &&
|
|
1046
|
+
normalizedPath(file.relativePath).endsWith('.asset') &&
|
|
1047
|
+
file.bytes.includes('locator'))
|
|
1035
1048
|
.flatMap((file) => {
|
|
1036
1049
|
const locators = parseEntityLocators(file, this.diagnostics);
|
|
1037
1050
|
if (locators[0] !== undefined)
|
|
@@ -1042,8 +1055,13 @@ export class MapWorkspaceIndex {
|
|
|
1042
1055
|
for (const file of this.activeFiles.all.filter((entry) => inConfiguredRoot(entry.relativePath, this.sourceRoots.localisation) &&
|
|
1043
1056
|
normalizedPath(entry.relativePath).endsWith('.yml'))) {
|
|
1044
1057
|
const document = parseLocalisation(file.bytes, file.displayPath);
|
|
1045
|
-
|
|
1046
|
-
|
|
1058
|
+
if (file.rootKind === 'mod' || file.rootKind === 'fixture') {
|
|
1059
|
+
addMapDiagnostics(this.diagnostics, document.diagnostics);
|
|
1060
|
+
}
|
|
1061
|
+
// Localisation is a supporting lookup table rather than connected map topology. Account for
|
|
1062
|
+
// it separately so a normal full-game language database cannot exhaust the domain-record
|
|
1063
|
+
// budget before provinces, states, regions, railways, and adjacencies are available.
|
|
1064
|
+
modelBudget.addLocalisationRecords(document.entries.length);
|
|
1047
1065
|
for (const entry of document.entries) {
|
|
1048
1066
|
this.localisationKeys.add(`${entry.language}:${entry.key}`);
|
|
1049
1067
|
const indexed = { file, document, entry };
|
|
@@ -4,6 +4,7 @@ import { ArtifactStore, boundedSourceHashEvidence, publicArtifactLink, } from '.
|
|
|
4
4
|
import { canonicalJson, compareCodeUnits, hashCanonical, sha256Bytes } from '../core/canonical.js';
|
|
5
5
|
import { sortDiagnostics } from '../core/diagnostics.js';
|
|
6
6
|
import { CoreEngine } from '../core/engine.js';
|
|
7
|
+
import { SymbolIndex } from '../core/index.js';
|
|
7
8
|
import { RenderBudget } from '../core/render-budget.js';
|
|
8
9
|
import { ServiceError } from '../core/result.js';
|
|
9
10
|
import { WorkspaceScanner } from '../core/scanner.js';
|
|
@@ -386,7 +387,6 @@ function sourceTextPatterns(roots) {
|
|
|
386
387
|
const normalized = normalizeSourceRoot(root);
|
|
387
388
|
return [`${normalized}/english/**/*.{yml,yaml}`, `${normalized}/*.{yml,yaml}`];
|
|
388
389
|
}),
|
|
389
|
-
'**/*.asset',
|
|
390
390
|
];
|
|
391
391
|
}
|
|
392
392
|
function provinceBitmapPatterns(defaultMaps, mapRoots) {
|
|
@@ -491,14 +491,15 @@ export class AgentNudger {
|
|
|
491
491
|
const selectorSnapshot = await this.engine.scan(workspaceId, { patterns: defaultMapPatterns(sourceRoots) }, principal, signal);
|
|
492
492
|
const bitmapPatterns = provinceBitmapPatterns(selectorSnapshot.files, sourceRoots.map);
|
|
493
493
|
const contentSnapshot = await this.engine.scan(workspaceId, { patterns: [...sourceTextPatterns(sourceRoots), ...bitmapPatterns] }, principal, signal);
|
|
494
|
+
const locatorSnapshot = await this.engine.scan(workspaceId, { patterns: ['**/*.asset'], rootKinds: ['mod', 'fixture'] }, principal, signal);
|
|
494
495
|
signal?.throwIfAborted();
|
|
495
|
-
const files = contentSnapshot.files;
|
|
496
|
+
const files = [...contentSnapshot.files, ...locatorSnapshot.files].sort((left, right) => left.loadOrder - right.loadOrder || compareCodeUnits(left.displayPath, right.displayPath));
|
|
496
497
|
const revision = hashCanonical(files.map(({ displayPath, loadOrder, sha256 }) => ({ displayPath, loadOrder, sha256 })));
|
|
497
498
|
return {
|
|
498
499
|
workspaceId,
|
|
499
500
|
revision,
|
|
500
501
|
files,
|
|
501
|
-
index: MapWorkspaceIndex.build(files, sourceRoots,
|
|
502
|
+
index: MapWorkspaceIndex.build(files, sourceRoots, SymbolIndex.build(files)),
|
|
502
503
|
};
|
|
503
504
|
}
|
|
504
505
|
async validate(workspaceId, principal, signal) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
|
|
2
2
|
import { sortDiagnostics } from '../core/diagnostics.js';
|
|
3
3
|
import { assertRenderDimensions } from '../core/render-budget.js';
|
|
4
|
-
import { addMapDiagnostic
|
|
4
|
+
import { addActiveMapDiagnostic, addMapDiagnostic } from './diagnostic-limit.js';
|
|
5
5
|
import { rgbKey } from './bmp.js';
|
|
6
6
|
import { MAP_PROVINCE_ENGINE_MAX_PIXELS } from './model.js';
|
|
7
7
|
const MAP_DIAGNOSTIC_DETAIL_SAMPLE_LIMIT = 100;
|
|
@@ -53,7 +53,8 @@ function definitionLocation(definition) {
|
|
|
53
53
|
return requiredLineLocation(definition.document, definition.line);
|
|
54
54
|
}
|
|
55
55
|
function addDiagnostic(diagnostics, options, diagnostic) {
|
|
56
|
-
|
|
56
|
+
const add = options.includeBaselineDiagnostics === true ? addActiveMapDiagnostic : addMapDiagnostic;
|
|
57
|
+
add(diagnostics, {
|
|
57
58
|
...diagnostic,
|
|
58
59
|
...(diagnostic.operationId !== undefined || options.operationId === undefined
|
|
59
60
|
? {}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { PACKAGE_NAME, PACKAGE_VERSION } from '../../version.js';
|
|
3
|
-
import { registerModsTool } from '../tools/mods.js';
|
|
4
3
|
import { registerFocusTools } from '../tools/focus.js';
|
|
5
4
|
import { registerGuiTools } from '../tools/gui.js';
|
|
6
5
|
import { registerMapTools } from '../tools/map.js';
|
|
7
6
|
import { registerEventTools } from '../tools/event.js';
|
|
8
7
|
import { registerMcpResources } from '../resources/register.js';
|
|
9
|
-
export const SERVER_INSTRUCTIONS = 'Use hoi4.
|
|
8
|
+
export const SERVER_INSTRUCTIONS = 'Use hoi4.focus_inspect and hoi4.focus_render for fast structural work; hoi4.focus_raster adds decoded icons and PNG output. Use hoi4.event_inspect to analyze event chains. Event tools are read-only. Focus, GUI, and map rewrite tools edit the current mod. Large evidence is returned through linked resources.';
|
|
10
9
|
export function createMcpServer(engine, context = {}) {
|
|
11
10
|
const server = new McpServer({
|
|
12
11
|
name: PACKAGE_NAME,
|
|
@@ -16,7 +15,6 @@ export function createMcpServer(engine, context = {}) {
|
|
|
16
15
|
capabilities: { logging: {} },
|
|
17
16
|
instructions: SERVER_INSTRUCTIONS,
|
|
18
17
|
});
|
|
19
|
-
registerModsTool(server, engine, context);
|
|
20
18
|
registerFocusTools(server, engine, context);
|
|
21
19
|
registerGuiTools(server, engine, context);
|
|
22
20
|
registerMapTools(server, engine, context);
|
|
@@ -306,11 +306,11 @@ export function registerEventTools(server, engine, context) {
|
|
|
306
306
|
outputSchema: eventInspectOutputSchema,
|
|
307
307
|
annotations: readOnlyEventTool,
|
|
308
308
|
}, async (input, extra) => {
|
|
309
|
-
const
|
|
309
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(input.workspaceId, context.principal);
|
|
310
310
|
try {
|
|
311
311
|
const progress = progressReporter(extra);
|
|
312
312
|
await progress.report(0, 3, 'Analyzing event chain');
|
|
313
|
-
const inspected = await viewer.inspect(inspectRequest(input, context, progress.signal));
|
|
313
|
+
const inspected = await viewer.inspect(inspectRequest({ ...input, workspaceId }, context, progress.signal));
|
|
314
314
|
await progress.report(2, 3, 'Linking complete event analysis');
|
|
315
315
|
const result = emptyServiceResult(workspaceId, {
|
|
316
316
|
mode: inspected.mode,
|
|
@@ -344,11 +344,11 @@ export function registerEventTools(server, engine, context) {
|
|
|
344
344
|
outputSchema: eventRenderOutputSchema,
|
|
345
345
|
annotations: readOnlyEventTool,
|
|
346
346
|
}, async (input, extra) => {
|
|
347
|
-
const
|
|
347
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(input.workspaceId, context.principal);
|
|
348
348
|
try {
|
|
349
349
|
const progress = progressReporter(extra);
|
|
350
350
|
await progress.report(0, 3, 'Rendering event-chain view');
|
|
351
|
-
const rendered = await viewer.renderAndStore(renderRequest(input, context, progress.signal));
|
|
351
|
+
const rendered = await viewer.renderAndStore(renderRequest({ ...input, workspaceId }, context, progress.signal));
|
|
352
352
|
await progress.report(2, 3, 'Linking complete event renders');
|
|
353
353
|
const result = emptyServiceResult(workspaceId, {
|
|
354
354
|
view: rendered.render.view,
|
|
@@ -389,11 +389,11 @@ export function registerEventTools(server, engine, context) {
|
|
|
389
389
|
outputSchema: eventCompareOutputSchema,
|
|
390
390
|
annotations: readOnlyEventTool,
|
|
391
391
|
}, async (input, extra) => {
|
|
392
|
-
const
|
|
392
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(input.workspaceId, context.principal);
|
|
393
393
|
try {
|
|
394
394
|
const progress = progressReporter(extra);
|
|
395
395
|
await progress.report(0, 3, 'Comparing event-chain graphs');
|
|
396
|
-
const compared = await viewer.compareAndStore(compareRequest(input, context, progress.signal));
|
|
396
|
+
const compared = await viewer.compareAndStore(compareRequest({ ...input, workspaceId }, context, progress.signal));
|
|
397
397
|
await progress.report(2, 3, 'Linking complete event comparison');
|
|
398
398
|
const { comparison } = compared;
|
|
399
399
|
const result = emptyServiceResult(workspaceId, {
|