unity-agentic-tools 0.3.3 → 0.4.1

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/dist/index.js CHANGED
@@ -205,7 +205,7 @@ var require_package = __commonJS((exports2, module2) => {
205
205
  module2.exports = {
206
206
  name: "unity-agentic-tools",
207
207
  packageManager: "bun@latest",
208
- version: "0.3.3",
208
+ version: "0.4.1",
209
209
  description: "Fast, token-efficient Unity YAML parser for AI agents",
210
210
  exports: {
211
211
  ".": {
@@ -284,6 +284,7 @@ __export(exports_src, {
284
284
  removeRemovedGameObject: () => removeRemovedGameObject,
285
285
  removeRemovedComponent: () => removeRemovedComponent,
286
286
  removePrefabOverride: () => removePrefabOverride,
287
+ removeComponentBatch: () => removeComponentBatch,
287
288
  removeComponent: () => removeComponent,
288
289
  read_settings: () => read_settings,
289
290
  isNativeModuleAvailable: () => isNativeModuleAvailable,
@@ -560,6 +561,12 @@ function find_unity_project_root(startDir) {
560
561
  }
561
562
  return null;
562
563
  }
564
+ function ensure_parent_dir(file_path) {
565
+ const dir = import_path4.dirname(file_path);
566
+ if (dir && dir !== "." && !import_fs4.existsSync(dir)) {
567
+ import_fs4.mkdirSync(dir, { recursive: true });
568
+ }
569
+ }
563
570
  function atomicWrite(filePath, content) {
564
571
  if (import_fs4.existsSync(filePath)) {
565
572
  try {
@@ -1510,6 +1517,7 @@ function search_project(options) {
1510
1517
  const files = walk_project_files(project_path, extensions);
1511
1518
  const scanner = new UnityScanner;
1512
1519
  const matches = [];
1520
+ let files_with_errors = 0;
1513
1521
  for (const file of files) {
1514
1522
  try {
1515
1523
  let gameObjects;
@@ -1596,6 +1604,7 @@ function search_project(options) {
1596
1604
  if (max_matches !== undefined && matches.length >= max_matches)
1597
1605
  break;
1598
1606
  } catch {
1607
+ files_with_errors++;
1599
1608
  continue;
1600
1609
  }
1601
1610
  }
@@ -1604,6 +1613,7 @@ function search_project(options) {
1604
1613
  project_path,
1605
1614
  total_files_scanned: files.length,
1606
1615
  total_matches: matches.length,
1616
+ files_with_errors: files_with_errors > 0 ? files_with_errors : undefined,
1607
1617
  cursor: 0,
1608
1618
  truncated: max_matches !== undefined && matches.length >= max_matches,
1609
1619
  matches
@@ -2208,6 +2218,7 @@ function resolveScriptGuid(script, projectPath) {
2208
2218
  }
2209
2219
  }
2210
2220
  if (matches[0].filePath?.endsWith(".dll")) {
2221
+ const classNameLower = targetName.toLowerCase();
2211
2222
  const dllBasename = path3.basename(matches[0].filePath).toLowerCase();
2212
2223
  for (const cacheName of ["package-cache.json", "local-package-cache.json"]) {
2213
2224
  const cachePath = path3.join(projectPath, ".unity-agentic", cacheName);
@@ -2215,6 +2226,11 @@ function resolveScriptGuid(script, projectPath) {
2215
2226
  continue;
2216
2227
  try {
2217
2228
  const cache = JSON.parse(import_fs8.readFileSync(cachePath, "utf-8"));
2229
+ for (const [guid, assetPath] of Object.entries(cache)) {
2230
+ if (assetPath.endsWith(".cs") && path3.basename(assetPath, ".cs").toLowerCase() === classNameLower) {
2231
+ return { guid, path: assetPath };
2232
+ }
2233
+ }
2218
2234
  for (const [guid, assetPath] of Object.entries(cache)) {
2219
2235
  if (assetPath.toLowerCase().endsWith(".dll") && path3.basename(assetPath).toLowerCase() === dllBasename) {
2220
2236
  return { guid, path: assetPath };
@@ -2240,6 +2256,7 @@ function resolveScriptGuid(script, projectPath) {
2240
2256
  }
2241
2257
  }
2242
2258
  if (!guid && m.filePath?.endsWith(".dll")) {
2259
+ const clsLower = (m.name ?? targetName).toLowerCase();
2243
2260
  const dllBase = path3.basename(m.filePath).toLowerCase();
2244
2261
  for (const cn of ["package-cache.json", "local-package-cache.json"]) {
2245
2262
  const cp = path3.join(projectPath, ".unity-agentic", cn);
@@ -2248,11 +2265,19 @@ function resolveScriptGuid(script, projectPath) {
2248
2265
  try {
2249
2266
  const c = JSON.parse(import_fs8.readFileSync(cp, "utf-8"));
2250
2267
  for (const [g, p] of Object.entries(c)) {
2251
- if (p.toLowerCase().endsWith(".dll") && path3.basename(p).toLowerCase() === dllBase) {
2268
+ if (p.endsWith(".cs") && path3.basename(p, ".cs").toLowerCase() === clsLower) {
2252
2269
  guid = g;
2253
2270
  break;
2254
2271
  }
2255
2272
  }
2273
+ if (!guid) {
2274
+ for (const [g, p] of Object.entries(c)) {
2275
+ if (p.toLowerCase().endsWith(".dll") && path3.basename(p).toLowerCase() === dllBase) {
2276
+ guid = g;
2277
+ break;
2278
+ }
2279
+ }
2280
+ }
2256
2281
  } catch {}
2257
2282
  if (guid)
2258
2283
  break;
@@ -2283,7 +2308,11 @@ function resolveScriptGuid(script, projectPath) {
2283
2308
  throw new Error(`Ambiguous type "${script}": found ${allResolved.length} matches (${paths}). ` + `Use a qualified name (e.g., "Namespace.${targetName}") or provide the full path.`);
2284
2309
  }
2285
2310
  }
2286
- } catch {}
2311
+ } catch (err) {
2312
+ if (err instanceof Error && err.message.startsWith("Ambiguous type")) {
2313
+ throw err;
2314
+ }
2315
+ }
2287
2316
  }
2288
2317
  const scriptNameLower = script.toLowerCase().replace(/\.cs$/, "");
2289
2318
  const dotIdx = scriptNameLower.lastIndexOf(".");
@@ -2310,6 +2339,54 @@ function resolveScriptGuid(script, projectPath) {
2310
2339
  }
2311
2340
  return null;
2312
2341
  }
2342
+ var ASSET_PPTR_MAP = {
2343
+ ".inputactions": { fileID: 11400000, type: 2 },
2344
+ ".asset": { fileID: 11400000, type: 2 },
2345
+ ".mat": { fileID: 2100000, type: 2 },
2346
+ ".prefab": { fileID: 100100000, type: 2 },
2347
+ ".controller": { fileID: 9100000, type: 2 },
2348
+ ".anim": { fileID: 7400000, type: 2 }
2349
+ };
2350
+ var ASSET_EXTENSIONS = new Set(Object.keys(ASSET_PPTR_MAP));
2351
+ function resolveAssetPathToPPtr(value, file_path, project_path) {
2352
+ if (value.startsWith("{fileID:") || value.startsWith("{"))
2353
+ return;
2354
+ const ext = path3.extname(value).toLowerCase();
2355
+ if (!ASSET_EXTENSIONS.has(ext))
2356
+ return;
2357
+ const resolved_project = project_path || find_unity_project_root(path3.dirname(file_path));
2358
+ if (!resolved_project)
2359
+ return null;
2360
+ const mapping = ASSET_PPTR_MAP[ext];
2361
+ let guid = null;
2362
+ const basename_val = path3.basename(value, ext);
2363
+ if (value.includes("/") || value.includes("\\")) {
2364
+ const abs = path3.isAbsolute(value) ? value : path3.join(resolved_project, value);
2365
+ guid = extractGuidFromMeta(abs + ".meta");
2366
+ }
2367
+ if (!guid) {
2368
+ const cache = load_guid_cache_for_file(file_path, resolved_project);
2369
+ if (cache) {
2370
+ const found = cache.find_by_name(basename_val, ext);
2371
+ if (found)
2372
+ guid = found.guid;
2373
+ }
2374
+ }
2375
+ if (!guid) {
2376
+ const candidates = [
2377
+ path3.join(resolved_project, "Assets", value),
2378
+ path3.join(resolved_project, value)
2379
+ ];
2380
+ for (const candidate of candidates) {
2381
+ guid = extractGuidFromMeta(candidate + ".meta");
2382
+ if (guid)
2383
+ break;
2384
+ }
2385
+ }
2386
+ if (!guid)
2387
+ return null;
2388
+ return `{fileID: ${mapping.fileID}, guid: ${guid}, type: ${mapping.type}}`;
2389
+ }
2313
2390
  function resolve_script_with_fields(script, project_path) {
2314
2391
  const resolved = resolveScriptGuid(script, project_path);
2315
2392
  if (!resolved)
@@ -2866,7 +2943,9 @@ ${element_indent2}- ${value}`);
2866
2943
  if (!header) {
2867
2944
  throw new Error(`Invalid Unity YAML block header in replacement text: "${new_text.slice(0, 80)}"`);
2868
2945
  }
2869
- this._raw = new_text;
2946
+ this._raw = new_text.endsWith(`
2947
+ `) ? new_text : new_text + `
2948
+ `;
2870
2949
  this._header = header;
2871
2950
  this._dirty = true;
2872
2951
  this._format_cache.clear();
@@ -2876,6 +2955,11 @@ ${element_indent2}- ${value}`);
2876
2955
  }
2877
2956
  _check_dirty(old_raw) {
2878
2957
  if (this._raw !== old_raw) {
2958
+ if (!this._raw.endsWith(`
2959
+ `)) {
2960
+ this._raw += `
2961
+ `;
2962
+ }
2879
2963
  this._dirty = true;
2880
2964
  return true;
2881
2965
  }
@@ -3937,14 +4021,82 @@ function read_project_version(projectPath) {
3937
4021
  }
3938
4022
 
3939
4023
  // src/editor/create.ts
4024
+ function md4(data) {
4025
+ function F(x, y, z) {
4026
+ return x & y | ~x & z;
4027
+ }
4028
+ function G(x, y, z) {
4029
+ return x & y | x & z | y & z;
4030
+ }
4031
+ function H(x, y, z) {
4032
+ return x ^ y ^ z;
4033
+ }
4034
+ function rotl(x, n) {
4035
+ return (x << n | x >>> 32 - n) >>> 0;
4036
+ }
4037
+ const len = data.length;
4038
+ const bitLen = len * 8;
4039
+ const padded_len = len + 9 + 63 & ~63;
4040
+ const buf = Buffer.alloc(padded_len);
4041
+ data.copy(buf);
4042
+ buf[len] = 128;
4043
+ buf.writeUInt32LE(bitLen >>> 0, padded_len - 8);
4044
+ buf.writeUInt32LE(Math.floor(bitLen / 4294967296) >>> 0, padded_len - 4);
4045
+ let a0 = 1732584193;
4046
+ let b0 = 4023233417;
4047
+ let c0 = 2562383102;
4048
+ let d0 = 271733878;
4049
+ const R1_K = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
4050
+ const R1_S = [3, 7, 11, 19, 3, 7, 11, 19, 3, 7, 11, 19, 3, 7, 11, 19];
4051
+ const R2_K = [0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15];
4052
+ const R2_S = [3, 5, 9, 13, 3, 5, 9, 13, 3, 5, 9, 13, 3, 5, 9, 13];
4053
+ const R3_K = [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15];
4054
+ const R3_S = [3, 9, 11, 15, 3, 9, 11, 15, 3, 9, 11, 15, 3, 9, 11, 15];
4055
+ for (let i = 0;i < padded_len; i += 64) {
4056
+ const X = [];
4057
+ for (let j = 0;j < 16; j++)
4058
+ X[j] = buf.readUInt32LE(i + j * 4);
4059
+ let a = a0, b = b0, c = c0, d = d0;
4060
+ for (let j = 0;j < 16; j++) {
4061
+ const t = a + F(b, c, d) + X[R1_K[j]] >>> 0;
4062
+ a = d;
4063
+ d = c;
4064
+ c = b;
4065
+ b = rotl(t, R1_S[j]);
4066
+ }
4067
+ for (let j = 0;j < 16; j++) {
4068
+ const t = a + G(b, c, d) + X[R2_K[j]] + 1518500249 >>> 0;
4069
+ a = d;
4070
+ d = c;
4071
+ c = b;
4072
+ b = rotl(t, R2_S[j]);
4073
+ }
4074
+ for (let j = 0;j < 16; j++) {
4075
+ const t = a + H(b, c, d) + X[R3_K[j]] + 1859775393 >>> 0;
4076
+ a = d;
4077
+ d = c;
4078
+ c = b;
4079
+ b = rotl(t, R3_S[j]);
4080
+ }
4081
+ a0 = a0 + a >>> 0;
4082
+ b0 = b0 + b >>> 0;
4083
+ c0 = c0 + c >>> 0;
4084
+ d0 = d0 + d >>> 0;
4085
+ }
4086
+ const result = Buffer.alloc(16);
4087
+ result.writeUInt32LE(a0, 0);
4088
+ result.writeUInt32LE(b0, 4);
4089
+ result.writeUInt32LE(c0, 8);
4090
+ result.writeUInt32LE(d0, 12);
4091
+ return result;
4092
+ }
3940
4093
  function compute_dll_script_file_id(namespace, class_name) {
3941
- const { createHash } = require("crypto");
3942
4094
  const full_name = namespace ? `${namespace}.${class_name}` : class_name;
3943
4095
  const name_bytes = Buffer.from(full_name, "utf-8");
3944
4096
  const input = Buffer.alloc(4 + name_bytes.length);
3945
4097
  input.writeInt32LE(114, 0);
3946
4098
  name_bytes.copy(input, 4);
3947
- const hash = createHash("md4").update(input).digest();
4099
+ const hash = md4(input);
3948
4100
  const a = hash.readInt32LE(0);
3949
4101
  const b = hash.readInt32LE(4);
3950
4102
  const c = hash.readInt32LE(8);
@@ -3957,13 +4109,13 @@ function get_layer_from_parent(doc, parentTransformId) {
3957
4109
  const parentTransform = doc.find_by_file_id(parentTransformId);
3958
4110
  if (!parentTransform)
3959
4111
  return 0;
3960
- const goMatch = parentTransform.raw.match(/m_GameObject:\s*\{fileID:\s*(\d+)\}/);
4112
+ const goMatch = parentTransform.raw.match(/m_GameObject:[ \t]*\{fileID:[ \t]*(\d+)\}/);
3961
4113
  if (!goMatch)
3962
4114
  return 0;
3963
4115
  const parentGo = doc.find_by_file_id(goMatch[1]);
3964
4116
  if (!parentGo)
3965
4117
  return 0;
3966
- const layerMatch = parentGo.raw.match(/m_Layer:\s*(\d+)/);
4118
+ const layerMatch = parentGo.raw.match(/m_Layer:[ \t]*(\d+)/);
3967
4119
  return layerMatch ? parseInt(layerMatch[1], 10) : 0;
3968
4120
  }
3969
4121
  function createGameObjectYAML(gameObjectId, transformId, name, parentTransformId = "0", rootOrder = 0, layer = 0) {
@@ -4284,11 +4436,14 @@ ${defaults}`;
4284
4436
  function addComponentToGameObject(doc, gameObjectId, componentId) {
4285
4437
  const goBlock = doc.find_by_file_id(gameObjectId);
4286
4438
  if (!goBlock)
4287
- return;
4288
- let raw = goBlock.raw;
4289
- raw = raw.replace(/(m_Component:\s*\n(?:\s*-\s*component:\s*\{fileID:\s*\d+\}\s*\n)*)/, `$1 - component: {fileID: ${componentId}}
4439
+ return false;
4440
+ const raw = goBlock.raw;
4441
+ const updated = raw.replace(/(m_Component:[ \t]*\n(?:[ \t]*-[ \t]*component:[ \t]*\{fileID:[ \t]*\d+\}[ \t]*\n)*)/, `$1 - component: {fileID: ${componentId}}
4290
4442
  `);
4291
- goBlock.replace_raw(raw);
4443
+ if (updated === raw)
4444
+ return false;
4445
+ goBlock.replace_raw(updated);
4446
+ return true;
4292
4447
  }
4293
4448
  function createMonoBehaviourYAML(componentId, gameObjectId, scriptGuid, fields, version, scriptFileId = 11500000, type_lookup) {
4294
4449
  const field_yaml = fields && fields.length > 0 ? generate_field_yaml(fields, version, " ", type_lookup) : `
@@ -4360,7 +4515,7 @@ function createGameObject(options) {
4360
4515
  if (parentBlock.class_id === 4) {
4361
4516
  parentTransformIdStr = parentIdStr;
4362
4517
  } else if (parentBlock.class_id === 1) {
4363
- const compMatch = parentBlock.raw.match(/m_Component:\s*\n\s*-\s*component:\s*\{fileID:\s*(\d+)\}/);
4518
+ const compMatch = parentBlock.raw.match(/m_Component:[ \t]*\n[ \t]*-[ \t]*component:[ \t]*\{fileID:[ \t]*(\d+)\}/);
4364
4519
  if (compMatch) {
4365
4520
  parentTransformIdStr = compMatch[1];
4366
4521
  } else {
@@ -4774,6 +4929,7 @@ Light:
4774
4929
  `;
4775
4930
  }
4776
4931
  try {
4932
+ ensure_parent_dir(output_path);
4777
4933
  import_fs10.writeFileSync(output_path, yaml, "utf-8");
4778
4934
  } catch (err) {
4779
4935
  return {
@@ -4912,6 +5068,7 @@ PrefabInstance:
4912
5068
  m_SourcePrefab: {fileID: 100100000, guid: ${sourceGuid}, type: 3}
4913
5069
  `;
4914
5070
  try {
5071
+ ensure_parent_dir(output_path);
4915
5072
  import_fs10.writeFileSync(output_path, variantYaml, "utf-8");
4916
5073
  } catch (err) {
4917
5074
  return {
@@ -5072,6 +5229,7 @@ ${yaml_lines.join(`
5072
5229
  }
5073
5230
  }
5074
5231
  try {
5232
+ ensure_parent_dir(output_path);
5075
5233
  import_fs10.writeFileSync(output_path, assetYaml, "utf-8");
5076
5234
  } catch (err) {
5077
5235
  return { success: false, output_path, error: `Failed to write asset file: ${err instanceof Error ? err.message : String(err)}` };
@@ -5267,7 +5425,7 @@ function addComponent(options) {
5267
5425
  let duplicateWarning;
5268
5426
  const goBlock = doc.find_by_file_id(gameObjectIdStr);
5269
5427
  if (goBlock && classId !== null) {
5270
- const compRefs = [...goBlock.raw.matchAll(/component:\s*\{fileID:\s*(\d+)\}/g)].map((m) => m[1]);
5428
+ const compRefs = [...goBlock.raw.matchAll(/component:[ \t]*\{fileID:[ \t]*(\d+)\}/g)].map((m) => m[1]);
5271
5429
  for (const refId of compRefs) {
5272
5430
  const compBlock = doc.find_by_file_id(refId);
5273
5431
  if (compBlock && compBlock.class_id === classId) {
@@ -5299,12 +5457,16 @@ function addComponent(options) {
5299
5457
  if (!resolved) {
5300
5458
  const hints = [];
5301
5459
  if (project_path) {
5460
+ const agenticDir = path5.join(project_path, ".unity-agentic");
5302
5461
  const cacheExists = load_guid_cache(project_path) !== null;
5303
- const registryExists = import_fs10.existsSync(path5.join(project_path, ".unity-agentic", "type-registry.json"));
5462
+ const registryExists = import_fs10.existsSync(path5.join(agenticDir, "type-registry.json"));
5463
+ const pkgCacheExists = import_fs10.existsSync(path5.join(agenticDir, "package-cache.json"));
5304
5464
  if (!cacheExists && !registryExists) {
5305
- hints.push(`No GUID cache or type registry found at ${path5.join(project_path, ".unity-agentic/")}. Run "unity-agentic-tools setup" first.`);
5465
+ hints.push(`No GUID cache or type registry found at ${agenticDir}/. Run "unity-agentic-tools setup" first.`);
5306
5466
  } else if (!registryExists) {
5307
5467
  hints.push('Type registry not found. Re-run "unity-agentic-tools setup" to rebuild.');
5468
+ } else if (!pkgCacheExists) {
5469
+ hints.push('Package cache not found. Re-run "unity-agentic-tools setup" to index package scripts.');
5308
5470
  }
5309
5471
  } else {
5310
5472
  hints.push("No Unity project detected. Provide --project or run from inside a Unity project directory.");
@@ -5348,9 +5510,33 @@ function addComponent(options) {
5348
5510
  if (variantInfo) {
5349
5511
  appendToAddedComponents(doc, variantInfo.prefab_instance_id, variantInfo.source_ref, componentIdStr);
5350
5512
  } else {
5351
- addComponentToGameObject(doc, gameObjectIdStr, componentIdStr);
5513
+ const added = addComponentToGameObject(doc, gameObjectIdStr, componentIdStr);
5514
+ if (!added) {
5515
+ return {
5516
+ success: false,
5517
+ file_path,
5518
+ error: `Failed to add component reference to GameObject ${gameObjectIdStr}: m_Component array not found or malformed`
5519
+ };
5520
+ }
5352
5521
  }
5353
5522
  doc.append_raw(componentYAML);
5523
+ if (!doc.validate()) {
5524
+ return {
5525
+ success: false,
5526
+ file_path,
5527
+ error: "Validation failed after adding component. The generated YAML may be malformed."
5528
+ };
5529
+ }
5530
+ if (!variantInfo) {
5531
+ const goBlockAfter = doc.find_by_file_id(gameObjectIdStr);
5532
+ if (!goBlockAfter || !goBlockAfter.raw.includes(`component: {fileID: ${componentIdStr}}`)) {
5533
+ return {
5534
+ success: false,
5535
+ file_path,
5536
+ error: `Component block appended but reference not found in GameObject ${gameObjectIdStr}'s m_Component list`
5537
+ };
5538
+ }
5539
+ }
5354
5540
  const saveResult = doc.save();
5355
5541
  if (!saveResult.success) {
5356
5542
  return {
@@ -5406,8 +5592,11 @@ function copyComponent(options) {
5406
5592
  const newIdStr = doc.generate_file_id();
5407
5593
  const newId = parseInt(newIdStr, 10);
5408
5594
  let clonedBlock = sourceBlock.raw.replace(new RegExp(`^(--- !u!${sourceBlock.class_id} &)${source_file_id}`), `$1${newId}`);
5409
- clonedBlock = clonedBlock.replace(/m_GameObject:\s*\{fileID:\s*\d+\}/, `m_GameObject: {fileID: ${targetGoId}}`);
5410
- addComponentToGameObject(doc, targetGoIdStr, newIdStr);
5595
+ clonedBlock = clonedBlock.replace(/m_GameObject:[ \t]*\{fileID:[ \t]*\d+\}/, `m_GameObject: {fileID: ${targetGoId}}`);
5596
+ const added = addComponentToGameObject(doc, targetGoIdStr, newIdStr);
5597
+ if (!added) {
5598
+ return { success: false, file_path, error: `Failed to wire component to target GameObject ${targetGoIdStr}: m_Component array not found or malformed` };
5599
+ }
5411
5600
  doc.append_raw(clonedBlock);
5412
5601
  if (!doc.validate()) {
5413
5602
  return { success: false, file_path, error: "Validation failed after copying component" };
@@ -5457,7 +5646,17 @@ function validate_value_type(current_value, new_value) {
5457
5646
  const incoming = new_value.trim();
5458
5647
  if (/^\{fileID:/.test(current)) {
5459
5648
  if (/^\{fileID:[ \t]*0[ \t]*\}$/.test(current)) {
5460
- return null;
5649
+ if (/^\{fileID:/.test(incoming))
5650
+ return null;
5651
+ if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(incoming))
5652
+ return null;
5653
+ if (/^(true|false|yes|no|on|off)$/i.test(incoming))
5654
+ return null;
5655
+ if (/^'[^']*'$/.test(incoming) || /^"[^"]*"$/.test(incoming))
5656
+ return null;
5657
+ if (/^\{.*:.*\}$/.test(incoming))
5658
+ return null;
5659
+ return `Current value is a null reference ({fileID: 0}). New value "${incoming}" is not a valid reference, number, boolean, quoted string, or compound value.`;
5461
5660
  }
5462
5661
  if (!/^\{fileID:/.test(incoming)) {
5463
5662
  return `Expected a reference value ({fileID: ...}), got "${incoming}"`;
@@ -5667,7 +5866,19 @@ function editProperty(options) {
5667
5866
  return result;
5668
5867
  }
5669
5868
  function editComponentByFileId(options) {
5670
- const { file_path, file_id, property, new_value } = options;
5869
+ const { file_path, file_id, property, project_path } = options;
5870
+ let { new_value } = options;
5871
+ const resolved = resolveAssetPathToPPtr(new_value, file_path, project_path);
5872
+ if (resolved === null) {
5873
+ return {
5874
+ success: false,
5875
+ file_path,
5876
+ error: `Could not resolve "${new_value}" to asset reference. Ensure GUID cache exists (run "setup <project>" first).`
5877
+ };
5878
+ }
5879
+ if (resolved !== undefined) {
5880
+ new_value = resolved;
5881
+ }
5671
5882
  const pathError = validate_file_path(file_path, "write");
5672
5883
  if (pathError) {
5673
5884
  return { success: false, file_path, error: pathError };
@@ -6492,8 +6703,56 @@ function removeRemovedGameObject(options) {
6492
6703
  }
6493
6704
  // src/editor/delete.ts
6494
6705
  var import_fs12 = require("fs");
6706
+ function find_component_by_type(doc, type_name, game_object, project_path) {
6707
+ let scope_blocks;
6708
+ if (game_object) {
6709
+ const goResult = doc.require_unique_game_object(game_object);
6710
+ if ("error" in goResult)
6711
+ return goResult;
6712
+ const comp_ids = [];
6713
+ const matches = goResult.raw.matchAll(/component:[ \t]*\{fileID:[ \t]*(-?\d+)\}/g);
6714
+ for (const m of matches)
6715
+ comp_ids.push(m[1]);
6716
+ scope_blocks = comp_ids.map((id) => doc.find_by_file_id(id)).filter((b) => b !== null);
6717
+ } else {
6718
+ scope_blocks = doc.blocks.filter((b) => b.class_id !== 1 && b.class_id !== 4 && b.class_id !== 224);
6719
+ }
6720
+ const candidates = [];
6721
+ const class_id = get_class_id(type_name);
6722
+ if (class_id !== null) {
6723
+ for (const block of scope_blocks) {
6724
+ if (block.class_id === class_id)
6725
+ candidates.push(block);
6726
+ }
6727
+ } else {
6728
+ let resolved = null;
6729
+ try {
6730
+ resolved = resolveScriptGuid(type_name, project_path);
6731
+ } catch {}
6732
+ if (resolved) {
6733
+ for (const block of scope_blocks) {
6734
+ if (block.class_id !== 114)
6735
+ continue;
6736
+ const scriptMatch = block.raw.match(/m_Script:[ \t]*\{[^}]*guid:[ \t]*([a-f0-9]+)/);
6737
+ if (scriptMatch && scriptMatch[1] === resolved.guid) {
6738
+ candidates.push(block);
6739
+ }
6740
+ }
6741
+ } else {
6742
+ return { error: `Component type "${type_name}" not found. For MonoBehaviour scripts, ensure "unity-agentic-tools setup" has been run.` };
6743
+ }
6744
+ }
6745
+ if (candidates.length === 0) {
6746
+ return { error: `No "${type_name}" component found${game_object ? ` on "${game_object}"` : ""} in ${doc.file_path}` };
6747
+ }
6748
+ if (candidates.length > 1) {
6749
+ const ids = candidates.map((b) => b.file_id).join(", ");
6750
+ return { error: `Multiple "${type_name}" components found (fileIDs: ${ids}). Use a numeric fileID to specify which one${game_object ? "" : ", or use --on <gameobject> to scope the search"}.` };
6751
+ }
6752
+ return candidates[0];
6753
+ }
6495
6754
  function removeComponent(options) {
6496
- const { file_path, file_id } = options;
6755
+ const { file_path, file_id, game_object, project_path } = options;
6497
6756
  const pathError = validate_file_path(file_path, "write");
6498
6757
  if (pathError) {
6499
6758
  return { success: false, file_path, error: pathError };
@@ -6507,9 +6766,18 @@ function removeComponent(options) {
6507
6766
  } catch (err) {
6508
6767
  return { success: false, file_path, error: `Failed to read file: ${err instanceof Error ? err.message : String(err)}` };
6509
6768
  }
6510
- const found = doc.find_by_file_id(file_id);
6511
- if (!found) {
6512
- return { success: false, file_path, error: `Component with file ID ${file_id} not found` };
6769
+ let found;
6770
+ if (/^-?\d+$/.test(file_id)) {
6771
+ found = doc.find_by_file_id(file_id);
6772
+ if (!found) {
6773
+ return { success: false, file_path, error: `Component with file ID ${file_id} not found` };
6774
+ }
6775
+ } else {
6776
+ const result = find_component_by_type(doc, file_id, game_object, project_path);
6777
+ if ("error" in result) {
6778
+ return { success: false, file_path, error: result.error };
6779
+ }
6780
+ found = result;
6513
6781
  }
6514
6782
  if (found.class_id === 1) {
6515
6783
  return { success: false, file_path, error: "Cannot remove a GameObject with remove-component. Use delete instead." };
@@ -6517,18 +6785,28 @@ function removeComponent(options) {
6517
6785
  if (found.class_id === 4) {
6518
6786
  return { success: false, file_path, error: "Cannot remove a Transform with remove-component. Use delete to remove the entire GameObject." };
6519
6787
  }
6520
- const goMatch = found.raw.match(/m_GameObject:\s*\{fileID:\s*(-?\d+)\}/);
6788
+ const resolved_file_id = found.file_id;
6789
+ const goMatch = found.raw.match(/m_GameObject:[ \t]*\{fileID:[ \t]*(-?\d+)\}/);
6521
6790
  if (goMatch) {
6522
6791
  const parentGoId = goMatch[1];
6523
6792
  const goBlock = doc.find_by_file_id(parentGoId);
6524
6793
  if (goBlock) {
6525
- const escaped = file_id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6794
+ const escaped = resolved_file_id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6526
6795
  const compLinePattern = new RegExp(`^[ \\t]*- component: \\{fileID: ${escaped}\\}[ \\t]*\\n`, "m");
6527
6796
  const modifiedRaw = goBlock.raw.replace(compLinePattern, "");
6797
+ if (modifiedRaw === goBlock.raw) {}
6528
6798
  goBlock.replace_raw(modifiedRaw);
6529
6799
  }
6530
6800
  }
6531
- doc.remove_block(file_id);
6801
+ const dangling = [];
6802
+ for (const block of doc.blocks) {
6803
+ if (block.file_id === resolved_file_id)
6804
+ continue;
6805
+ if (block.raw.includes(`{fileID: ${resolved_file_id}}`)) {
6806
+ dangling.push(block.file_id);
6807
+ }
6808
+ }
6809
+ doc.remove_block(resolved_file_id);
6532
6810
  if (!doc.validate()) {
6533
6811
  return { success: false, file_path, error: "Validation failed after removing component" };
6534
6812
  }
@@ -6539,8 +6817,45 @@ function removeComponent(options) {
6539
6817
  return {
6540
6818
  success: true,
6541
6819
  file_path,
6542
- removed_file_id: file_id,
6543
- removed_class_id: found.class_id
6820
+ removed_file_id: resolved_file_id,
6821
+ removed_class_id: found.class_id,
6822
+ warning: dangling.length > 0 ? `Dangling references to deleted component found in blocks: ${dangling.join(", ")}` : undefined
6823
+ };
6824
+ }
6825
+ function removeComponentBatch(options) {
6826
+ const { project_path, component_type, game_object } = options;
6827
+ const files = walk_project_files(project_path, [".unity", ".prefab"]);
6828
+ const removals = [];
6829
+ const errors = [];
6830
+ let skipped = 0;
6831
+ for (const file of files) {
6832
+ const result = removeComponent({
6833
+ file_path: file,
6834
+ file_id: component_type,
6835
+ game_object,
6836
+ project_path
6837
+ });
6838
+ if (result.success && result.removed_file_id) {
6839
+ removals.push({
6840
+ file,
6841
+ file_id: result.removed_file_id,
6842
+ class_id: result.removed_class_id ?? 0
6843
+ });
6844
+ } else if (result.error?.includes("not found") || result.error?.startsWith('No "')) {
6845
+ skipped++;
6846
+ } else if (result.error) {
6847
+ errors.push({ file, error: result.error });
6848
+ }
6849
+ }
6850
+ return {
6851
+ success: errors.length === 0,
6852
+ project_path,
6853
+ component: component_type,
6854
+ files_scanned: files.length,
6855
+ files_modified: removals.length,
6856
+ removals,
6857
+ skipped,
6858
+ errors
6544
6859
  };
6545
6860
  }
6546
6861
  function deleteGameObject(options) {
@@ -6565,7 +6880,7 @@ function deleteGameObject(options) {
6565
6880
  const goBlock = goResult;
6566
6881
  const goId = goBlock.file_id;
6567
6882
  const componentIds = new Set;
6568
- const compMatches = goBlock.raw.matchAll(/component:\s*\{fileID:\s*(-?\d+)\}/g);
6883
+ const compMatches = goBlock.raw.matchAll(/component:[ \t]*\{fileID:[ \t]*(-?\d+)\}/g);
6569
6884
  for (const cm of compMatches) {
6570
6885
  componentIds.add(cm[1]);
6571
6886
  }
@@ -6575,7 +6890,7 @@ function deleteGameObject(options) {
6575
6890
  const block = doc.find_by_file_id(compId);
6576
6891
  if (block && block.class_id === 4) {
6577
6892
  transformId = compId;
6578
- const fatherMatch = block.raw.match(/m_Father:\s*\{fileID:\s*(-?\d+)\}/);
6893
+ const fatherMatch = block.raw.match(/m_Father:[ \t]*\{fileID:[ \t]*(-?\d+)\}/);
6579
6894
  if (fatherMatch) {
6580
6895
  fatherId = fatherMatch[1];
6581
6896
  }
@@ -6643,7 +6958,7 @@ function deletePrefabInstance(options) {
6643
6958
  }
6644
6959
  const piId = piBlock.file_id;
6645
6960
  const allToRemove = new Set([piId]);
6646
- const piRefPattern = new RegExp(`m_PrefabInstance:\\s*\\{fileID:\\s*${piId}\\}`);
6961
+ const piRefPattern = new RegExp(`m_PrefabInstance:[ \\t]*\\{fileID:[ \\t]*${piId}\\}`);
6647
6962
  for (const block of doc.blocks) {
6648
6963
  if (block.is_stripped && piRefPattern.test(block.raw)) {
6649
6964
  allToRemove.add(block.file_id);
@@ -6653,13 +6968,13 @@ function deletePrefabInstance(options) {
6653
6968
  const addedGOMatch = piBlock.raw.match(addedGOPattern);
6654
6969
  if (addedGOMatch) {
6655
6970
  const addedGOSection = addedGOMatch[1];
6656
- const goMatches = addedGOSection.matchAll(/fileID:\s*(\d+)/g);
6971
+ const goMatches = addedGOSection.matchAll(/fileID:[ \t]*(\d+)/g);
6657
6972
  for (const match of goMatches) {
6658
6973
  const goId = match[1];
6659
6974
  allToRemove.add(goId);
6660
6975
  const goBlock = doc.find_by_file_id(goId);
6661
6976
  if (goBlock) {
6662
- const compMatch = goBlock.raw.match(/component:\s*\{fileID:\s*(\d+)\}/);
6977
+ const compMatch = goBlock.raw.match(/component:[ \t]*\{fileID:[ \t]*(\d+)\}/);
6663
6978
  if (compMatch) {
6664
6979
  const transformId = compMatch[1];
6665
6980
  allToRemove.add(transformId);
@@ -6675,12 +6990,12 @@ function deletePrefabInstance(options) {
6675
6990
  const addedCompMatch = piBlock.raw.match(addedCompPattern);
6676
6991
  if (addedCompMatch) {
6677
6992
  const addedCompSection = addedCompMatch[1];
6678
- const compMatches = addedCompSection.matchAll(/fileID:\s*(\d+)/g);
6993
+ const compMatches = addedCompSection.matchAll(/fileID:[ \t]*(\d+)/g);
6679
6994
  for (const match of compMatches) {
6680
6995
  allToRemove.add(match[1]);
6681
6996
  }
6682
6997
  }
6683
- const parentMatch = piBlock.raw.match(/m_TransformParent:\s*\{fileID:\s*(\d+)\}/);
6998
+ const parentMatch = piBlock.raw.match(/m_TransformParent:[ \t]*\{fileID:[ \t]*(\d+)\}/);
6684
6999
  const parentTransformId = parentMatch ? parentMatch[1] : "0";
6685
7000
  let strippedRootTransformId = null;
6686
7001
  for (const id of allToRemove) {