ai agent
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* Agent Skill generator/installer for the RA3 Mod XML MCP tools.
|
||||
*
|
||||
* The Skill focuses on the functionality itself: when to use it and how to
|
||||
* use the RA3 Mod XML MCP query tools. It intentionally avoids referencing
|
||||
* any project-specific docs (e.g. docs/codebase-navigation-guide.md) so the
|
||||
* agent is not distracted by unrelated workspace guidance.
|
||||
*
|
||||
* Pure TypeScript: no VS Code dependency.
|
||||
*/
|
||||
|
||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { defaultAgentHome } from "./snapshot";
|
||||
|
||||
export const SKILL_NAME = "ra3-mod-xml";
|
||||
export const SKILL_MARKER_FILE = ".ra3modxml-skill.json";
|
||||
|
||||
const SKILL_MD = `---
|
||||
name: ra3-mod-xml
|
||||
description: Use the RA3 Mod XML semantic index to find asset definitions, references, outgoing references, active files, defines, and include sources in SAGE / BinaryAssetBuilder XML projects such as Command & Conquer: Red Alert 3 mods. Use this when you need exact facts about mod assets instead of guessing or full-text searching the XML tree.
|
||||
---
|
||||
|
||||
# RA3 Mod XML Index
|
||||
|
||||
This skill provides access to the semantic index built by the RA3 Mod XML VS Code extension.
|
||||
|
||||
## When this skill applies
|
||||
|
||||
Use these tools only for SAGE / BinaryAssetBuilder mod XML projects — the kind
|
||||
used by Command & Conquer: Red Alert 3 mods.
|
||||
|
||||
Positive signals (any single one is enough to try the tools):
|
||||
|
||||
- \`Data/Mod.xml\` exists.
|
||||
- \`Data/additionalmaps/mapmetadata_*.xml\` exists.
|
||||
- A \`*.babproj\` file exists.
|
||||
- XML whose root element is \`<AssetDeclaration>\`.
|
||||
- XML that uses \`<Includes><Include source="DATA:…" /></Includes>\` or declares
|
||||
\`xmlns="uri:ea.com:eala:asset"\`.
|
||||
|
||||
Do **not** use these tools for unrelated repositories. In particular, do not
|
||||
use them merely because a project contains XML, a build script, copied \`.xsd\`
|
||||
files, or a folder named \`Data\`. A file named \`CnC3Types.xsd\` refers to
|
||||
C&C3 (Tiberium Wars / Kane's Wrath) and is **not** by itself evidence of a
|
||||
Red Alert 3 mod. When the repository is not a SAGE / RA3 mod project, ignore
|
||||
this skill entirely.
|
||||
|
||||
If you are unsure whether the current project is in scope, call \`get_status\`
|
||||
first: it is cheap and reports the \`projectDir\` the index belongs to. Stop
|
||||
using the index tools when:
|
||||
|
||||
- the state is \`no_index\`, or
|
||||
- the reported \`projectDir\` does not match the workspace root you are working in.
|
||||
|
||||
In those cases read the files directly instead. Never present index results
|
||||
from one project as if they belonged to another.
|
||||
|
||||
## When to use
|
||||
|
||||
Use this skill when you need any of the following:
|
||||
|
||||
- Find where an asset id is defined (GameObject, WeaponTemplate, Texture, etc.).
|
||||
- Find which files/positions reference an asset id.
|
||||
- Find which assets an asset references, and through which element/attribute.
|
||||
- List assets of a particular type.
|
||||
- Check whether a file is actually part of the active include graph (i.e. not a dead file).
|
||||
- Resolve an Include source string.
|
||||
- Look up a $DEFINE constant.
|
||||
- Get the current index status and statistics.
|
||||
|
||||
Do not use full-text search over the XML tree when one of the MCP query tools
|
||||
can answer the question directly.
|
||||
|
||||
## How to use
|
||||
|
||||
1. Call \`get_status\` first when you are unsure whether an index is available,
|
||||
current, or belongs to the project you are working in.
|
||||
2. Use narrow queries:
|
||||
- \`find_asset(id, type?)\` for definition locations.
|
||||
- \`find_references(id, type?)\` for incoming semantic references.
|
||||
- \`get_asset_references(id, type?, depth?, targetTypes?)\` for outgoing
|
||||
references (what this asset uses, and where that link is written).
|
||||
- \`list_assets_by_type(type, prefix?, limit?)\` for browsing assets.
|
||||
- \`is_file_active(path)\` to determine whether a file is included in an indexed stream.
|
||||
- \`find_define(name)\` for $DEFINE constants.
|
||||
- \`resolve_include(source)\` for Include source candidates.
|
||||
3. Asset ids are case-insensitive.
|
||||
4. If an id exists for multiple asset types, pass the type filter to avoid mixing definitions.
|
||||
5. If the returned index state is \`stale\`, \`ready_xml\`, or \`building\`, treat
|
||||
results as provisional.
|
||||
6. Never read or dump the whole index snapshot; query narrowly.
|
||||
|
||||
## Following references with get_asset_references
|
||||
|
||||
Use \`get_asset_references\` to answer "which weapon / model / upgrade / die-object
|
||||
does this asset actually use" without reading source first. It returns **edges**,
|
||||
not a flat list, so provenance is preserved:
|
||||
|
||||
- \`from\` is the asset you asked about.
|
||||
- \`to\` is the resolved definition (file + line), or null when unresolved.
|
||||
- \`via.element\` / \`via.parent\` / \`via.attribute\` say exactly which XML
|
||||
element and attribute created the link.
|
||||
- \`source\` is the file/line where that link is written.
|
||||
- \`definedIn\` is present when the link comes from an \`inheritFrom\` ancestor's
|
||||
XML rather than from the asset's own file. Inheritance is walked at the same
|
||||
depth, so a base asset's weapon slot configuration is reported together with
|
||||
the derived asset's own modules.
|
||||
|
||||
Practical rules:
|
||||
|
||||
- Start with the default \`depth: 1\` (the queried asset only). Raise it to 2 or 3
|
||||
only for a specific node you already decided to follow. The maximum is 3.
|
||||
- Pass \`targetTypes\` to cut noise, e.g. \`["WeaponTemplate"]\`, or
|
||||
\`["GameObject"]\` for \`CreateObjectDie\`-style die-object links.
|
||||
- If \`truncated\` is true, read \`omittedByTargetType\` and narrow the query
|
||||
(smaller \`targetTypes\`, lower \`depth\`) instead of blindly retrying.
|
||||
- Call \`find_references\` in the opposite direction: it tells you who else would
|
||||
be affected by a change.
|
||||
- Merged/inherited *effective values* are not computed. When \`xai:joinAction\`
|
||||
(\`Replace\` / \`Remove\`) appears in the merge path, open the file and confirm
|
||||
the real result yourself.
|
||||
`;
|
||||
|
||||
const QUERY_GUIDE_MD = `# RA3 Mod XML query tool reference
|
||||
|
||||
The MCP server exposes these tools:
|
||||
|
||||
- get_status()
|
||||
- find_asset(id, type?)
|
||||
- find_references(id, type?)
|
||||
- get_asset_references(id, type?, depth?, targetTypes?, maxEdges?, includeUnresolved?)
|
||||
- list_assets_by_type(type, prefix?, limit?)
|
||||
- is_file_active(path)
|
||||
- find_define(name)
|
||||
- resolve_include(source)
|
||||
- get_usage_guide()
|
||||
|
||||
## Result metadata
|
||||
|
||||
Every query result includes an \`index\` object:
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"state": "ready",
|
||||
"projectDir": "D:/Mods/Example",
|
||||
"complete": true,
|
||||
"stale": false
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
Possible states:
|
||||
|
||||
- no_index: no index snapshot exists.
|
||||
- building: a rebuild is in progress.
|
||||
- ready_xml: XML assets are available but art assets may be incomplete.
|
||||
- ready: complete index.
|
||||
- stale: index may be outdated.
|
||||
- error: last build failed.
|
||||
|
||||
Always check \`index.projectDir\`. If it does not match the project you are
|
||||
working on, discard the result and read files directly instead.
|
||||
|
||||
## get_asset_references
|
||||
|
||||
Returns outgoing reference edges with full provenance:
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"index": { "state": "ready", "projectDir": "D:/Mods/Example" },
|
||||
"data": {
|
||||
"roots": [{ "type": "GameObject", "id": "AthenaCannon", "file": "...", "line": 12 }],
|
||||
"edges": [
|
||||
{
|
||||
"depth": 1,
|
||||
"from": { "type": "GameObject", "id": "AthenaCannon" },
|
||||
"to": { "type": "WeaponTemplate", "id": "AthenaCannonWeapon", "file": "...", "line": 88 },
|
||||
"via": { "kind": "attribute", "element": "Weapon", "parent": "WeaponSlotHardpoint", "attribute": "Template" },
|
||||
"source": { "file": "D:/Mods/Example/Data/Allied/Units/AthenaCannon.xml", "line": 40, "character": 24 }
|
||||
}
|
||||
],
|
||||
"nodes": [],
|
||||
"truncated": false,
|
||||
"omittedByTargetType": {},
|
||||
"warnings": []
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
\`via.kind\` is one of \`attribute\`, \`content\` or \`inheritFrom\`. Edges carrying
|
||||
\`definedIn\` come from an \`inheritFrom\` ancestor's XML.
|
||||
|
||||
This tool requires a **live** index (VS Code open with the project indexed).
|
||||
It is not available from the on-disk snapshot because element context is not
|
||||
stored there. When live is unavailable the tool returns an explicit error
|
||||
instead of an empty result.
|
||||
`;
|
||||
|
||||
export interface SkillInstallRecord {
|
||||
/** Skill directory (the directory containing SKILL.md). */
|
||||
path: string;
|
||||
/** Extension version that installed/updated this copy. */
|
||||
sourceVersion: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a managed copy of the Skill into `targetDir` (the directory that
|
||||
* should contain SKILL.md).
|
||||
*/
|
||||
export async function writeSkillTo(
|
||||
targetDir: string,
|
||||
sourceVersion: string,
|
||||
): Promise<void> {
|
||||
await mkdir(join(targetDir, "references"), { recursive: true });
|
||||
await writeFile(join(targetDir, "SKILL.md"), SKILL_MD, "utf8");
|
||||
await writeFile(
|
||||
join(targetDir, "references", "query-guide.md"),
|
||||
QUERY_GUIDE_MD,
|
||||
"utf8",
|
||||
);
|
||||
const marker: SkillInstallRecord = {
|
||||
path: targetDir,
|
||||
sourceVersion,
|
||||
};
|
||||
await writeFile(
|
||||
join(targetDir, SKILL_MARKER_FILE),
|
||||
JSON.stringify(marker, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
/** Conventional ~/.agents/skills/<skill-name> path. */
|
||||
export function agentsSkillsDirForUser(home = homedir()): string {
|
||||
return join(home, ".agents", "skills", SKILL_NAME);
|
||||
}
|
||||
|
||||
/** Conventional ~/.claude/skills/<skill-name> path (Claude Code). */
|
||||
export function claudeSkillsDirForUser(home = homedir()): string {
|
||||
return join(home, ".claude", "skills", SKILL_NAME);
|
||||
}
|
||||
|
||||
/** Path to the managed-install record under the agent home. */
|
||||
export function skillInstallRecordPath(agentHome = defaultAgentHome()): string {
|
||||
return join(agentHome, "skill-install.json");
|
||||
}
|
||||
|
||||
/** Reads the managed skill install record, or returns an empty list. */
|
||||
export async function readSkillInstallRecord(
|
||||
agentHome = defaultAgentHome(),
|
||||
): Promise<SkillInstallRecord[]> {
|
||||
try {
|
||||
const text = await readFile(skillInstallRecordPath(agentHome), "utf8");
|
||||
const parsed = JSON.parse(text) as { installed?: SkillInstallRecord[] };
|
||||
return Array.isArray(parsed.installed) ? parsed.installed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Writes the managed skill install record. */
|
||||
export async function writeSkillInstallRecord(
|
||||
installed: SkillInstallRecord[],
|
||||
agentHome = defaultAgentHome(),
|
||||
): Promise<void> {
|
||||
const file = skillInstallRecordPath(agentHome);
|
||||
await mkdir(dirname(file), { recursive: true });
|
||||
await writeFile(
|
||||
file,
|
||||
JSON.stringify({ installed }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the Skill into several directories and records each managed copy.
|
||||
* Returns the directories successfully written.
|
||||
*/
|
||||
export async function installSkillToDirectories(
|
||||
directories: string[],
|
||||
sourceVersion: string,
|
||||
agentHome = defaultAgentHome(),
|
||||
): Promise<string[]> {
|
||||
const installed = await readSkillInstallRecord(agentHome);
|
||||
const succeeded: string[] = [];
|
||||
for (const dir of directories) {
|
||||
try {
|
||||
await writeSkillTo(dir, sourceVersion);
|
||||
if (!installed.some((r) => r.path === dir)) {
|
||||
installed.push({ path: dir, sourceVersion });
|
||||
} else {
|
||||
const record = installed.find((r) => r.path === dir);
|
||||
if (record) record.sourceVersion = sourceVersion;
|
||||
}
|
||||
succeeded.push(dir);
|
||||
} catch {
|
||||
// Keep going; caller can surface per-directory failures.
|
||||
}
|
||||
}
|
||||
await writeSkillInstallRecord(installed, agentHome);
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
/** Removes one managed Skill directory and its install record entry. */
|
||||
export async function uninstallSkillFromDirectory(
|
||||
directory: string,
|
||||
agentHome = defaultAgentHome(),
|
||||
): Promise<void> {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
const installed = (await readSkillInstallRecord(agentHome)).filter(
|
||||
(r) => r.path !== directory,
|
||||
);
|
||||
await writeSkillInstallRecord(installed, agentHome);
|
||||
}
|
||||
|
||||
/** Re-writes every recorded Skill copy with the current extension version. */
|
||||
export async function syncInstalledSkills(
|
||||
sourceVersion: string,
|
||||
agentHome = defaultAgentHome(),
|
||||
): Promise<SkillInstallRecord[]> {
|
||||
const installed = await readSkillInstallRecord(agentHome);
|
||||
const synced: SkillInstallRecord[] = [];
|
||||
for (const record of installed) {
|
||||
try {
|
||||
await writeSkillTo(record.path, sourceVersion);
|
||||
synced.push({ path: record.path, sourceVersion });
|
||||
} catch {
|
||||
// Skip unreadable/missing targets; the record will be cleaned below.
|
||||
}
|
||||
}
|
||||
await writeSkillInstallRecord(synced, agentHome);
|
||||
return synced;
|
||||
}
|
||||
Reference in New Issue
Block a user