hoi4-agent-tools 1.1.0 → 2.0.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 +19 -0
- package/README.md +21 -18
- package/dist/hoi4_agent_tools/core/engine.js +12 -0
- package/dist/hoi4_agent_tools/core/workspace.js +31 -3
- 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/gui/layout.js +122 -27
- package/dist/hoi4_agent_tools/gui/renderer.js +16 -0
- package/dist/hoi4_agent_tools/mcp/resources/register.js +1 -1
- package/dist/hoi4_agent_tools/mcp/server/create.js +4 -2
- 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/mcp/tools/focus.js +6 -3
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +6 -3
- 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/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/focus.md +1 -1
- package/docs/gui.md +3 -1
- package/docs/map.md +1 -1
- package/docs/setup.md +10 -7
- package/package.json +7 -3
- package/server.json +3 -3
- package/dist/hoi4_agent_tools/mcp/tools/mods.js +0 -54
|
@@ -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 = engine.resolver.resolveWorkspaceId(input.workspaceId, context.principal);
|
|
310
|
+
try {
|
|
311
|
+
const progress = progressReporter(extra);
|
|
312
|
+
await progress.report(0, 3, 'Analyzing event chain');
|
|
313
|
+
const inspected = await viewer.inspect(inspectRequest({ ...input, workspaceId }, 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 = engine.resolver.resolveWorkspaceId(input.workspaceId, context.principal);
|
|
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, workspaceId }, 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 = engine.resolver.resolveWorkspaceId(input.workspaceId, context.principal);
|
|
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, workspaceId }, 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
|
+
}
|
|
@@ -451,7 +451,8 @@ export function registerFocusTools(server, engine, context) {
|
|
|
451
451
|
outputSchema: focusInspectOutputSchema,
|
|
452
452
|
annotations: artifactProducing,
|
|
453
453
|
}, async (input, extra) => {
|
|
454
|
-
const { workspaceId, relativePath } = input;
|
|
454
|
+
const { workspaceId: requestedWorkspaceId, relativePath } = input;
|
|
455
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
|
|
455
456
|
try {
|
|
456
457
|
const progress = progressReporter(extra);
|
|
457
458
|
await progress.report(0, 3, 'Building shared workspace index');
|
|
@@ -667,7 +668,8 @@ export function registerFocusTools(server, engine, context) {
|
|
|
667
668
|
outputSchema: focusRenderOutputSchema,
|
|
668
669
|
annotations: artifactProducing,
|
|
669
670
|
}, async (input, extra) => {
|
|
670
|
-
const { workspaceId, relativePath } = input;
|
|
671
|
+
const { workspaceId: requestedWorkspaceId, relativePath } = input;
|
|
672
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
|
|
671
673
|
try {
|
|
672
674
|
const progress = progressReporter(extra);
|
|
673
675
|
await progress.report(0, 3, 'Importing and indexing focus source');
|
|
@@ -790,7 +792,8 @@ export function registerFocusTools(server, engine, context) {
|
|
|
790
792
|
openWorldHint: false,
|
|
791
793
|
},
|
|
792
794
|
}, async (input, extra) => {
|
|
793
|
-
const { workspaceId, relativePath, createIfMissing, horizontalSpacing, verticalSpacing, padding, reviewScale, } = input;
|
|
795
|
+
const { workspaceId: requestedWorkspaceId, relativePath, createIfMissing, horizontalSpacing, verticalSpacing, padding, reviewScale, } = input;
|
|
796
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
|
|
794
797
|
try {
|
|
795
798
|
requireServerScope(context, 'hoi4:write');
|
|
796
799
|
const progress = progressReporter(extra);
|
|
@@ -149,7 +149,8 @@ export function registerGuiTools(server, engine, context) {
|
|
|
149
149
|
inputSchema: guiInspectInput,
|
|
150
150
|
outputSchema: guiScanOutputSchema,
|
|
151
151
|
annotations: artifactProducing,
|
|
152
|
-
}, async ({ workspaceId, windowName, scenario, relatedScenarios }, extra) => {
|
|
152
|
+
}, async ({ workspaceId: requestedWorkspaceId, windowName, scenario, relatedScenarios }, extra) => {
|
|
153
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
|
|
153
154
|
try {
|
|
154
155
|
const progress = progressReporter(extra);
|
|
155
156
|
await progress.report(0, 3, 'Building shared index and GUI inspection');
|
|
@@ -253,7 +254,8 @@ export function registerGuiTools(server, engine, context) {
|
|
|
253
254
|
}
|
|
254
255
|
});
|
|
255
256
|
const render = async (input, extra, code) => {
|
|
256
|
-
const
|
|
257
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(input.workspaceId, context.principal);
|
|
258
|
+
const { windowName } = input;
|
|
257
259
|
try {
|
|
258
260
|
const progress = progressReporter(extra);
|
|
259
261
|
await progress.report(0, 4, 'Building shared index and GUI source graph');
|
|
@@ -444,7 +446,8 @@ export function registerGuiTools(server, engine, context) {
|
|
|
444
446
|
openWorldHint: false,
|
|
445
447
|
},
|
|
446
448
|
}, async (input, extra) => {
|
|
447
|
-
const
|
|
449
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(input.workspaceId, context.principal);
|
|
450
|
+
const { relativePath, windowName, scenario } = input;
|
|
448
451
|
try {
|
|
449
452
|
requireServerScope(context, 'hoi4:write');
|
|
450
453
|
const progress = progressReporter(extra);
|
|
@@ -364,7 +364,8 @@ export function registerMapTools(server, engine, context) {
|
|
|
364
364
|
.strict(),
|
|
365
365
|
outputSchema: mapWorkspaceInspectOutputSchema,
|
|
366
366
|
annotations: artifactProducing,
|
|
367
|
-
}, async ({ workspaceId, provinceIds, stateIds, regionIds, allocationRequests }, extra) => {
|
|
367
|
+
}, async ({ workspaceId: requestedWorkspaceId, provinceIds, stateIds, regionIds, allocationRequests }, extra) => {
|
|
368
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
|
|
368
369
|
try {
|
|
369
370
|
const progress = progressReporter(extra);
|
|
370
371
|
await progress.report(0, 3, 'Inspecting and validating the map');
|
|
@@ -512,7 +513,8 @@ export function registerMapTools(server, engine, context) {
|
|
|
512
513
|
.strict(),
|
|
513
514
|
outputSchema: mapRenderOutputSchema,
|
|
514
515
|
annotations: artifactProducing,
|
|
515
|
-
}, async ({ workspaceId, layer, overlays, scale }, extra) => {
|
|
516
|
+
}, async ({ workspaceId: requestedWorkspaceId, layer, overlays, scale }, extra) => {
|
|
517
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
|
|
516
518
|
try {
|
|
517
519
|
const progress = progressReporter(extra);
|
|
518
520
|
await progress.report(0, 3, 'Rendering offline map layer');
|
|
@@ -557,7 +559,8 @@ export function registerMapTools(server, engine, context) {
|
|
|
557
559
|
idempotentHint: false,
|
|
558
560
|
openWorldHint: false,
|
|
559
561
|
},
|
|
560
|
-
}, async ({ workspaceId, operations, diffScale }, extra) => {
|
|
562
|
+
}, async ({ workspaceId: requestedWorkspaceId, operations, diffScale }, extra) => {
|
|
563
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
|
|
561
564
|
try {
|
|
562
565
|
requireServerScope(context, 'hoi4:write');
|
|
563
566
|
const progress = progressReporter(extra);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
|
-
import { loadConfiguration } from './core/configuration.js';
|
|
4
|
+
import { loadConfiguration, serverConfigurationSchema, } from './core/configuration.js';
|
|
4
5
|
import { CoreEngine } from './core/engine.js';
|
|
5
6
|
import { ServiceError } from './core/result.js';
|
|
6
7
|
import { WorkspaceResolver } from './core/workspace.js';
|
|
8
|
+
import { CONFIG_VERSION } from './version.js';
|
|
7
9
|
export function defaultConfigurationPath() {
|
|
8
10
|
const configRoot = process.env.APPDATA ?? process.env.XDG_CONFIG_HOME ?? path.join(homedir(), '.config');
|
|
9
11
|
return path.join(configRoot, 'hoi4-agent-tools', 'config.json');
|
|
@@ -21,8 +23,116 @@ export function configurationPath(arguments_ = process.argv.slice(2)) {
|
|
|
21
23
|
return path.resolve(process.env.HOI4_AGENT_CONFIG);
|
|
22
24
|
return defaultConfigurationPath();
|
|
23
25
|
}
|
|
26
|
+
async function isDirectory(candidate) {
|
|
27
|
+
try {
|
|
28
|
+
return (await stat(candidate)).isDirectory();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function isFile(candidate) {
|
|
35
|
+
try {
|
|
36
|
+
return (await stat(candidate)).isFile();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function findCurrentModRoot(start = process.cwd()) {
|
|
43
|
+
for (let cursor = path.resolve(start);;) {
|
|
44
|
+
if ((await isFile(path.join(cursor, 'descriptor.mod'))) ||
|
|
45
|
+
((await isDirectory(path.join(cursor, 'common'))) &&
|
|
46
|
+
((await isDirectory(path.join(cursor, 'history'))) ||
|
|
47
|
+
(await isDirectory(path.join(cursor, 'interface'))) ||
|
|
48
|
+
(await isDirectory(path.join(cursor, 'map')))))) {
|
|
49
|
+
return cursor;
|
|
50
|
+
}
|
|
51
|
+
const parent = path.dirname(cursor);
|
|
52
|
+
if (parent === cursor)
|
|
53
|
+
break;
|
|
54
|
+
cursor = parent;
|
|
55
|
+
}
|
|
56
|
+
throw new ServiceError('AUTO_MOD_ROOT_NOT_FOUND', 'Start the MCP from inside a Hearts of Iron IV mod or configure a server workspace', { currentDirectory: path.resolve(start) });
|
|
57
|
+
}
|
|
58
|
+
async function firstExistingDirectory(candidates) {
|
|
59
|
+
for (const candidate of candidates) {
|
|
60
|
+
if (await isDirectory(candidate))
|
|
61
|
+
return path.resolve(candidate);
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
function automaticGameRootCandidates() {
|
|
66
|
+
const home = homedir();
|
|
67
|
+
return [
|
|
68
|
+
...(process.env.HOI4_GAME_ROOT === undefined ? [] : [process.env.HOI4_GAME_ROOT]),
|
|
69
|
+
path.join(process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)', 'Steam', 'steamapps', 'common', 'Hearts of Iron IV'),
|
|
70
|
+
path.join(home, '.local', 'share', 'Steam', 'steamapps', 'common', 'Hearts of Iron IV'),
|
|
71
|
+
path.join(home, '.steam', 'steam', 'steamapps', 'common', 'Hearts of Iron IV'),
|
|
72
|
+
path.join(home, 'Library', 'Application Support', 'Steam', 'steamapps', 'common', 'Hearts of Iron IV'),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
function automaticStorageRoots() {
|
|
76
|
+
const home = homedir();
|
|
77
|
+
const dataRoot = process.env.XDG_DATA_HOME ?? process.env.LOCALAPPDATA ?? path.join(home, '.local', 'share');
|
|
78
|
+
const stateRoot = process.env.XDG_STATE_HOME ?? process.env.LOCALAPPDATA ?? path.join(home, '.local', 'state');
|
|
79
|
+
return {
|
|
80
|
+
serverStateRoot: path.join(stateRoot, 'hoi4-agent-tools', 'state'),
|
|
81
|
+
workspaceStorageRoot: path.join(dataRoot, 'hoi4-agent-tools', 'workspaces'),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function automaticWorkspaceId(modRoot) {
|
|
85
|
+
const slug = path
|
|
86
|
+
.basename(modRoot)
|
|
87
|
+
.normalize('NFKD')
|
|
88
|
+
.toLowerCase()
|
|
89
|
+
.replace(/[^a-z0-9]+/gu, '_')
|
|
90
|
+
.replace(/^_+|_+$/gu, '')
|
|
91
|
+
.slice(0, 48);
|
|
92
|
+
return `auto_${slug.length === 0 ? 'mod' : slug}`;
|
|
93
|
+
}
|
|
94
|
+
/** Build an in-memory configuration when a local agent starts inside a mod. */
|
|
95
|
+
export async function automaticConfiguration(start = process.cwd()) {
|
|
96
|
+
const modRoot = await findCurrentModRoot(start);
|
|
97
|
+
const gameRoot = await firstExistingDirectory(automaticGameRootCandidates());
|
|
98
|
+
const storage = automaticStorageRoots();
|
|
99
|
+
return serverConfigurationSchema.parse({
|
|
100
|
+
version: CONFIG_VERSION,
|
|
101
|
+
serverStateRoot: storage.serverStateRoot,
|
|
102
|
+
workspaceStorageRoot: storage.workspaceStorageRoot,
|
|
103
|
+
...(gameRoot === undefined ? {} : { gameRoot }),
|
|
104
|
+
workspaces: [
|
|
105
|
+
{
|
|
106
|
+
id: automaticWorkspaceId(modRoot),
|
|
107
|
+
name: path.basename(modRoot),
|
|
108
|
+
root: modRoot,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async function configurationExists(configPath) {
|
|
114
|
+
try {
|
|
115
|
+
return (await stat(configPath)).isFile();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
24
121
|
export async function createEngine(configPath = configurationPath()) {
|
|
25
|
-
|
|
122
|
+
let configuration;
|
|
123
|
+
try {
|
|
124
|
+
configuration = await loadConfiguration(configPath);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const useAutomaticConfiguration = configPath === defaultConfigurationPath() &&
|
|
128
|
+
process.env.HOI4_AGENT_CONFIG === undefined &&
|
|
129
|
+
error instanceof ServiceError &&
|
|
130
|
+
error.code === 'CONFIG_READ_FAILED' &&
|
|
131
|
+
!(await configurationExists(configPath));
|
|
132
|
+
if (!useAutomaticConfiguration)
|
|
133
|
+
throw error;
|
|
134
|
+
configuration = await automaticConfiguration();
|
|
135
|
+
}
|
|
26
136
|
const resolver = await WorkspaceResolver.create(configuration);
|
|
27
137
|
const engine = new CoreEngine(resolver);
|
|
28
138
|
await engine.initialize();
|