aui-agent-builder 0.3.103 → 0.3.104

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 (52) hide show
  1. package/dist/api-client/index.d.ts +304 -19
  2. package/dist/api-client/index.d.ts.map +1 -1
  3. package/dist/api-client/index.js +337 -69
  4. package/dist/api-client/index.js.map +1 -1
  5. package/dist/commands/agents.d.ts +55 -1
  6. package/dist/commands/agents.d.ts.map +1 -1
  7. package/dist/commands/agents.js +193 -50
  8. package/dist/commands/agents.js.map +1 -1
  9. package/dist/commands/import-agent.d.ts +23 -0
  10. package/dist/commands/import-agent.d.ts.map +1 -1
  11. package/dist/commands/import-agent.js +802 -151
  12. package/dist/commands/import-agent.js.map +1 -1
  13. package/dist/commands/legacy/push-records-mode.d.ts +166 -0
  14. package/dist/commands/legacy/push-records-mode.d.ts.map +1 -0
  15. package/dist/commands/legacy/push-records-mode.js +2536 -0
  16. package/dist/commands/legacy/push-records-mode.js.map +1 -0
  17. package/dist/commands/pull-agent.d.ts +8 -0
  18. package/dist/commands/pull-agent.d.ts.map +1 -1
  19. package/dist/commands/pull-agent.js +567 -126
  20. package/dist/commands/pull-agent.js.map +1 -1
  21. package/dist/commands/push.d.ts +78 -107
  22. package/dist/commands/push.d.ts.map +1 -1
  23. package/dist/commands/push.js +942 -1804
  24. package/dist/commands/push.js.map +1 -1
  25. package/dist/commands/util/agent-mode.d.ts +69 -0
  26. package/dist/commands/util/agent-mode.d.ts.map +1 -0
  27. package/dist/commands/util/agent-mode.js +101 -0
  28. package/dist/commands/util/agent-mode.js.map +1 -0
  29. package/dist/commands/validate.d.ts.map +1 -1
  30. package/dist/commands/validate.js +23 -7
  31. package/dist/commands/validate.js.map +1 -1
  32. package/dist/commands/version-snapshot.d.ts.map +1 -1
  33. package/dist/commands/version-snapshot.js +253 -49
  34. package/dist/commands/version-snapshot.js.map +1 -1
  35. package/dist/commands/version.d.ts +15 -1
  36. package/dist/commands/version.d.ts.map +1 -1
  37. package/dist/commands/version.js +102 -7
  38. package/dist/commands/version.js.map +1 -1
  39. package/dist/config/index.d.ts +16 -1
  40. package/dist/config/index.d.ts.map +1 -1
  41. package/dist/config/index.js.map +1 -1
  42. package/dist/index.js +20 -5
  43. package/dist/index.js.map +1 -1
  44. package/dist/ui/views/ImportAgentView.d.ts +15 -0
  45. package/dist/ui/views/ImportAgentView.d.ts.map +1 -1
  46. package/dist/ui/views/ImportAgentView.js +8 -3
  47. package/dist/ui/views/ImportAgentView.js.map +1 -1
  48. package/dist/utils/index.d.ts +80 -0
  49. package/dist/utils/index.d.ts.map +1 -1
  50. package/dist/utils/index.js +330 -0
  51. package/dist/utils/index.js.map +1 -1
  52. package/package.json +1 -1
@@ -1,19 +1,22 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import { render } from "ink";
5
+ import { Box } from "ink";
5
6
  import inquirer from "inquirer";
6
7
  import chalk from "chalk";
7
8
  import { getConfig, loadSession, saveProjectConfig, loadAgentSettingsApiKey, saveAgentSettingsApiKey, } from "../config/index.js";
8
9
  import { AUIClient, AUIAPIError } from "../api-client/index.js";
9
10
  import { getTracer, SpanStatusCode, setUserContext } from "../telemetry.js";
10
11
  import { getMissingGitConfig, initAndCommitBaseline } from "../utils/git.js";
11
- import { AuthenticationError } from "../errors/index.js";
12
+ import { disassembleBundleToFiles, normalizeBundleFileBody, readBundleTools, } from "../utils/index.js";
13
+ import { AuthenticationError, CLIError, ConfigError } from "../errors/index.js";
12
14
  import { KBViewClient } from "../api-client/kb-view-client.js";
13
15
  import { exportToFolder, buildScope, } from "../services/kb-view.service.js";
14
16
  import { fetchSchemas } from "../services/pull-schema.service.js";
15
17
  import { Header, StatusLine, Spinner, ErrorDisplay, Hint, } from "../ui/components/index.js";
16
18
  import { ImportAgentView, ImportSessionInfo, ImportWarnings, } from "../ui/views/ImportAgentView.js";
19
+ import { detectAgentBundleMode, isModeMismatchError, } from "./util/agent-mode.js";
17
20
  // ─── Ink Rendering Helpers ───
18
21
  function log(node) {
19
22
  const { unmount } = render(node);
@@ -108,29 +111,143 @@ async function _importAgent(parentSpan, agentId, options = {}) {
108
111
  environment: config.environment,
109
112
  });
110
113
  applyAgentSettingsKey(client, options.apiKey);
114
+ // `selectedAgent` is definitely assigned by either branch below
115
+ // (explicit-id path throws on resolution failure; interactive path
116
+ // returns early), but TS can't follow assignment through the
117
+ // try/fallback pair so we declare with `!` and assert below.
111
118
  let selectedAgent;
112
119
  let knownAgentInfo;
113
120
  // Resolve agent: explicit ID > session agent (when --version is given) > full interactive flow
114
121
  const effectiveAgentId = agentId || (options.version ? session.network_id : undefined);
115
122
  if (effectiveAgentId) {
123
+ // ─── Dual-shape id resolution ───
124
+ // The positional `[agent-id]` arg historically accepted only the
125
+ // legacy `network_id` (24-char hex of a `Network` document).
126
+ // After the v3 agent-settings migration, users naturally copy IDs
127
+ // from the new Swagger ("agent_id (path)" on `/pull` / `/push`) —
128
+ // those are `agent_management_id`s (also 24-char hex, but a
129
+ // different document family). Same regex, different collection.
130
+ //
131
+ // Try agent-management FIRST (it's the new path and the most
132
+ // common id today), fall back to the legacy networks lookup on
133
+ // 404 so existing scripts that pass `network_id` keep working.
134
+ // Whichever resolves wins, and we cache the resolved `AgentInfo`
135
+ // as `knownAgentInfo` so `selectVersionForAgent` / `doImport`
136
+ // skip a duplicate lookup AND the new `/pull` endpoint is
137
+ // reachable on the first import attempt.
116
138
  const spinner = startSpinner("Fetching agent...");
139
+ let resolved = false;
140
+ let lastErr;
141
+ // 1) Treat the id as an agent-management UUID.
117
142
  try {
118
- const response = await client.networks.get(effectiveAgentId);
119
- selectedAgent = response.data;
120
- if (selectedAgent.account || selectedAgent.organization) {
121
- client.setScope({
122
- ...(selectedAgent.account ? { accountId: selectedAgent.account } : {}),
123
- ...(selectedAgent.organization ? { organizationId: selectedAgent.organization } : {}),
124
- });
143
+ const ami = await client.agentManagement.getAgent(effectiveAgentId);
144
+ knownAgentInfo = ami;
145
+ selectedAgent = agentInfoToNetwork(ami);
146
+ // Bias the client scope toward the agent's own org/account so
147
+ // downstream listAgents/listVersions calls don't 403 against the
148
+ // session's default scope.
149
+ const scopeUpdate = {};
150
+ if (ami.scope?.account_id)
151
+ scopeUpdate.accountId = ami.scope.account_id;
152
+ if (ami.scope?.organization_id)
153
+ scopeUpdate.organizationId = ami.scope.organization_id;
154
+ if (scopeUpdate.accountId || scopeUpdate.organizationId) {
155
+ client.setScope(scopeUpdate);
125
156
  }
126
- spinner.succeed(`Found agent: ${selectedAgent.name}`);
157
+ spinner.succeed(`Found agent (via agent-management): ${ami.name}`);
158
+ resolved = true;
127
159
  }
128
- catch (error) {
129
- spinner.fail(isAuthError(error) ? "Authentication failed" : "Agent not found");
130
- handleAuthError(error);
131
- throw error;
160
+ catch (err) {
161
+ lastErr = err;
162
+ const status = err instanceof AUIAPIError
163
+ ? err.status
164
+ : (err.statusCode ??
165
+ err.status);
166
+ // Fall through to the legacy networks lookup on ANY non-auth
167
+ // error — not just 404. Rationale (2026-05-24):
168
+ //
169
+ // - 404 ⇒ id isn't an agent_management_id; try as network_id.
170
+ // - 422 ⇒ the new agent-settings response schema has stricter
171
+ // validation than some legacy docs (e.g. the post-`kind`
172
+ // rollout returned 422 for any pre-rollout agent until
173
+ // a backfill landed). The legacy networks endpoint
174
+ // lives in a different service and doesn't share that
175
+ // validation, so it can still resolve the same agent.
176
+ // - 5xx / network blip ⇒ a transient agent-settings failure
177
+ // shouldn't block the import when there's an alternate
178
+ // resolver available; the legacy attempt will either
179
+ // succeed (problem masked) or 4xx itself (we report
180
+ // the more informative agent-settings error via
181
+ // `lastErr` at the end of step 2).
182
+ //
183
+ // Auth errors (401/403) STILL fail loudly here — no point in
184
+ // trying the next endpoint with the same credentials.
185
+ if (isAuthError(err)) {
186
+ spinner.fail("Authentication failed");
187
+ handleAuthError(err);
188
+ throw err;
189
+ }
190
+ if (process.env.AUI_DEBUG) {
191
+ console.log(`[debug] agentManagement.getAgent(${effectiveAgentId}) → ${status ?? "non-auth error"}; trying legacy networks.get next`);
192
+ }
193
+ }
194
+ // 2) Fall back: treat the id as a legacy network_id.
195
+ if (!resolved) {
196
+ try {
197
+ const response = await client.networks.get(effectiveAgentId);
198
+ selectedAgent = response.data;
199
+ if (selectedAgent.account || selectedAgent.organization) {
200
+ client.setScope({
201
+ ...(selectedAgent.account ? { accountId: selectedAgent.account } : {}),
202
+ ...(selectedAgent.organization
203
+ ? { organizationId: selectedAgent.organization }
204
+ : {}),
205
+ });
206
+ }
207
+ spinner.succeed(`Found agent (via legacy networks): ${selectedAgent.name}`);
208
+ resolved = true;
209
+ }
210
+ catch (error) {
211
+ // Neither id shape resolved. Surface the FIRST failure
212
+ // (agent-management) since that's the new-world canonical
213
+ // path and is the more informative error in the migration
214
+ // window. Fall back to the second error if the first was
215
+ // suppressed for some reason.
216
+ spinner.fail(isAuthError(error) || isAuthError(lastErr)
217
+ ? "Authentication failed"
218
+ : "Agent not found (tried both agent-management and legacy networks)");
219
+ const errToReport = lastErr ?? error;
220
+ handleAuthError(errToReport);
221
+ throw errToReport;
222
+ }
132
223
  }
133
224
  }
225
+ else if (options.templates) {
226
+ // ─── Templates picker (NETWORK_CATEGORY-scoped, kind=template) ──
227
+ //
228
+ // Templates don't live under an account — they're category-scoped
229
+ // and visible to every account in the org. The default
230
+ // `listAgents` call only returns NETWORK-scoped regulars, so we
231
+ // need the new `scope_type=NETWORK_CATEGORY` + `kind=template`
232
+ // filters (added to the API client 2026-05-24). The walk becomes:
233
+ //
234
+ // org → category → template → version
235
+ //
236
+ // (vs. the regular org → account → agent → version)
237
+ //
238
+ // We still pass through `selectVersionForAgent` afterwards —
239
+ // template version management uses the same /v1/agents/{id}/versions
240
+ // endpoints regardless of `kind`.
241
+ const orgResult = await selectOrgForImport(client, session);
242
+ if (!orgResult)
243
+ return;
244
+ log(_jsx(ImportSessionInfo, { orgName: orgResult.orgName, accountName: "(templates are category-scoped)", environment: session.environment }));
245
+ const result = await selectTemplateFromList(client, options.category);
246
+ if (!result)
247
+ return;
248
+ selectedAgent = result.network;
249
+ knownAgentInfo = result.agentInfo;
250
+ }
134
251
  else {
135
252
  // Step 1: Choose organization
136
253
  const orgResult = await selectOrgForImport(client, session);
@@ -153,15 +270,29 @@ async function _importAgent(parentSpan, agentId, options = {}) {
153
270
  const networkId = selectedAgent._id || selectedAgent.id;
154
271
  parentSpan.setAttribute("import.agent_id", networkId);
155
272
  parentSpan.setAttribute("import.agent_name", selectedAgent.name);
156
- const selectedVersion = await selectVersionForAgent(client, networkId, options.version, knownAgentInfo);
273
+ const { version: selectedVersion, agentInfo: resolvedAgentInfo } = await selectVersionForAgent(client, networkId, options.version, knownAgentInfo);
274
+ // `selectVersionForAgent` may resolve the agent-management record
275
+ // internally (via `listAgents(network_id=…)`) even when the upstream
276
+ // `selectAgentFromList` had to fall back to legacy networks. Prefer
277
+ // the freshly-resolved id so `doImport` can reach the new `/pull`
278
+ // endpoint instead of silently degrading to the legacy export path.
279
+ const effectiveAgentMgmtId = knownAgentInfo?.id ?? resolvedAgentInfo?.id;
157
280
  if (selectedVersion) {
158
281
  parentSpan.setAttribute("import.version_id", selectedVersion.id);
159
282
  parentSpan.setAttribute("import.version", `v${selectedVersion.version_number}`);
160
283
  }
284
+ if (effectiveAgentMgmtId) {
285
+ parentSpan.setAttribute("import.agent_management_id", effectiveAgentMgmtId);
286
+ }
161
287
  const vLabel = selectedVersion
162
288
  ? `v${selectedVersion.version_number}`
163
289
  : undefined;
164
- await doImport(client, selectedAgent, config, options, parentSpan, selectedVersion?.id, vLabel, selectedVersion?.status, knownAgentInfo?.id);
290
+ // Prefer the AgentInfo fetched in step 1 (`knownAgentInfo`); fall
291
+ // back to the version-resolver's lookup (`resolvedAgentInfo`).
292
+ // Either way, downstream `detectAgentBundleMode` reuses it instead
293
+ // of re-fetching.
294
+ const effectiveAgentInfo = knownAgentInfo ?? resolvedAgentInfo;
295
+ await doImport(client, selectedAgent, config, options, parentSpan, selectedVersion?.id, vLabel, selectedVersion?.status, effectiveAgentMgmtId, effectiveAgentInfo);
165
296
  }
166
297
  // ─── Import from another account ───
167
298
  export async function importFromOtherAccount(options = {}) {
@@ -230,11 +361,13 @@ async function _importFromOtherAccount(parentSpan, options = {}) {
230
361
  if (!result)
231
362
  return;
232
363
  const networkId = result.network._id || result.network.id;
233
- const selectedVersion = await selectVersionForAgent(client, networkId, undefined, result.agentInfo);
364
+ const { version: selectedVersion, agentInfo: resolvedAgentInfo } = await selectVersionForAgent(client, networkId, undefined, result.agentInfo);
365
+ const effectiveAgentMgmtId = result.agentInfo?.id ?? resolvedAgentInfo?.id;
366
+ const effectiveAgentInfo = result.agentInfo ?? resolvedAgentInfo;
234
367
  const vLabel = selectedVersion
235
368
  ? `v${selectedVersion.version_number}`
236
369
  : undefined;
237
- await doImport(client, result.network, config, options, parentSpan, selectedVersion?.id, vLabel, selectedVersion?.status, result.agentInfo?.id);
370
+ await doImport(client, result.network, config, options, parentSpan, selectedVersion?.id, vLabel, selectedVersion?.status, effectiveAgentMgmtId, effectiveAgentInfo);
238
371
  }
239
372
  catch (error) {
240
373
  spinner.fail("Failed to fetch accounts");
@@ -330,11 +463,13 @@ async function _importFromOtherOrg(parentSpan, options = {}) {
330
463
  if (!result)
331
464
  return;
332
465
  const networkId = result.network._id || result.network.id;
333
- const selectedVersion = await selectVersionForAgent(client, networkId, undefined, result.agentInfo);
466
+ const { version: selectedVersion, agentInfo: resolvedAgentInfo } = await selectVersionForAgent(client, networkId, undefined, result.agentInfo);
467
+ const effectiveAgentMgmtId = result.agentInfo?.id ?? resolvedAgentInfo?.id;
468
+ const effectiveAgentInfo = result.agentInfo ?? resolvedAgentInfo;
334
469
  const vLabel = selectedVersion
335
470
  ? `v${selectedVersion.version_number}`
336
471
  : undefined;
337
- await doImport(client, result.network, config, options, parentSpan, selectedVersion?.id, vLabel, selectedVersion?.status, result.agentInfo?.id);
472
+ await doImport(client, result.network, config, options, parentSpan, selectedVersion?.id, vLabel, selectedVersion?.status, effectiveAgentMgmtId, effectiveAgentInfo);
338
473
  }
339
474
  catch (error) {
340
475
  orgSpinner.fail("Failed to fetch organizations");
@@ -444,6 +579,97 @@ function agentInfoToNetwork(a) {
444
579
  updatedAt: a.updated_at,
445
580
  };
446
581
  }
582
+ /**
583
+ * Picker for template (kind=template, NETWORK_CATEGORY-scoped) agents.
584
+ *
585
+ * Mirrors `selectAgentFromList` but calls `listAgents` with the new
586
+ * `scope_type=NETWORK_CATEGORY` + `kind=template` filters so only
587
+ * templates show up. No legacy `networks.list()` fallback — there's no
588
+ * equivalent legacy concept for templates (they only exist in the
589
+ * agent-management world).
590
+ *
591
+ * If `categoryFilter` is provided, it's resolved to a category id (via
592
+ * the same id/key/name lookup `agents --create --category` uses) and
593
+ * forwarded as `network_category_id` so the listing only returns
594
+ * templates in that category. Invalid filter ⇒ helpful error + bail.
595
+ */
596
+ async function selectTemplateFromList(client, categoryFilter) {
597
+ let resolvedCategoryId;
598
+ if (categoryFilter) {
599
+ const catSpinner = startSpinner(`Resolving category "${categoryFilter}"...`);
600
+ try {
601
+ const catResp = await client.categories.list();
602
+ const catUpper = categoryFilter.toUpperCase();
603
+ const match = catResp.data.find((c) => c._id === categoryFilter ||
604
+ c.key?.toUpperCase() === catUpper ||
605
+ c.name?.toUpperCase() === catUpper);
606
+ if (match) {
607
+ resolvedCategoryId = match._id;
608
+ catSpinner.succeed(`Category: ${match.name} (${match.key})`);
609
+ }
610
+ else {
611
+ catSpinner.fail(`Category "${categoryFilter}" not found.`);
612
+ const available = catResp.data
613
+ .map((c) => `${c.name} (${c.key})`)
614
+ .slice(0, 10)
615
+ .join(", ");
616
+ log(_jsx(StatusLine, { kind: "muted", label: `Available: ${available}${catResp.data.length > 10 ? `, … (${catResp.data.length} total)` : ""}` }));
617
+ return null;
618
+ }
619
+ }
620
+ catch (err) {
621
+ catSpinner.warn(`Could not resolve category — listing all templates instead. (${err instanceof Error ? err.message : err})`);
622
+ }
623
+ }
624
+ const spinner = startSpinner("Fetching templates...");
625
+ try {
626
+ const allTemplates = [];
627
+ let page = 1;
628
+ let hasMore = true;
629
+ while (hasMore) {
630
+ const response = await client.agentManagement.listAgents(client.getOrganizationId(), page, 50, {
631
+ scope_type: "NETWORK_CATEGORY",
632
+ kind: "template",
633
+ ...(resolvedCategoryId ? { network_category_id: resolvedCategoryId } : {}),
634
+ });
635
+ allTemplates.push(...response.items);
636
+ hasMore = page < response.pages;
637
+ page++;
638
+ }
639
+ if (allTemplates.length === 0) {
640
+ spinner.succeed("No templates found");
641
+ log(_jsx(StatusLine, { kind: "warning", label: resolvedCategoryId
642
+ ? `No templates in this category. Use \`aui agents --create --template --category <id> --name <name>\` to create one.`
643
+ : "No templates found in this organization. Use `aui agents --create --template --category <id> --name <name>` to create one." }));
644
+ return null;
645
+ }
646
+ spinner.succeed(`Found ${allTemplates.length} template(s)`);
647
+ const { chosen } = await inquirer.prompt([
648
+ {
649
+ type: "list",
650
+ name: "chosen",
651
+ message: "Select template to import:",
652
+ choices: allTemplates.map((a) => {
653
+ // Surface the category id when it's the differentiator —
654
+ // templates with the same name across categories is a real
655
+ // case worth disambiguating in the picker.
656
+ const cat = a.scope?.network_category_id
657
+ ? ` (${a.scope.network_category_id.slice(0, 8)}…)`
658
+ : "";
659
+ return { name: `${a.name}${cat}`, value: a };
660
+ }),
661
+ pageSize: 15,
662
+ },
663
+ ]);
664
+ const selected = chosen;
665
+ return { network: agentInfoToNetwork(selected), agentInfo: selected };
666
+ }
667
+ catch (error) {
668
+ spinner.fail("Failed to fetch templates");
669
+ handleAuthError(error);
670
+ throw error;
671
+ }
672
+ }
447
673
  async function selectAgentFromList(client) {
448
674
  const spinner = startSpinner("Fetching agents...");
449
675
  try {
@@ -519,6 +745,21 @@ async function selectAgentFromList(client) {
519
745
  }
520
746
  }
521
747
  // ─── Shared: select version for an agent ───
748
+ /**
749
+ * Resolve a version (and the underlying agent-management record) for an
750
+ * agent identified by `networkId`. Returns BOTH so the caller can
751
+ * forward the agent-management UUID into `doImport` — without it the
752
+ * new `/pull` blob endpoint can never be reached (it's keyed on
753
+ * `agent_id` = agent-management UUID, not network_id).
754
+ *
755
+ * Previously this returned just `AgentVersion | null`. When the
756
+ * upstream `selectAgentFromList` fell back to the legacy
757
+ * `networks.list()` (because agentManagement `listAgents` returned 0),
758
+ * the agent-management UUID was resolved here locally but lost on the
759
+ * way back, so `doImport` saw `agentManagementId === undefined` and
760
+ * silently took the legacy export path. Returning `agentInfo` plugs
761
+ * that hole.
762
+ */
522
763
  async function selectVersionForAgent(client, networkId, versionIdOverride, knownAgentInfo) {
523
764
  const spinner = startSpinner("Fetching agent versions...");
524
765
  try {
@@ -558,7 +799,7 @@ async function selectVersionForAgent(client, networkId, versionIdOverride, known
558
799
  }
559
800
  if (!agentInfo) {
560
801
  spinner.succeed("No version management found — importing latest");
561
- return null;
802
+ return { version: null, agentInfo: undefined };
562
803
  }
563
804
  if (process.env.AUI_DEBUG) {
564
805
  console.log(`[debug] resolved agent: id=${agentInfo.id} name=${agentInfo.name} network=${agentInfo.scope.network_id} active_version=${agentInfo.active_version_id}`);
@@ -574,19 +815,74 @@ async function selectVersionForAgent(client, networkId, versionIdOverride, known
574
815
  }
575
816
  if (allVersions.length === 0) {
576
817
  spinner.succeed("No versions found — importing latest");
577
- return null;
818
+ return { version: null, agentInfo };
578
819
  }
579
820
  spinner.succeed(`Found ${allVersions.length} version(s)`);
580
821
  if (versionIdOverride) {
581
822
  const match = allVersions.find((v) => v.id === versionIdOverride);
582
- if (!match) {
583
- log(_jsx(StatusLine, { kind: "error", label: `Version not found: ${versionIdOverride}` }));
584
- log(_jsx(Hint, { message: "Run `aui agent --versions` to see available versions." }));
585
- throw new Error(`Version not found: ${versionIdOverride}`);
823
+ if (match) {
824
+ const vLabel = `v${match.version_number}`;
825
+ log(_jsx(StatusLine, { kind: "info", label: `Version: ${vLabel} [${match.status}]` }));
826
+ return { version: match, agentInfo };
586
827
  }
587
- const vLabel = `v${match.version_number}`;
588
- log(_jsx(StatusLine, { kind: "info", label: `Version: ${vLabel} [${match.status}]` }));
589
- return match;
828
+ // ─── Multi-agent fallback for shared network_ids ──────────────
829
+ // It's legitimate for one `network_id` to map to MULTIPLE
830
+ // agent-management records (e.g. duplicates from data migration,
831
+ // or one record per account scope under the same network). We
832
+ // picked the first match from `listAgents(network_id=…)` above,
833
+ // but the user's `--version <id>` may belong to a SIBLING agent
834
+ // record. Before throwing "version not found", paginate every
835
+ // agent for this network_id and try `getVersion(candidate.id,
836
+ // versionIdOverride)` on each — return the first one that
837
+ // succeeds (and switch `agentInfo` to that candidate so
838
+ // downstream `/pull` uses the right `agent_id`).
839
+ const probeSpinner = startSpinner(`Version not on agent ${agentInfo.id.slice(0, 10)}…; searching sibling agents for the same network...`);
840
+ try {
841
+ const allCandidates = [];
842
+ let probePage = 1;
843
+ let probeHasMore = true;
844
+ while (probeHasMore) {
845
+ const resp = await client.agentManagement.listAgents(client.getOrganizationId(), probePage, 50, { network_id: networkId });
846
+ allCandidates.push(...resp.items);
847
+ probeHasMore = probePage < resp.pages;
848
+ probePage++;
849
+ }
850
+ const siblings = allCandidates.filter((c) => c.id !== agentInfo.id);
851
+ if (process.env.AUI_DEBUG) {
852
+ console.log(`[debug] multi-agent fallback: found ${siblings.length} sibling agent(s) for network_id=${networkId}; probing each for version ${versionIdOverride}`);
853
+ }
854
+ for (const sibling of siblings) {
855
+ try {
856
+ const ver = await client.agentManagement.getVersion(sibling.id, versionIdOverride);
857
+ probeSpinner.succeed(`Found version on sibling agent ${sibling.id.slice(0, 10)}… (${sibling.name})`);
858
+ const vLabel = `v${ver.version_number}`;
859
+ log(_jsx(StatusLine, { kind: "info", label: `Version: ${vLabel} [${ver.status}] (agent: ${sibling.name})` }));
860
+ return { version: ver, agentInfo: sibling };
861
+ }
862
+ catch (err) {
863
+ const status = err.statusCode ??
864
+ err.status;
865
+ if (status !== 404) {
866
+ // Non-404 (auth / 5xx) — bubble immediately so we
867
+ // don't silently skip a real failure.
868
+ probeSpinner.fail("Sibling-agent probe failed");
869
+ throw err;
870
+ }
871
+ if (process.env.AUI_DEBUG) {
872
+ console.log(`[debug] multi-agent fallback: getVersion(${sibling.id}, ${versionIdOverride}) → 404; trying next sibling`);
873
+ }
874
+ }
875
+ }
876
+ probeSpinner.fail(`Version ${versionIdOverride} not found on any of ${allCandidates.length} agent record(s) for network ${networkId}`);
877
+ }
878
+ catch (probeErr) {
879
+ if (probeSpinner.stop)
880
+ probeSpinner.stop();
881
+ throw probeErr;
882
+ }
883
+ log(_jsx(StatusLine, { kind: "error", label: `Version not found: ${versionIdOverride}` }));
884
+ log(_jsx(Hint, { message: "Run `aui agent --versions` to see available versions on this agent, or verify the version_id from the Playground's `Versions management` dialog." }));
885
+ throw new Error(`Version not found: ${versionIdOverride}`);
590
886
  }
591
887
  // Auto-select active version if one exists
592
888
  if (agentInfo.active_version_id) {
@@ -594,7 +890,7 @@ async function selectVersionForAgent(client, networkId, versionIdOverride, known
594
890
  if (active) {
595
891
  const vLabel = `v${active.version_number}`;
596
892
  log(_jsx(StatusLine, { kind: "info", label: `Using active version: ${vLabel}` }));
597
- return active;
893
+ return { version: active, agentInfo };
598
894
  }
599
895
  }
600
896
  const { selectedVersion } = await inquirer.prompt([
@@ -624,7 +920,7 @@ async function selectVersionForAgent(client, networkId, versionIdOverride, known
624
920
  const vLabel = `v${selectedVersion.version_number}`;
625
921
  log(_jsx(StatusLine, { kind: "info", label: `Version: ${vLabel}` }));
626
922
  }
627
- return selectedVersion;
923
+ return { version: selectedVersion, agentInfo };
628
924
  }
629
925
  catch (error) {
630
926
  if (error.message?.startsWith("Version not found:"))
@@ -632,11 +928,19 @@ async function selectVersionForAgent(client, networkId, versionIdOverride, known
632
928
  spinner.fail("Failed to fetch versions");
633
929
  handleAuthError(error);
634
930
  log(_jsx(StatusLine, { kind: "muted", label: "Continuing without version selection..." }));
635
- return null;
931
+ return { version: null, agentInfo: undefined };
636
932
  }
637
933
  }
638
934
  // ─── Shared: perform the actual import ───
639
- async function doImport(client, selectedAgent, config, options, span, versionId, versionLabel, versionStatus, agentManagementId) {
935
+ async function doImport(client, selectedAgent, config, options, span, versionId, versionLabel, versionStatus, agentManagementId,
936
+ // The full `AgentInfo` if a previous step already fetched it (e.g.
937
+ // `_importAgent` step 1 or `selectAgentFromList` in the picker
938
+ // flow). Passing it lets `detectAgentBundleMode` skip its own
939
+ // `GET /v1/agents/{id}` round-trip — saving one network call per
940
+ // import and avoiding the duplicate-fetch the user observed in
941
+ // the trace (2026-05-24). Undefined is safe; the dispatcher
942
+ // falls back to its own fetch.
943
+ knownAgentInfo) {
640
944
  const networkCategoryId = getCategoryId(selectedAgent);
641
945
  if (!networkCategoryId) {
642
946
  log(_jsx(StatusLine, { kind: "warning", label: "No category ID found for this agent. Some exports may fail." }));
@@ -672,111 +976,379 @@ async function doImport(client, selectedAgent, config, options, span, versionId,
672
976
  }
673
977
  cleanAgentFiles(outputDir);
674
978
  }
675
- const spinner = startSpinner("Exporting agent data (7 endpoints)...");
676
- try {
677
- const networkId = selectedAgent._id || selectedAgent.id;
678
- let generalSettings = {
679
- general_settings: { name: selectedAgent.name },
680
- };
681
- let parameters = { parameters: [] };
682
- let entities = { entities: [] };
683
- let integrations = { integrations: [] };
684
- let tools = { tools: [] };
685
- let rules = { rules: [] };
686
- let exportData = await client.exportAgent(networkId, networkCategoryId || "", versionId, options.scopeLevel);
687
- const hasAuthErrors = exportData.errors.some((e) => /\b(401|403)\b/.test(e) ||
688
- /unauthorized|forbidden|not.*member/i.test(e));
689
- if (hasAuthErrors && !loadAgentSettingsApiKey()) {
690
- spinner.warn("Authentication failed for some endpoints.");
691
- log(_jsx(StatusLine, { kind: "warning", label: "Your access token may not have permission for the agent-settings endpoints." }));
692
- log(_jsx(Hint, { message: "You can provide an API key as a fallback. It will be saved to ~/.aui/agent-settings-key" }));
693
- const { key } = await inquirer.prompt([
694
- {
695
- type: "password",
696
- name: "key",
697
- message: "Paste the Agent Settings API key (or press Enter to skip):",
698
- mask: "*",
699
- },
700
- ]);
701
- if (key && key.trim()) {
702
- saveAgentSettingsApiKey(key.trim());
703
- client.setAgentSettingsApiKey(key.trim());
704
- log(_jsx(StatusLine, { kind: "success", label: "Key saved. Retrying..." }));
705
- const retrySpinner = startSpinner("Retrying agent configuration fetch...");
706
- exportData = await client.exportAgent(networkId, networkCategoryId || "", versionId, options.scopeLevel);
707
- retrySpinner.stop();
708
- }
709
- }
710
- generalSettings =
711
- normalizeGeneralSettings(exportData.general_settings) || generalSettings;
712
- parameters =
713
- normalizeWrapped("parameters", exportData.parameters) || parameters;
714
- entities = normalizeWrapped("entities", exportData.entities) || entities;
715
- integrations =
716
- normalizeWrapped("integrations", exportData.integrations) || integrations;
717
- tools = normalizeWrapped("tools", exportData.tools) || tools;
718
- rules = normalizeWrapped("rules", exportData.rules) || rules;
719
- if (exportData.errors.length > 0) {
720
- if (span) {
721
- span.setStatus({ code: SpanStatusCode.ERROR, message: exportData.errors.join("; ") });
722
- for (const e of exportData.errors) {
723
- span.recordException(new Error(e));
979
+ const networkId = selectedAgent._id || selectedAgent.id;
980
+ // ─── Filename routing (server bundle → local folder layout) ───
981
+ // The push/pull endpoint disallows path separators in filenames, so
982
+ // tool files live at the root of the bundle (e.g. `web_search.aui.json`).
983
+ // Locally we keep them under `tools/<name>.aui.json` for ergonomics.
984
+ const CANONICAL_ROOT_FILES = new Set([
985
+ "agent.aui.json",
986
+ "parameters.aui.json",
987
+ "entities.aui.json",
988
+ "integrations.aui.json",
989
+ "rules.aui.json",
990
+ "tools.aui.json",
991
+ "manifest.json",
992
+ ]);
993
+ const targetPathForBundleFile = (bundleDir, filename) => CANONICAL_ROOT_FILES.has(filename)
994
+ ? path.join(bundleDir, filename)
995
+ : path.join(bundleDir, "tools", filename);
996
+ // ─── Default in-memory bundle (used by both new + legacy paths) ───
997
+ let generalSettings = {
998
+ general_settings: { name: selectedAgent.name },
999
+ };
1000
+ let parameters = { parameters: [] };
1001
+ let entities = { entities: [] };
1002
+ let integrations = { integrations: [] };
1003
+ let tools = { tools: [] };
1004
+ let rules = { rules: [] };
1005
+ let writtenFiles = [];
1006
+ let paramCount = 0;
1007
+ let entityCount = 0;
1008
+ let integrationCount = 0;
1009
+ let toolCount = 0;
1010
+ let ruleCount = 0;
1011
+ let resolvedVersionTag;
1012
+ // ─── Last-ditch agent_management_id resolve ───
1013
+ // The new `/pull` endpoint is keyed on the agent-management UUID,
1014
+ // NOT network_id. If neither `_importAgent` nor `selectVersionForAgent`
1015
+ // managed to resolve it (e.g. the user took the legacy `networks.list()`
1016
+ // path because agentManagement `listAgents(org_id=…)` returned 0 items),
1017
+ // try ONE more `listAgents(network_id=…)` before giving up. Without
1018
+ // this, the explicit-`--agent-id` branch silently took the legacy
1019
+ // export path on every import.
1020
+ let effectiveAgentMgmtId = agentManagementId;
1021
+ if (!effectiveAgentMgmtId && versionId && networkId) {
1022
+ try {
1023
+ const resp = await client.agentManagement.listAgents(client.getOrganizationId(), 1, 50, { network_id: networkId });
1024
+ const match = resp.items.find((a) => a.scope.network_id === networkId || a.id === networkId);
1025
+ if (match) {
1026
+ effectiveAgentMgmtId = match.id;
1027
+ if (process.env.AUI_DEBUG) {
1028
+ console.log(`[debug] doImport: last-ditch listAgents(network_id=${networkId}) → ${match.id}`);
724
1029
  }
725
- span.setAttribute("import.failed_endpoints", exportData.errors);
726
1030
  }
727
- spinner.warn(`Fetched agent configuration (${exportData.errors.length} endpoint(s) failed)`);
728
- log(_jsx(ImportWarnings, { errors: exportData.errors }));
729
1031
  }
730
- else {
731
- spinner.succeed("Agent configuration fetched (all 6 endpoints OK)");
732
- }
733
- if (!fs.existsSync(outputDir)) {
734
- fs.mkdirSync(outputDir, { recursive: true });
735
- }
736
- const toolsDir = path.join(outputDir, "tools");
737
- if (!fs.existsSync(toolsDir)) {
738
- fs.mkdirSync(toolsDir, { recursive: true });
739
- }
740
- const writtenFiles = [];
741
- writeJson(path.join(outputDir, "agent.aui.json"), generalSettings);
742
- writtenFiles.push("agent.aui.json");
743
- writeJson(path.join(outputDir, "parameters.aui.json"), parameters);
744
- writtenFiles.push("parameters.aui.json");
745
- writeJson(path.join(outputDir, "entities.aui.json"), entities);
746
- writtenFiles.push("entities.aui.json");
747
- writeJson(path.join(outputDir, "integrations.aui.json"), integrations);
748
- writtenFiles.push("integrations.aui.json");
749
- writeJson(path.join(outputDir, "rules.aui.json"), rules);
750
- writtenFiles.push("rules.aui.json");
751
- const toolsData = tools;
752
- if (toolsData.tools &&
753
- Array.isArray(toolsData.tools) &&
754
- toolsData.tools.length > 0) {
755
- for (const tool of toolsData.tools) {
756
- const toolCode = (tool.code ||
757
- tool.name ||
758
- tool.tool_name ||
759
- "unknown");
760
- const fileName = `${toolCode.toLowerCase().replace(/\s+/g, "_")}.aui.json`;
761
- writeJson(path.join(toolsDir, fileName), { tool });
762
- writtenFiles.push(`tools/${fileName}`);
1032
+ catch (err) {
1033
+ if (process.env.AUI_DEBUG) {
1034
+ console.log(`[debug] doImport: last-ditch listAgents failed: ${err instanceof Error ? err.message : err}`);
763
1035
  }
764
1036
  }
765
- else {
766
- writeJson(path.join(outputDir, "tools.aui.json"), tools);
767
- writtenFiles.push("tools.aui.json");
1037
+ }
1038
+ // ─── Use the new pull endpoint (strict, no legacy fallback) ───
1039
+ //
1040
+ // STRICT MODE (2026-05-18 owner directive): the v3 agent-settings
1041
+ // /pull endpoint is the only supported import path. If it fails for
1042
+ // any reason (404, auth, 5xx, network), we surface a clear error
1043
+ // instead of falling back to the legacy per-entity exports. The
1044
+ // legacy `client.exportAgent` flow is preserved below but commented
1045
+ // out — it's the bootstrap path for agents that haven't been
1046
+ // Pushed via the new endpoint yet, and may be re-enabled later if a
1047
+ // migration adapter is needed.
1048
+ //
1049
+ // What this means for the user:
1050
+ // - The agent MUST have at least one Push via the new endpoint
1051
+ // (creates the first blob revision at `v{N}.1`).
1052
+ // - The agent MUST have an `agent_management_id` (resolvable via
1053
+ // `listAgents(network_id=…)`) and a `version_id`.
1054
+ // - 404 from /pull → "no blobs at this version" → user must run
1055
+ // `aui push` first (or re-import via the deprecated bootstrap
1056
+ // path if temporarily re-enabled).
1057
+ // - Any other error bubbles unchanged.
1058
+ if (!versionId) {
1059
+ throw new ConfigError("No version_id available for the new /pull endpoint.", {
1060
+ suggestion: "Pass --version <versionId>, or re-run `aui import` interactively so a version can be selected. Legacy agents without version management cannot be imported under the strict no-fallback policy.",
1061
+ });
1062
+ }
1063
+ if (!effectiveAgentMgmtId) {
1064
+ throw new ConfigError("Could not resolve agent_management_id for this agent.", {
1065
+ suggestion: "Pass the agent_management_id directly as the positional arg, or check that the agent exists in agent-management (`listAgents(network_id=…)`).",
1066
+ });
1067
+ }
1068
+ // ─── Mode dispatch ────────────────────────────────────────────────
1069
+ // Restored 2026-05-24. Detect `Agent.bundle_mode` once via the agent
1070
+ // record we already resolved, then either:
1071
+ //
1072
+ // - bundle mode → call new `/pull` endpoint (existing logic
1073
+ // below; strict 404 → "run aui push first")
1074
+ // - records mode → fall back to per-entity `/view` endpoints via
1075
+ // `client.exportAgent(...)` and reconstruct
1076
+ // canonical .aui.json files locally
1077
+ //
1078
+ // The two surfaces are mutually exclusive at the API layer; calling
1079
+ // the wrong one returns 422 (or empty payload for legacy reads
1080
+ // against blob-mode), which the user can't recover from at the
1081
+ // CLI level. We dispatch BEFORE any /pull or /view call so the
1082
+ // first attempt is always the right surface.
1083
+ // Reuse the AgentInfo from step 1 / selectAgentFromList when we have
1084
+ // it — saves one `GET /v1/agents/{id}` and avoids the duplicate-fetch
1085
+ // observed in the 2026-05-24 trace.
1086
+ const importModeResolution = await detectAgentBundleMode(client, effectiveAgentMgmtId, knownAgentInfo);
1087
+ if (span) {
1088
+ span.setAttribute("import.dispatch.mode", importModeResolution.mode);
1089
+ span.setAttribute("import.dispatch.mode_source", importModeResolution.source);
1090
+ }
1091
+ const useLegacyImport = importModeResolution.mode === "records";
1092
+ const spinner = startSpinner(useLegacyImport
1093
+ ? "Fetching agent data (legacy /view endpoints)..."
1094
+ : options.tag
1095
+ ? `Pulling bundle at tag ${options.tag} via new /pull endpoint...`
1096
+ : `Pulling agent bundle via new /pull endpoint...`);
1097
+ try {
1098
+ if (useLegacyImport) {
1099
+ // ─── Records-mode legacy export ─────────────────────────────
1100
+ // Same shape as the legacy block restored in `pull-agent.tsx`
1101
+ // (kept in sync). Hits the per-entity /view endpoints once,
1102
+ // optionally retries with an API-key prompt on auth failure,
1103
+ // then writes the canonical .aui.json layout to disk.
1104
+ if (span)
1105
+ span.setAttribute("import.path", "legacy_export");
1106
+ let exportData = await client.exportAgent(networkId, networkCategoryId || "", versionId, options.scopeLevel);
1107
+ const hasAuthErrors = exportData.errors.some((e) => /\b(401|403)\b/.test(e) ||
1108
+ /unauthorized|forbidden|not.*member/i.test(e));
1109
+ if (hasAuthErrors && !loadAgentSettingsApiKey()) {
1110
+ spinner.warn("Authentication failed for some endpoints.");
1111
+ log(_jsx(StatusLine, { kind: "warning", label: "Your access token may not have permission for the agent-settings endpoints." }));
1112
+ log(_jsx(Hint, { message: "You can provide an API key as a fallback. It will be saved to ~/.aui/agent-settings-key" }));
1113
+ const { key } = await inquirer.prompt([
1114
+ {
1115
+ type: "password",
1116
+ name: "key",
1117
+ message: "Paste the Agent Settings API key (or press Enter to skip):",
1118
+ mask: "*",
1119
+ },
1120
+ ]);
1121
+ if (key && key.trim()) {
1122
+ saveAgentSettingsApiKey(key.trim());
1123
+ client.setAgentSettingsApiKey(key.trim());
1124
+ log(_jsx(StatusLine, { kind: "success", label: "Key saved. Retrying..." }));
1125
+ const retrySpinner = startSpinner("Retrying agent configuration fetch...");
1126
+ exportData = await client.exportAgent(networkId, networkCategoryId || "", versionId, options.scopeLevel);
1127
+ retrySpinner.stop();
1128
+ }
1129
+ }
1130
+ generalSettings =
1131
+ normalizeGeneralSettings(exportData.general_settings) ||
1132
+ generalSettings;
1133
+ parameters =
1134
+ normalizeWrapped("parameters", exportData.parameters) || parameters;
1135
+ entities =
1136
+ normalizeWrapped("entities", exportData.entities) || entities;
1137
+ integrations =
1138
+ normalizeWrapped("integrations", exportData.integrations) ||
1139
+ integrations;
1140
+ tools = normalizeWrapped("tools", exportData.tools) || tools;
1141
+ rules = normalizeWrapped("rules", exportData.rules) || rules;
1142
+ if (exportData.errors.length > 0) {
1143
+ if (span) {
1144
+ span.setAttribute("import.failed_endpoints", exportData.errors.join(","));
1145
+ }
1146
+ spinner.warn(`Fetched agent configuration (${exportData.errors.length} endpoint(s) failed)`);
1147
+ log(_jsx(ImportWarnings, { errors: exportData.errors }));
1148
+ }
1149
+ else {
1150
+ spinner.succeed("Agent configuration fetched (all 6 endpoints OK)");
1151
+ }
1152
+ if (!fs.existsSync(outputDir)) {
1153
+ fs.mkdirSync(outputDir, { recursive: true });
1154
+ }
1155
+ const toolsDir = path.join(outputDir, "tools");
1156
+ if (!fs.existsSync(toolsDir)) {
1157
+ fs.mkdirSync(toolsDir, { recursive: true });
1158
+ }
1159
+ writeJson(path.join(outputDir, "agent.aui.json"), generalSettings);
1160
+ writtenFiles.push("agent.aui.json");
1161
+ writeJson(path.join(outputDir, "parameters.aui.json"), parameters);
1162
+ writtenFiles.push("parameters.aui.json");
1163
+ writeJson(path.join(outputDir, "entities.aui.json"), entities);
1164
+ writtenFiles.push("entities.aui.json");
1165
+ writeJson(path.join(outputDir, "integrations.aui.json"), integrations);
1166
+ writtenFiles.push("integrations.aui.json");
1167
+ writeJson(path.join(outputDir, "rules.aui.json"), rules);
1168
+ writtenFiles.push("rules.aui.json");
1169
+ // Per-tool files match the disassembler layout: one
1170
+ // `tools/<tool_code>.aui.json` per tool. Empty tools list
1171
+ // collapses to a single `tools.aui.json` so downstream code
1172
+ // always finds a baseline.
1173
+ const toolsData = tools;
1174
+ if (toolsData.tools &&
1175
+ Array.isArray(toolsData.tools) &&
1176
+ toolsData.tools.length > 0) {
1177
+ for (const tool of toolsData.tools) {
1178
+ const toolCode = (tool.code ||
1179
+ tool.name ||
1180
+ tool.tool_name ||
1181
+ "unknown");
1182
+ const fileName = `${toolCode
1183
+ .toLowerCase()
1184
+ .replace(/\s+/g, "_")}.aui.json`;
1185
+ writeJson(path.join(toolsDir, fileName), { tool });
1186
+ writtenFiles.push(`tools/${fileName}`);
1187
+ }
1188
+ }
1189
+ else {
1190
+ writeJson(path.join(outputDir, "tools.aui.json"), tools);
1191
+ writtenFiles.push("tools.aui.json");
1192
+ }
1193
+ paramCount = (parameters?.parameters || []).length;
1194
+ entityCount = (entities?.entities || []).length;
1195
+ integrationCount =
1196
+ (integrations?.integrations || []).length;
1197
+ toolCount = (toolsData.tools || []).length;
1198
+ ruleCount = (rules?.rules || []).length;
1199
+ // Skip the rest of the new-endpoint branch (the blob disassembler
1200
+ // code below). Drop into the shared finalization block by
1201
+ // simply not entering the `else` block. We DO continue past
1202
+ // this point to write `.auirc` etc. — same as the new path.
768
1203
  }
1204
+ else {
1205
+ // ─── Blob-mode (new /pull endpoint) ─────────────────────────
1206
+ let pullData = null;
1207
+ try {
1208
+ pullData = await tryPullViaBlobEndpoint(client, effectiveAgentMgmtId, versionId, options.tag);
1209
+ }
1210
+ catch (pullErr) {
1211
+ if (pullErr instanceof PullModeMismatch) {
1212
+ // Server changed the agent's mode between detection and
1213
+ // this call. Re-run with `useLegacyImport=true`. Recursing
1214
+ // into _doImport would duplicate every preflight — instead
1215
+ // do the legacy export inline. We hit the same code path
1216
+ // as the records-mode branch above, so we just bail out
1217
+ // and ask the user to rerun.
1218
+ if (span) {
1219
+ span.setAttribute("import.dispatch.mode_changed_midflight", true);
1220
+ }
1221
+ spinner.warn("Server reports the agent is now in records-mode.");
1222
+ throw new CLIError("Server reports this agent is now in DB-records mode mid-import.", {
1223
+ suggestion: "Re-run `aui import` — the CLI re-detects the agent's mode on every invocation and will pick the legacy per-entity import path on the next run.",
1224
+ });
1225
+ }
1226
+ throw pullErr;
1227
+ }
1228
+ if (!pullData) {
1229
+ // 404 from /pull — agent has never been Pushed via the new
1230
+ // endpoint, or the requested tag doesn't exist on that version.
1231
+ spinner.fail("No blob revision found at this version.");
1232
+ throw new CLIError(options.tag
1233
+ ? `No blob revision found at tag ${options.tag} for version ${versionId}.`
1234
+ : `No blob revision found for version ${versionId}.`, {
1235
+ suggestion: "Either (a) push to this version first via `aui push` to create the initial blob revision, or (b) pick a different version with `aui agent --versions`.",
1236
+ });
1237
+ }
1238
+ // ─── New bundle path: disassemble + write files ───
1239
+ if (!fs.existsSync(outputDir)) {
1240
+ fs.mkdirSync(outputDir, { recursive: true });
1241
+ }
1242
+ const toolsDir = path.join(outputDir, "tools");
1243
+ if (!fs.existsSync(toolsDir)) {
1244
+ fs.mkdirSync(toolsDir, { recursive: true });
1245
+ }
1246
+ const disassembled = disassembleBundleToFiles(pullData.bundle);
1247
+ for (const entry of disassembled) {
1248
+ const targetPath = path.join(outputDir, entry.relativePath);
1249
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
1250
+ // Normalize to canonical on-disk shape before writing. The
1251
+ // disassembler already produces canonical shapes; this is a
1252
+ // belt+suspenders pass for any edge case the disassembler
1253
+ // might mishandle on legacy bundle data.
1254
+ const basename = path.basename(entry.relativePath);
1255
+ const normalized = normalizeBundleFileBody(basename, entry.body);
1256
+ writeJson(targetPath, normalized);
1257
+ writtenFiles.push(entry.relativePath);
1258
+ if (basename === "agent.aui.json") {
1259
+ generalSettings = normalized;
1260
+ }
1261
+ else if (basename === "parameters.aui.json") {
1262
+ parameters = normalized;
1263
+ paramCount = (normalized?.parameters || []).length;
1264
+ }
1265
+ else if (basename === "entities.aui.json") {
1266
+ entities = normalized;
1267
+ entityCount = (normalized?.entities || []).length;
1268
+ }
1269
+ else if (basename === "integrations.aui.json") {
1270
+ integrations = normalized;
1271
+ integrationCount = (normalized?.integrations || []).length;
1272
+ }
1273
+ else if (basename === "rules.aui.json") {
1274
+ rules = normalized;
1275
+ ruleCount = (normalized?.rules || []).length;
1276
+ }
1277
+ else if (entry.relativePath.startsWith("tools/")) {
1278
+ toolCount++;
1279
+ }
1280
+ else if (basename === "tools.aui.json") {
1281
+ tools = normalized;
1282
+ toolCount += (normalized?.tools || []).length;
1283
+ }
1284
+ }
1285
+ resolvedVersionTag = pullData.version_tag;
1286
+ if (span) {
1287
+ span.setAttribute("import.path", "blob_endpoint");
1288
+ if (resolvedVersionTag) {
1289
+ span.setAttribute("import.version_tag", resolvedVersionTag);
1290
+ }
1291
+ }
1292
+ spinner.succeed(`Pulled bundle via /pull (${disassembled.length} file(s)) at ${resolvedVersionTag || `v${versionLabel}`}`);
1293
+ // ─── Empty-domain warning banner ─────────────────────────────────
1294
+ // The server can legitimately return a bundle with an empty
1295
+ // domain (e.g. tools[]=[]) — it's not a CLI error. But it IS
1296
+ // usually a sign the user pulled the wrong version (a v8 line
1297
+ // that has no tools while v9-LIVE has them, etc.). Surface
1298
+ // every empty domain prominently so the user can decide whether
1299
+ // to re-import a different version instead of being surprised
1300
+ // when their `tools/` folder is empty after import.
1301
+ const emptyDomains = [];
1302
+ if (!pullData.bundle.general_settings)
1303
+ emptyDomains.push("general_settings");
1304
+ if (!pullData.bundle.parameters || pullData.bundle.parameters.length === 0)
1305
+ emptyDomains.push("parameters");
1306
+ if (!pullData.bundle.entities || pullData.bundle.entities.length === 0)
1307
+ emptyDomains.push("entities");
1308
+ if (!pullData.bundle.integrations || pullData.bundle.integrations.length === 0)
1309
+ emptyDomains.push("integrations");
1310
+ if (!pullData.bundle.rules || pullData.bundle.rules.length === 0)
1311
+ emptyDomains.push("rules");
1312
+ // Tools live under `agent_tools` on the live `/pull` response and
1313
+ // under `tools` per the OpenAPI schema. Either counts as "present".
1314
+ const bundleTools = readBundleTools(pullData.bundle);
1315
+ if (!bundleTools || bundleTools.length === 0)
1316
+ emptyDomains.push("tools");
1317
+ if (emptyDomains.length > 0) {
1318
+ log(_jsxs(Box, { flexDirection: "column", paddingX: 1, marginY: 1, children: [_jsx(StatusLine, { kind: "warning", label: `Bundle at ${resolvedVersionTag || `v${versionLabel}`} has 0 ${emptyDomains.join(", 0 ")}.` }), _jsx(Hint, { message: "If you expected content in these domains, you may be on the wrong version. " +
1319
+ "Check the LIVE version in the Playground or run `aui agent --versions` to see all versions on this agent, then re-import with `--version <id>`." })] }));
1320
+ if (span) {
1321
+ span.setAttribute("import.empty_domains", emptyDomains.join(","));
1322
+ }
1323
+ }
1324
+ } // end of bundle-mode `else` branch (matches `if (useLegacyImport)` above)
1325
+ // Persist agent + version + revision context to `.auirc`.
1326
+ // `version_label` = `v{version_number}` (e.g. "v8") — the version
1327
+ // document's display label, used by telemetry / version UX.
1328
+ // `version_tag` = full revision tag from the manifest (e.g.
1329
+ // "v8.14") — used as the default `?version_tag=` on the next
1330
+ // `aui pull` so users re-pull the exact revision they imported
1331
+ // without having to remember it.
1332
+ //
1333
+ // Prefer `effectiveAgentMgmtId` (resolved via the dual-shape
1334
+ // helper above) over the original `agentManagementId` arg so the
1335
+ // saved id reflects whatever actually worked, even when the
1336
+ // upstream `selectAgentFromList` had to fall back to legacy
1337
+ // `networks.list()`.
769
1338
  saveProjectConfig({
770
1339
  agent_code: selectedAgent.niceName ||
771
1340
  selectedAgent.name.toLowerCase().replace(/\s+/g, "-"),
772
1341
  agent_id: networkId,
773
- ...(agentManagementId ? { agent_management_id: agentManagementId } : {}),
1342
+ ...(effectiveAgentMgmtId
1343
+ ? { agent_management_id: effectiveAgentMgmtId }
1344
+ : {}),
774
1345
  environment: config.environment,
775
1346
  account_id: client.getAccountId() || config.accountId,
776
1347
  organization_id: client.getOrganizationId() || config.organizationId,
777
1348
  network_category_id: networkCategoryId,
778
1349
  ...(versionId ? { version_id: versionId } : {}),
779
1350
  ...(versionLabel ? { version_label: versionLabel } : {}),
1351
+ ...(resolvedVersionTag ? { version_tag: resolvedVersionTag } : {}),
780
1352
  ...(options.scopeLevel ? { scope_level: options.scopeLevel } : {}),
781
1353
  }, outputDir);
782
1354
  writtenFiles.push(".auirc");
@@ -793,36 +1365,63 @@ async function doImport(client, selectedAgent, config, options, span, versionId,
793
1365
  catch {
794
1366
  // optional
795
1367
  }
796
- const paramCount = parameters?.parameters?.length || 0;
797
- const entityCount = entities?.entities?.length || 0;
798
- const integrationCount = integrations?.integrations?.length || 0;
799
- const toolCount = (toolsData.tools || []).length;
800
- const ruleCount = rules?.rules?.length || 0;
1368
+ // ─── Skip KB export for templates ─────────────────────────────
1369
+ //
1370
+ // Templates are NETWORK_CATEGORY-scoped (no `scope.network_id`,
1371
+ // no `scope.account_id`). KB exports are inherently network-
1372
+ // scoped the `knowledge-base-manager/v1/knowledge-bases/view/
1373
+ // export` endpoint expects valid `network_id` + `account_id`
1374
+ // query params. Calling it for a template results in:
1375
+ //
1376
+ // GET .../export?network_id=<template_agent_mgmt_id>
1377
+ // &account_id= ← empty!
1378
+ // → 422 {"detail":[{"loc":["query","account_id"],
1379
+ // "msg":"Id must be of type PydanticObjectId",
1380
+ // "input":""}]}
1381
+ //
1382
+ // (The bogus `network_id` substitution happens because
1383
+ // `agentInfoToNetwork` falls back to `agent.id` when
1384
+ // `scope.network_id` is null.) The right answer is to not
1385
+ // make the call at all — templates don't have knowledge bases
1386
+ // in this data model.
1387
+ //
1388
+ // Detect templates by either `kind === "template"` (canonical,
1389
+ // present on AgentInfo from the new agent-settings response)
1390
+ // OR `scope.type === "NETWORK_CATEGORY"` (back-compat for any
1391
+ // future agent kinds that also live at category scope and
1392
+ // share the same constraint).
1393
+ const isTemplateForKb = knownAgentInfo?.kind === "template" ||
1394
+ knownAgentInfo?.scope?.type === "NETWORK_CATEGORY";
801
1395
  const kbExportPromise = options.skipKbFiles
802
1396
  ? Promise.resolve({ kind: "skipped" })
803
- : (async () => {
804
- try {
805
- const kbClient = new KBViewClient({
806
- authToken: config.authToken,
807
- apiKey: loadAgentSettingsApiKey() || options.apiKey,
808
- organizationId: config.organizationId || "",
809
- environment: config.environment || "staging",
810
- });
811
- const scope = buildScope({
812
- networkId,
813
- organizationId: client.getOrganizationId() || config.organizationId || "",
814
- accountId: client.getAccountId() || config.accountId || "",
815
- });
816
- const result = await exportToFolder(kbClient, scope, outputDir, options.withKbFiles ?? false);
817
- return { kind: "ok", result };
818
- }
819
- catch (kbError) {
820
- return {
821
- kind: "error",
822
- message: kbError instanceof Error ? kbError.message : String(kbError),
823
- };
824
- }
825
- })();
1397
+ : isTemplateForKb
1398
+ ? Promise.resolve({
1399
+ kind: "skipped",
1400
+ reason: "templates are NETWORK_CATEGORY-scoped and don't have knowledge bases",
1401
+ })
1402
+ : (async () => {
1403
+ try {
1404
+ const kbClient = new KBViewClient({
1405
+ authToken: config.authToken,
1406
+ apiKey: loadAgentSettingsApiKey() || options.apiKey,
1407
+ organizationId: config.organizationId || "",
1408
+ environment: config.environment || "staging",
1409
+ });
1410
+ const scope = buildScope({
1411
+ networkId,
1412
+ organizationId: client.getOrganizationId() || config.organizationId || "",
1413
+ accountId: client.getAccountId() || config.accountId || "",
1414
+ });
1415
+ const result = await exportToFolder(kbClient, scope, outputDir, options.withKbFiles ?? false);
1416
+ return { kind: "ok", result };
1417
+ }
1418
+ catch (kbError) {
1419
+ return {
1420
+ kind: "error",
1421
+ message: kbError instanceof Error ? kbError.message : String(kbError),
1422
+ };
1423
+ }
1424
+ })();
826
1425
  const schemaPromise = (async () => {
827
1426
  try {
828
1427
  const data = await fetchSchemas({
@@ -851,7 +1450,9 @@ async function doImport(client, selectedAgent, config, options, span, versionId,
851
1450
  parallelSpinner.stop();
852
1451
  // Knowledge-hub status
853
1452
  if (kbOutcome.kind === "skipped") {
854
- log(_jsx(StatusLine, { kind: "muted", label: "Skipping knowledge hub export (--skip-kb-files)" }));
1453
+ log(_jsx(StatusLine, { kind: "muted", label: kbOutcome.reason
1454
+ ? `Skipping knowledge hub export — ${kbOutcome.reason}.`
1455
+ : "Skipping knowledge hub export (--skip-kb-files)" }));
855
1456
  }
856
1457
  else if (kbOutcome.kind === "ok") {
857
1458
  const r = kbOutcome.result;
@@ -902,6 +1503,11 @@ async function doImport(client, selectedAgent, config, options, span, versionId,
902
1503
  gitOk,
903
1504
  writtenFiles,
904
1505
  dirName,
1506
+ // Forward `kind` + `scope.type` from the cached AgentInfo so the
1507
+ // view can show the template banner. Both default-safe (the view
1508
+ // falls back to "regular" / no-scope-row when these are omitted).
1509
+ kind: knownAgentInfo?.kind,
1510
+ scopeType: knownAgentInfo?.scope?.type,
905
1511
  };
906
1512
  log(_jsx(ImportAgentView, { data: summaryData }));
907
1513
  }
@@ -911,6 +1517,51 @@ async function doImport(client, selectedAgent, config, options, span, versionId,
911
1517
  throw error;
912
1518
  }
913
1519
  }
1520
+ // ─── New pull endpoint (with 404 fallback to legacy export) ───
1521
+ //
1522
+ // The new bundle endpoint replaces the per-entity multi-endpoint export
1523
+ // for any agent that has been Pushed via the new endpoint. We try it
1524
+ // first and silently fall through to `exportAgent` on 404 (agent has
1525
+ // no blobs yet — typical for pre-migration agents). Other errors are
1526
+ // propagated so we don't mask auth / network issues.
1527
+ //
1528
+ // `versionTag` is optional; when omitted the server picks the version
1529
+ // row's current revision (latest successful Push on that version).
1530
+ /**
1531
+ * Sentinel thrown by `tryPullViaBlobEndpoint` when the server reports
1532
+ * the target agent is in records-mode. Caller catches this and
1533
+ * dispatches to the legacy `client.exportAgent(...)` path instead.
1534
+ * Distinct from a plain 404 (no blob revision yet) — that's a
1535
+ * non-error, while this requires switching surfaces.
1536
+ */
1537
+ class PullModeMismatch extends Error {
1538
+ constructor() {
1539
+ super("Server reports this agent is in records-mode (422)");
1540
+ this.name = "PullModeMismatch";
1541
+ }
1542
+ }
1543
+ async function tryPullViaBlobEndpoint(client, agentManagementId, versionId, versionTag) {
1544
+ try {
1545
+ return await client.agentManagement.pullVersionBlobs(agentManagementId, versionId, { versionTag });
1546
+ }
1547
+ catch (err) {
1548
+ const status = err instanceof AUIAPIError ? err.status : undefined;
1549
+ if (status === 404) {
1550
+ // No blobs at this version (or no blob at the requested tag) yet —
1551
+ // caller surfaces a "run aui push first" error.
1552
+ return null;
1553
+ }
1554
+ if (isModeMismatchError(err)) {
1555
+ // Server flipped `bundle_mode` since our up-front detect.
1556
+ // Caller swaps to the legacy export branch.
1557
+ throw new PullModeMismatch();
1558
+ }
1559
+ // Anything else (auth, 5xx, network) is a real failure: bubble up so
1560
+ // the import surfaces a clear error rather than silently degrading
1561
+ // and ending up with stale data.
1562
+ throw err;
1563
+ }
1564
+ }
914
1565
  // ─── Response normalizers ───
915
1566
  function normalizeGeneralSettings(raw) {
916
1567
  if (!raw)