@aeonsparx/pi-unreal 0.0.0-stage → 1.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/LICENSE +21 -0
- package/README.md +202 -2
- package/docs/internals.md +184 -0
- package/extensions/unreal/index.ts +682 -0
- package/extensions/unreal/lib/cache.ts +190 -0
- package/extensions/unreal/lib/compiledb.ts +491 -0
- package/extensions/unreal/lib/descriptors.ts +631 -0
- package/extensions/unreal/lib/engine-resolver.ts +443 -0
- package/extensions/unreal/lib/pi-tools.ts +402 -0
- package/extensions/unreal/lib/plugin-resolver.ts +305 -0
- package/extensions/unreal/lib/query.ts +272 -0
- package/extensions/unreal/lib/registry.ts +208 -0
- package/extensions/unreal/lib/scan.ts +510 -0
- package/extensions/unreal/lib/store.ts +900 -0
- package/extensions/unreal/lib/tools.ts +614 -0
- package/extensions/unreal/lib/ubt.ts +373 -0
- package/extensions/unreal/lib/util.ts +210 -0
- package/package.json +59 -4
- package/prompts/unreal-context.md +26 -0
- package/skills/ue-engine-resolution/SKILL.md +96 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi tool wrappers for the compiler-free UE source index.
|
|
3
|
+
*
|
|
4
|
+
* Four tools, one idea: an agent asking "where is X" gets one line, not 176 files of grep output.
|
|
5
|
+
* Nothing here needs clangd, clang, Visual Studio, or Rider — the data is (a) a definition scan of
|
|
6
|
+
* `.h`/`.cpp` text and (b) `UnrealBuildTool -Mode=Query` output for compile facts.
|
|
7
|
+
*
|
|
8
|
+
* Cost discipline is the design constraint: project/plugin indexes build in seconds and are built on
|
|
9
|
+
* demand; an Engine/Source index scans tens of thousands of files, so `unreal_index` is the only tool
|
|
10
|
+
* that pays for it. A lookup that wants engine sources reports the cost and how to pay it — it never
|
|
11
|
+
* stalls the agent.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Type } from "typebox";
|
|
15
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
|
|
17
|
+
import { normalizePath, realFs, truncateLines, type JsonValue } from "./util.ts";
|
|
18
|
+
import { buildContext, compileDbReport, fileContext, indexBuild, indexStatus, symbolList, symbolLookup, type Scope } from "./tools.ts";
|
|
19
|
+
|
|
20
|
+
/* ──────────────────────────────────────────────────────────────────────────── schemas */
|
|
21
|
+
|
|
22
|
+
const ScopeSchema = Type.Optional(
|
|
23
|
+
Type.Union([Type.Literal("project"), Type.Literal("engine"), Type.Literal("all")], {
|
|
24
|
+
description: "Which sources to search. 'project' (default) = the project's modules plus its enabled plugins. 'engine' = Engine/Source. 'all' = both.",
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
const EngineAreasSchema = Type.Optional(
|
|
28
|
+
Type.Array(Type.String(), {
|
|
29
|
+
// Must match DEFAULT_ENGINE_AREAS in lib/store.ts — the schema description is what agents read.
|
|
30
|
+
description: 'Engine/Source sub-areas for engine-scope work, e.g. ["Runtime"] or ["Runtime/Core","Runtime/CoreUObject"]. Default: ["Runtime"] (DEFAULT_ENGINE_AREAS in lib/store.ts); "Editor" and "Developer" are opt-in and roughly triple the scan.',
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
const LimitSchema = Type.Optional(Type.Number({ description: "Maximum rows to return. Default 25." }));
|
|
34
|
+
const TargetSchema = Type.Optional(Type.String({ description: "UBT target whose compile environment to report, e.g. 'LpEditor', 'UnrealEditor'. Default: the project's editor target." }));
|
|
35
|
+
const PlatformSchema = Type.Optional(Type.String({ description: "Target platform. Default: this machine's platform (Win64 on Windows)." }));
|
|
36
|
+
const ConfigurationSchema = Type.Optional(Type.String({ description: "Target configuration: Development (default), DebugGame, Shipping." }));
|
|
37
|
+
|
|
38
|
+
/** Every tool takes the workspace directory; the session cwd is the default so agents rarely pass it. */
|
|
39
|
+
const CwdSchema = Type.Optional(Type.String({ description: "Workspace directory containing the .uproject. Default: the session cwd." }));
|
|
40
|
+
|
|
41
|
+
const SymbolParams = Type.Object({
|
|
42
|
+
cwd: CwdSchema,
|
|
43
|
+
query: Type.String({ description: "Symbol name: 'AActor', 'UAnimInstance', 'AActor:Tick' (qualified), or a bare method name together with 'owner'." }),
|
|
44
|
+
owner: Type.Optional(Type.String({ description: "Owner type for a member lookup: query 'Tick' + owner 'AActor'. A bare method name is resolved through its owner." })),
|
|
45
|
+
scope: ScopeSchema,
|
|
46
|
+
engineAreas: EngineAreasSchema,
|
|
47
|
+
module: Type.Optional(Type.String({ description: "Restrict hits to one module, e.g. 'Engine'." })),
|
|
48
|
+
prefix: Type.Optional(Type.Boolean({ description: "Allow prefix matching (name families, typos). Default false: exact + qualified-member only." })),
|
|
49
|
+
limit: LimitSchema,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const ListParams = Type.Object({
|
|
53
|
+
cwd: CwdSchema,
|
|
54
|
+
prefix: Type.String({ description: "Symbol-name prefix, e.g. 'ULp' or 'ANiMy'." }),
|
|
55
|
+
scope: ScopeSchema,
|
|
56
|
+
engineAreas: EngineAreasSchema,
|
|
57
|
+
kind: Type.Optional(
|
|
58
|
+
Type.Union([Type.Literal("class"), Type.Literal("struct"), Type.Literal("enum"), Type.Literal("union"), Type.Literal("function"), Type.Literal("delegate"), Type.Literal("property"), Type.Literal("alias")], {
|
|
59
|
+
description: "Filter by definition kind.",
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
module: Type.Optional(Type.String({ description: "Restrict hits to one module." })),
|
|
63
|
+
limit: Type.Optional(Type.Number({ description: "Maximum names to return. Default 40." })),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const ContextParams = Type.Object({
|
|
67
|
+
cwd: CwdSchema,
|
|
68
|
+
file: Type.Optional(Type.String({ description: "Source file to describe (.h/.cpp in the project, a plugin, or Engine/Source): its module, declarations, include paths, defines file." })),
|
|
69
|
+
module: Type.Optional(Type.String({ description: "Module to describe instead of a file, e.g. 'GameplayAbilities' — dependency lists, include paths, defines file, standard, SDK." })),
|
|
70
|
+
target: TargetSchema,
|
|
71
|
+
platform: PlatformSchema,
|
|
72
|
+
configuration: ConfigurationSchema,
|
|
73
|
+
includeDefines: Type.Optional(Type.Boolean({ description: "Also read the module's preprocessor define list. Needs the UBT Query cache (~7s the first time). Default false." })),
|
|
74
|
+
engineAreas: EngineAreasSchema,
|
|
75
|
+
refresh: Type.Optional(Type.Boolean({ description: "Re-run UnrealBuildTool Query instead of using the cached JSON." })),
|
|
76
|
+
limit: LimitSchema,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const IndexParams = Type.Object({
|
|
80
|
+
cwd: CwdSchema,
|
|
81
|
+
action: Type.Union([Type.Literal("build"), Type.Literal("status"), Type.Literal("compiledb")], { description: "'build' creates or refreshes an index. 'status' lists what is cached and what building will cost. 'compiledb' runs UnrealBuildTool -Mode=GenerateClangDatabase and normalizes it for clangd (minutes, ~1 GB for the engine scope)." }),
|
|
82
|
+
scope: Type.Optional(Type.Union([Type.Literal("project"), Type.Literal("engine"), Type.Literal("project+engine")], { description: "Index scope for 'build': 'project' (default, seconds) or 'engine' (minutes the first time). For 'compiledb': 'project' (default) = project + enabled plugin sources, 'project+engine' adds all of Engine/Source." })),
|
|
83
|
+
mode: Type.Optional(Type.Union([Type.Literal("definitions"), Type.Literal("full")], { description: "'definitions' = symbol -> file:line only (default, fast). 'full' also records per-file include sets and needs UnrealBuildTool Query (~7s, no compiler)." })),
|
|
84
|
+
engineAreas: EngineAreasSchema,
|
|
85
|
+
target: TargetSchema,
|
|
86
|
+
platform: PlatformSchema,
|
|
87
|
+
configuration: ConfigurationSchema,
|
|
88
|
+
refresh: Type.Optional(Type.Boolean({ description: "Force a rebuild, and re-run UnrealBuildTool Query in 'full' mode. Default false." })),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/* ──────────────────────────────────────────────────────────────────────────── rendering */
|
|
92
|
+
|
|
93
|
+
const jsonOf = (value: unknown): JsonValue => JSON.parse(JSON.stringify(value ?? null)) as JsonValue;
|
|
94
|
+
|
|
95
|
+
const shortPath = (file: string | undefined, cwd: string) => {
|
|
96
|
+
if (!file) return "?";
|
|
97
|
+
const f = normalizePath(file);
|
|
98
|
+
const base = normalizePath(cwd);
|
|
99
|
+
return f.startsWith(`${base}/`) ? f.slice(base.length + 1) : f;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const rowLine = (row: { symbol: string; kind: string; file: string; line: number; module: string; scope?: string }, cwd: string) =>
|
|
103
|
+
` ${row.kind} ${row.symbol} ${shortPath(row.file, cwd)}:${row.line} [${row.module}${row.scope ? ` · ${row.scope}` : ""}]`;
|
|
104
|
+
|
|
105
|
+
function result(text: string, details: unknown, isError = false) {
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: "text" as const, text: truncateLines(text, 70).text }],
|
|
108
|
+
details: details as never,
|
|
109
|
+
structuredContent: jsonOf(details),
|
|
110
|
+
...(isError ? { isError: true } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const bullets = (label: string, items: string[]) => (items.length ? items.map((n) => `· ${label}: ${n}`) : []);
|
|
115
|
+
|
|
116
|
+
/* ────────────────────────────────────────────────────────────────────────── output schemas */
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The structured payloads nest heterogeneously (`moduleRefs` is a module list for a file and a
|
|
120
|
+
* per-module map for a build), and the point of `structuredContent` is machine consumption —
|
|
121
|
+
* nested objects are declared permissively so a shape change in the library never makes a tool
|
|
122
|
+
* call fail validation.
|
|
123
|
+
*/
|
|
124
|
+
const Loose = Type.Object({}, { additionalProperties: true });
|
|
125
|
+
const LooseList = Type.Array(Loose);
|
|
126
|
+
const Strings = Type.Array(Type.String());
|
|
127
|
+
const DefRowOut = Type.Object({
|
|
128
|
+
symbol: Type.String(),
|
|
129
|
+
kind: Type.String(),
|
|
130
|
+
file: Type.String(),
|
|
131
|
+
line: Type.Number(),
|
|
132
|
+
module: Type.String(),
|
|
133
|
+
scope: Type.String(),
|
|
134
|
+
marker: Type.String(),
|
|
135
|
+
rank: Type.Number(),
|
|
136
|
+
match: Type.String(),
|
|
137
|
+
});
|
|
138
|
+
const IndexRefOut = Type.Object({ key: Type.String(), scope: Type.String(), defs: Type.Number(), generatedAt: Type.Number() });
|
|
139
|
+
|
|
140
|
+
const SymbolOut = Type.Object({ scope: Type.String(), total: Type.Number(), results: Type.Array(DefRowOut), ranks: LooseList, indexes: Type.Array(IndexRefOut), notes: Strings, warnings: Type.Optional(Strings) });
|
|
141
|
+
const ListOut = Type.Object({ scope: Type.String(), total: Type.Number(), symbols: Type.Array(DefRowOut), indexes: Type.Array(IndexRefOut), notes: Strings, warnings: Type.Optional(Strings) });
|
|
142
|
+
const ContextOut = Type.Object({ unit: Loose, counts: Loose, source: Type.String(), warnings: Type.Optional(Strings), notes: Type.Optional(Strings) });
|
|
143
|
+
const IndexOut = Type.Object({
|
|
144
|
+
key: Type.Optional(Type.String()),
|
|
145
|
+
mode: Type.Optional(Type.String()),
|
|
146
|
+
scope: Type.Optional(Type.String()),
|
|
147
|
+
healthy: Type.Optional(Type.Boolean()),
|
|
148
|
+
issues: Type.Optional(Strings),
|
|
149
|
+
ageMs: Type.Optional(Type.Number()),
|
|
150
|
+
counts: Type.Optional(Loose),
|
|
151
|
+
roots: Type.Optional(Type.Union([Strings, LooseList, Type.Number()])),
|
|
152
|
+
source: Type.Optional(Type.String()),
|
|
153
|
+
target: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
154
|
+
platform: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
155
|
+
configuration: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
156
|
+
query: Type.Optional(Loose),
|
|
157
|
+
built: Type.Optional(Type.Boolean()),
|
|
158
|
+
durationMs: Type.Optional(Type.Number()),
|
|
159
|
+
actions: Type.Optional(Strings),
|
|
160
|
+
warnings: Type.Optional(Strings),
|
|
161
|
+
notes: Type.Optional(Strings),
|
|
162
|
+
status: Type.Optional(Loose),
|
|
163
|
+
indexes: Type.Optional(LooseList),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
/* ──────────────────────────────────────────────────────────────────────────── tools */
|
|
167
|
+
|
|
168
|
+
export const unrealSymbol = defineTool({
|
|
169
|
+
name: "unreal_symbol",
|
|
170
|
+
label: "UE symbol",
|
|
171
|
+
description: "Find where a C++ type, function, USTRUCT, UENUM or UPROPERTY is defined — project and enabled plugins by default, Engine/Source on request — one line per hit with its owning module.",
|
|
172
|
+
promptSnippet: "Locate a UE C++ symbol definition (project, plugins, engine) with module attribution",
|
|
173
|
+
promptGuidelines: [
|
|
174
|
+
"Use unreal_symbol instead of grepping Engine/Source for a class or function name: grep returns every file that mentions the name (a hot engine class is ~176 files / 150 KB of context); this returns definition lines only.",
|
|
175
|
+
"Default scope is 'project' (project modules + enabled plugins). Engine types (AActor, UAnimInstance, FWorldDelegates) need scope 'engine', which requires an engine index: unreal_index action build scope engine — minutes the first time, cached afterwards, and engineAreas [\"Runtime\"] makes it much cheaper.",
|
|
176
|
+
"Qualify member lookups: query 'Tick' with owner 'AActor', or query 'AActor:Tick'. Use prefix: true for name families ('ULp', 'ANiMy'), never to silently accept a typo.",
|
|
177
|
+
"A definition line is a starting point, not an answer: read that file at that line. For engine call graphs clangd (MCP) is more precise; for module/link facts use unreal_context.",
|
|
178
|
+
],
|
|
179
|
+
parameters: SymbolParams,
|
|
180
|
+
outputSchema: SymbolOut,
|
|
181
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
182
|
+
executionMode: "parallel",
|
|
183
|
+
|
|
184
|
+
async execute(_id, params, _signal, _update, ctx) {
|
|
185
|
+
const query = params.owner ? `${params.owner}:${params.query}` : params.query;
|
|
186
|
+
const out = await symbolLookup({ cwd: params.cwd ?? ctx.cwd, query, scope: params.scope as Scope | undefined, engineAreas: params.engineAreas, module: params.module, prefix: params.prefix, limit: params.limit, fs: realFs });
|
|
187
|
+
const rows = out.results.map((row) => rowLine(row, params.cwd ?? ctx.cwd));
|
|
188
|
+
const text = [`unreal_symbol '${query}' scope=${out.scope}: ${out.total} definition(s)`, ...rows, ...out.notes.map((n) => `· ${n}`), ...(out.warnings ?? []).map((w) => `! ${w}`)].join("\n");
|
|
189
|
+
return result(text, out, out.total === 0);
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
export const unrealSymbolList = defineTool({
|
|
194
|
+
name: "unreal_symbol_list",
|
|
195
|
+
label: "UE symbols by prefix",
|
|
196
|
+
description: "List symbol names starting with a prefix — what a project defines (ULp*, FP*) or an engine family (UAnim*, ANiMy*).",
|
|
197
|
+
promptSnippet: "List UE symbol names by prefix (project, plugins, engine)",
|
|
198
|
+
promptGuidelines: [
|
|
199
|
+
"Use unreal_symbol_list to discover names before a lookup: 'ULp' shows the project's own classes; engine scope shows prefix families.",
|
|
200
|
+
"Keep limit small (20-40): a one-letter prefix over an engine index matches thousands of names.",
|
|
201
|
+
],
|
|
202
|
+
parameters: ListParams,
|
|
203
|
+
outputSchema: ListOut,
|
|
204
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
205
|
+
executionMode: "parallel",
|
|
206
|
+
|
|
207
|
+
async execute(_id, params, _signal, _update, ctx) {
|
|
208
|
+
const out = await symbolList({ cwd: params.cwd ?? ctx.cwd, prefix: params.prefix, scope: params.scope as Scope | undefined, kind: params.kind, module: params.module, engineAreas: params.engineAreas, limit: params.limit ?? 40, fs: realFs });
|
|
209
|
+
const rows = out.symbols.map((row) => rowLine(row, params.cwd ?? ctx.cwd));
|
|
210
|
+
const text = [`unreal_symbol_list '${params.prefix}' scope=${out.scope}: ${out.total} symbol(s)`, ...rows, ...out.notes.map((n) => `· ${n}`)].join("\n");
|
|
211
|
+
return result(text, out);
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
export const unrealContext = defineTool({
|
|
216
|
+
name: "unreal_context",
|
|
217
|
+
label: "UE compile context",
|
|
218
|
+
description: "Answer 'what do I need to use X': the module directory, the real Definitions.h defines, C++ standard, SDK, include paths, module dependency lists (public/private/circular/dynamic), and what a header declares.",
|
|
219
|
+
promptSnippet: "Report a UE module's or file's real compile context (UBT Query + scan data)",
|
|
220
|
+
promptGuidelines: [
|
|
221
|
+
"Before adding a Build.cs dependency, ask unreal_context for the module: its public/private dependency lists are the authoritative 'use this type, link this module' answer.",
|
|
222
|
+
"Pass 'file' for a header to get its module, its declarations, and the include paths its target compiles with — cheaper than a repo-wide grep for the #include line.",
|
|
223
|
+
"The define list lives in Intermediate/Build/.../Definitions.<Module>.h (UBT generates it). UBT Query reports defines: [] — this tool reads that file, so 'defines' is real, not guessed.",
|
|
224
|
+
],
|
|
225
|
+
parameters: ContextParams,
|
|
226
|
+
outputSchema: ContextOut,
|
|
227
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
228
|
+
executionMode: "parallel",
|
|
229
|
+
|
|
230
|
+
async execute(_id, params, _signal, _update, ctx) {
|
|
231
|
+
if (!params.file && !params.module) {
|
|
232
|
+
return result("unreal_context needs 'file' (a .h/.cpp) or 'module' (a module name).", { error: "no file or module" }, true);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (params.file) {
|
|
236
|
+
const fc = await fileContext({
|
|
237
|
+
cwd: params.cwd ?? ctx.cwd,
|
|
238
|
+
file: params.file,
|
|
239
|
+
mode: params.includeDefines ? "full" : "definitions",
|
|
240
|
+
includeDefines: params.includeDefines,
|
|
241
|
+
engineAreas: params.engineAreas,
|
|
242
|
+
refresh: params.refresh,
|
|
243
|
+
limit: params.limit,
|
|
244
|
+
fs: realFs,
|
|
245
|
+
});
|
|
246
|
+
const c = fc.compile;
|
|
247
|
+
const lines = [
|
|
248
|
+
`unreal_context ${shortPath(fc.file, params.cwd ?? ctx.cwd)} mode=${fc.mode} module=${fc.module ?? "?"}${fc.indexKey ? ` index=${fc.indexKey}` : ""}`,
|
|
249
|
+
];
|
|
250
|
+
if (c) {
|
|
251
|
+
lines.push(
|
|
252
|
+
` file kind=${c.kind}${c.standard ? ` standard=${c.standard}` : ""}${c.compiler ? ` compiler=${c.compiler}` : ""}`,
|
|
253
|
+
` include paths: ${c.includePaths.length} · forced includes: ${c.forcedIncludes.length} · declares: ${c.declares.length} · includes: ${c.includes.length}`,
|
|
254
|
+
` deps: ${c.moduleRefs.public.length} public + ${c.moduleRefs.private.length} private`,
|
|
255
|
+
);
|
|
256
|
+
if (c.moduleRefs.publicInclude.length) lines.push(` publicInclude: ${c.moduleRefs.publicInclude.join(", ")}`);
|
|
257
|
+
if (c.moduleRefs.circular.length) lines.push(` circular: ${c.moduleRefs.circular.join(", ")}`);
|
|
258
|
+
if (c.moduleRefs.dynamicallyLoaded.length) lines.push(` dynamicallyLoaded: ${c.moduleRefs.dynamicallyLoaded.join(", ")}`);
|
|
259
|
+
if (c.declares.length) lines.push(` declares: ${c.declares.slice(0, 24).join(", ")}`);
|
|
260
|
+
if (c.definesFile) lines.push(` defines file: ${shortPath(c.definesFile, params.cwd ?? ctx.cwd)}`);
|
|
261
|
+
if (c.forcedIncludes.length) lines.push(` forced includes: ${c.forcedIncludes.slice(0, 3).map((f) => shortPath(f, params.cwd ?? ctx.cwd)).join(", ")}`);
|
|
262
|
+
lines.push(...c.notes.map((n) => `· ${n}`));
|
|
263
|
+
}
|
|
264
|
+
if (fc.defines?.length) lines.push(` defines: ${fc.defines.slice(0, 40).join(" ")}`);
|
|
265
|
+
if (fc.symbols?.length) lines.push(` symbols in file: ${fc.symbols.slice(0, 20).map((s) => `${s.kind} ${s.symbol}@${s.line}`).join(", ")}`);
|
|
266
|
+
const text = [...lines, ...fc.notes.map((n) => `· ${n}`), ...(fc.warnings ?? []).map((w) => `! ${w}`), ...(fc.error ? [`! ${fc.error}`] : [])].join("\n");
|
|
267
|
+
return result(text, fc, !!fc.error);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const out = await buildContext({
|
|
271
|
+
cwd: params.cwd ?? ctx.cwd,
|
|
272
|
+
module: params.module,
|
|
273
|
+
target: params.target,
|
|
274
|
+
platform: params.platform,
|
|
275
|
+
configuration: params.configuration,
|
|
276
|
+
includeDefines: params.includeDefines,
|
|
277
|
+
engineAreas: params.engineAreas,
|
|
278
|
+
refresh: params.refresh,
|
|
279
|
+
limit: params.limit,
|
|
280
|
+
fs: realFs,
|
|
281
|
+
});
|
|
282
|
+
const m = out.module;
|
|
283
|
+
const lines = [
|
|
284
|
+
`unreal_context module ${m?.name ?? params.module} target=${out.target?.target} ${out.target?.platform} ${out.target?.configuration} (${out.target?.source ?? "?"})`,
|
|
285
|
+
` dir: ${shortPath(m?.directory, params.cwd ?? ctx.cwd) ?? "?"}${m?.standard ? ` standard=${m.standard}` : ""}${m?.windowsSdkVersion ? ` sdk=${m.windowsSdkVersion}` : ""}`,
|
|
286
|
+
` deps: ${m?.dependencies ?? 0} · public headers: ${m?.publicHeaders ?? 0} · private headers: ${m?.privateHeaders ?? 0} · include paths: ${m?.includePaths ?? 0} · total headers: ${out.headerCount}`,
|
|
287
|
+
` query cache: ${out.queryCache?.source ?? "?"}${out.queryCache?.durationMs ? ` (${JSON.stringify(out.queryCache.durationMs)} ms)` : ""}`,
|
|
288
|
+
];
|
|
289
|
+
lines.push(...bullets("publicInclude", m?.publicInclude ?? []), ...bullets("privateInclude", m?.privateInclude ?? []), ...bullets("circular", m?.circular ?? []), ...bullets("dynamicallyLoaded", m?.dynamicallyLoaded ?? []));
|
|
290
|
+
if (m?.definesFile) lines.push(` defines file: ${shortPath(m.definesFile, params.cwd ?? ctx.cwd)}`);
|
|
291
|
+
if (out.defines?.length) lines.push(` defines: ${out.defines.slice(0, 40).join(" ")}`);
|
|
292
|
+
if (m?.compilerPath) lines.push(` compiler: ${m.compilerPath}`);
|
|
293
|
+
if (out.projectModules?.length) lines.push(` project modules: ${out.projectModules.map((entry) => entry.name).slice(0, 20).join(", ")}`);
|
|
294
|
+
const text = [...lines, ...out.notes.map((n) => `· ${n}`), ...(out.warnings ?? []).map((w) => `! ${w}`), ...(out.error ? [`! ${out.error}`] : [])].join("\n");
|
|
295
|
+
return result(text, out, !!out.error);
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
export const unrealIndex = defineTool({
|
|
300
|
+
name: "unreal_index",
|
|
301
|
+
label: "UE index",
|
|
302
|
+
description: "Build or report the source index: project/plugins (seconds), Engine/Source (minutes the first time), and the UnrealBuildTool Query cache behind the compile facts.",
|
|
303
|
+
promptSnippet: "Build/report the UE symbol index and UBT Query cache",
|
|
304
|
+
promptGuidelines: [
|
|
305
|
+
"Run unreal_index action 'status' first: it prints what is cached, how old it is, and what building will cost.",
|
|
306
|
+
"An engine-scope build is a minutes-long scan the first time. Ask the user before building it, and prefer engineAreas [\"Runtime\"] over the whole tree when the question is about gameplay base classes.",
|
|
307
|
+
"mode 'full' runs UnrealBuildTool -Mode=Query (~7s, needs no compiler, and runs in UBT's own Query environment so it cannot collide with a live build) and records include edges; 'definitions' is a text scan only.",
|
|
308
|
+
"After a rebase, a branch switch, adding a plugin, or an engine version change, rebuild with refresh: true — indexes are keyed by scan-root identity, not by the project name.",
|
|
309
|
+
"action 'compiledb' is the clangd enrichment path: minutes of UnrealBuildTool work, ~1 GB for the engine scope. Ask the user before running it, pass check: '<file>' to prove the result with clangd --check, and treat the database as machine-local (never committed).",
|
|
310
|
+
],
|
|
311
|
+
parameters: IndexParams,
|
|
312
|
+
outputSchema: IndexOut,
|
|
313
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
314
|
+
executionMode: "sequential",
|
|
315
|
+
|
|
316
|
+
async execute(_id, params, _signal, _update, ctx) {
|
|
317
|
+
if (params.action === "status") {
|
|
318
|
+
const out = indexStatus(params.cwd ?? ctx.cwd, realFs);
|
|
319
|
+
const lines = [
|
|
320
|
+
`cache: ${out.cacheDir}`,
|
|
321
|
+
`query cache: ${out.query.usable ? `${out.query.target.target} ${out.query.target.platform} ${out.query.target.configuration} (${out.query.target.source})` : "not built"}`,
|
|
322
|
+
` TargetDetails: ${out.query.targetDetails ? `${Math.round(out.query.targetDetails.bytes / 1024)} KB, ${Math.round(out.query.targetDetails.ageMs / 1000)}s old` : "absent"}`,
|
|
323
|
+
` TargetModuleReferences: ${out.query.moduleReferences ? `${Math.round(out.query.moduleReferences.bytes / 1024)} KB, ${Math.round(out.query.moduleReferences.ageMs / 1000)}s old` : "absent"}`,
|
|
324
|
+
];
|
|
325
|
+
for (const index of out.indexes) {
|
|
326
|
+
lines.push(`index ${index.key} ${index.scope || "?"} ${index.mode} ${index.defs} symbols / ${index.files} files ${index.source}${index.target ? ` target=${index.target}` : ""} ${Math.round(index.ageMs / 1000)}s old ${index.sizeMb}MB${index.issues.length ? ` issues: ${index.issues.join("; ")}` : ""}`);
|
|
327
|
+
}
|
|
328
|
+
if (!out.indexes.length) lines.push("no symbol index yet — unreal_index action build scope project (seconds)");
|
|
329
|
+
const text = [...lines, ...(out.query.notes ?? []).map((n) => `· ${n}`)].join("\n");
|
|
330
|
+
return result(text, out);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (params.action === "compiledb") {
|
|
334
|
+
const db = await compileDbReport({
|
|
335
|
+
cwd: params.cwd ?? ctx.cwd,
|
|
336
|
+
scope: (params.scope === "project+engine" ? "project+engine" : "project") as "project" | "project+engine",
|
|
337
|
+
target: params.target,
|
|
338
|
+
platform: params.platform,
|
|
339
|
+
configuration: params.configuration,
|
|
340
|
+
refresh: params.refresh,
|
|
341
|
+
fs: realFs,
|
|
342
|
+
});
|
|
343
|
+
const text = [
|
|
344
|
+
`unreal_index compiledb → ${db.ok ? "ok" : "FAILED"}`,
|
|
345
|
+
...(db.target ? [` target ${db.target} ${db.platform ?? ""} ${db.configuration ?? ""}`] : []),
|
|
346
|
+
` ${db.entries} clangd-ready entries${"path" in db ? ` at ${db.path}` : ""} ${db.reused ? "(reused)" : ""}`,
|
|
347
|
+
...(db.notes ?? []).map((n) => `· ${n}`),
|
|
348
|
+
...(db.warnings ?? []).map((w) => `! ${w}`),
|
|
349
|
+
].join("\n");
|
|
350
|
+
return result(text, db as unknown as Record<string, JsonValue>, !db.ok);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const out = await indexBuild({
|
|
354
|
+
cwd: params.cwd ?? ctx.cwd,
|
|
355
|
+
scope: (params.scope ?? "project") as "project" | "engine",
|
|
356
|
+
mode: (params.mode ?? "definitions") as "definitions" | "full",
|
|
357
|
+
engineAreas: params.engineAreas,
|
|
358
|
+
target: params.target,
|
|
359
|
+
platform: params.platform,
|
|
360
|
+
configuration: params.configuration,
|
|
361
|
+
refresh: params.refresh,
|
|
362
|
+
fs: realFs,
|
|
363
|
+
});
|
|
364
|
+
const seconds = out.durationMs != null ? (out.durationMs / 1000).toFixed(1) : "?";
|
|
365
|
+
const text = [
|
|
366
|
+
`unreal_index build scope=${out.scope} mode=${out.mode} → ${out.key}`,
|
|
367
|
+
` ${out.symbols} symbols / ${out.files} files / ${out.modules} modules from ${out.roots} roots ${out.built ? `in ${seconds}s` : "(cached, reused as is)"}`,
|
|
368
|
+
` areas: ${(out.engineAreas ?? []).join(", ")}`,
|
|
369
|
+
` dir: ${out.indexDir ?? "not built"}`,
|
|
370
|
+
...out.notes.map((n) => `· ${n}`),
|
|
371
|
+
...(out.warnings ?? []).map((w) => `! ${w}`),
|
|
372
|
+
].join("\n");
|
|
373
|
+
return result(text, out, !out.built && out.symbols === 0);
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
export const indexTools = [unrealSymbol, unrealSymbolList, unrealContext, unrealIndex];
|
|
378
|
+
|
|
379
|
+
/** `/unreal-symbol <name>` — prefix-tolerant lookup across project and engine indexes. */
|
|
380
|
+
export function unrealSymbolCommand(query: string, cwd: string) {
|
|
381
|
+
return symbolLookup({ cwd, query, scope: "all", prefix: true, fs: realFs }).then((out) => {
|
|
382
|
+
const rows = out.results.map((row) => rowLine(row, cwd));
|
|
383
|
+
return [`unreal_symbol '${query}': ${out.total} definition(s)`, ...rows, ...out.notes.map((n) => `· ${n}`)].join("\n");
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** `/unreal-module <name>` — module identity + dependency lists; needs the Query cache, not a symbol index. */
|
|
388
|
+
export function unrealModuleCommand(name: string, cwd: string) {
|
|
389
|
+
return buildContext({ cwd, module: name, includeDefines: true, fs: realFs }).then((out) => {
|
|
390
|
+
const m = out.module;
|
|
391
|
+
return [
|
|
392
|
+
`module ${m?.name ?? name}`,
|
|
393
|
+
` dir: ${shortPath(m?.directory, cwd)} target=${out.target?.target} ${out.target?.platform} ${out.target?.configuration}`,
|
|
394
|
+
` standard=${m?.standard ?? "?"} sdk=${m?.windowsSdkVersion ?? "?"} deps=${m?.dependencies ?? 0}`,
|
|
395
|
+
` publicInclude: ${(m?.publicInclude ?? []).join(", ") || "?"}`,
|
|
396
|
+
` privateInclude: ${(m?.privateInclude ?? []).join(", ") || "?"}`,
|
|
397
|
+
` defines file: ${shortPath(m?.definesFile, cwd)}`,
|
|
398
|
+
...out.notes.map((n) => `· ${n}`),
|
|
399
|
+
...(out.warnings ?? []).map((w) => `! ${w}`),
|
|
400
|
+
].join("\n");
|
|
401
|
+
});
|
|
402
|
+
}
|