aui-agent-builder 0.3.76 → 0.3.78

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 (36) hide show
  1. package/README.md +157 -26
  2. package/dist/api-client/index.d.ts +41 -0
  3. package/dist/api-client/index.d.ts.map +1 -1
  4. package/dist/api-client/index.js +98 -0
  5. package/dist/api-client/index.js.map +1 -1
  6. package/dist/commands/agents.d.ts.map +1 -1
  7. package/dist/commands/agents.js +6 -8
  8. package/dist/commands/agents.js.map +1 -1
  9. package/dist/commands/import-agent.js +8 -8
  10. package/dist/commands/import-agent.js.map +1 -1
  11. package/dist/commands/integration.js +1 -1
  12. package/dist/commands/integration.js.map +1 -1
  13. package/dist/commands/login.js +1 -1
  14. package/dist/commands/login.js.map +1 -1
  15. package/dist/commands/pull-agent.js +1 -1
  16. package/dist/commands/pull-agent.js.map +1 -1
  17. package/dist/commands/push.js +228 -85
  18. package/dist/commands/push.js.map +1 -1
  19. package/dist/commands/serve.d.ts.map +1 -1
  20. package/dist/commands/serve.js +1 -4
  21. package/dist/commands/serve.js.map +1 -1
  22. package/dist/commands/version-snapshot.d.ts +21 -0
  23. package/dist/commands/version-snapshot.d.ts.map +1 -0
  24. package/dist/commands/version-snapshot.js +669 -0
  25. package/dist/commands/version-snapshot.js.map +1 -0
  26. package/dist/commands/version.d.ts +2 -1
  27. package/dist/commands/version.d.ts.map +1 -1
  28. package/dist/commands/version.js +111 -42
  29. package/dist/commands/version.js.map +1 -1
  30. package/dist/index.js +71 -9
  31. package/dist/index.js.map +1 -1
  32. package/dist/ui/views/PushView.d.ts +3 -1
  33. package/dist/ui/views/PushView.d.ts.map +1 -1
  34. package/dist/ui/views/PushView.js +8 -2
  35. package/dist/ui/views/PushView.js.map +1 -1
  36. package/package.json +1 -1
@@ -11,7 +11,7 @@ import { findAuiFiles, parseAuiFile } from "../utils/index.js";
11
11
  import { validate } from "./validate.js";
12
12
  import { getTracer, SpanStatusCode, setUserContext } from "../telemetry.js";
13
13
  import { getItemLevelDiff } from "../utils/git.js";
14
- import { AuthenticationError, ConfigError, ValidationError } from "../errors/index.js";
14
+ import { AuthenticationError, CLIError, ConfigError, ValidationError } from "../errors/index.js";
15
15
  import { StatusLine, Spinner, ErrorDisplay, Hint, } from "../ui/components/index.js";
16
16
  import { colors, icons } from "../ui/theme.js";
17
17
  import { PushFileSummary, PushChangesView, PushTaskLine, PushFinalSummary, } from "../ui/views/PushView.js";
@@ -459,15 +459,18 @@ async function _push(pushSpan, agentCode, options = {}) {
459
459
  if (savedApiKey && !options.apiKey) {
460
460
  client.setAgentSettingsApiKey(savedApiKey);
461
461
  }
462
- // Only do version management if this project was imported with versioning
463
- // (has version_id in .auirc). Old agents without versions push to live scope.
462
+ // Version management: push is only allowed to draft versions.
463
+ // If the project has version_id in .auirc or --version-id is passed,
464
+ // we validate it's a draft. If no version context exists, we auto-detect
465
+ // available drafts. Push is rejected if no draft is found.
464
466
  let prePushDraft = null;
465
467
  if (projectConfig.version_id || options.versionId) {
466
468
  prePushDraft = await resolveVersionDraft(config, projectConfig, session, options.versionId);
467
- if (prePushDraft?.versionId) {
468
- agentSettingsParams.version_id = prePushDraft.versionId;
469
- log(_jsx(StatusLine, { kind: "info", label: `Pushing into version draft: ${prePushDraft.label}` }));
469
+ if (!prePushDraft) {
470
+ return;
470
471
  }
472
+ agentSettingsParams.version_id = prePushDraft.versionId;
473
+ log(_jsx(StatusLine, { kind: "info", label: `Pushing into draft version: ${prePushDraft.label}` }));
471
474
  }
472
475
  const pushTasks = buildPushTasks(diff, fileData, projectRoot, getFileDiff);
473
476
  pushSpan.setAttribute("push.task_count", pushTasks.length);
@@ -667,33 +670,150 @@ async function _push(pushSpan, agentCode, options = {}) {
667
670
  if (pushFailures.length > 0) {
668
671
  pushSpan.setAttribute("push.failures", JSON.stringify(pushFailures));
669
672
  }
673
+ // ─── Push Snapshot (last step — after entity push) ───
674
+ // The snapshot captures the local file state and uploads it as the
675
+ // source of truth for this push. Runs regardless of entity-push outcomes
676
+ // so the file history is preserved even on partial DB failures.
677
+ // Files = source of truth; DB updates are best-effort.
678
+ //
679
+ // IMPORTANT: snapshot failures must be loud — they are NOT counted in
680
+ // the entity `failed` counter, so without explicit surfacing here and
681
+ // a thrown CLIError at the end of `_push`, a snapshot failure would
682
+ // be hidden behind the "All N change(s) pushed successfully" summary
683
+ // and the process would exit 0.
684
+ // Retry policy: snapshot upload is the source-of-truth step; transient
685
+ // failures (network blips, 5xx, timeouts) are common with multipart
686
+ // uploads, so we retry up to 3 times with exponential backoff (1s, 2s,
687
+ // 4s) before giving up. 4xx responses are still retried — keeping the
688
+ // policy dumb here mirrors the single retry already in the api-client
689
+ // layer for entity pushes.
690
+ let snapshotSucceeded = false;
691
+ let snapshotError;
692
+ let snapshotAttempts = 0;
693
+ if (prePushDraft) {
694
+ const SNAPSHOT_MAX_ATTEMPTS = 4;
695
+ const SNAPSHOT_RETRY_BASE_MS = 1000;
696
+ for (let attempt = 1; attempt <= SNAPSHOT_MAX_ATTEMPTS; attempt++) {
697
+ snapshotAttempts = attempt;
698
+ const label = attempt === 1
699
+ ? "Pushing snapshot (file state)..."
700
+ : `Retrying snapshot push (attempt ${attempt}/${SNAPSHOT_MAX_ATTEMPTS})...`;
701
+ if (json)
702
+ stderrLog(label);
703
+ const snapshotSpinner = json ? null : startSpinner(label);
704
+ let attemptError;
705
+ try {
706
+ const snapshotResult = await pushSnapshot(client, prePushDraft.agentId, prePushDraft.versionId, projectRoot, fileData);
707
+ if (snapshotResult.success) {
708
+ const okMsg = attempt === 1
709
+ ? `Snapshot pushed (${fileData.length} file(s))`
710
+ : `Snapshot pushed (${fileData.length} file(s), attempt ${attempt}/${SNAPSHOT_MAX_ATTEMPTS})`;
711
+ if (snapshotSpinner)
712
+ snapshotSpinner.succeed(okMsg);
713
+ else
714
+ stderrLog(okMsg);
715
+ snapshotSucceeded = true;
716
+ snapshotError = undefined;
717
+ break;
718
+ }
719
+ attemptError = snapshotResult.error || "Unknown snapshot error";
720
+ }
721
+ catch (error) {
722
+ attemptError = error instanceof Error ? error.message : String(error);
723
+ }
724
+ snapshotError = attemptError;
725
+ const isLast = attempt === SNAPSHOT_MAX_ATTEMPTS;
726
+ const failMsg = isLast
727
+ ? `Snapshot push failed after ${attempt} attempt(s): ${attemptError}`
728
+ : `Snapshot push failed (attempt ${attempt}/${SNAPSHOT_MAX_ATTEMPTS}): ${attemptError}`;
729
+ if (snapshotSpinner)
730
+ snapshotSpinner.fail(failMsg);
731
+ else
732
+ stderrLog(failMsg);
733
+ if (isLast)
734
+ break;
735
+ const delayMs = SNAPSHOT_RETRY_BASE_MS * Math.pow(2, attempt - 1);
736
+ if (json)
737
+ stderrLog(`Waiting ${delayMs}ms before retry...`);
738
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
739
+ }
740
+ pushSpan.setAttribute("push.snapshot.success", snapshotSucceeded);
741
+ pushSpan.setAttribute("push.snapshot.attempts", snapshotAttempts);
742
+ if (!snapshotSucceeded && snapshotError) {
743
+ pushSpan.setAttribute("push.snapshot.error", snapshotError);
744
+ }
745
+ }
746
+ const snapshotStatus = prePushDraft
747
+ ? snapshotSucceeded
748
+ ? "succeeded"
749
+ : "failed"
750
+ : undefined;
670
751
  const memoryPath = writePushMemory(projectRoot, agentCodeStr, agentIdStr, pushTasks, succeededFiles, pushFailures);
752
+ // ─── Baseline Update ───
753
+ // Only commit baseline if snapshot succeeded (or no draft = legacy mode).
754
+ // This ensures: if snapshot fails, user re-runs `aui push` to retry both
755
+ // failed entity pushes AND the snapshot. Local files remain the source
756
+ // of truth until the server has captured them.
671
757
  let baselineUpdated = false;
672
- if (failed > 0 && succeeded > 0) {
673
- if (succeededFiles.length > 0) {
674
- commitBaselineFiles(projectRoot, succeededFiles, `pushed ${succeeded} change(s)`);
758
+ const canCommitBaseline = !prePushDraft || snapshotSucceeded;
759
+ if (canCommitBaseline) {
760
+ if (failed > 0 && succeeded > 0) {
761
+ if (succeededFiles.length > 0) {
762
+ commitBaselineFiles(projectRoot, succeededFiles, `pushed ${succeeded} change(s)`);
763
+ baselineUpdated = true;
764
+ }
765
+ }
766
+ else if (failed === 0) {
767
+ commitBaseline(projectRoot, "pushed changes");
675
768
  baselineUpdated = true;
676
769
  }
677
770
  }
678
- else if (failed === 0) {
679
- commitBaseline(projectRoot, "pushed changes");
680
- baselineUpdated = true;
771
+ log(_jsx(PushFinalSummary, { succeeded: succeeded, failed: failed, baselineUpdated: baselineUpdated, logDir: logRelPath, memoryPath: memoryPath, snapshotStatus: snapshotStatus, snapshotError: snapshotError }));
772
+ if (failed > 0) {
773
+ log(_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(StatusLine, { kind: "warning", label: `${failed} entity change(s) failed to push to DB.` }), pushFailures.map((f) => (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "red", children: [" ", icons.error, " ", f.label] }), _jsxs(Text, { color: colors.muted, children: [" Error: ", f.error] }), f.file && _jsxs(Text, { color: colors.muted, children: [" File: ", f.file] })] }, f.label))), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.info, bold: true, children: "What to do next: " }), _jsxs(Text, { color: colors.muted, children: ["Fix the issues above and re-run ", _jsx(Text, { bold: true, children: "aui push" }), " to retry the failed changes."] })] })] }));
681
774
  }
682
- log(_jsx(PushFinalSummary, { succeeded: succeeded, failed: failed, baselineUpdated: baselineUpdated, logDir: logRelPath, memoryPath: memoryPath }));
775
+ // ─── Snapshot result message ───
683
776
  if (prePushDraft) {
684
- log(_jsx(StatusLine, { kind: "info", label: `Changes pushed into draft ${prePushDraft.label}. Run: aui version publish → aui version activate` }));
777
+ if (!snapshotSucceeded) {
778
+ log(_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(StatusLine, { kind: "error", label: `Snapshot upload failed${snapshotError ? `: ${snapshotError}` : ""}` }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.info, bold: true, children: "What to do next: " }), _jsxs(Text, { color: colors.muted, children: ["Re-run ", _jsx(Text, { bold: true, children: "aui push" }), " to retry the snapshot. Your local files are the source of truth \u2014 they remain unchanged."] })] })] }));
779
+ }
780
+ else if (failed === 0) {
781
+ log(_jsx(StatusLine, { kind: "info", label: `All changes pushed into draft ${prePushDraft.label}. Run: aui version publish → aui version activate` }));
782
+ }
783
+ else {
784
+ log(_jsx(StatusLine, { kind: "warning", label: `Snapshot saved for draft ${prePushDraft.label}, but ${failed} DB update(s) failed. Re-run "aui push" to retry the DB updates before publishing.` }));
785
+ }
685
786
  }
787
+ const snapshotFailed = snapshotStatus === "failed";
686
788
  pushSpan.setAttribute("push.exit_reason", failed > 0
687
789
  ? succeeded > 0
688
790
  ? "partial_failure"
689
791
  : "failed"
690
- : "completed");
792
+ : snapshotFailed
793
+ ? "snapshot_failed"
794
+ : "completed");
691
795
  pushSpan.setAttribute("push.succeeded_count", succeeded);
692
796
  pushSpan.setAttribute("push.failed_count", failed);
797
+ // Snapshot failure must propagate as a non-zero exit code. Without this
798
+ // throw, `failed === 0` would let the command exit 0 and CI/CD would
799
+ // never notice the snapshot upload failed. handleError() also produces
800
+ // a structured JSON envelope when --json is set.
801
+ if (snapshotFailed) {
802
+ throw new CLIError(`Snapshot upload failed${snapshotError ? `: ${snapshotError}` : ""}`, {
803
+ suggestion: "Re-run `aui push` to retry the snapshot. Your local files are the source of truth — they remain unchanged.",
804
+ });
805
+ }
693
806
  }
694
807
  catch (error) {
695
808
  if (spinner)
696
809
  spinner.fail("Push failed");
810
+ // CLIErrors carry actionable info and a meaningful exit code — let
811
+ // handleError() format them and exit non-zero. Without this re-throw
812
+ // a snapshot-failure CLIError thrown above would be swallowed in TUI
813
+ // mode and the process would exit 0.
814
+ if (error instanceof CLIError) {
815
+ throw error;
816
+ }
697
817
  if (!json)
698
818
  log(_jsx(ErrorDisplay, { error: error }));
699
819
  else
@@ -711,110 +831,133 @@ async function resolveVersionDraft(config, projectConfig, session, explicitVersi
711
831
  const key = loadAgentSettingsApiKey();
712
832
  if (key)
713
833
  client.setAgentSettingsApiKey(key);
714
- // If user passed --version-id, use it directly
715
- if (explicitVersionId) {
834
+ let agentInfo;
835
+ const agentMgmtId = session.agent_management_id;
836
+ // Project's network_id (from .auirc) takes priority over session — when
837
+ // you're inside a project, that's the agent you mean. Session agent may
838
+ // point at a different agent (e.g. last `aui agents --switch`).
839
+ const projectNetworkId = projectConfig.agent_id;
840
+ const fallbackNetworkId = session.network_id;
841
+ if (projectNetworkId) {
716
842
  try {
717
- const agentMgmtId = session.agent_management_id;
718
- if (agentMgmtId) {
719
- const ver = await client.agentManagement.getVersion(agentMgmtId, explicitVersionId);
720
- const label = `v${ver.version_number}.${ver.version_revision_number}`;
721
- return { versionId: ver.id, label };
722
- }
843
+ const resp = await client.agentManagement.listAgents(client.getOrganizationId(), 1, 50, { network_id: projectNetworkId });
844
+ agentInfo = resp.items.find((a) => a.scope.network_id === projectNetworkId || a.id === projectNetworkId);
723
845
  }
724
846
  catch {
725
- // fall through
847
+ // listing failed, fall through
726
848
  }
727
- return { versionId: explicitVersionId, label: explicitVersionId.slice(0, 10) };
728
849
  }
729
- let agentInfo;
730
- const agentMgmtId = session.agent_management_id;
731
- const networkId = projectConfig.agent_id || session.network_id;
732
- if (agentMgmtId) {
850
+ // Fall back to session's agent_management_id only when not inside a project
851
+ if (!agentInfo && !projectNetworkId && agentMgmtId) {
733
852
  try {
734
853
  agentInfo = await client.agentManagement.getAgent(agentMgmtId);
735
854
  }
736
855
  catch {
737
- // stale ID
856
+ // stale ID, fall through
738
857
  }
739
858
  }
740
- if (!agentInfo && networkId) {
859
+ // Last resort: session's network_id
860
+ if (!agentInfo && fallbackNetworkId) {
741
861
  try {
742
- const allAgents = [];
743
- let page = 1;
744
- let hasMore = true;
745
- while (hasMore) {
746
- const resp = await client.agentManagement.listAgents(client.getOrganizationId(), page, 50);
747
- allAgents.push(...resp.items);
748
- hasMore = page < resp.pages;
749
- page++;
750
- }
751
- agentInfo = allAgents.find((a) => a.scope.network_id === networkId || a.id === networkId);
862
+ const resp = await client.agentManagement.listAgents(client.getOrganizationId(), 1, 50, { network_id: fallbackNetworkId });
863
+ agentInfo = resp.items.find((a) => a.scope.network_id === fallbackNetworkId || a.id === fallbackNetworkId);
752
864
  }
753
865
  catch {
754
866
  // no agent-management available
755
867
  }
756
868
  }
757
- if (!agentInfo)
869
+ if (!agentInfo) {
870
+ log(_jsx(ErrorDisplay, { message: "Could not resolve agent for version management.", suggestion: "Run `aui import-agent` to link an agent, or check your session with `aui status`." }));
758
871
  return null;
872
+ }
873
+ // If user passed --version-id, validate it's a draft
874
+ if (explicitVersionId) {
875
+ try {
876
+ const ver = await client.agentManagement.getVersion(agentInfo.id, explicitVersionId);
877
+ if (ver.status !== "draft") {
878
+ log(_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(StatusLine, { kind: "error", label: `Version v${ver.version_number} is "${ver.status}" — you can only push to a draft version.` }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.info, bold: true, children: "What to do next:" }), _jsxs(Text, { color: colors.muted, children: [" 1. Create a new draft version: ", _jsx(Text, { bold: true, children: "aui version create" })] }), _jsxs(Text, { color: colors.muted, children: [" 2. Then push with: ", _jsxs(Text, { bold: true, children: ["aui push --version-id ", '<new-draft-id>'] })] })] })] }));
879
+ return null;
880
+ }
881
+ const label = `v${ver.version_number}`;
882
+ return { versionId: ver.id, label, agentId: agentInfo.id };
883
+ }
884
+ catch (error) {
885
+ log(_jsx(ErrorDisplay, { message: `Could not fetch version "${explicitVersionId}": ${error instanceof Error ? error.message : String(error)}`, suggestion: "Check the version ID with `aui version list` and try again." }));
886
+ return null;
887
+ }
888
+ }
889
+ // Resolve from .auirc version_id or auto-detect drafts
759
890
  let allVersions = [];
760
891
  try {
761
892
  const versionsResp = await client.agentManagement.listVersions(agentInfo.id, 1, 50);
762
893
  allVersions = versionsResp.items;
763
894
  }
764
895
  catch {
896
+ log(_jsx(ErrorDisplay, { message: "Could not fetch versions for this agent.", suggestion: "Check your connection and try again. Use `aui version list` to debug." }));
765
897
  return null;
766
898
  }
899
+ // If .auirc has a version_id, validate it's still a draft
900
+ if (projectConfig.version_id) {
901
+ const configVersion = allVersions.find((v) => v.id === projectConfig.version_id);
902
+ if (configVersion) {
903
+ if (configVersion.status !== "draft") {
904
+ log(_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(StatusLine, { kind: "error", label: `The version in your .auirc (v${configVersion.version_number}) is "${configVersion.status}" — you can only push to a draft version.` }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.info, bold: true, children: "What to do next:" }), _jsxs(Text, { color: colors.muted, children: [" 1. Create a new draft version: ", _jsx(Text, { bold: true, children: "aui version create" })] }), _jsxs(Text, { color: colors.muted, children: [" 2. Import the new draft: ", _jsxs(Text, { bold: true, children: ["aui import-agent --version ", '<new-draft-id>'] })] }), _jsxs(Text, { color: colors.muted, children: [" 3. Then push your changes: ", _jsx(Text, { bold: true, children: "aui push" })] })] })] }));
905
+ return null;
906
+ }
907
+ const label = `v${configVersion.version_number}`;
908
+ return { versionId: configVersion.id, label, agentId: agentInfo.id };
909
+ }
910
+ }
911
+ // Auto-detect drafts
767
912
  const drafts = allVersions.filter((v) => v.status === "draft");
768
- const latestPublished = allVersions.find((v) => v.status === "published" || v.status === "archived");
913
+ if (drafts.length === 0) {
914
+ log(_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(StatusLine, { kind: "error", label: "No draft version found \u2014 you can only push to a draft version." }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.info, bold: true, children: "What to do next:" }), _jsxs(Text, { color: colors.muted, children: [" 1. Create a new draft version: ", _jsx(Text, { bold: true, children: "aui version create" })] }), _jsxs(Text, { color: colors.muted, children: [" 2. Import the draft: ", _jsxs(Text, { bold: true, children: ["aui import-agent --version ", '<draft-id>'] })] }), _jsxs(Text, { color: colors.muted, children: [" 3. Then push your changes: ", _jsx(Text, { bold: true, children: "aui push" })] })] })] }));
915
+ return null;
916
+ }
769
917
  if (drafts.length === 1) {
770
918
  const draft = drafts[0];
771
- const label = `v${draft.version_number}.${draft.version_revision_number}`;
772
- return { versionId: draft.id, label };
919
+ const label = `v${draft.version_number}`;
920
+ return { versionId: draft.id, label, agentId: agentInfo.id };
773
921
  }
774
- if (drafts.length > 1) {
775
- const json = isJsonMode();
776
- if (json) {
777
- const draft = drafts[0];
778
- const label = `v${draft.version_number}.${draft.version_revision_number}`;
779
- return { versionId: draft.id, label };
780
- }
781
- const { selected } = await inquirer.prompt([
782
- {
783
- type: "list",
784
- name: "selected",
785
- message: `${drafts.length} drafts found. Push into which draft?`,
786
- choices: drafts.map((d) => {
787
- const dl = `v${d.version_number}.${d.version_revision_number}`;
788
- return {
789
- name: `${dl} — ${d.stats?.total ?? "?"} entities ${d.label || ""}`,
790
- value: d,
791
- };
792
- }),
793
- pageSize: 10,
794
- },
795
- ]);
796
- const chosen = selected;
797
- const label = `v${chosen.version_number}.${chosen.version_revision_number}`;
798
- return { versionId: chosen.id, label };
922
+ // Multiple drafts — auto-pick the latest (drafts are sorted DESC by created_at)
923
+ // No interactive prompt: keeps `aui push` non-blocking. Use --version-id to
924
+ // explicitly target a different draft.
925
+ const latest = drafts[0];
926
+ const latestLabel = `v${latest.version_number}`;
927
+ if (!isJsonMode()) {
928
+ log(_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(StatusLine, { kind: "info", label: `${drafts.length} drafts found — auto-selecting latest: ${latestLabel}` }), _jsxs(Text, { color: colors.muted, children: [" Tip: pass ", _jsxs(Text, { bold: true, children: ["--version-id ", '<id>'] }), " to target a specific draft."] })] }));
799
929
  }
800
- // No drafts create a revision from latest published, or v1.0 from scope
801
- const bumpMode = latestPublished ? "version_revision_number" : "version_number";
802
- const versionSpinner = startSpinner("Creating version draft...");
803
- try {
804
- const draft = await client.agentManagement.createDraft(agentInfo.id, {
805
- source: latestPublished ? "version" : "agent-scope",
806
- ...(latestPublished ? { from_version: latestPublished.id } : {}),
807
- bump_mode: bumpMode,
808
- created_by: session.user_id,
930
+ return { versionId: latest.id, label: latestLabel, agentId: agentInfo.id };
931
+ }
932
+ /**
933
+ * Push a snapshot of all local agent files to the server via multipart upload.
934
+ * This captures the current state of the files to maintain version history.
935
+ * Version bump happens automatically on the server when snapshot succeeds.
936
+ *
937
+ * If any files fail, the user must re-run `aui push` to retry.
938
+ */
939
+ async function pushSnapshot(client, agentId, versionId, projectRoot, fileData) {
940
+ const filesToUpload = [];
941
+ for (const fd of fileData) {
942
+ const fullPath = path.join(projectRoot, fd.file);
943
+ if (!fs.existsSync(fullPath))
944
+ continue;
945
+ const content = fs.readFileSync(fullPath);
946
+ filesToUpload.push({
947
+ filePath: fullPath,
948
+ fileName: path.basename(fd.file),
949
+ content,
809
950
  });
810
- const label = `v${draft.version_number}.${draft.version_revision_number}`;
811
- versionSpinner.succeed(`Version draft created: ${label}`);
812
- return { versionId: draft.id, label };
813
951
  }
814
- catch (error) {
815
- versionSpinner.fail(`Version draft failed: ${error instanceof Error ? error.message : String(error)}`);
816
- return null;
952
+ if (filesToUpload.length === 0) {
953
+ return { success: true, failed: [] };
817
954
  }
955
+ const result = await client.agentManagement.pushSnapshot(agentId, versionId, filesToUpload);
956
+ return {
957
+ success: result.success,
958
+ failed: result.failed,
959
+ error: result.error,
960
+ };
818
961
  }
819
962
  // ─── Agent Settings Params Resolution ───
820
963
  async function resolveAgentSettingsParams(config, projectConfig, session, projectRoot, scopeLevel) {