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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Source: the official HOI4 1.19.2 effects documentation reviewed 2026-07-10.
|
|
6
6
|
* Keep this list synchronized during compatibility reviews.
|
|
7
7
|
*/
|
|
8
|
-
export const
|
|
8
|
+
export const nativeEffectKeys = new Set([
|
|
9
9
|
'activate_advisor',
|
|
10
10
|
'activate_decision',
|
|
11
11
|
'activate_mission',
|
|
@@ -560,3 +560,5 @@ export const nativeFocusEffectKeys = new Set([
|
|
|
560
560
|
'while_loop_effect',
|
|
561
561
|
'white_peace',
|
|
562
562
|
]);
|
|
563
|
+
/** Backward-compatible focus-domain name for the shared official effect catalog. */
|
|
564
|
+
export const nativeFocusEffectKeys = nativeEffectKeys;
|
|
@@ -6,7 +6,7 @@ const artifactChunkMetadataKey = 'io.github.klimpaskov/hoi4-agent-tools.artifact
|
|
|
6
6
|
const fatalUtf8Decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
7
7
|
const artifactResourceMetadata = {
|
|
8
8
|
title: 'HOI4 review artifact',
|
|
9
|
-
description: 'Complete plans, diagnostics, layouts, previews, and diffs. Reads accept zero-based byte offset and positive byte length query selectors; each response reports its exact byte range and continuation URI in _meta.',
|
|
9
|
+
description: 'Complete plans, diagnostics, layouts, previews, event graphs, routes, state and scope reports, comparisons, and diffs. Reads accept zero-based byte offset and positive byte length query selectors; each response reports its exact byte range and continuation URI in _meta.',
|
|
10
10
|
_meta: {
|
|
11
11
|
[artifactChunkMetadataKey]: {
|
|
12
12
|
version: 1,
|
|
@@ -4,7 +4,9 @@ import { registerModsTool } from '../tools/mods.js';
|
|
|
4
4
|
import { registerFocusTools } from '../tools/focus.js';
|
|
5
5
|
import { registerGuiTools } from '../tools/gui.js';
|
|
6
6
|
import { registerMapTools } from '../tools/map.js';
|
|
7
|
+
import { registerEventTools } from '../tools/event.js';
|
|
7
8
|
import { registerMcpResources } from '../resources/register.js';
|
|
9
|
+
export const SERVER_INSTRUCTIONS = 'Use hoi4.mods to select a workspace. Use hoi4.event_inspect for scan, roots, trace, explain_path, state_flow, lint, and impact; hoi4.event_render and hoi4.event_compare return linked evidence. JSON resources are authoritative. Event tools are read-only. Only focus, GUI, and map rewrite tools edit configured mods.';
|
|
8
10
|
export function createMcpServer(engine, context = {}) {
|
|
9
11
|
const server = new McpServer({
|
|
10
12
|
name: PACKAGE_NAME,
|
|
@@ -12,11 +14,13 @@ export function createMcpServer(engine, context = {}) {
|
|
|
12
14
|
websiteUrl: 'https://github.com/klimPaskov/hoi4-agent-tools',
|
|
13
15
|
}, {
|
|
14
16
|
capabilities: { logging: {} },
|
|
17
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
15
18
|
});
|
|
16
19
|
registerModsTool(server, engine, context);
|
|
17
20
|
registerFocusTools(server, engine, context);
|
|
18
21
|
registerGuiTools(server, engine, context);
|
|
19
22
|
registerMapTools(server, engine, context);
|
|
23
|
+
registerEventTools(server, engine, context);
|
|
20
24
|
registerMcpResources(server, engine, context);
|
|
21
25
|
return server;
|
|
22
26
|
}
|
|
@@ -42,24 +42,18 @@ const emptyToolDataSchema = z.object({}).strict();
|
|
|
42
42
|
const compactPublicOperationResultSchema = z
|
|
43
43
|
.object({
|
|
44
44
|
status: z.enum(['ok', 'blocked', 'error']),
|
|
45
|
-
code: z.string()
|
|
46
|
-
workspaceId: z.string()
|
|
47
|
-
filesScanned: z.array(z.
|
|
48
|
-
proposedFiles: z.array(z.
|
|
49
|
-
changedFiles: z.array(z.
|
|
45
|
+
code: z.string(),
|
|
46
|
+
workspaceId: z.string(),
|
|
47
|
+
filesScanned: z.array(z.unknown()).max(MAX_INLINE_FILES_SCANNED),
|
|
48
|
+
proposedFiles: z.array(z.unknown()).max(MAX_INLINE_PROPOSED_FILES),
|
|
49
|
+
changedFiles: z.array(z.unknown()).max(MAX_INLINE_CHANGED_FILES),
|
|
50
50
|
diagnostics: z.array(z.unknown()).max(MAX_INLINE_DIAGNOSTICS),
|
|
51
51
|
artifacts: z.array(z.unknown()).max(MAX_INLINE_ARTIFACT_LINKS),
|
|
52
|
-
validation: z
|
|
53
|
-
.object({
|
|
54
|
-
passed: z.boolean(),
|
|
55
|
-
checks: z.array(z.unknown()).max(MAX_INLINE_VALIDATION_CHECKS),
|
|
56
|
-
})
|
|
57
|
-
.strict(),
|
|
52
|
+
validation: z.unknown(),
|
|
58
53
|
blockers: z.array(z.unknown()).max(MAX_INLINE_BLOCKERS),
|
|
59
|
-
data: z.
|
|
54
|
+
data: z.object({}).catchall(z.unknown()),
|
|
60
55
|
})
|
|
61
|
-
.strict()
|
|
62
|
-
.describe('Stable operation envelope; data is tool-specific and artifacts link bulk evidence.');
|
|
56
|
+
.strict();
|
|
63
57
|
/**
|
|
64
58
|
* Advertise one compact operation envelope while retaining exact per-tool runtime validation.
|
|
65
59
|
* The generic operation-result schema remains available for stored envelopes.
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { publicArtifactLink } from '../../core/artifacts.js';
|
|
3
|
+
import { compareCodeUnits } from '../../core/canonical.js';
|
|
4
|
+
import { emptyServiceResult } from '../../core/result.js';
|
|
5
|
+
import { EventChainViewer, eventDiagnostics, } from '../../event/service.js';
|
|
6
|
+
import { eventDirectionSchema, eventGraphReferenceSchema, eventImpactSubjectSchema, eventInspectModeSchema, eventProposedSourceSchema, eventRenderViewSchema, eventSelectorSchema, eventStateSubjectSchema, } from '../../schemas/event.js';
|
|
7
|
+
import { workspaceIdSchema } from '../../schemas/common.js';
|
|
8
|
+
import { compactValidatedInputSchema } from '../server/context-schemas.js';
|
|
9
|
+
import { nonNegativeIntegerSchema, sha256Schema } from '../server/output-schemas.js';
|
|
10
|
+
import { progressReporter } from '../server/progress.js';
|
|
11
|
+
import { errorResult, setInlineFilesScanned, strictOperationResultSchema, toolResult, } from '../server/result.js';
|
|
12
|
+
const nestedSelectorSchema = compactValidatedInputSchema(eventSelectorSchema, 'Event selector; see docs/events.md.');
|
|
13
|
+
const nestedStateSubjectSchema = compactValidatedInputSchema(eventStateSubjectSchema, 'State kind and name.');
|
|
14
|
+
const nestedImpactSubjectSchema = compactValidatedInputSchema(eventImpactSubjectSchema, 'Impact kind and name.');
|
|
15
|
+
const nestedGraphReferenceSchema = compactValidatedInputSchema(eventGraphReferenceSchema, 'Cached revision or graph artifact URI.');
|
|
16
|
+
const nestedProposedSourceSchema = compactValidatedInputSchema(eventProposedSourceSchema, 'In-memory source overlay; never written.');
|
|
17
|
+
const eventInspectInputSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
workspaceId: workspaceIdSchema,
|
|
20
|
+
mode: eventInspectModeSchema,
|
|
21
|
+
selector: nestedSelectorSchema.optional(),
|
|
22
|
+
from: nestedSelectorSchema.optional(),
|
|
23
|
+
to: nestedSelectorSchema.optional(),
|
|
24
|
+
direction: eventDirectionSchema.optional(),
|
|
25
|
+
maxDepth: z.number().int().min(1).max(64).optional(),
|
|
26
|
+
maxNodes: z.number().int().min(1).max(5_000).optional(),
|
|
27
|
+
maxEdges: z.number().int().min(1).max(20_000).optional(),
|
|
28
|
+
expandHelpers: z.boolean().optional(),
|
|
29
|
+
stateSubject: nestedStateSubjectSchema.optional(),
|
|
30
|
+
impactSubject: nestedImpactSubjectSchema.optional(),
|
|
31
|
+
refresh: z.boolean().optional(),
|
|
32
|
+
})
|
|
33
|
+
.strict()
|
|
34
|
+
.superRefine((value, context) => {
|
|
35
|
+
if (value.mode === 'trace' && value.selector === undefined) {
|
|
36
|
+
context.addIssue({ code: 'custom', path: ['selector'], message: 'Trace requires selector' });
|
|
37
|
+
}
|
|
38
|
+
if (value.mode === 'explain_path' && (value.from === undefined || value.to === undefined)) {
|
|
39
|
+
context.addIssue({
|
|
40
|
+
code: 'custom',
|
|
41
|
+
path: value.from === undefined ? ['from'] : ['to'],
|
|
42
|
+
message: 'Path explanation requires from and to',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (value.mode === 'impact' && value.impactSubject === undefined) {
|
|
46
|
+
context.addIssue({
|
|
47
|
+
code: 'custom',
|
|
48
|
+
path: ['impactSubject'],
|
|
49
|
+
message: 'Impact analysis requires impactSubject',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const eventRenderInputSchema = z
|
|
54
|
+
.object({
|
|
55
|
+
workspaceId: workspaceIdSchema,
|
|
56
|
+
view: eventRenderViewSchema,
|
|
57
|
+
selector: nestedSelectorSchema.optional(),
|
|
58
|
+
direction: eventDirectionSchema.optional(),
|
|
59
|
+
maxDepth: z.number().int().min(1).max(64).optional(),
|
|
60
|
+
maxNodes: z.number().int().min(1).max(240).optional(),
|
|
61
|
+
expandHelpers: z.boolean().optional(),
|
|
62
|
+
includeHtml: z.boolean().optional(),
|
|
63
|
+
refresh: z.boolean().optional(),
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
66
|
+
const eventCompareInputSchema = z
|
|
67
|
+
.object({
|
|
68
|
+
workspaceId: workspaceIdSchema,
|
|
69
|
+
before: nestedGraphReferenceSchema.optional(),
|
|
70
|
+
after: nestedGraphReferenceSchema.optional(),
|
|
71
|
+
proposedSources: z.array(nestedProposedSourceSchema).min(1).max(64).optional(),
|
|
72
|
+
render: z.boolean().optional(),
|
|
73
|
+
maxRenderNodes: z.number().int().min(1).max(240).optional(),
|
|
74
|
+
refresh: z.boolean().optional(),
|
|
75
|
+
})
|
|
76
|
+
.strict()
|
|
77
|
+
.superRefine((value, context) => {
|
|
78
|
+
if (value.after !== undefined && value.proposedSources !== undefined) {
|
|
79
|
+
context.addIssue({
|
|
80
|
+
code: 'custom',
|
|
81
|
+
path: ['after'],
|
|
82
|
+
message: 'after and proposedSources are mutually exclusive',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
const eventGraphCountsSchema = z
|
|
87
|
+
.object({
|
|
88
|
+
events: nonNegativeIntegerSchema,
|
|
89
|
+
options: nonNegativeIntegerSchema,
|
|
90
|
+
entries: nonNegativeIntegerSchema,
|
|
91
|
+
helpers: nonNegativeIntegerSchema,
|
|
92
|
+
unresolvedNodes: nonNegativeIntegerSchema,
|
|
93
|
+
terminals: nonNegativeIntegerSchema,
|
|
94
|
+
edges: nonNegativeIntegerSchema,
|
|
95
|
+
derivedEdges: nonNegativeIntegerSchema,
|
|
96
|
+
stateAccesses: nonNegativeIntegerSchema,
|
|
97
|
+
issues: nonNegativeIntegerSchema,
|
|
98
|
+
diagnostics: nonNegativeIntegerSchema,
|
|
99
|
+
blockingDiagnostics: nonNegativeIntegerSchema,
|
|
100
|
+
skippedSources: nonNegativeIntegerSchema,
|
|
101
|
+
artifacts: nonNegativeIntegerSchema,
|
|
102
|
+
})
|
|
103
|
+
.strict();
|
|
104
|
+
const inspectBoundarySchema = z
|
|
105
|
+
.object({
|
|
106
|
+
direction: eventDirectionSchema,
|
|
107
|
+
maxDepth: nonNegativeIntegerSchema,
|
|
108
|
+
maxNodes: nonNegativeIntegerSchema,
|
|
109
|
+
maxEdges: nonNegativeIntegerSchema,
|
|
110
|
+
expandHelpers: z.boolean(),
|
|
111
|
+
refresh: z.boolean(),
|
|
112
|
+
})
|
|
113
|
+
.strict();
|
|
114
|
+
const renderHashSchema = z
|
|
115
|
+
.object({
|
|
116
|
+
json: sha256Schema,
|
|
117
|
+
svg: sha256Schema,
|
|
118
|
+
png: sha256Schema,
|
|
119
|
+
html: sha256Schema.optional(),
|
|
120
|
+
})
|
|
121
|
+
.strict();
|
|
122
|
+
const eventInspectOutputSchema = strictOperationResultSchema(z
|
|
123
|
+
.object({
|
|
124
|
+
mode: eventInspectModeSchema,
|
|
125
|
+
revision: sha256Schema,
|
|
126
|
+
graphHash: sha256Schema,
|
|
127
|
+
counts: eventGraphCountsSchema,
|
|
128
|
+
boundary: inspectBoundarySchema,
|
|
129
|
+
})
|
|
130
|
+
.strict());
|
|
131
|
+
const eventRenderOutputSchema = strictOperationResultSchema(z
|
|
132
|
+
.object({
|
|
133
|
+
view: eventRenderViewSchema,
|
|
134
|
+
revision: sha256Schema,
|
|
135
|
+
graphHash: sha256Schema,
|
|
136
|
+
layoutHash: sha256Schema,
|
|
137
|
+
hashes: renderHashSchema,
|
|
138
|
+
counts: eventGraphCountsSchema.extend({
|
|
139
|
+
selectedNodes: nonNegativeIntegerSchema,
|
|
140
|
+
omittedNodes: nonNegativeIntegerSchema,
|
|
141
|
+
branchRenders: nonNegativeIntegerSchema,
|
|
142
|
+
}),
|
|
143
|
+
boundary: z
|
|
144
|
+
.object({
|
|
145
|
+
direction: eventDirectionSchema,
|
|
146
|
+
maxDepth: nonNegativeIntegerSchema,
|
|
147
|
+
maxNodes: nonNegativeIntegerSchema,
|
|
148
|
+
expandHelpers: z.boolean(),
|
|
149
|
+
includeHtml: z.boolean(),
|
|
150
|
+
refresh: z.boolean(),
|
|
151
|
+
})
|
|
152
|
+
.strict(),
|
|
153
|
+
})
|
|
154
|
+
.strict());
|
|
155
|
+
const eventCompareOutputSchema = strictOperationResultSchema(z
|
|
156
|
+
.object({
|
|
157
|
+
beforeRevision: sha256Schema,
|
|
158
|
+
afterRevision: sha256Schema,
|
|
159
|
+
beforeGraphHash: sha256Schema,
|
|
160
|
+
afterGraphHash: sha256Schema,
|
|
161
|
+
renderHashes: renderHashSchema.optional(),
|
|
162
|
+
counts: z
|
|
163
|
+
.object({
|
|
164
|
+
changes: nonNegativeIntegerSchema,
|
|
165
|
+
addedNodes: nonNegativeIntegerSchema,
|
|
166
|
+
removedNodes: nonNegativeIntegerSchema,
|
|
167
|
+
changedNodes: nonNegativeIntegerSchema,
|
|
168
|
+
addedEdges: nonNegativeIntegerSchema,
|
|
169
|
+
removedEdges: nonNegativeIntegerSchema,
|
|
170
|
+
changedEdges: nonNegativeIntegerSchema,
|
|
171
|
+
addedStateAccesses: nonNegativeIntegerSchema,
|
|
172
|
+
removedStateAccesses: nonNegativeIntegerSchema,
|
|
173
|
+
changedStateAccesses: nonNegativeIntegerSchema,
|
|
174
|
+
addedStateLinks: nonNegativeIntegerSchema,
|
|
175
|
+
removedStateLinks: nonNegativeIntegerSchema,
|
|
176
|
+
changedStateLinks: nonNegativeIntegerSchema,
|
|
177
|
+
addedDiagnostics: nonNegativeIntegerSchema,
|
|
178
|
+
resolvedDiagnostics: nonNegativeIntegerSchema,
|
|
179
|
+
addedUnresolved: nonNegativeIntegerSchema,
|
|
180
|
+
resolvedUnresolved: nonNegativeIntegerSchema,
|
|
181
|
+
disconnectedRoots: nonNegativeIntegerSchema,
|
|
182
|
+
disconnectedBranches: nonNegativeIntegerSchema,
|
|
183
|
+
disconnectedTerminals: nonNegativeIntegerSchema,
|
|
184
|
+
beforeSkippedSources: nonNegativeIntegerSchema,
|
|
185
|
+
afterSkippedSources: nonNegativeIntegerSchema,
|
|
186
|
+
artifacts: nonNegativeIntegerSchema,
|
|
187
|
+
})
|
|
188
|
+
.strict(),
|
|
189
|
+
boundary: z
|
|
190
|
+
.object({
|
|
191
|
+
proposedSources: nonNegativeIntegerSchema,
|
|
192
|
+
render: z.boolean(),
|
|
193
|
+
maxRenderNodes: nonNegativeIntegerSchema,
|
|
194
|
+
refresh: z.boolean(),
|
|
195
|
+
})
|
|
196
|
+
.strict(),
|
|
197
|
+
})
|
|
198
|
+
.strict());
|
|
199
|
+
const readOnlyEventTool = {
|
|
200
|
+
readOnlyHint: true,
|
|
201
|
+
destructiveHint: false,
|
|
202
|
+
idempotentHint: true,
|
|
203
|
+
openWorldHint: false,
|
|
204
|
+
};
|
|
205
|
+
function graphCounts(graph, artifacts) {
|
|
206
|
+
const diagnostics = eventDiagnostics(graph);
|
|
207
|
+
return {
|
|
208
|
+
events: graph.statistics.eventCount,
|
|
209
|
+
options: graph.statistics.optionCount,
|
|
210
|
+
entries: graph.statistics.entryCount,
|
|
211
|
+
helpers: graph.statistics.helperCount,
|
|
212
|
+
unresolvedNodes: graph.statistics.unresolvedNodeCount,
|
|
213
|
+
terminals: graph.statistics.terminalCount,
|
|
214
|
+
edges: graph.statistics.edgeCount,
|
|
215
|
+
derivedEdges: graph.statistics.derivedEdgeCount,
|
|
216
|
+
stateAccesses: graph.statistics.stateAccessCount,
|
|
217
|
+
issues: graph.statistics.issueCount,
|
|
218
|
+
diagnostics: diagnostics.length,
|
|
219
|
+
blockingDiagnostics: diagnostics.filter(({ severity }) => severity === 'error' || severity === 'blocker').length,
|
|
220
|
+
skippedSources: graph.skippedSourceCount,
|
|
221
|
+
artifacts,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function graphValidation(graph) {
|
|
225
|
+
const counts = graphCounts(graph, 0);
|
|
226
|
+
return {
|
|
227
|
+
passed: graph.complete && counts.blockingDiagnostics === 0,
|
|
228
|
+
checks: [
|
|
229
|
+
{
|
|
230
|
+
id: 'event-analysis',
|
|
231
|
+
passed: graph.complete && counts.blockingDiagnostics === 0,
|
|
232
|
+
message: graph.complete
|
|
233
|
+
? `${counts.blockingDiagnostics} blocking event-chain diagnostics; full evidence is linked`
|
|
234
|
+
: `${graph.skippedSourceCount} event-analysis source(s) were skipped; full evidence is linked`,
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function inspectRequest(input, context, signal) {
|
|
240
|
+
return {
|
|
241
|
+
workspaceId: input.workspaceId,
|
|
242
|
+
mode: input.mode,
|
|
243
|
+
...(input.selector === undefined
|
|
244
|
+
? {}
|
|
245
|
+
: { selector: input.selector }),
|
|
246
|
+
...(input.from === undefined
|
|
247
|
+
? {}
|
|
248
|
+
: { from: input.from }),
|
|
249
|
+
...(input.to === undefined ? {} : { to: input.to }),
|
|
250
|
+
...(input.direction === undefined ? {} : { direction: input.direction }),
|
|
251
|
+
...(input.maxDepth === undefined ? {} : { maxDepth: input.maxDepth }),
|
|
252
|
+
...(input.maxNodes === undefined ? {} : { maxNodes: input.maxNodes }),
|
|
253
|
+
...(input.maxEdges === undefined ? {} : { maxEdges: input.maxEdges }),
|
|
254
|
+
...(input.expandHelpers === undefined ? {} : { expandHelpers: input.expandHelpers }),
|
|
255
|
+
...(input.stateSubject === undefined ? {} : { stateSubject: input.stateSubject }),
|
|
256
|
+
...(input.impactSubject === undefined ? {} : { impactSubject: input.impactSubject }),
|
|
257
|
+
...(input.refresh === undefined ? {} : { refresh: input.refresh }),
|
|
258
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
259
|
+
signal,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function renderRequest(input, context, signal) {
|
|
263
|
+
return {
|
|
264
|
+
workspaceId: input.workspaceId,
|
|
265
|
+
view: input.view,
|
|
266
|
+
...(input.selector === undefined
|
|
267
|
+
? {}
|
|
268
|
+
: { selector: input.selector }),
|
|
269
|
+
...(input.direction === undefined ? {} : { direction: input.direction }),
|
|
270
|
+
...(input.maxDepth === undefined ? {} : { maxDepth: input.maxDepth }),
|
|
271
|
+
...(input.maxNodes === undefined ? {} : { maxNodes: input.maxNodes }),
|
|
272
|
+
...(input.expandHelpers === undefined ? {} : { expandHelpers: input.expandHelpers }),
|
|
273
|
+
...(input.includeHtml === undefined ? {} : { includeHtml: input.includeHtml }),
|
|
274
|
+
...(input.refresh === undefined ? {} : { refresh: input.refresh }),
|
|
275
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
276
|
+
signal,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function compareRequest(input, context, signal) {
|
|
280
|
+
return {
|
|
281
|
+
workspaceId: input.workspaceId,
|
|
282
|
+
...(input.before === undefined
|
|
283
|
+
? {}
|
|
284
|
+
: { before: input.before }),
|
|
285
|
+
...(input.after === undefined
|
|
286
|
+
? {}
|
|
287
|
+
: { after: input.after }),
|
|
288
|
+
...(input.proposedSources === undefined
|
|
289
|
+
? {}
|
|
290
|
+
: {
|
|
291
|
+
proposedSources: input.proposedSources,
|
|
292
|
+
}),
|
|
293
|
+
...(input.render === undefined ? {} : { render: input.render }),
|
|
294
|
+
...(input.maxRenderNodes === undefined ? {} : { maxRenderNodes: input.maxRenderNodes }),
|
|
295
|
+
...(input.refresh === undefined ? {} : { refresh: input.refresh }),
|
|
296
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
297
|
+
signal,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
export function registerEventTools(server, engine, context) {
|
|
301
|
+
const viewer = new EventChainViewer(engine);
|
|
302
|
+
server.registerTool('hoi4.event_inspect', {
|
|
303
|
+
title: 'Inspect event chains',
|
|
304
|
+
description: 'Scan, find roots, trace, explain paths, inspect state flow, lint, or assess impact. Full source-linked reports are resources.',
|
|
305
|
+
inputSchema: eventInspectInputSchema,
|
|
306
|
+
outputSchema: eventInspectOutputSchema,
|
|
307
|
+
annotations: readOnlyEventTool,
|
|
308
|
+
}, async (input, extra) => {
|
|
309
|
+
const { workspaceId } = input;
|
|
310
|
+
try {
|
|
311
|
+
const progress = progressReporter(extra);
|
|
312
|
+
await progress.report(0, 3, 'Analyzing event chain');
|
|
313
|
+
const inspected = await viewer.inspect(inspectRequest(input, context, progress.signal));
|
|
314
|
+
await progress.report(2, 3, 'Linking complete event analysis');
|
|
315
|
+
const result = emptyServiceResult(workspaceId, {
|
|
316
|
+
mode: inspected.mode,
|
|
317
|
+
revision: inspected.graph.revision,
|
|
318
|
+
graphHash: inspected.graphHash,
|
|
319
|
+
counts: graphCounts(inspected.graph, inspected.artifacts.length),
|
|
320
|
+
boundary: {
|
|
321
|
+
direction: input.direction ?? 'both',
|
|
322
|
+
maxDepth: input.maxDepth ?? 8,
|
|
323
|
+
maxNodes: input.maxNodes ?? 500,
|
|
324
|
+
maxEdges: input.maxEdges ?? 2_000,
|
|
325
|
+
expandHelpers: input.expandHelpers ?? (input.mode === 'explain_path' ? true : false),
|
|
326
|
+
refresh: input.refresh ?? input.mode === 'scan',
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
result.code = inspected.graph.complete ? 'EVENT_INSPECTED' : 'EVENT_INSPECTED_PARTIAL';
|
|
330
|
+
setInlineFilesScanned(result, inspected.graph.filesScanned);
|
|
331
|
+
result.artifacts = inspected.artifacts.map(publicArtifactLink);
|
|
332
|
+
result.validation = graphValidation(inspected.graph);
|
|
333
|
+
await progress.report(3, 3, 'Event inspection complete');
|
|
334
|
+
return toolResult(result);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
return errorResult(error, workspaceId);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
server.registerTool('hoi4.event_render', {
|
|
341
|
+
title: 'Render event chains',
|
|
342
|
+
description: 'Render deterministic JSON, SVG, PNG, and optional HTML for an event-chain view. Complete artifacts retain source links.',
|
|
343
|
+
inputSchema: eventRenderInputSchema,
|
|
344
|
+
outputSchema: eventRenderOutputSchema,
|
|
345
|
+
annotations: readOnlyEventTool,
|
|
346
|
+
}, async (input, extra) => {
|
|
347
|
+
const { workspaceId } = input;
|
|
348
|
+
try {
|
|
349
|
+
const progress = progressReporter(extra);
|
|
350
|
+
await progress.report(0, 3, 'Rendering event-chain view');
|
|
351
|
+
const rendered = await viewer.renderAndStore(renderRequest(input, context, progress.signal));
|
|
352
|
+
await progress.report(2, 3, 'Linking complete event renders');
|
|
353
|
+
const result = emptyServiceResult(workspaceId, {
|
|
354
|
+
view: rendered.render.view,
|
|
355
|
+
revision: rendered.graph.revision,
|
|
356
|
+
graphHash: rendered.graphHash,
|
|
357
|
+
layoutHash: rendered.render.layout.layoutHash,
|
|
358
|
+
hashes: rendered.render.hashes,
|
|
359
|
+
counts: {
|
|
360
|
+
...graphCounts(rendered.graph, rendered.artifacts.length),
|
|
361
|
+
selectedNodes: rendered.render.selectedNodeIds.length,
|
|
362
|
+
omittedNodes: rendered.render.omittedNodeCount,
|
|
363
|
+
branchRenders: rendered.branches.length,
|
|
364
|
+
},
|
|
365
|
+
boundary: {
|
|
366
|
+
direction: input.direction ?? 'both',
|
|
367
|
+
maxDepth: input.maxDepth ?? 4,
|
|
368
|
+
maxNodes: input.maxNodes ?? 120,
|
|
369
|
+
expandHelpers: input.expandHelpers ?? false,
|
|
370
|
+
includeHtml: input.includeHtml ?? false,
|
|
371
|
+
refresh: input.refresh ?? false,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
result.code = rendered.graph.complete ? 'EVENT_RENDERED' : 'EVENT_RENDERED_PARTIAL';
|
|
375
|
+
setInlineFilesScanned(result, rendered.graph.filesScanned);
|
|
376
|
+
result.artifacts = rendered.artifacts.map(publicArtifactLink);
|
|
377
|
+
result.validation = graphValidation(rendered.graph);
|
|
378
|
+
await progress.report(3, 3, 'Event render complete');
|
|
379
|
+
return toolResult(result);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
return errorResult(error, workspaceId);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
server.registerTool('hoi4.event_compare', {
|
|
386
|
+
title: 'Compare event chains',
|
|
387
|
+
description: 'Compare cached, artifact-backed, current, or in-memory proposed event graphs without writing source. Full changes are resources.',
|
|
388
|
+
inputSchema: eventCompareInputSchema,
|
|
389
|
+
outputSchema: eventCompareOutputSchema,
|
|
390
|
+
annotations: readOnlyEventTool,
|
|
391
|
+
}, async (input, extra) => {
|
|
392
|
+
const { workspaceId } = input;
|
|
393
|
+
try {
|
|
394
|
+
const progress = progressReporter(extra);
|
|
395
|
+
await progress.report(0, 3, 'Comparing event-chain graphs');
|
|
396
|
+
const compared = await viewer.compareAndStore(compareRequest(input, context, progress.signal));
|
|
397
|
+
await progress.report(2, 3, 'Linking complete event comparison');
|
|
398
|
+
const { comparison } = compared;
|
|
399
|
+
const result = emptyServiceResult(workspaceId, {
|
|
400
|
+
beforeRevision: comparison.beforeRevision,
|
|
401
|
+
afterRevision: comparison.afterRevision,
|
|
402
|
+
beforeGraphHash: comparison.beforeGraphHash,
|
|
403
|
+
afterGraphHash: comparison.afterGraphHash,
|
|
404
|
+
...(compared.render === undefined ? {} : { renderHashes: compared.render.hashes }),
|
|
405
|
+
counts: {
|
|
406
|
+
changes: comparison.changes.length,
|
|
407
|
+
addedNodes: comparison.addedNodeIds.length,
|
|
408
|
+
removedNodes: comparison.removedNodeIds.length,
|
|
409
|
+
changedNodes: comparison.changedNodeIds.length,
|
|
410
|
+
addedEdges: comparison.addedEdgeIds.length,
|
|
411
|
+
removedEdges: comparison.removedEdgeIds.length,
|
|
412
|
+
changedEdges: comparison.changedEdgeIds.length,
|
|
413
|
+
addedStateAccesses: comparison.addedStateAccessIds.length,
|
|
414
|
+
removedStateAccesses: comparison.removedStateAccessIds.length,
|
|
415
|
+
changedStateAccesses: comparison.changedStateAccessIds.length,
|
|
416
|
+
addedStateLinks: comparison.addedStateLinkIds.length,
|
|
417
|
+
removedStateLinks: comparison.removedStateLinkIds.length,
|
|
418
|
+
changedStateLinks: comparison.changedStateLinkIds.length,
|
|
419
|
+
addedDiagnostics: comparison.addedIssueIds.length,
|
|
420
|
+
resolvedDiagnostics: comparison.resolvedIssueIds.length,
|
|
421
|
+
addedUnresolved: comparison.addedUnresolvedIds.length,
|
|
422
|
+
resolvedUnresolved: comparison.resolvedUnresolvedIds.length,
|
|
423
|
+
disconnectedRoots: comparison.newlyDisconnectedRootIds.length,
|
|
424
|
+
disconnectedBranches: comparison.newlyDisconnectedBranchIds.length,
|
|
425
|
+
disconnectedTerminals: comparison.newlyDisconnectedTerminalIds.length,
|
|
426
|
+
beforeSkippedSources: compared.before.skippedSourceCount,
|
|
427
|
+
afterSkippedSources: compared.after.skippedSourceCount,
|
|
428
|
+
artifacts: compared.artifacts.length,
|
|
429
|
+
},
|
|
430
|
+
boundary: {
|
|
431
|
+
proposedSources: input.proposedSources?.length ?? 0,
|
|
432
|
+
render: input.render ?? true,
|
|
433
|
+
maxRenderNodes: input.maxRenderNodes ?? 120,
|
|
434
|
+
refresh: compared.refresh,
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
result.code =
|
|
438
|
+
compared.before.complete && compared.after.complete
|
|
439
|
+
? 'EVENT_COMPARED'
|
|
440
|
+
: 'EVENT_COMPARED_PARTIAL';
|
|
441
|
+
setInlineFilesScanned(result, [...new Set([...compared.before.filesScanned, ...compared.after.filesScanned])].sort(compareCodeUnits));
|
|
442
|
+
result.artifacts = compared.artifacts.map(publicArtifactLink);
|
|
443
|
+
const beforeValidation = graphValidation(compared.before);
|
|
444
|
+
const afterValidation = graphValidation(compared.after);
|
|
445
|
+
result.validation = {
|
|
446
|
+
passed: beforeValidation.passed && afterValidation.passed,
|
|
447
|
+
checks: [...beforeValidation.checks, ...afterValidation.checks].map((check, index) => ({
|
|
448
|
+
...check,
|
|
449
|
+
id: `${index === 0 ? 'before' : 'after'}-${check.id}`,
|
|
450
|
+
})),
|
|
451
|
+
};
|
|
452
|
+
await progress.report(3, 3, 'Event comparison complete');
|
|
453
|
+
return toolResult(result);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
return errorResult(error, workspaceId);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { SOURCE_MAX_BYTES } from '../core/source/index.js';
|
|
3
|
+
import { workspaceRelativePathSchema } from './common.js';
|
|
4
|
+
const eventIdSchema = z.string().min(1).max(256);
|
|
5
|
+
const eventSourcePathSchema = z.string().min(1).max(1024);
|
|
6
|
+
const eventNodeIdSchema = z.string().min(1).max(1024);
|
|
7
|
+
const eventSha256Schema = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
8
|
+
export const eventInspectModeSchema = z.enum([
|
|
9
|
+
'scan',
|
|
10
|
+
'roots',
|
|
11
|
+
'trace',
|
|
12
|
+
'explain_path',
|
|
13
|
+
'state_flow',
|
|
14
|
+
'lint',
|
|
15
|
+
'impact',
|
|
16
|
+
]);
|
|
17
|
+
export const eventDirectionSchema = z.enum(['upstream', 'downstream', 'both']);
|
|
18
|
+
export const eventRenderViewSchema = z.enum([
|
|
19
|
+
'overview',
|
|
20
|
+
'neighborhood',
|
|
21
|
+
'options',
|
|
22
|
+
'entries',
|
|
23
|
+
'reachability',
|
|
24
|
+
'timing',
|
|
25
|
+
'state',
|
|
26
|
+
'targets',
|
|
27
|
+
'scope',
|
|
28
|
+
'terminals',
|
|
29
|
+
'unresolved',
|
|
30
|
+
]);
|
|
31
|
+
export const eventFeatureManifestSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
id: z.string().min(1).max(128).optional(),
|
|
34
|
+
eventIds: z.array(eventIdSchema).max(2_000).optional(),
|
|
35
|
+
namespaces: z.array(z.string().min(1).max(256)).max(500).optional(),
|
|
36
|
+
sourcePaths: z.array(eventSourcePathSchema).max(2_000).optional(),
|
|
37
|
+
nodeIds: z.array(eventNodeIdSchema).max(5_000).optional(),
|
|
38
|
+
})
|
|
39
|
+
.strict();
|
|
40
|
+
export const eventSelectorSchema = z.discriminatedUnion('kind', [
|
|
41
|
+
z.object({ kind: z.literal('event'), eventId: eventIdSchema }).strict(),
|
|
42
|
+
z.object({ kind: z.literal('namespace'), namespace: z.string().min(1).max(256) }).strict(),
|
|
43
|
+
z.object({ kind: z.literal('file'), sourcePath: eventSourcePathSchema }).strict(),
|
|
44
|
+
z
|
|
45
|
+
.object({
|
|
46
|
+
kind: z.literal('source'),
|
|
47
|
+
sourcePath: eventSourcePathSchema,
|
|
48
|
+
line: z.number().int().positive(),
|
|
49
|
+
column: z.number().int().positive().optional(),
|
|
50
|
+
})
|
|
51
|
+
.strict(),
|
|
52
|
+
z.object({ kind: z.literal('node'), nodeId: eventNodeIdSchema }).strict(),
|
|
53
|
+
z.object({ kind: z.literal('manifest'), manifest: eventFeatureManifestSchema }).strict(),
|
|
54
|
+
]);
|
|
55
|
+
export const eventStateSubjectSchema = z
|
|
56
|
+
.object({
|
|
57
|
+
kind: z.enum([
|
|
58
|
+
'country_flag',
|
|
59
|
+
'global_flag',
|
|
60
|
+
'state_flag',
|
|
61
|
+
'variable',
|
|
62
|
+
'global_variable',
|
|
63
|
+
'array',
|
|
64
|
+
'event_target',
|
|
65
|
+
'global_event_target',
|
|
66
|
+
'saved_scope',
|
|
67
|
+
]),
|
|
68
|
+
name: z.string().min(1).max(512),
|
|
69
|
+
})
|
|
70
|
+
.strict();
|
|
71
|
+
export const eventImpactSubjectSchema = z
|
|
72
|
+
.object({
|
|
73
|
+
kind: z.enum(['event', 'helper', 'flag', 'variable', 'array', 'event_target', 'saved_scope']),
|
|
74
|
+
name: z.string().min(1).max(512),
|
|
75
|
+
})
|
|
76
|
+
.strict();
|
|
77
|
+
export const eventGraphReferenceSchema = z
|
|
78
|
+
.object({
|
|
79
|
+
revision: eventSha256Schema.optional(),
|
|
80
|
+
artifactUri: z
|
|
81
|
+
.string()
|
|
82
|
+
.min(1)
|
|
83
|
+
.max(8_192)
|
|
84
|
+
.regex(/^hoi4-agent:\/\//u)
|
|
85
|
+
.optional(),
|
|
86
|
+
})
|
|
87
|
+
.strict()
|
|
88
|
+
.refine(({ revision, artifactUri }) => Number(revision !== undefined) + Number(artifactUri !== undefined) === 1, 'Provide exactly one event graph revision or artifact URI');
|
|
89
|
+
export const eventProposedSourceSchema = z
|
|
90
|
+
.object({
|
|
91
|
+
relativePath: workspaceRelativePathSchema,
|
|
92
|
+
source: z.string().max(SOURCE_MAX_BYTES).nullable(),
|
|
93
|
+
expectedSourceHash: eventSha256Schema.optional(),
|
|
94
|
+
})
|
|
95
|
+
.strict();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
export const PACKAGE_NAME = 'hoi4-agent-tools';
|
|
3
|
-
export const PACKAGE_VERSION = '1.
|
|
3
|
+
export const PACKAGE_VERSION = '1.2.0';
|
|
4
4
|
export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
|
|
5
5
|
export const CONFIG_VERSION = 1;
|
|
6
6
|
export const TRANSACTION_VERSION = 1;
|
package/docs/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Documentation
|
|
2
2
|
|
|
3
3
|
- [Setup](setup.md): install the server, configure HOI4 paths, and connect an MCP client.
|
|
4
|
+
- [Event chains](events.md): inspect, trace, lint, render, and compare event chains.
|
|
4
5
|
- [Focus trees](focus.md): inspect, create, render, and clean national or continuous focus content.
|
|
5
6
|
- [Scripted GUIs](gui.md): inspect linked UI source, render states, and create or repair GUIs.
|
|
6
7
|
- [Maps](map.md): inspect and edit provinces, states, regions, adjacency, supply, and railways.
|
package/docs/development.md
CHANGED
|
@@ -19,4 +19,14 @@ npm run inspector
|
|
|
19
19
|
|
|
20
20
|
Keep changes focused and include tests for behavior changes. CI fixtures must be synthetic and project-owned; never commit installed-game or third-party-mod content. Public tool or schema changes require compatibility review and a versioned release.
|
|
21
21
|
|
|
22
|
+
The event-chain acceptance fixture contains more than 300 project-owned event definitions and exercises routes, options, timing, state flow, scope changes, unresolved dynamic calls, rendering, and comparison. Local opt-in tests can also read an installed game and an external mod without copying their sources:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
HOI4_GAME_ROOT="/games/Hearts of Iron IV" \
|
|
26
|
+
HOI4_EXTERNAL_MOD_ROOT="/projects/hoi4-mod" \
|
|
27
|
+
npm run test:local
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Add `HOI4_DEPENDENCY_ROOTS` as the platform path-delimited list when the external mod has dependencies. Local tests remain read-only.
|
|
31
|
+
|
|
22
32
|
See the package [Security Policy](../SECURITY.md) for private vulnerability reporting.
|