@aeonsparx/pi-unreal 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -0
- package/README.md +85 -88
- package/docs/internals.md +66 -5
- package/extensions/unreal/index.ts +18 -4
- package/extensions/unreal/lib/pi-tools.ts +70 -20
- package/extensions/unreal/lib/store.ts +633 -43
- package/extensions/unreal/lib/tools.ts +491 -31
- package/extensions/unreal/lib/ubt.ts +7 -0
- package/package.json +2 -1
- package/prompts/unreal-context.md +10 -0
- package/skills/ue-engine-resolution/SKILL.md +38 -5
|
@@ -15,19 +15,19 @@ import { Type } from "typebox";
|
|
|
15
15
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
|
|
17
17
|
import { normalizePath, realFs, truncateLines, type JsonValue } from "./util.ts";
|
|
18
|
-
import { buildContext, compileDbReport, fileContext, indexBuild, indexStatus, symbolList, symbolLookup, type Scope } from "./tools.ts";
|
|
18
|
+
import { buildContext, compileDbReport, fileContext, headerReport, indexBuild, indexStatus, symbolList, symbolLookup, type Scope } from "./tools.ts";
|
|
19
19
|
|
|
20
20
|
/* ──────────────────────────────────────────────────────────────────────────── schemas */
|
|
21
21
|
|
|
22
22
|
const ScopeSchema = Type.Optional(
|
|
23
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.",
|
|
24
|
+
description: "Which sources to search. 'project' (default) = the project's modules plus its enabled plugins. 'engine' = Engine/Source areas + the project's enabled plugin roots (plugin-shipped types like Mover/PoseSearch live there). 'all' = both.",
|
|
25
25
|
}),
|
|
26
26
|
);
|
|
27
27
|
const EngineAreasSchema = Type.Optional(
|
|
28
28
|
Type.Array(Type.String(), {
|
|
29
29
|
// Must match DEFAULT_ENGINE_AREAS in lib/store.ts — the schema description is what agents read.
|
|
30
|
-
description: 'Engine
|
|
30
|
+
description: 'Engine scan dials, rooted at the engine install. Engine/Source areas: ["Runtime"] (default, DEFAULT_ENGINE_AREAS in lib/store.ts), ["Editor"], ["Runtime/Core"]. Engine-plugin areas: ["Plugins"] = every Engine/Plugins source tree (~2.8x the Runtime scan — this is where Mover, PoseSearch, PhysicsControl, BlendStack, SmartObjects, StateTree and GameplayAbilities live) or one subtree, e.g. ["Plugins/Animation/PhysicsControl"]. Engine-scope indexes always include the plugin roots the .uproject enables; these areas add the rest.',
|
|
31
31
|
}),
|
|
32
32
|
);
|
|
33
33
|
const LimitSchema = Type.Optional(Type.Number({ description: "Maximum rows to return. Default 25." }));
|
|
@@ -46,6 +46,7 @@ const SymbolParams = Type.Object({
|
|
|
46
46
|
engineAreas: EngineAreasSchema,
|
|
47
47
|
module: Type.Optional(Type.String({ description: "Restrict hits to one module, e.g. 'Engine'." })),
|
|
48
48
|
prefix: Type.Optional(Type.Boolean({ description: "Allow prefix matching (name families, typos). Default false: exact + qualified-member only." })),
|
|
49
|
+
substring: Type.Optional(Type.Boolean({ description: "Allow substring matching: a linear scan of the definition table (rank 3, listed after exact/prefix hits). Finds qualified members like 'PhysicsControlBPLibrary:Enable' from a bare 'Enable'. Costs ~0.3s per 76 MB of rows. Default false." })),
|
|
49
50
|
limit: LimitSchema,
|
|
50
51
|
});
|
|
51
52
|
|
|
@@ -65,8 +66,8 @@ const ListParams = Type.Object({
|
|
|
65
66
|
|
|
66
67
|
const ContextParams = Type.Object({
|
|
67
68
|
cwd: CwdSchema,
|
|
68
|
-
file: Type.Optional(Type.String({ description: "Source file to describe (.h/.cpp in the project, a plugin, or Engine/
|
|
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." })),
|
|
69
|
+
file: Type.Optional(Type.String({ description: "Source file to describe (.h/.cpp in the project, a plugin, Engine/Source, or Engine/Plugins): its module, declarations, include paths, defines file." })),
|
|
70
|
+
module: Type.Optional(Type.String({ description: "Module to describe instead of a file, e.g. 'GameplayAbilities' — dependency lists, include paths, defines file, standard, SDK, and whether the target's module graph contains it." })),
|
|
70
71
|
target: TargetSchema,
|
|
71
72
|
platform: PlatformSchema,
|
|
72
73
|
configuration: ConfigurationSchema,
|
|
@@ -78,9 +79,10 @@ const ContextParams = Type.Object({
|
|
|
78
79
|
|
|
79
80
|
const IndexParams = Type.Object({
|
|
80
81
|
cwd: CwdSchema,
|
|
81
|
-
action: Type.Union([Type.Literal("build"), Type.Literal("status"), Type.Literal("compiledb")], { description: "'build' creates or refreshes
|
|
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." })),
|
|
82
|
+
action: Type.Union([Type.Literal("build"), Type.Literal("status"), Type.Literal("headers"), Type.Literal("compiledb")], { description: "'build' creates or refreshes a symbol index. 'status' lists what is cached, what each index covers by root kind, and what building will cost. 'headers' builds the path-only header index and answers 'does this header exist in this engine version' (walks file paths, no content read: seconds). 'compiledb' runs UnrealBuildTool -Mode=GenerateClangDatabase and normalizes it for clangd (minutes, ~1 GB for the engine scope)." }),
|
|
83
|
+
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' (Engine/Source areas + the project's enabled plugin roots; minutes the first time). For 'headers': 'engine' (default) = engine sources + every Engine/Plugins tree, 'project' = project + its plugins, 'project+engine' = both. For 'compiledb': 'project' (default) = project + enabled plugin sources, 'project+engine' adds all of Engine/Source and Engine/Plugins." })),
|
|
83
84
|
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)." })),
|
|
85
|
+
name: Type.Optional(Type.String({ description: "For action 'headers': the header to answer, e.g. 'MoverState.h', 'PhysicsControlComponent.h', or a partial path 'Mover/Public/MoverTypes.h'. Absent = build/report only." })),
|
|
84
86
|
engineAreas: EngineAreasSchema,
|
|
85
87
|
target: TargetSchema,
|
|
86
88
|
platform: PlatformSchema,
|
|
@@ -135,11 +137,24 @@ const DefRowOut = Type.Object({
|
|
|
135
137
|
rank: Type.Number(),
|
|
136
138
|
match: Type.String(),
|
|
137
139
|
});
|
|
138
|
-
const IndexRefOut = Type.Object({ key: Type.String(), scope: Type.String(), defs: Type.Number(), generatedAt: Type.Number() });
|
|
140
|
+
const IndexRefOut = Type.Object({ key: Type.String(), scope: Type.String(), defs: Type.Number(), files: Type.Optional(Type.Number()), generatedAt: Type.Number() });
|
|
139
141
|
|
|
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) });
|
|
142
|
+
const SymbolOut = Type.Object({ scope: Type.String(), total: Type.Number(), results: Type.Array(DefRowOut), ranks: LooseList, indexes: Type.Array(IndexRefOut), coverage: Type.Optional(LooseList), notes: Strings, warnings: Type.Optional(Strings) });
|
|
141
143
|
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({
|
|
144
|
+
const ContextOut = Type.Object({
|
|
145
|
+
unit: Loose,
|
|
146
|
+
counts: Loose,
|
|
147
|
+
source: Type.String(),
|
|
148
|
+
availability: Type.Optional(Loose),
|
|
149
|
+
headerOnly: Type.Optional(Type.Boolean()),
|
|
150
|
+
headerKind: Type.Optional(Type.String()),
|
|
151
|
+
pluginName: Type.Optional(Type.String()),
|
|
152
|
+
pluginSource: Type.Optional(Type.String()),
|
|
153
|
+
enabledByDefault: Type.Optional(Type.Boolean()),
|
|
154
|
+
referencedInUproject: Type.Optional(Type.Boolean()),
|
|
155
|
+
warnings: Type.Optional(Strings),
|
|
156
|
+
notes: Type.Optional(Strings),
|
|
157
|
+
});
|
|
143
158
|
const IndexOut = Type.Object({
|
|
144
159
|
key: Type.Optional(Type.String()),
|
|
145
160
|
mode: Type.Optional(Type.String()),
|
|
@@ -161,6 +176,14 @@ const IndexOut = Type.Object({
|
|
|
161
176
|
notes: Type.Optional(Strings),
|
|
162
177
|
status: Type.Optional(Loose),
|
|
163
178
|
indexes: Type.Optional(LooseList),
|
|
179
|
+
headers: Type.Optional(LooseList),
|
|
180
|
+
rootKinds: Type.Optional(Type.String()),
|
|
181
|
+
coverage: Type.Optional(LooseList),
|
|
182
|
+
files: Type.Optional(Type.Number()),
|
|
183
|
+
exists: Type.Optional(Type.Boolean()),
|
|
184
|
+
name: Type.Optional(Type.String()),
|
|
185
|
+
found: Type.Optional(LooseList),
|
|
186
|
+
candidates: Type.Optional(LooseList),
|
|
164
187
|
});
|
|
165
188
|
|
|
166
189
|
/* ──────────────────────────────────────────────────────────────────────────── tools */
|
|
@@ -168,12 +191,13 @@ const IndexOut = Type.Object({
|
|
|
168
191
|
export const unrealSymbol = defineTool({
|
|
169
192
|
name: "unreal_symbol",
|
|
170
193
|
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.",
|
|
194
|
+
description: "Find where a C++ type, function, USTRUCT, UENUM or UPROPERTY is defined — project and enabled plugins by default, Engine/Source plus enabled plugin roots on request — one line per hit with its owning module.",
|
|
172
195
|
promptSnippet: "Locate a UE C++ symbol definition (project, plugins, engine) with module attribution",
|
|
173
196
|
promptGuidelines: [
|
|
174
197
|
"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,
|
|
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.",
|
|
198
|
+
"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, engineAreas [\"Runtime\"] keeps it to the engine's own sources. An engine index covers Engine/Source areas plus the plugin roots the .uproject enables; types shipped by plugins the project does NOT reference (PhysicsControl, BlendStack, StateTree, GameplayAbilities...) need engineAreas [\"Plugins\"].",
|
|
199
|
+
"Qualify member lookups: query 'Tick' with owner 'AActor', or query 'AActor:Tick'. Use prefix: true for name families ('ULp', 'ANiMy') and substring: true for qualified members ('Enable' finds 'UPhysicsControlBPLibrary:Enable'), never to silently accept a typo.",
|
|
200
|
+
"A 0-row answer is a statement about the roots that were scanned, not about the engine: read the coverage lines it prints. Asking whether a header exists in this engine version is a path question — use unreal_index action 'headers' name 'X.h', not a symbol lookup.",
|
|
177
201
|
"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
202
|
],
|
|
179
203
|
parameters: SymbolParams,
|
|
@@ -183,7 +207,7 @@ export const unrealSymbol = defineTool({
|
|
|
183
207
|
|
|
184
208
|
async execute(_id, params, _signal, _update, ctx) {
|
|
185
209
|
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 });
|
|
210
|
+
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, substring: params.substring, limit: params.limit, fs: realFs });
|
|
187
211
|
const rows = out.results.map((row) => rowLine(row, params.cwd ?? ctx.cwd));
|
|
188
212
|
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
213
|
return result(text, out, out.total === 0);
|
|
@@ -299,14 +323,15 @@ export const unrealContext = defineTool({
|
|
|
299
323
|
export const unrealIndex = defineTool({
|
|
300
324
|
name: "unreal_index",
|
|
301
325
|
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",
|
|
326
|
+
description: "Build or report the source index: project/plugins (seconds), Engine/Source plus enabled plugin roots (minutes the first time), the path-only header index that answers 'does this header exist', and the UnrealBuildTool Query cache behind the compile facts.",
|
|
327
|
+
promptSnippet: "Build/report the UE symbol index, header index, and UBT Query cache",
|
|
304
328
|
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.",
|
|
329
|
+
"Run unreal_index action 'status' first: it prints what is cached, how old it is, which root kinds each index covers, and what building will cost. A 0 from a lookup is only meaningful against the index status names — an index key encodes its root set, so an index you did not build is not what was searched.",
|
|
330
|
+
"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; engineAreas [\"Plugins\"] is the dial for plugin-shipped types (~2.8x the Runtime scan), [\"Plugins/Animation/PhysicsControl\"] for one plugin.",
|
|
331
|
+
"action 'headers' answers 'does this header exist in this engine version': a path walk only (no content read, seconds, cached), scoped by root kind and reporting the owning plugin, its EnabledByDefault and its .uproject status. Invented header names come back as NOT a file path plus the closest real file names, never as types.",
|
|
307
332
|
"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
333
|
"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).",
|
|
334
|
+
"action 'compiledb' is the clangd enrichment path: minutes of UnrealBuildTool work, ~1 GB for the engine scope (project+engine now includes Engine/Plugins sources, which is where plugin .cpp bodies live). 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
335
|
],
|
|
311
336
|
parameters: IndexParams,
|
|
312
337
|
outputSchema: IndexOut,
|
|
@@ -324,12 +349,37 @@ export const unrealIndex = defineTool({
|
|
|
324
349
|
];
|
|
325
350
|
for (const index of out.indexes) {
|
|
326
351
|
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("; ")}` : ""}`);
|
|
352
|
+
lines.push(` covers ${index.coverage}${index.areas?.length ? ` engineAreas: ${index.areas.join(", ")}` : ""}`);
|
|
327
353
|
}
|
|
328
354
|
if (!out.indexes.length) lines.push("no symbol index yet — unreal_index action build scope project (seconds)");
|
|
329
|
-
const
|
|
355
|
+
for (const header of out.headers) {
|
|
356
|
+
lines.push(`header index ${header.key} ${header.files} header paths from ${header.roots} roots (${header.coverage})${header.areas?.length ? ` areas: ${header.areas.join(", ")}` : ""} ${Math.round(header.ageMs / 1000)}s old ${header.sizeMb}MB${header.issues.length ? ` issues: ${header.issues.join("; ")}` : ""}`);
|
|
357
|
+
}
|
|
358
|
+
if (!out.headers.length) lines.push('no header index yet — unreal_index action headers builds the path-only index (seconds, no content scan) and answers "does this header exist in this engine version"');
|
|
359
|
+
const text = [...lines, ...out.notes.map((n) => `· ${n}`), ...(out.query.notes ?? []).map((n) => `· ${n}`)].join("\n");
|
|
330
360
|
return result(text, out);
|
|
331
361
|
}
|
|
332
362
|
|
|
363
|
+
if (params.action === "headers") {
|
|
364
|
+
const out = await headerReport({
|
|
365
|
+
cwd: params.cwd ?? ctx.cwd,
|
|
366
|
+
name: params.name,
|
|
367
|
+
scope: (params.scope === "project" ? "project" : params.scope === "project+engine" ? "all" : "engine") as "project" | "engine" | "all",
|
|
368
|
+
build: true,
|
|
369
|
+
engineAreas: params.engineAreas,
|
|
370
|
+
refresh: params.refresh,
|
|
371
|
+
fs: realFs,
|
|
372
|
+
});
|
|
373
|
+
const lines = [`unreal_index headers${out.key ? ` → ${out.key}` : ""}`, ` ${out.files} header paths from ${out.roots} roots (${out.rootKinds})${out.built ? "" : " not built"}`];
|
|
374
|
+
if (out.name) {
|
|
375
|
+
lines.push(` '${out.name}': ${out.exists ? "EXISTS" : "does NOT exist as a file path in this engine version"}`);
|
|
376
|
+
for (const row of out.found) lines.push(` ${row.file} [${row.module}${row.plugin ? ` · plugin ${row.plugin} [${row.pluginSource}] EnabledByDefault ${row.enabledByDefault} · .uproject reference ${row.referencedInUproject ? "yes" : "NO"}` : ""}]`);
|
|
377
|
+
if (out.candidates.length) lines.push(` closest real file names (files, not types):\n${out.candidates.map((row) => ` ${row.file} [${row.module}]`).join("\n")}`);
|
|
378
|
+
}
|
|
379
|
+
const text = [...lines, ...out.notes.map((n) => `· ${n}`), ...(out.warnings ?? []).map((w) => `! ${w}`)].join("\n");
|
|
380
|
+
return result(text, out as unknown as Record<string, JsonValue>, Boolean(out.name) && !out.exists);
|
|
381
|
+
}
|
|
382
|
+
|
|
333
383
|
if (params.action === "compiledb") {
|
|
334
384
|
const db = await compileDbReport({
|
|
335
385
|
cwd: params.cwd ?? ctx.cwd,
|