unity-agentic-tools 0.4.0 → 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/cli.js CHANGED
@@ -2299,7 +2299,7 @@ var require_package = __commonJS((exports2, module2) => {
2299
2299
  module2.exports = {
2300
2300
  name: "unity-agentic-tools",
2301
2301
  packageManager: "bun@latest",
2302
- version: "0.4.0",
2302
+ version: "0.4.1",
2303
2303
  description: "Fast, token-efficient Unity YAML parser for AI agents",
2304
2304
  exports: {
2305
2305
  ".": {
@@ -3289,6 +3289,54 @@ function resolveScriptGuid(script, projectPath) {
3289
3289
  }
3290
3290
  return null;
3291
3291
  }
3292
+ var ASSET_PPTR_MAP = {
3293
+ ".inputactions": { fileID: 11400000, type: 2 },
3294
+ ".asset": { fileID: 11400000, type: 2 },
3295
+ ".mat": { fileID: 2100000, type: 2 },
3296
+ ".prefab": { fileID: 100100000, type: 2 },
3297
+ ".controller": { fileID: 9100000, type: 2 },
3298
+ ".anim": { fileID: 7400000, type: 2 }
3299
+ };
3300
+ var ASSET_EXTENSIONS = new Set(Object.keys(ASSET_PPTR_MAP));
3301
+ function resolveAssetPathToPPtr(value, file_path, project_path) {
3302
+ if (value.startsWith("{fileID:") || value.startsWith("{"))
3303
+ return;
3304
+ const ext = path.extname(value).toLowerCase();
3305
+ if (!ASSET_EXTENSIONS.has(ext))
3306
+ return;
3307
+ const resolved_project = project_path || find_unity_project_root(path.dirname(file_path));
3308
+ if (!resolved_project)
3309
+ return null;
3310
+ const mapping = ASSET_PPTR_MAP[ext];
3311
+ let guid = null;
3312
+ const basename_val = path.basename(value, ext);
3313
+ if (value.includes("/") || value.includes("\\")) {
3314
+ const abs = path.isAbsolute(value) ? value : path.join(resolved_project, value);
3315
+ guid = extractGuidFromMeta(abs + ".meta");
3316
+ }
3317
+ if (!guid) {
3318
+ const cache = load_guid_cache_for_file(file_path, resolved_project);
3319
+ if (cache) {
3320
+ const found = cache.find_by_name(basename_val, ext);
3321
+ if (found)
3322
+ guid = found.guid;
3323
+ }
3324
+ }
3325
+ if (!guid) {
3326
+ const candidates = [
3327
+ path.join(resolved_project, "Assets", value),
3328
+ path.join(resolved_project, value)
3329
+ ];
3330
+ for (const candidate of candidates) {
3331
+ guid = extractGuidFromMeta(candidate + ".meta");
3332
+ if (guid)
3333
+ break;
3334
+ }
3335
+ }
3336
+ if (!guid)
3337
+ return null;
3338
+ return `{fileID: ${mapping.fileID}, guid: ${guid}, type: ${mapping.type}}`;
3339
+ }
3292
3340
  function resolve_script_with_fields(script, project_path) {
3293
3341
  const resolved = resolveScriptGuid(script, project_path);
3294
3342
  if (!resolved)
@@ -3845,7 +3893,9 @@ ${element_indent2}- ${value}`);
3845
3893
  if (!header) {
3846
3894
  throw new Error(`Invalid Unity YAML block header in replacement text: "${new_text.slice(0, 80)}"`);
3847
3895
  }
3848
- this._raw = new_text;
3896
+ this._raw = new_text.endsWith(`
3897
+ `) ? new_text : new_text + `
3898
+ `;
3849
3899
  this._header = header;
3850
3900
  this._dirty = true;
3851
3901
  this._format_cache.clear();
@@ -3855,6 +3905,11 @@ ${element_indent2}- ${value}`);
3855
3905
  }
3856
3906
  _check_dirty(old_raw) {
3857
3907
  if (this._raw !== old_raw) {
3908
+ if (!this._raw.endsWith(`
3909
+ `)) {
3910
+ this._raw += `
3911
+ `;
3912
+ }
3858
3913
  this._dirty = true;
3859
3914
  return true;
3860
3915
  }
@@ -4937,14 +4992,82 @@ function get_project_info(projectPath) {
4937
4992
  }
4938
4993
 
4939
4994
  // src/editor/create.ts
4995
+ function md4(data) {
4996
+ function F(x, y, z) {
4997
+ return x & y | ~x & z;
4998
+ }
4999
+ function G(x, y, z) {
5000
+ return x & y | x & z | y & z;
5001
+ }
5002
+ function H(x, y, z) {
5003
+ return x ^ y ^ z;
5004
+ }
5005
+ function rotl(x, n) {
5006
+ return (x << n | x >>> 32 - n) >>> 0;
5007
+ }
5008
+ const len = data.length;
5009
+ const bitLen = len * 8;
5010
+ const padded_len = len + 9 + 63 & ~63;
5011
+ const buf = Buffer.alloc(padded_len);
5012
+ data.copy(buf);
5013
+ buf[len] = 128;
5014
+ buf.writeUInt32LE(bitLen >>> 0, padded_len - 8);
5015
+ buf.writeUInt32LE(Math.floor(bitLen / 4294967296) >>> 0, padded_len - 4);
5016
+ let a0 = 1732584193;
5017
+ let b0 = 4023233417;
5018
+ let c0 = 2562383102;
5019
+ let d0 = 271733878;
5020
+ const R1_K = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
5021
+ const R1_S = [3, 7, 11, 19, 3, 7, 11, 19, 3, 7, 11, 19, 3, 7, 11, 19];
5022
+ const R2_K = [0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15];
5023
+ const R2_S = [3, 5, 9, 13, 3, 5, 9, 13, 3, 5, 9, 13, 3, 5, 9, 13];
5024
+ const R3_K = [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15];
5025
+ const R3_S = [3, 9, 11, 15, 3, 9, 11, 15, 3, 9, 11, 15, 3, 9, 11, 15];
5026
+ for (let i = 0;i < padded_len; i += 64) {
5027
+ const X = [];
5028
+ for (let j = 0;j < 16; j++)
5029
+ X[j] = buf.readUInt32LE(i + j * 4);
5030
+ let a = a0, b = b0, c = c0, d = d0;
5031
+ for (let j = 0;j < 16; j++) {
5032
+ const t = a + F(b, c, d) + X[R1_K[j]] >>> 0;
5033
+ a = d;
5034
+ d = c;
5035
+ c = b;
5036
+ b = rotl(t, R1_S[j]);
5037
+ }
5038
+ for (let j = 0;j < 16; j++) {
5039
+ const t = a + G(b, c, d) + X[R2_K[j]] + 1518500249 >>> 0;
5040
+ a = d;
5041
+ d = c;
5042
+ c = b;
5043
+ b = rotl(t, R2_S[j]);
5044
+ }
5045
+ for (let j = 0;j < 16; j++) {
5046
+ const t = a + H(b, c, d) + X[R3_K[j]] + 1859775393 >>> 0;
5047
+ a = d;
5048
+ d = c;
5049
+ c = b;
5050
+ b = rotl(t, R3_S[j]);
5051
+ }
5052
+ a0 = a0 + a >>> 0;
5053
+ b0 = b0 + b >>> 0;
5054
+ c0 = c0 + c >>> 0;
5055
+ d0 = d0 + d >>> 0;
5056
+ }
5057
+ const result = Buffer.alloc(16);
5058
+ result.writeUInt32LE(a0, 0);
5059
+ result.writeUInt32LE(b0, 4);
5060
+ result.writeUInt32LE(c0, 8);
5061
+ result.writeUInt32LE(d0, 12);
5062
+ return result;
5063
+ }
4940
5064
  function compute_dll_script_file_id(namespace, class_name) {
4941
- const { createHash } = require("crypto");
4942
5065
  const full_name = namespace ? `${namespace}.${class_name}` : class_name;
4943
5066
  const name_bytes = Buffer.from(full_name, "utf-8");
4944
5067
  const input = Buffer.alloc(4 + name_bytes.length);
4945
5068
  input.writeInt32LE(114, 0);
4946
5069
  name_bytes.copy(input, 4);
4947
- const hash = createHash("md4").update(input).digest();
5070
+ const hash = md4(input);
4948
5071
  const a = hash.readInt32LE(0);
4949
5072
  const b = hash.readInt32LE(4);
4950
5073
  const c = hash.readInt32LE(8);
@@ -4957,13 +5080,13 @@ function get_layer_from_parent(doc, parentTransformId) {
4957
5080
  const parentTransform = doc.find_by_file_id(parentTransformId);
4958
5081
  if (!parentTransform)
4959
5082
  return 0;
4960
- const goMatch = parentTransform.raw.match(/m_GameObject:\s*\{fileID:\s*(\d+)\}/);
5083
+ const goMatch = parentTransform.raw.match(/m_GameObject:[ \t]*\{fileID:[ \t]*(\d+)\}/);
4961
5084
  if (!goMatch)
4962
5085
  return 0;
4963
5086
  const parentGo = doc.find_by_file_id(goMatch[1]);
4964
5087
  if (!parentGo)
4965
5088
  return 0;
4966
- const layerMatch = parentGo.raw.match(/m_Layer:\s*(\d+)/);
5089
+ const layerMatch = parentGo.raw.match(/m_Layer:[ \t]*(\d+)/);
4967
5090
  return layerMatch ? parseInt(layerMatch[1], 10) : 0;
4968
5091
  }
4969
5092
  function createGameObjectYAML(gameObjectId, transformId, name, parentTransformId = "0", rootOrder = 0, layer = 0) {
@@ -5284,11 +5407,14 @@ ${defaults}`;
5284
5407
  function addComponentToGameObject(doc, gameObjectId, componentId) {
5285
5408
  const goBlock = doc.find_by_file_id(gameObjectId);
5286
5409
  if (!goBlock)
5287
- return;
5288
- let raw = goBlock.raw;
5289
- raw = raw.replace(/(m_Component:\s*\n(?:\s*-\s*component:\s*\{fileID:\s*\d+\}\s*\n)*)/, `$1 - component: {fileID: ${componentId}}
5410
+ return false;
5411
+ const raw = goBlock.raw;
5412
+ const updated = raw.replace(/(m_Component:[ \t]*\n(?:[ \t]*-[ \t]*component:[ \t]*\{fileID:[ \t]*\d+\}[ \t]*\n)*)/, `$1 - component: {fileID: ${componentId}}
5290
5413
  `);
5291
- goBlock.replace_raw(raw);
5414
+ if (updated === raw)
5415
+ return false;
5416
+ goBlock.replace_raw(updated);
5417
+ return true;
5292
5418
  }
5293
5419
  function createMonoBehaviourYAML(componentId, gameObjectId, scriptGuid, fields, version, scriptFileId = 11500000, type_lookup) {
5294
5420
  const field_yaml = fields && fields.length > 0 ? generate_field_yaml(fields, version, " ", type_lookup) : `
@@ -5360,7 +5486,7 @@ function createGameObject(options) {
5360
5486
  if (parentBlock.class_id === 4) {
5361
5487
  parentTransformIdStr = parentIdStr;
5362
5488
  } else if (parentBlock.class_id === 1) {
5363
- const compMatch = parentBlock.raw.match(/m_Component:\s*\n\s*-\s*component:\s*\{fileID:\s*(\d+)\}/);
5489
+ const compMatch = parentBlock.raw.match(/m_Component:[ \t]*\n[ \t]*-[ \t]*component:[ \t]*\{fileID:[ \t]*(\d+)\}/);
5364
5490
  if (compMatch) {
5365
5491
  parentTransformIdStr = compMatch[1];
5366
5492
  } else {
@@ -6004,7 +6130,7 @@ function createPrefabInstance(options) {
6004
6130
  if (parentBlock.class_id === 4) {
6005
6131
  parentTransformIdStr = parentIdStr;
6006
6132
  } else if (parentBlock.class_id === 1) {
6007
- const compMatch = parentBlock.raw.match(/m_Component:\s*\n\s*-\s*component:\s*\{fileID:\s*(\d+)\}/);
6133
+ const compMatch = parentBlock.raw.match(/m_Component:[ \t]*\n[ \t]*-[ \t]*component:[ \t]*\{fileID:[ \t]*(\d+)\}/);
6008
6134
  if (compMatch) {
6009
6135
  parentTransformIdStr = compMatch[1];
6010
6136
  } else {
@@ -6434,7 +6560,7 @@ function addComponent(options) {
6434
6560
  let duplicateWarning;
6435
6561
  const goBlock = doc.find_by_file_id(gameObjectIdStr);
6436
6562
  if (goBlock && classId !== null) {
6437
- const compRefs = [...goBlock.raw.matchAll(/component:\s*\{fileID:\s*(\d+)\}/g)].map((m) => m[1]);
6563
+ const compRefs = [...goBlock.raw.matchAll(/component:[ \t]*\{fileID:[ \t]*(\d+)\}/g)].map((m) => m[1]);
6438
6564
  for (const refId of compRefs) {
6439
6565
  const compBlock = doc.find_by_file_id(refId);
6440
6566
  if (compBlock && compBlock.class_id === classId) {
@@ -6519,9 +6645,33 @@ function addComponent(options) {
6519
6645
  if (variantInfo) {
6520
6646
  appendToAddedComponents(doc, variantInfo.prefab_instance_id, variantInfo.source_ref, componentIdStr);
6521
6647
  } else {
6522
- addComponentToGameObject(doc, gameObjectIdStr, componentIdStr);
6648
+ const added = addComponentToGameObject(doc, gameObjectIdStr, componentIdStr);
6649
+ if (!added) {
6650
+ return {
6651
+ success: false,
6652
+ file_path,
6653
+ error: `Failed to add component reference to GameObject ${gameObjectIdStr}: m_Component array not found or malformed`
6654
+ };
6655
+ }
6523
6656
  }
6524
6657
  doc.append_raw(componentYAML);
6658
+ if (!doc.validate()) {
6659
+ return {
6660
+ success: false,
6661
+ file_path,
6662
+ error: "Validation failed after adding component. The generated YAML may be malformed."
6663
+ };
6664
+ }
6665
+ if (!variantInfo) {
6666
+ const goBlockAfter = doc.find_by_file_id(gameObjectIdStr);
6667
+ if (!goBlockAfter || !goBlockAfter.raw.includes(`component: {fileID: ${componentIdStr}}`)) {
6668
+ return {
6669
+ success: false,
6670
+ file_path,
6671
+ error: `Component block appended but reference not found in GameObject ${gameObjectIdStr}'s m_Component list`
6672
+ };
6673
+ }
6674
+ }
6525
6675
  const saveResult = doc.save();
6526
6676
  if (!saveResult.success) {
6527
6677
  return {
@@ -6577,8 +6727,11 @@ function copyComponent(options) {
6577
6727
  const newIdStr = doc.generate_file_id();
6578
6728
  const newId = parseInt(newIdStr, 10);
6579
6729
  let clonedBlock = sourceBlock.raw.replace(new RegExp(`^(--- !u!${sourceBlock.class_id} &)${source_file_id}`), `$1${newId}`);
6580
- clonedBlock = clonedBlock.replace(/m_GameObject:\s*\{fileID:\s*\d+\}/, `m_GameObject: {fileID: ${targetGoId}}`);
6581
- addComponentToGameObject(doc, targetGoIdStr, newIdStr);
6730
+ clonedBlock = clonedBlock.replace(/m_GameObject:[ \t]*\{fileID:[ \t]*\d+\}/, `m_GameObject: {fileID: ${targetGoId}}`);
6731
+ const added = addComponentToGameObject(doc, targetGoIdStr, newIdStr);
6732
+ if (!added) {
6733
+ return { success: false, file_path, error: `Failed to wire component to target GameObject ${targetGoIdStr}: m_Component array not found or malformed` };
6734
+ }
6582
6735
  doc.append_raw(clonedBlock);
6583
6736
  if (!doc.validate()) {
6584
6737
  return { success: false, file_path, error: "Validation failed after copying component" };
@@ -7235,7 +7388,17 @@ function validate_value_type(current_value, new_value) {
7235
7388
  const incoming = new_value.trim();
7236
7389
  if (/^\{fileID:/.test(current)) {
7237
7390
  if (/^\{fileID:[ \t]*0[ \t]*\}$/.test(current)) {
7238
- return null;
7391
+ if (/^\{fileID:/.test(incoming))
7392
+ return null;
7393
+ if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(incoming))
7394
+ return null;
7395
+ if (/^(true|false|yes|no|on|off)$/i.test(incoming))
7396
+ return null;
7397
+ if (/^'[^']*'$/.test(incoming) || /^"[^"]*"$/.test(incoming))
7398
+ return null;
7399
+ if (/^\{.*:.*\}$/.test(incoming))
7400
+ return null;
7401
+ return `Current value is a null reference ({fileID: 0}). New value "${incoming}" is not a valid reference, number, boolean, quoted string, or compound value.`;
7239
7402
  }
7240
7403
  if (!/^\{fileID:/.test(incoming)) {
7241
7404
  return `Expected a reference value ({fileID: ...}), got "${incoming}"`;
@@ -7445,7 +7608,19 @@ function editProperty(options) {
7445
7608
  return result;
7446
7609
  }
7447
7610
  function editComponentByFileId(options) {
7448
- const { file_path, file_id, property, new_value } = options;
7611
+ const { file_path, file_id, property, project_path } = options;
7612
+ let { new_value } = options;
7613
+ const resolved = resolveAssetPathToPPtr(new_value, file_path, project_path);
7614
+ if (resolved === null) {
7615
+ return {
7616
+ success: false,
7617
+ file_path,
7618
+ error: `Could not resolve "${new_value}" to asset reference. Ensure GUID cache exists (run "setup <project>" first).`
7619
+ };
7620
+ }
7621
+ if (resolved !== undefined) {
7622
+ new_value = resolved;
7623
+ }
7449
7624
  const pathError = validate_file_path(file_path, "write");
7450
7625
  if (pathError) {
7451
7626
  return { success: false, file_path, error: pathError };
@@ -8853,6 +9028,7 @@ function search_project(options) {
8853
9028
  const files = walk_project_files(project_path, extensions);
8854
9029
  const scanner = new UnityScanner;
8855
9030
  const matches = [];
9031
+ let files_with_errors = 0;
8856
9032
  for (const file of files) {
8857
9033
  try {
8858
9034
  let gameObjects;
@@ -8939,6 +9115,7 @@ function search_project(options) {
8939
9115
  if (max_matches !== undefined && matches.length >= max_matches)
8940
9116
  break;
8941
9117
  } catch {
9118
+ files_with_errors++;
8942
9119
  continue;
8943
9120
  }
8944
9121
  }
@@ -8947,6 +9124,7 @@ function search_project(options) {
8947
9124
  project_path,
8948
9125
  total_files_scanned: files.length,
8949
9126
  total_matches: matches.length,
9127
+ files_with_errors: files_with_errors > 0 ? files_with_errors : undefined,
8950
9128
  cursor: 0,
8951
9129
  truncated: max_matches !== undefined && matches.length >= max_matches,
8952
9130
  matches
@@ -9220,7 +9398,7 @@ function removeComponent(options) {
9220
9398
  return { success: false, file_path, error: "Cannot remove a Transform with remove-component. Use delete to remove the entire GameObject." };
9221
9399
  }
9222
9400
  const resolved_file_id = found.file_id;
9223
- const goMatch = found.raw.match(/m_GameObject:\s*\{fileID:\s*(-?\d+)\}/);
9401
+ const goMatch = found.raw.match(/m_GameObject:[ \t]*\{fileID:[ \t]*(-?\d+)\}/);
9224
9402
  if (goMatch) {
9225
9403
  const parentGoId = goMatch[1];
9226
9404
  const goBlock = doc.find_by_file_id(parentGoId);
@@ -9228,9 +9406,18 @@ function removeComponent(options) {
9228
9406
  const escaped = resolved_file_id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9229
9407
  const compLinePattern = new RegExp(`^[ \\t]*- component: \\{fileID: ${escaped}\\}[ \\t]*\\n`, "m");
9230
9408
  const modifiedRaw = goBlock.raw.replace(compLinePattern, "");
9409
+ if (modifiedRaw === goBlock.raw) {}
9231
9410
  goBlock.replace_raw(modifiedRaw);
9232
9411
  }
9233
9412
  }
9413
+ const dangling = [];
9414
+ for (const block of doc.blocks) {
9415
+ if (block.file_id === resolved_file_id)
9416
+ continue;
9417
+ if (block.raw.includes(`{fileID: ${resolved_file_id}}`)) {
9418
+ dangling.push(block.file_id);
9419
+ }
9420
+ }
9234
9421
  doc.remove_block(resolved_file_id);
9235
9422
  if (!doc.validate()) {
9236
9423
  return { success: false, file_path, error: "Validation failed after removing component" };
@@ -9243,7 +9430,8 @@ function removeComponent(options) {
9243
9430
  success: true,
9244
9431
  file_path,
9245
9432
  removed_file_id: resolved_file_id,
9246
- removed_class_id: found.class_id
9433
+ removed_class_id: found.class_id,
9434
+ warning: dangling.length > 0 ? `Dangling references to deleted component found in blocks: ${dangling.join(", ")}` : undefined
9247
9435
  };
9248
9436
  }
9249
9437
  function removeComponentBatch(options) {
@@ -9304,7 +9492,7 @@ function deleteGameObject(options) {
9304
9492
  const goBlock = goResult;
9305
9493
  const goId = goBlock.file_id;
9306
9494
  const componentIds = new Set;
9307
- const compMatches = goBlock.raw.matchAll(/component:\s*\{fileID:\s*(-?\d+)\}/g);
9495
+ const compMatches = goBlock.raw.matchAll(/component:[ \t]*\{fileID:[ \t]*(-?\d+)\}/g);
9308
9496
  for (const cm of compMatches) {
9309
9497
  componentIds.add(cm[1]);
9310
9498
  }
@@ -9314,7 +9502,7 @@ function deleteGameObject(options) {
9314
9502
  const block = doc.find_by_file_id(compId);
9315
9503
  if (block && block.class_id === 4) {
9316
9504
  transformId = compId;
9317
- const fatherMatch = block.raw.match(/m_Father:\s*\{fileID:\s*(-?\d+)\}/);
9505
+ const fatherMatch = block.raw.match(/m_Father:[ \t]*\{fileID:[ \t]*(-?\d+)\}/);
9318
9506
  if (fatherMatch) {
9319
9507
  fatherId = fatherMatch[1];
9320
9508
  }
@@ -9382,7 +9570,7 @@ function deletePrefabInstance(options) {
9382
9570
  }
9383
9571
  const piId = piBlock.file_id;
9384
9572
  const allToRemove = new Set([piId]);
9385
- const piRefPattern = new RegExp(`m_PrefabInstance:\\s*\\{fileID:\\s*${piId}\\}`);
9573
+ const piRefPattern = new RegExp(`m_PrefabInstance:[ \\t]*\\{fileID:[ \\t]*${piId}\\}`);
9386
9574
  for (const block of doc.blocks) {
9387
9575
  if (block.is_stripped && piRefPattern.test(block.raw)) {
9388
9576
  allToRemove.add(block.file_id);
@@ -9392,13 +9580,13 @@ function deletePrefabInstance(options) {
9392
9580
  const addedGOMatch = piBlock.raw.match(addedGOPattern);
9393
9581
  if (addedGOMatch) {
9394
9582
  const addedGOSection = addedGOMatch[1];
9395
- const goMatches = addedGOSection.matchAll(/fileID:\s*(\d+)/g);
9583
+ const goMatches = addedGOSection.matchAll(/fileID:[ \t]*(\d+)/g);
9396
9584
  for (const match of goMatches) {
9397
9585
  const goId = match[1];
9398
9586
  allToRemove.add(goId);
9399
9587
  const goBlock = doc.find_by_file_id(goId);
9400
9588
  if (goBlock) {
9401
- const compMatch = goBlock.raw.match(/component:\s*\{fileID:\s*(\d+)\}/);
9589
+ const compMatch = goBlock.raw.match(/component:[ \t]*\{fileID:[ \t]*(\d+)\}/);
9402
9590
  if (compMatch) {
9403
9591
  const transformId = compMatch[1];
9404
9592
  allToRemove.add(transformId);
@@ -9414,12 +9602,12 @@ function deletePrefabInstance(options) {
9414
9602
  const addedCompMatch = piBlock.raw.match(addedCompPattern);
9415
9603
  if (addedCompMatch) {
9416
9604
  const addedCompSection = addedCompMatch[1];
9417
- const compMatches = addedCompSection.matchAll(/fileID:\s*(\d+)/g);
9605
+ const compMatches = addedCompSection.matchAll(/fileID:[ \t]*(\d+)/g);
9418
9606
  for (const match of compMatches) {
9419
9607
  allToRemove.add(match[1]);
9420
9608
  }
9421
9609
  }
9422
- const parentMatch = piBlock.raw.match(/m_TransformParent:\s*\{fileID:\s*(\d+)\}/);
9610
+ const parentMatch = piBlock.raw.match(/m_TransformParent:[ \t]*\{fileID:[ \t]*(\d+)\}/);
9423
9611
  const parentTransformId = parentMatch ? parentMatch[1] : "0";
9424
9612
  let strippedRootTransformId = null;
9425
9613
  for (const id of allToRemove) {
@@ -11123,10 +11311,12 @@ function parse_log_line_timestamp(line) {
11123
11311
  }
11124
11312
  function parse_log_entries(lines) {
11125
11313
  const entries = [];
11126
- const error_re = /^(error|exception|Error|Exception|ERROR)/i;
11127
- const warning_re = /^(warning|Warning|WARNING)/i;
11314
+ const error_re = /\b(error|exception)\b/i;
11315
+ const runtime_error_re = /\b(NullReferenceException|IndexOutOfRangeException|ArgumentException|MissingReferenceException|StackOverflowException|DivideByZeroException|InvalidOperationException|KeyNotFoundException|FormatException|ObjectDisposedException|MissingComponentException|UnassignedReferenceException)\b/;
11316
+ const warning_re = /\bwarning\b/i;
11128
11317
  const compile_re = /Assets\/.*\.cs\(\d+,\d+\):\s*error\s+CS/;
11129
11318
  const import_error_re = /Failed to import|Error while importing|Could not create asset|Unable to import|Shader error in|Import of asset .* failed/i;
11319
+ const assertion_re = /^Assertion failed/;
11130
11320
  const stack_re = /^\s+at\s+|^\s*\(Filename:/;
11131
11321
  for (let i = 0;i < lines.length; i++) {
11132
11322
  const line = lines[i];
@@ -11135,7 +11325,9 @@ function parse_log_entries(lines) {
11135
11325
  let level = "info";
11136
11326
  if (import_error_re.test(line))
11137
11327
  level = "import_error";
11138
- else if (error_re.test(line) || compile_re.test(line))
11328
+ else if (compile_re.test(line))
11329
+ level = "error";
11330
+ else if (error_re.test(line) || runtime_error_re.test(line) || assertion_re.test(line))
11139
11331
  level = "error";
11140
11332
  else if (warning_re.test(line))
11141
11333
  level = "warning";
@@ -12307,10 +12499,6 @@ function build_read_command(getScanner) {
12307
12499
  }
12308
12500
  }
12309
12501
  }
12310
- if (options.search) {
12311
- const re = new RegExp(options.search, "i");
12312
- lines = lines.filter((l) => re.test(l));
12313
- }
12314
12502
  if (options.errors || options.warnings || options.compileErrors || options.importErrors) {
12315
12503
  const entries = parse_log_entries(lines);
12316
12504
  let filtered = entries;
@@ -12324,6 +12512,10 @@ function build_read_command(getScanner) {
12324
12512
  } else if (options.warnings) {
12325
12513
  filtered = entries.filter((e) => e.level === "warning");
12326
12514
  }
12515
+ if (options.search) {
12516
+ const re = new RegExp(options.search, "i");
12517
+ filtered = filtered.filter((e) => re.test(e.message) || e.stack_trace && e.stack_trace.some((s) => re.test(s)));
12518
+ }
12327
12519
  const tail_filtered = parseInt(options.tail, 10);
12328
12520
  if (isNaN(tail_filtered) || tail_filtered < 1) {
12329
12521
  console.log(JSON.stringify({ error: `Invalid --tail value "${options.tail}". Must be a positive integer.` }, null, 2));
@@ -12339,6 +12531,10 @@ function build_read_command(getScanner) {
12339
12531
  }, null, 2));
12340
12532
  return;
12341
12533
  }
12534
+ if (options.search) {
12535
+ const re = new RegExp(options.search, "i");
12536
+ lines = lines.filter((l) => re.test(l));
12537
+ }
12342
12538
  const tail = parseInt(options.tail, 10);
12343
12539
  if (isNaN(tail) || tail < 1) {
12344
12540
  console.log(JSON.stringify({ error: `Invalid --tail value "${options.tail}". Must be a positive integer.` }, null, 2));
@@ -13126,7 +13322,7 @@ function build_update_command(getScanner) {
13126
13322
  if (!result.success)
13127
13323
  process.exitCode = 1;
13128
13324
  });
13129
- cmd.command("component <file> <file_id> <property> <value>").description("Edit any component property by file ID. Supports dotted paths (m_LocalPosition.x) and array paths (m_Materials.Array.data[0]). Quote paths with brackets or use dot notation (data.0) to avoid shell glob expansion").option("-j, --json", "Output as JSON").action((file, file_id, property, value, _options) => {
13325
+ cmd.command("component <file> <file_id> <property> <value>").description("Edit any component property by file ID. Supports dotted paths (m_LocalPosition.x) and array paths (m_Materials.Array.data[0]). Quote paths with brackets or use dot notation (data.0) to avoid shell glob expansion").option("-j, --json", "Output as JSON").option("-p, --project <path>", "Unity project path (for asset reference resolution)").action((file, file_id, property, value, options) => {
13130
13326
  if (property === "m_RootOrder") {
13131
13327
  const num = Number(value);
13132
13328
  if (!Number.isInteger(num) || num < 0) {
@@ -13139,7 +13335,8 @@ function build_update_command(getScanner) {
13139
13335
  file_path: file,
13140
13336
  file_id,
13141
13337
  property,
13142
- new_value: value
13338
+ new_value: value,
13339
+ project_path: options.project
13143
13340
  });
13144
13341
  console.log(JSON.stringify(result, null, 2));
13145
13342
  if (!result.success)
@@ -15322,6 +15519,30 @@ async function stream_editor(options) {
15322
15519
  connect(`ws://127.0.0.1:${config.port}/unity-agentic`);
15323
15520
  });
15324
15521
  }
15522
+ async function ping_editor(port, timeout_ms = 2000) {
15523
+ return new Promise((resolve7) => {
15524
+ const timer = setTimeout(() => {
15525
+ resolve7({ reachable: false, error: `Timeout after ${timeout_ms}ms` });
15526
+ }, timeout_ms);
15527
+ try {
15528
+ const ws = new WebSocket(`ws://127.0.0.1:${port}/unity-agentic`);
15529
+ ws.onopen = () => {
15530
+ clearTimeout(timer);
15531
+ try {
15532
+ ws.close();
15533
+ } catch {}
15534
+ resolve7({ reachable: true });
15535
+ };
15536
+ ws.onerror = (err) => {
15537
+ clearTimeout(timer);
15538
+ resolve7({ reachable: false, error: String(err) });
15539
+ };
15540
+ } catch (err) {
15541
+ clearTimeout(timer);
15542
+ resolve7({ reachable: false, error: String(err) });
15543
+ }
15544
+ });
15545
+ }
15325
15546
  function resolve_config(options) {
15326
15547
  if (options.port) {
15327
15548
  return { port: options.port, pid: 0, version: "unknown" };
@@ -15394,11 +15615,14 @@ function build_editor_command() {
15394
15615
  process.exitCode = 1;
15395
15616
  return;
15396
15617
  }
15618
+ const ping = await ping_editor(config.port, 2000);
15397
15619
  console.log(JSON.stringify({
15398
15620
  port: config.port,
15399
15621
  pid: config.pid,
15400
15622
  version: config.version,
15401
- project_path
15623
+ project_path,
15624
+ bridge_reachable: ping.reachable,
15625
+ ...ping.reachable ? {} : { bridge_error: ping.error }
15402
15626
  }, null, 2));
15403
15627
  });
15404
15628
  cmd.command("play").description("Enter play mode").action(async function() {
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.4.0",
208
+ version: "0.4.1",
209
209
  description: "Fast, token-efficient Unity YAML parser for AI agents",
210
210
  exports: {
211
211
  ".": {
@@ -1517,6 +1517,7 @@ function search_project(options) {
1517
1517
  const files = walk_project_files(project_path, extensions);
1518
1518
  const scanner = new UnityScanner;
1519
1519
  const matches = [];
1520
+ let files_with_errors = 0;
1520
1521
  for (const file of files) {
1521
1522
  try {
1522
1523
  let gameObjects;
@@ -1603,6 +1604,7 @@ function search_project(options) {
1603
1604
  if (max_matches !== undefined && matches.length >= max_matches)
1604
1605
  break;
1605
1606
  } catch {
1607
+ files_with_errors++;
1606
1608
  continue;
1607
1609
  }
1608
1610
  }
@@ -1611,6 +1613,7 @@ function search_project(options) {
1611
1613
  project_path,
1612
1614
  total_files_scanned: files.length,
1613
1615
  total_matches: matches.length,
1616
+ files_with_errors: files_with_errors > 0 ? files_with_errors : undefined,
1614
1617
  cursor: 0,
1615
1618
  truncated: max_matches !== undefined && matches.length >= max_matches,
1616
1619
  matches
@@ -2336,6 +2339,54 @@ function resolveScriptGuid(script, projectPath) {
2336
2339
  }
2337
2340
  return null;
2338
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
+ }
2339
2390
  function resolve_script_with_fields(script, project_path) {
2340
2391
  const resolved = resolveScriptGuid(script, project_path);
2341
2392
  if (!resolved)
@@ -2892,7 +2943,9 @@ ${element_indent2}- ${value}`);
2892
2943
  if (!header) {
2893
2944
  throw new Error(`Invalid Unity YAML block header in replacement text: "${new_text.slice(0, 80)}"`);
2894
2945
  }
2895
- this._raw = new_text;
2946
+ this._raw = new_text.endsWith(`
2947
+ `) ? new_text : new_text + `
2948
+ `;
2896
2949
  this._header = header;
2897
2950
  this._dirty = true;
2898
2951
  this._format_cache.clear();
@@ -2902,6 +2955,11 @@ ${element_indent2}- ${value}`);
2902
2955
  }
2903
2956
  _check_dirty(old_raw) {
2904
2957
  if (this._raw !== old_raw) {
2958
+ if (!this._raw.endsWith(`
2959
+ `)) {
2960
+ this._raw += `
2961
+ `;
2962
+ }
2905
2963
  this._dirty = true;
2906
2964
  return true;
2907
2965
  }
@@ -3963,14 +4021,82 @@ function read_project_version(projectPath) {
3963
4021
  }
3964
4022
 
3965
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
+ }
3966
4093
  function compute_dll_script_file_id(namespace, class_name) {
3967
- const { createHash } = require("crypto");
3968
4094
  const full_name = namespace ? `${namespace}.${class_name}` : class_name;
3969
4095
  const name_bytes = Buffer.from(full_name, "utf-8");
3970
4096
  const input = Buffer.alloc(4 + name_bytes.length);
3971
4097
  input.writeInt32LE(114, 0);
3972
4098
  name_bytes.copy(input, 4);
3973
- const hash = createHash("md4").update(input).digest();
4099
+ const hash = md4(input);
3974
4100
  const a = hash.readInt32LE(0);
3975
4101
  const b = hash.readInt32LE(4);
3976
4102
  const c = hash.readInt32LE(8);
@@ -3983,13 +4109,13 @@ function get_layer_from_parent(doc, parentTransformId) {
3983
4109
  const parentTransform = doc.find_by_file_id(parentTransformId);
3984
4110
  if (!parentTransform)
3985
4111
  return 0;
3986
- const goMatch = parentTransform.raw.match(/m_GameObject:\s*\{fileID:\s*(\d+)\}/);
4112
+ const goMatch = parentTransform.raw.match(/m_GameObject:[ \t]*\{fileID:[ \t]*(\d+)\}/);
3987
4113
  if (!goMatch)
3988
4114
  return 0;
3989
4115
  const parentGo = doc.find_by_file_id(goMatch[1]);
3990
4116
  if (!parentGo)
3991
4117
  return 0;
3992
- const layerMatch = parentGo.raw.match(/m_Layer:\s*(\d+)/);
4118
+ const layerMatch = parentGo.raw.match(/m_Layer:[ \t]*(\d+)/);
3993
4119
  return layerMatch ? parseInt(layerMatch[1], 10) : 0;
3994
4120
  }
3995
4121
  function createGameObjectYAML(gameObjectId, transformId, name, parentTransformId = "0", rootOrder = 0, layer = 0) {
@@ -4310,11 +4436,14 @@ ${defaults}`;
4310
4436
  function addComponentToGameObject(doc, gameObjectId, componentId) {
4311
4437
  const goBlock = doc.find_by_file_id(gameObjectId);
4312
4438
  if (!goBlock)
4313
- return;
4314
- let raw = goBlock.raw;
4315
- 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}}
4316
4442
  `);
4317
- goBlock.replace_raw(raw);
4443
+ if (updated === raw)
4444
+ return false;
4445
+ goBlock.replace_raw(updated);
4446
+ return true;
4318
4447
  }
4319
4448
  function createMonoBehaviourYAML(componentId, gameObjectId, scriptGuid, fields, version, scriptFileId = 11500000, type_lookup) {
4320
4449
  const field_yaml = fields && fields.length > 0 ? generate_field_yaml(fields, version, " ", type_lookup) : `
@@ -4386,7 +4515,7 @@ function createGameObject(options) {
4386
4515
  if (parentBlock.class_id === 4) {
4387
4516
  parentTransformIdStr = parentIdStr;
4388
4517
  } else if (parentBlock.class_id === 1) {
4389
- 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+)\}/);
4390
4519
  if (compMatch) {
4391
4520
  parentTransformIdStr = compMatch[1];
4392
4521
  } else {
@@ -5296,7 +5425,7 @@ function addComponent(options) {
5296
5425
  let duplicateWarning;
5297
5426
  const goBlock = doc.find_by_file_id(gameObjectIdStr);
5298
5427
  if (goBlock && classId !== null) {
5299
- 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]);
5300
5429
  for (const refId of compRefs) {
5301
5430
  const compBlock = doc.find_by_file_id(refId);
5302
5431
  if (compBlock && compBlock.class_id === classId) {
@@ -5381,9 +5510,33 @@ function addComponent(options) {
5381
5510
  if (variantInfo) {
5382
5511
  appendToAddedComponents(doc, variantInfo.prefab_instance_id, variantInfo.source_ref, componentIdStr);
5383
5512
  } else {
5384
- 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
+ }
5385
5521
  }
5386
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
+ }
5387
5540
  const saveResult = doc.save();
5388
5541
  if (!saveResult.success) {
5389
5542
  return {
@@ -5439,8 +5592,11 @@ function copyComponent(options) {
5439
5592
  const newIdStr = doc.generate_file_id();
5440
5593
  const newId = parseInt(newIdStr, 10);
5441
5594
  let clonedBlock = sourceBlock.raw.replace(new RegExp(`^(--- !u!${sourceBlock.class_id} &)${source_file_id}`), `$1${newId}`);
5442
- clonedBlock = clonedBlock.replace(/m_GameObject:\s*\{fileID:\s*\d+\}/, `m_GameObject: {fileID: ${targetGoId}}`);
5443
- 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
+ }
5444
5600
  doc.append_raw(clonedBlock);
5445
5601
  if (!doc.validate()) {
5446
5602
  return { success: false, file_path, error: "Validation failed after copying component" };
@@ -5490,7 +5646,17 @@ function validate_value_type(current_value, new_value) {
5490
5646
  const incoming = new_value.trim();
5491
5647
  if (/^\{fileID:/.test(current)) {
5492
5648
  if (/^\{fileID:[ \t]*0[ \t]*\}$/.test(current)) {
5493
- 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.`;
5494
5660
  }
5495
5661
  if (!/^\{fileID:/.test(incoming)) {
5496
5662
  return `Expected a reference value ({fileID: ...}), got "${incoming}"`;
@@ -5700,7 +5866,19 @@ function editProperty(options) {
5700
5866
  return result;
5701
5867
  }
5702
5868
  function editComponentByFileId(options) {
5703
- 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
+ }
5704
5882
  const pathError = validate_file_path(file_path, "write");
5705
5883
  if (pathError) {
5706
5884
  return { success: false, file_path, error: pathError };
@@ -6608,7 +6786,7 @@ function removeComponent(options) {
6608
6786
  return { success: false, file_path, error: "Cannot remove a Transform with remove-component. Use delete to remove the entire GameObject." };
6609
6787
  }
6610
6788
  const resolved_file_id = found.file_id;
6611
- const goMatch = found.raw.match(/m_GameObject:\s*\{fileID:\s*(-?\d+)\}/);
6789
+ const goMatch = found.raw.match(/m_GameObject:[ \t]*\{fileID:[ \t]*(-?\d+)\}/);
6612
6790
  if (goMatch) {
6613
6791
  const parentGoId = goMatch[1];
6614
6792
  const goBlock = doc.find_by_file_id(parentGoId);
@@ -6616,9 +6794,18 @@ function removeComponent(options) {
6616
6794
  const escaped = resolved_file_id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6617
6795
  const compLinePattern = new RegExp(`^[ \\t]*- component: \\{fileID: ${escaped}\\}[ \\t]*\\n`, "m");
6618
6796
  const modifiedRaw = goBlock.raw.replace(compLinePattern, "");
6797
+ if (modifiedRaw === goBlock.raw) {}
6619
6798
  goBlock.replace_raw(modifiedRaw);
6620
6799
  }
6621
6800
  }
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
+ }
6622
6809
  doc.remove_block(resolved_file_id);
6623
6810
  if (!doc.validate()) {
6624
6811
  return { success: false, file_path, error: "Validation failed after removing component" };
@@ -6631,7 +6818,8 @@ function removeComponent(options) {
6631
6818
  success: true,
6632
6819
  file_path,
6633
6820
  removed_file_id: resolved_file_id,
6634
- removed_class_id: found.class_id
6821
+ removed_class_id: found.class_id,
6822
+ warning: dangling.length > 0 ? `Dangling references to deleted component found in blocks: ${dangling.join(", ")}` : undefined
6635
6823
  };
6636
6824
  }
6637
6825
  function removeComponentBatch(options) {
@@ -6692,7 +6880,7 @@ function deleteGameObject(options) {
6692
6880
  const goBlock = goResult;
6693
6881
  const goId = goBlock.file_id;
6694
6882
  const componentIds = new Set;
6695
- const compMatches = goBlock.raw.matchAll(/component:\s*\{fileID:\s*(-?\d+)\}/g);
6883
+ const compMatches = goBlock.raw.matchAll(/component:[ \t]*\{fileID:[ \t]*(-?\d+)\}/g);
6696
6884
  for (const cm of compMatches) {
6697
6885
  componentIds.add(cm[1]);
6698
6886
  }
@@ -6702,7 +6890,7 @@ function deleteGameObject(options) {
6702
6890
  const block = doc.find_by_file_id(compId);
6703
6891
  if (block && block.class_id === 4) {
6704
6892
  transformId = compId;
6705
- const fatherMatch = block.raw.match(/m_Father:\s*\{fileID:\s*(-?\d+)\}/);
6893
+ const fatherMatch = block.raw.match(/m_Father:[ \t]*\{fileID:[ \t]*(-?\d+)\}/);
6706
6894
  if (fatherMatch) {
6707
6895
  fatherId = fatherMatch[1];
6708
6896
  }
@@ -6770,7 +6958,7 @@ function deletePrefabInstance(options) {
6770
6958
  }
6771
6959
  const piId = piBlock.file_id;
6772
6960
  const allToRemove = new Set([piId]);
6773
- const piRefPattern = new RegExp(`m_PrefabInstance:\\s*\\{fileID:\\s*${piId}\\}`);
6961
+ const piRefPattern = new RegExp(`m_PrefabInstance:[ \\t]*\\{fileID:[ \\t]*${piId}\\}`);
6774
6962
  for (const block of doc.blocks) {
6775
6963
  if (block.is_stripped && piRefPattern.test(block.raw)) {
6776
6964
  allToRemove.add(block.file_id);
@@ -6780,13 +6968,13 @@ function deletePrefabInstance(options) {
6780
6968
  const addedGOMatch = piBlock.raw.match(addedGOPattern);
6781
6969
  if (addedGOMatch) {
6782
6970
  const addedGOSection = addedGOMatch[1];
6783
- const goMatches = addedGOSection.matchAll(/fileID:\s*(\d+)/g);
6971
+ const goMatches = addedGOSection.matchAll(/fileID:[ \t]*(\d+)/g);
6784
6972
  for (const match of goMatches) {
6785
6973
  const goId = match[1];
6786
6974
  allToRemove.add(goId);
6787
6975
  const goBlock = doc.find_by_file_id(goId);
6788
6976
  if (goBlock) {
6789
- const compMatch = goBlock.raw.match(/component:\s*\{fileID:\s*(\d+)\}/);
6977
+ const compMatch = goBlock.raw.match(/component:[ \t]*\{fileID:[ \t]*(\d+)\}/);
6790
6978
  if (compMatch) {
6791
6979
  const transformId = compMatch[1];
6792
6980
  allToRemove.add(transformId);
@@ -6802,12 +6990,12 @@ function deletePrefabInstance(options) {
6802
6990
  const addedCompMatch = piBlock.raw.match(addedCompPattern);
6803
6991
  if (addedCompMatch) {
6804
6992
  const addedCompSection = addedCompMatch[1];
6805
- const compMatches = addedCompSection.matchAll(/fileID:\s*(\d+)/g);
6993
+ const compMatches = addedCompSection.matchAll(/fileID:[ \t]*(\d+)/g);
6806
6994
  for (const match of compMatches) {
6807
6995
  allToRemove.add(match[1]);
6808
6996
  }
6809
6997
  }
6810
- const parentMatch = piBlock.raw.match(/m_TransformParent:\s*\{fileID:\s*(\d+)\}/);
6998
+ const parentMatch = piBlock.raw.match(/m_TransformParent:[ \t]*\{fileID:[ \t]*(\d+)\}/);
6811
6999
  const parentTransformId = parentMatch ? parentMatch[1] : "0";
6812
7000
  let strippedRootTransformId = null;
6813
7001
  for (const id of allToRemove) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "unity-agentic-tools",
3
3
  "packageManager": "bun@latest",
4
- "version": "0.4.0",
4
+ "version": "0.4.1",
5
5
  "description": "Fast, token-efficient Unity YAML parser for AI agents",
6
6
  "exports": {
7
7
  ".": {