aui-agent-builder 0.3.101 → 0.3.103

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.
Files changed (54) hide show
  1. package/README.md +11 -17
  2. package/dist/api-client/kb-view-client.d.ts +26 -12
  3. package/dist/api-client/kb-view-client.d.ts.map +1 -1
  4. package/dist/api-client/kb-view-client.js.map +1 -1
  5. package/dist/commands/add-integration.d.ts.map +1 -1
  6. package/dist/commands/add-integration.js +18 -47
  7. package/dist/commands/add-integration.js.map +1 -1
  8. package/dist/commands/add-tool.d.ts.map +1 -1
  9. package/dist/commands/add-tool.js +15 -69
  10. package/dist/commands/add-tool.js.map +1 -1
  11. package/dist/commands/import-agent.d.ts +1 -0
  12. package/dist/commands/import-agent.d.ts.map +1 -1
  13. package/dist/commands/import-agent.js +24 -87
  14. package/dist/commands/import-agent.js.map +1 -1
  15. package/dist/commands/init.d.ts.map +1 -1
  16. package/dist/commands/init.js +43 -60
  17. package/dist/commands/init.js.map +1 -1
  18. package/dist/commands/pull-agent.d.ts +1 -0
  19. package/dist/commands/pull-agent.d.ts.map +1 -1
  20. package/dist/commands/pull-agent.js +23 -80
  21. package/dist/commands/pull-agent.js.map +1 -1
  22. package/dist/commands/push.d.ts +45 -0
  23. package/dist/commands/push.d.ts.map +1 -1
  24. package/dist/commands/push.js +171 -204
  25. package/dist/commands/push.js.map +1 -1
  26. package/dist/commands/rag.js +1 -1
  27. package/dist/commands/rag.js.map +1 -1
  28. package/dist/commands/validate.d.ts.map +1 -1
  29. package/dist/commands/validate.js +33 -121
  30. package/dist/commands/validate.js.map +1 -1
  31. package/dist/commands/widget.d.ts.map +1 -1
  32. package/dist/commands/widget.js +21 -140
  33. package/dist/commands/widget.js.map +1 -1
  34. package/dist/index.js +2 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/services/diff.service.d.ts.map +1 -1
  37. package/dist/services/diff.service.js +3 -37
  38. package/dist/services/diff.service.js.map +1 -1
  39. package/dist/services/kb-view.service.d.ts +12 -27
  40. package/dist/services/kb-view.service.d.ts.map +1 -1
  41. package/dist/services/kb-view.service.js +134 -106
  42. package/dist/services/kb-view.service.js.map +1 -1
  43. package/dist/services/pull-schema.service.d.ts.map +1 -1
  44. package/dist/services/pull-schema.service.js +3 -5
  45. package/dist/services/pull-schema.service.js.map +1 -1
  46. package/dist/telemetry.d.ts +19 -2
  47. package/dist/telemetry.d.ts.map +1 -1
  48. package/dist/telemetry.js +24 -2
  49. package/dist/telemetry.js.map +1 -1
  50. package/dist/utils/index.d.ts +2 -70
  51. package/dist/utils/index.d.ts.map +1 -1
  52. package/dist/utils/index.js +35 -243
  53. package/dist/utils/index.js.map +1 -1
  54. package/package.json +1 -1
@@ -7,7 +7,7 @@ import inquirer from "inquirer";
7
7
  import { getConfig, loadProjectConfig, saveProjectConfig, loadAgentSettingsApiKey, saveAgentSettingsApiKey, } from "../config/index.js";
8
8
  import { getValidSession } from "../services/auth.service.js";
9
9
  import { AUIClient, applyScopeLevel } from "../api-client/index.js";
10
- import { findAuiFiles, parseAuiFile, classifyAuiPath } from "../utils/index.js";
10
+ import { findAuiFiles, parseAuiFile } from "../utils/index.js";
11
11
  import { validate } from "./validate.js";
12
12
  import { getTracer, SpanStatusCode, setUserContext, setAgentContext, } from "../telemetry.js";
13
13
  import { trace } from "@opentelemetry/api";
@@ -295,84 +295,35 @@ async function _push(pushSpan, agentCode, options = {}) {
295
295
  const fileData = [];
296
296
  try {
297
297
  const files = await findAuiFiles(projectRoot);
298
- // First pass: index sibling widget files by their tool slug so we can
299
- // fold them back into the parent tool when we encounter the tool file.
300
- // This keeps the in-memory push payload identical to the legacy layout
301
- // where `widget` was inline on the tool.
302
- const widgetBySlug = new Map();
303
- for (const file of files) {
304
- const rel = path.relative(projectRoot, file).replace(/\\/g, "/");
305
- const cls = classifyAuiPath(rel);
306
- if (cls.kind === "tool-widget" && cls.slug) {
307
- widgetBySlug.set(cls.slug, file);
308
- }
309
- }
310
298
  for (const file of files) {
311
299
  const parsed = parseAuiFile(file);
312
300
  if (!parsed)
313
301
  continue;
314
302
  const relativePath = path.relative(projectRoot, file);
315
- const normalized = relativePath.replace(/\\/g, "/");
316
- const cls = classifyAuiPath(normalized);
317
303
  let type = "unknown";
318
- let data = parsed;
319
- switch (cls.kind) {
320
- case "agent":
321
- type = "general_settings";
322
- break;
323
- case "parameters":
324
- type = "parameters";
325
- break;
326
- case "entities":
327
- type = "entities";
328
- break;
329
- case "rules":
330
- type = "rules";
331
- break;
332
- case "integrations-list":
333
- type = "integrations";
334
- break;
335
- case "integration":
336
- type = "integration";
337
- break;
338
- case "tool":
339
- type = "tool";
340
- // Fold the sibling widget file (when present) into the tool
341
- // object so downstream task-builders see a "tool with widget"
342
- // shape regardless of how the project is laid out on disk.
343
- if (cls.slug && widgetBySlug.has(cls.slug)) {
344
- const widget = parseAuiFile(widgetBySlug.get(cls.slug));
345
- const toolObj = parsed.tool ??
346
- parsed;
347
- const hasWidget = widget && Object.keys(widget).length > 0 ? widget : null;
348
- const mergedTool = { ...toolObj, widget: hasWidget };
349
- data = { tool: mergedTool };
350
- }
351
- break;
352
- case "tool-widget":
353
- // Folded into the parent tool above — don't emit a standalone
354
- // entry. The push machinery treats widget edits as edits to the
355
- // tool (the diff machinery handles file-level change detection).
356
- continue;
357
- default: {
358
- // Fall back to the historical "guess by top-level key" behaviour
359
- // so any unusual files keep working.
360
- const isToolPath = normalized.startsWith("tools/") || normalized.startsWith("tools\\");
361
- if (parsed.general_settings)
362
- type = "general_settings";
363
- else if (parsed.tool || isToolPath)
364
- type = "tool";
365
- else if (parsed.parameters)
366
- type = "parameters";
367
- else if (parsed.entities)
368
- type = "entities";
369
- else if (parsed.integrations)
370
- type = "integrations";
371
- else if (parsed.rules)
372
- type = "rules";
373
- }
374
- }
375
- fileData.push({ file: relativePath, type, data });
304
+ const isToolPath = relativePath.startsWith("tools/") || relativePath.startsWith("tools\\");
305
+ // Recognize agent.aui.json by filename (not just by the wrapper key),
306
+ // mirroring `validate.tsx:222` which accepts both
307
+ // { "general_settings": {...} } (wrapped — `aui pull` writes this)
308
+ // {...top-level fields...} (flat — Agent Builder skills write this)
309
+ // Without this, a flat agent.aui.json fell through to type "unknown"
310
+ // and `buildPushTasks` never had a chance to emit a
311
+ // patch-general-settings task — same "silently dropped" class of bug
312
+ // the tool-modify branch had.
313
+ const isSettingsPath = relativePath === "agent.aui.json";
314
+ if (parsed.general_settings || isSettingsPath)
315
+ type = "general_settings";
316
+ else if (parsed.tool || isToolPath)
317
+ type = "tool";
318
+ else if (parsed.parameters)
319
+ type = "parameters";
320
+ else if (parsed.entities)
321
+ type = "entities";
322
+ else if (parsed.integrations)
323
+ type = "integrations";
324
+ else if (parsed.rules)
325
+ type = "rules";
326
+ fileData.push({ file: relativePath, type, data: parsed });
376
327
  }
377
328
  if (json)
378
329
  stderrLog(`Read ${fileData.length} files`);
@@ -611,12 +562,18 @@ async function _push(pushSpan, agentCode, options = {}) {
611
562
  pushSpan.setAttribute("push.diff.modified", diff.totalModified);
612
563
  pushSpan.setAttribute("push.diff.deleted", diff.totalDeleted);
613
564
  }
614
- if (pushTasks.length === 0) {
565
+ // pushKnowledgeHubs runs as a separate step; short-circuiting on
566
+ // pushTasks alone would silently no-op KB-only pushes.
567
+ const hasKbChanges = diff?.changedFiles?.some((c) => c.file.startsWith("knowledge-hubs/")) ??
568
+ false;
569
+ if (pushTasks.length === 0 && !hasKbChanges) {
615
570
  pushSpan.setAttribute("push.exit_reason", "no_pushable_tasks");
616
571
  log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "success", label: "No pushable changes detected." }) }));
617
572
  return;
618
573
  }
619
- log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "info", label: `Pushing ${pushTasks.length} change(s)...` }) }));
574
+ if (pushTasks.length > 0) {
575
+ log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "info", label: `Pushing ${pushTasks.length} change(s)...` }) }));
576
+ }
620
577
  let succeeded = 0;
621
578
  let failed = 0;
622
579
  let authFailed = false;
@@ -1592,7 +1549,7 @@ async function pushKnowledgeHubs(projectRoot, projectConfig) {
1592
1549
  scope,
1593
1550
  created_by: userId,
1594
1551
  knowledge_base_name: kbData.name,
1595
- knowledge_base_description: kbData.description,
1552
+ knowledge_base_description: kbData.description ?? undefined,
1596
1553
  });
1597
1554
  span.setStatus({ code: SpanStatusCode.OK });
1598
1555
  if (importResult.knowledge_base_id) {
@@ -1635,6 +1592,69 @@ async function pushKnowledgeHubs(projectRoot, projectConfig) {
1635
1592
  }
1636
1593
  });
1637
1594
  }
1595
+ // importFiles ingests binaries only; sidecar edits + non-file
1596
+ // Resources reach the server through importJson. After importFiles
1597
+ // so binary re-extracts don't clobber manifest edits.
1598
+ const hasJsonPayload = kbData.tabular_files.length > 0 || kbData.markdown_files.length > 0;
1599
+ if (hasJsonPayload) {
1600
+ const kbJsonTracer = getTracer();
1601
+ await kbJsonTracer.startActiveSpan("aui.push.task.kb-import-json", async (span) => {
1602
+ span.setAttribute("push.task.type", "kb-import-json");
1603
+ span.setAttribute("push.task.label", `Sync knowledge base manifest: ${kbData.name || kbDirName}`);
1604
+ span.setAttribute("push.task.file", `knowledge-hubs/${kbDirName}/kb.json`);
1605
+ span.setAttribute("push.task.kb_name", kbData.name || kbDirName);
1606
+ span.setAttribute("push.task.tabular_count", kbData.tabular_files.length);
1607
+ span.setAttribute("push.task.markdown_count", kbData.markdown_files.length);
1608
+ await setUserContext(span);
1609
+ await setAgentContext(span, {
1610
+ agentId: projectConfig.agent_management_id,
1611
+ agentCode: projectConfig.agent_code,
1612
+ versionId: projectConfig.version_id,
1613
+ versionLabel: projectConfig.version_label,
1614
+ networkId: kbNetworkId,
1615
+ accountId: projectConfig.account_id,
1616
+ organizationId: projectConfig.organization_id,
1617
+ networkCategoryId: projectConfig.network_category_id,
1618
+ });
1619
+ try {
1620
+ await kbViewClient.importJson({
1621
+ scope,
1622
+ knowledge_bases: [
1623
+ {
1624
+ name: kbData.name,
1625
+ description: kbData.description,
1626
+ tabular_files: kbData.tabular_files,
1627
+ markdown_files: kbData.markdown_files,
1628
+ },
1629
+ ],
1630
+ created_by: userId,
1631
+ network_id: scope.network_id,
1632
+ });
1633
+ span.setStatus({ code: SpanStatusCode.OK });
1634
+ }
1635
+ catch (importErr) {
1636
+ hadUploadFailure = true;
1637
+ const errMsg = importErr instanceof Error
1638
+ ? importErr.message
1639
+ : String(importErr);
1640
+ span.setStatus({ code: SpanStatusCode.ERROR, message: errMsg });
1641
+ span.recordException(importErr instanceof Error ? importErr : new Error(errMsg));
1642
+ span.setAttribute("push.task.error", errMsg);
1643
+ if (importErr.statusCode) {
1644
+ span.setAttribute("push.task.error_status_code", importErr.statusCode);
1645
+ }
1646
+ failures.push({
1647
+ label: `Sync knowledge base manifest: ${kbData.name || kbDirName}`,
1648
+ file: `knowledge-hubs/${kbDirName}/kb.json`,
1649
+ error: errMsg,
1650
+ });
1651
+ log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "error", label: `Failed to sync manifest for "${kbData.name || kbDirName}": ${errMsg}` }) }));
1652
+ }
1653
+ finally {
1654
+ span.end();
1655
+ }
1656
+ });
1657
+ }
1638
1658
  }
1639
1659
  if (hadUploadFailure) {
1640
1660
  kbSpinner.fail(`Knowledge base push completed with errors`);
@@ -1689,10 +1709,7 @@ function getArrayFileInfoForPush(filePath, dir) {
1689
1709
  return null;
1690
1710
  const content = JSON.parse(fs.readFileSync(fullPath, "utf-8"));
1691
1711
  const isToolPath = filePath.startsWith("tools/") || filePath.startsWith("tools\\");
1692
- // Per-tool and per-integration files are single-entity payloads, not
1693
- // array-of-entities files — they don't need item-level diffing.
1694
- const isPerIntegration = filePath.startsWith("integrations/") || filePath.startsWith("integrations\\");
1695
- if (content.tool || isToolPath || content.integration || isPerIntegration)
1712
+ if (content.tool || isToolPath)
1696
1713
  return null;
1697
1714
  if (content.parameters)
1698
1715
  return { arrayKey: "parameters", itemKey: "code", label: "Parameters" };
@@ -1852,59 +1869,28 @@ function diffObjects(oldObj, newObj, prefix = "") {
1852
1869
  }
1853
1870
  return diffs;
1854
1871
  }
1855
- function buildPushTasks(diff, fileData, projectRoot, getFileDiffFn) {
1872
+ /**
1873
+ * Build the ordered list of API tasks the push will execute, given a git
1874
+ * diff summary, the parsed file contents, and an injectable per-file diff
1875
+ * function.
1876
+ *
1877
+ * `getFileDiffFn` is parameterized so tests can stub out `git show` without
1878
+ * a real git repo. `getItemLevelDiffFn` is optional and defaults to the
1879
+ * production implementation; tests inject it so the array-keyed branches
1880
+ * (parameters, entities, integrations) are exercisable without git too.
1881
+ *
1882
+ * Exported for unit-test access only — production callers should keep
1883
+ * going through `_push`, which threads in the real `getFileDiff` and
1884
+ * `getItemLevelDiff` from `../utils/git.js`.
1885
+ */
1886
+ export function buildPushTasks(diff, fileData, projectRoot, getFileDiffFn, getItemLevelDiffFn) {
1887
+ const getItems = getItemLevelDiffFn ?? getItemLevelDiff;
1856
1888
  const tasks = [];
1857
- // Up-cast: a change to the sibling widget file is semantically a change
1858
- // to its parent tool, not a standalone entity. Rewrite each such row so
1859
- // the downstream task-builder picks up the merged tool from `fileData`.
1860
- //
1861
- // We resolve the *actual* on-disk tool file for the slug by consulting
1862
- // `fileData` (which holds the canonical relative paths emitted by the
1863
- // file walker), so the up-cast is filename-naming-scheme agnostic — it
1864
- // works for `<slug>_tool.aui.json`, the prior `tool.aui.json`, and the
1865
- // pre-split legacy `tools/<slug>.aui.json`.
1866
- const toolFileBySlug = new Map();
1867
- for (const fd of fileData) {
1868
- if (fd.type !== "tool")
1869
- continue;
1870
- const normalized = fd.file.replace(/\\/g, "/");
1871
- const cls = classifyAuiPath(normalized);
1872
- if (cls.kind === "tool" && cls.slug) {
1873
- toolFileBySlug.set(cls.slug, normalized);
1874
- }
1875
- }
1876
- const consolidatedFiles = [];
1877
- const seenToolFiles = new Set();
1878
1889
  for (const change of diff.changedFiles) {
1879
- const normalized = change.file.replace(/\\/g, "/");
1880
- const cls = classifyAuiPath(normalized);
1881
- if (cls.kind === "tool-widget" && cls.slug) {
1882
- const toolFile = toolFileBySlug.get(cls.slug);
1883
- // If the tool file isn't on disk anymore (mid-edit, half-written
1884
- // project), there's nothing for the widget change to attach to.
1885
- // Drop the row — push will simply not patch this tool.
1886
- if (!toolFile)
1887
- continue;
1888
- if (seenToolFiles.has(toolFile))
1889
- continue;
1890
- seenToolFiles.add(toolFile);
1891
- consolidatedFiles.push({ status: "modified", file: toolFile });
1892
- continue;
1893
- }
1894
- if (cls.kind === "tool" && cls.slug) {
1895
- seenToolFiles.add(change.file);
1896
- }
1897
- consolidatedFiles.push(change);
1898
- }
1899
- for (const change of consolidatedFiles) {
1900
- const normalizedChange = change.file.replace(/\\/g, "/");
1901
- const changeCls = classifyAuiPath(normalizedChange);
1902
1890
  if (change.status === "deleted") {
1903
- const isToolFile = changeCls.kind === "tool" ||
1904
- (change.file.includes("tools/") && change.file.endsWith(".aui.json"));
1891
+ const isToolFile = change.file.includes("tools/") && change.file.endsWith(".aui.json");
1905
1892
  if (isToolFile) {
1906
- const basename = (changeCls.slug ??
1907
- change.file.split("/").pop().replace(".aui.json", ""));
1893
+ const basename = change.file.split("/").pop().replace(".aui.json", "");
1908
1894
  const toolName = basename.toUpperCase().replace(/-/g, "_");
1909
1895
  tasks.push({
1910
1896
  type: "delete-tool",
@@ -1913,17 +1899,6 @@ function buildPushTasks(diff, fileData, projectRoot, getFileDiffFn) {
1913
1899
  file: change.file,
1914
1900
  body: {},
1915
1901
  });
1916
- continue;
1917
- }
1918
- if (changeCls.kind === "integration") {
1919
- const code = changeCls.slug;
1920
- tasks.push({
1921
- type: "delete-integration",
1922
- label: `Delete integration: ${code}`,
1923
- file: change.file,
1924
- body: {},
1925
- itemCode: code,
1926
- });
1927
1902
  }
1928
1903
  continue;
1929
1904
  }
@@ -1932,33 +1907,6 @@ function buildPushTasks(diff, fileData, projectRoot, getFileDiffFn) {
1932
1907
  f.file.endsWith("/" + change.file));
1933
1908
  if (!fd)
1934
1909
  continue;
1935
- // ── Per-file integration change (new layout) ──
1936
- if (fd.type === "integration") {
1937
- const integrationObj = (fd.data.integration ??
1938
- fd.data);
1939
- const code = integrationObj.code ?? changeCls.slug ?? "";
1940
- if (!code)
1941
- continue;
1942
- if (change.status === "added") {
1943
- tasks.push({
1944
- type: "create-integration",
1945
- label: `Create integration: ${code}`,
1946
- file: change.file,
1947
- body: integrationObj,
1948
- itemCode: code,
1949
- });
1950
- }
1951
- else if (change.status === "modified") {
1952
- tasks.push({
1953
- type: "patch-integration",
1954
- label: `Update integration: ${code}`,
1955
- file: change.file,
1956
- body: integrationObj,
1957
- itemCode: code,
1958
- });
1959
- }
1960
- continue;
1961
- }
1962
1910
  if (fd.type === "tool") {
1963
1911
  const toolData = fd.data?.tool || fd.data;
1964
1912
  if (!toolData || !toolData.code)
@@ -1974,22 +1922,29 @@ function buildPushTasks(diff, fileData, projectRoot, getFileDiffFn) {
1974
1922
  });
1975
1923
  }
1976
1924
  else if (change.status === "modified") {
1977
- // A change that came from a sibling `widget.aui.json` got re-pointed
1978
- // at the tool file above. Its field diff against the tool file
1979
- // itself may be empty (only the widget changed) — we must still
1980
- // PATCH the tool with the full merged body so the new widget lands.
1981
- const fieldDiffs = getFileDiffFn(projectRoot, change.file);
1982
- const patchBody = buildToolPatchBody(fieldDiffs);
1983
- const widgetOnlyChange = fieldDiffs.length === 0 && Object.keys(patchBody).length === 0;
1984
- if (Object.keys(patchBody).length > 0 || widgetOnlyChange) {
1985
- tasks.push({
1986
- type: "patch-tool",
1987
- label: `Update tool: ${toolCode}`,
1988
- toolName,
1989
- file: change.file,
1990
- body: toolData,
1991
- });
1992
- }
1925
+ // `change.status === "modified"` comes from `getDiffSummary` (git
1926
+ // status). That alone is the source of truth that the file content
1927
+ // diverged from the baseline; always push the patch. The full
1928
+ // `toolData` body is sent (consistent with the create-tool branch
1929
+ // immediately above); the platform PATCH is idempotent for fields
1930
+ // whose value didn't actually change, so this is safe.
1931
+ //
1932
+ // Pre-fix this branch routed the changes through a
1933
+ // `buildToolPatchBody` gate that only matched diff paths starting
1934
+ // with the wrapped-shape `tool.` prefix. Tool files written in flat
1935
+ // shape (no `{ "tool": {...} }` wrapper — accepted by the loader at
1936
+ // ~L2675 and by `aui validate`) diffed to top-level paths
1937
+ // (`response_type`, `widget`, `goal`, …), the gate returned an
1938
+ // empty body, and the `patch-tool` task was silently dropped.
1939
+ // Symptom users hit: `aui diff` showed the change, `aui push`
1940
+ // printed "No pushable changes detected" and exited 0.
1941
+ tasks.push({
1942
+ type: "patch-tool",
1943
+ label: `Update tool: ${toolCode}`,
1944
+ toolName,
1945
+ file: change.file,
1946
+ body: toolData,
1947
+ });
1993
1948
  }
1994
1949
  }
1995
1950
  else if (fd.type === "general_settings") {
@@ -2006,7 +1961,14 @@ function buildPushTasks(diff, fileData, projectRoot, getFileDiffFn) {
2006
1961
  }
2007
1962
  }
2008
1963
  else if (change.status === "added") {
2009
- const settings = fd.data?.general_settings;
1964
+ // Mirror the tool loader's `(fd.data as any)?.tool || fd.data`
1965
+ // tolerance: accept both `{ "general_settings": {...} }` (wrapped)
1966
+ // and `{...}` (flat). `normalizeGeneralSettings` in
1967
+ // `pull-agent.tsx` / `import-agent.tsx` and `aui validate`
1968
+ // (`commands/validate.tsx:222`) already permit both layouts —
1969
+ // staying in lockstep with them here prevents the same
1970
+ // "silently dropped" class of bug the tool-modify branch had.
1971
+ const settings = fd.data?.general_settings ?? fd.data;
2010
1972
  if (settings) {
2011
1973
  tasks.push({
2012
1974
  type: "patch-general-settings",
@@ -2018,7 +1980,7 @@ function buildPushTasks(diff, fileData, projectRoot, getFileDiffFn) {
2018
1980
  }
2019
1981
  }
2020
1982
  else if (fd.type === "parameters") {
2021
- const itemDiffs = getItemLevelDiff(projectRoot, change.file, "parameters", "code");
1983
+ const itemDiffs = getItems(projectRoot, change.file, "parameters", "code");
2022
1984
  for (const item of itemDiffs) {
2023
1985
  if (item.status === "added") {
2024
1986
  tasks.push({
@@ -2050,7 +2012,7 @@ function buildPushTasks(diff, fileData, projectRoot, getFileDiffFn) {
2050
2012
  }
2051
2013
  }
2052
2014
  else if (fd.type === "entities") {
2053
- const itemDiffs = getItemLevelDiff(projectRoot, change.file, "entities", "name");
2015
+ const itemDiffs = getItems(projectRoot, change.file, "entities", "name");
2054
2016
  for (const item of itemDiffs) {
2055
2017
  if (item.status === "added") {
2056
2018
  tasks.push({
@@ -2082,7 +2044,7 @@ function buildPushTasks(diff, fileData, projectRoot, getFileDiffFn) {
2082
2044
  }
2083
2045
  }
2084
2046
  else if (fd.type === "integrations") {
2085
- const itemDiffs = getItemLevelDiff(projectRoot, change.file, "integrations", "code");
2047
+ const itemDiffs = getItems(projectRoot, change.file, "integrations", "code");
2086
2048
  for (const item of itemDiffs) {
2087
2049
  if (item.status === "added") {
2088
2050
  tasks.push({
@@ -2125,29 +2087,34 @@ function buildPushTasks(diff, fileData, projectRoot, getFileDiffFn) {
2125
2087
  }
2126
2088
  return tasks;
2127
2089
  }
2128
- function buildToolPatchBody(fieldDiffs) {
2090
+ // NOTE: `buildToolPatchBody` was removed when the tool-modify branch above
2091
+ // switched to pushing on any `change.status === "modified"`. Its sole job
2092
+ // had been to gate the patch-tool task on a `tool.<field>` diff-path prefix,
2093
+ // which silently dropped every edit on flat-shape (no-wrapper) tool files.
2094
+ // See the inline comment in the modify branch (~L2688) for the full history.
2095
+ export function buildSettingsPatchBody(fieldDiffs) {
2129
2096
  const body = {};
2130
2097
  for (const fd of fieldDiffs) {
2131
2098
  if (fd.operation === "removed")
2132
2099
  continue;
2133
2100
  const parts = fd.path.split(".");
2134
- if (parts[0] === "tool" && parts.length >= 2) {
2135
- const topLevelKey = parts[1];
2136
- body[topLevelKey] = fd.newValue;
2137
- }
2138
- }
2139
- return body;
2140
- }
2141
- function buildSettingsPatchBody(fieldDiffs) {
2142
- const body = {};
2143
- for (const fd of fieldDiffs) {
2144
- if (fd.operation === "removed")
2101
+ if (parts.length === 0 || !parts[0])
2145
2102
  continue;
2146
- const parts = fd.path.split(".");
2147
- if (parts[0] === "general_settings" && parts.length >= 2) {
2148
- const topLevelKey = parts[1];
2149
- body[topLevelKey] = fd.newValue;
2150
- }
2103
+ // general_settings (agent.aui.json) files come in two equally-supported
2104
+ // shapes:
2105
+ // wrapped: { "general_settings": { ...fields... } } → paths look like
2106
+ // `general_settings.<field>...`
2107
+ // flat: { ...fields... } → paths look like
2108
+ // `<field>...`
2109
+ // `aui validate` (`commands/validate.tsx:222`) and
2110
+ // `normalizeGeneralSettings` in `import-agent.tsx` / `pull-agent.tsx`
2111
+ // already accept both layouts. Pre-fix this body builder only matched
2112
+ // the wrapped form, which silently dropped edits to flat-shape files.
2113
+ // Match the loader behavior here so every change reaches the platform.
2114
+ const topLevelKey = parts[0] === "general_settings" && parts.length >= 2
2115
+ ? parts[1]
2116
+ : parts[0];
2117
+ body[topLevelKey] = fd.newValue;
2151
2118
  }
2152
2119
  return body;
2153
2120
  }