aui-agent-builder 0.3.103 → 0.3.105

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 +886 -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 +2575 -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 +598 -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 +1037 -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,28 +111,202 @@ 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
+ // Resolution order (first success wins, then we cache the resolved
132
+ // `AgentInfo` as `knownAgentInfo` so `selectVersionForAgent` /
133
+ // `doImport` skip a duplicate lookup AND the new `/pull` endpoint
134
+ // is reachable on the first import attempt):
135
+ //
136
+ // 0) [TEMP, 2026-05-26] listAgents(network_id=<input>) — the
137
+ // Agent Builder BFF is currently passing us a `network_id`
138
+ // instead of the canonical `agent_management_id`. The old
139
+ // flow (step 1 below) would `getAgent(network_id)` first,
140
+ // get a 404/422, and only THEN fall through — wasting a
141
+ // round-trip on every import and surfacing a confusing
142
+ // "wrong endpoint" error in BFF logs. Calling listAgents
143
+ // first short-circuits the failure entirely when the input
144
+ // really is a network_id.
145
+ //
146
+ // Remove once the BFF / Playground is updated to send
147
+ // `agent_management_id` directly.
148
+ //
149
+ // 1) getAgent(<input>) — treat the id as an agent_management_id.
150
+ // Still the canonical path for callers that already have it.
151
+ //
152
+ // 2) networks.get(<input>) — last-ditch legacy networks lookup.
153
+ // Kept for back-compat with old scripts that have neither
154
+ // flow available (e.g. agent-management endpoint down).
116
155
  const spinner = startSpinner("Fetching agent...");
156
+ let resolved = false;
157
+ let lastErr;
158
+ // 0) [TEMP] listAgents(network_id=<input>) — handles the BFF
159
+ // sending network_id. If the input is actually an
160
+ // agent_management_id, listAgents returns no items (an
161
+ // agent_management_id is almost never also a network_id of a
162
+ // DIFFERENT agent), and we fall through to step 1.
117
163
  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
- });
164
+ const resp = await client.agentManagement.listAgents(client.getOrganizationId(), 1, 50, { network_id: effectiveAgentId });
165
+ // `find` (not `[0]`) to be defensive about the server returning
166
+ // unrelated rows. We require `scope.network_id === <input>` so
167
+ // siblings with a different network_id but same listing page
168
+ // can't accidentally win.
169
+ const match = resp.items.find((a) => a.scope.network_id === effectiveAgentId);
170
+ if (match) {
171
+ knownAgentInfo = match;
172
+ selectedAgent = agentInfoToNetwork(match);
173
+ // Bias scope toward the agent's own org/account (mirrors the
174
+ // step-1 success branch — keep both in sync).
175
+ const scopeUpdate = {};
176
+ if (match.scope?.account_id)
177
+ scopeUpdate.accountId = match.scope.account_id;
178
+ if (match.scope?.organization_id)
179
+ scopeUpdate.organizationId = match.scope.organization_id;
180
+ if (scopeUpdate.accountId || scopeUpdate.organizationId) {
181
+ client.setScope(scopeUpdate);
182
+ }
183
+ spinner.succeed(`Found agent (via listAgents network_id lookup): ${match.name}`);
184
+ resolved = true;
185
+ }
186
+ else if (process.env.AUI_DEBUG) {
187
+ console.log(`[debug] listAgents(network_id=${effectiveAgentId}) returned ${resp.items.length} row(s) but none matched; trying getAgent next`);
125
188
  }
126
- spinner.succeed(`Found agent: ${selectedAgent.name}`);
127
189
  }
128
- catch (error) {
129
- spinner.fail(isAuthError(error) ? "Authentication failed" : "Agent not found");
130
- handleAuthError(error);
131
- throw error;
190
+ catch (err) {
191
+ // Listing failed (auth / 5xx / org context wrong). Don't bail
192
+ // — fall through to the existing dual-shape resolution. Worst
193
+ // case we make the same call sequence we'd have made before
194
+ // this workaround was added.
195
+ if (process.env.AUI_DEBUG) {
196
+ console.log(`[debug] listAgents(network_id=${effectiveAgentId}) failed: ${err instanceof Error ? err.message : err}; trying getAgent next`);
197
+ }
132
198
  }
199
+ // 1) Treat the id as an agent-management UUID.
200
+ if (!resolved) {
201
+ try {
202
+ const ami = await client.agentManagement.getAgent(effectiveAgentId);
203
+ knownAgentInfo = ami;
204
+ selectedAgent = agentInfoToNetwork(ami);
205
+ // Bias the client scope toward the agent's own org/account so
206
+ // downstream listAgents/listVersions calls don't 403 against the
207
+ // session's default scope.
208
+ const scopeUpdate = {};
209
+ if (ami.scope?.account_id)
210
+ scopeUpdate.accountId = ami.scope.account_id;
211
+ if (ami.scope?.organization_id)
212
+ scopeUpdate.organizationId = ami.scope.organization_id;
213
+ if (scopeUpdate.accountId || scopeUpdate.organizationId) {
214
+ client.setScope(scopeUpdate);
215
+ }
216
+ spinner.succeed(`Found agent (via agent-management): ${ami.name}`);
217
+ resolved = true;
218
+ }
219
+ catch (err) {
220
+ lastErr = err;
221
+ const status = err instanceof AUIAPIError
222
+ ? err.status
223
+ : (err.statusCode ??
224
+ err.status);
225
+ // Fall through to the legacy networks lookup on ANY non-auth
226
+ // error — not just 404. Rationale (2026-05-24):
227
+ //
228
+ // - 404 ⇒ id isn't an agent_management_id; try as network_id.
229
+ // - 422 ⇒ the new agent-settings response schema has stricter
230
+ // validation than some legacy docs (e.g. the post-`kind`
231
+ // rollout returned 422 for any pre-rollout agent until
232
+ // a backfill landed). The legacy networks endpoint
233
+ // lives in a different service and doesn't share that
234
+ // validation, so it can still resolve the same agent.
235
+ // - 5xx / network blip ⇒ a transient agent-settings failure
236
+ // shouldn't block the import when there's an alternate
237
+ // resolver available; the legacy attempt will either
238
+ // succeed (problem masked) or 4xx itself (we report
239
+ // the more informative agent-settings error via
240
+ // `lastErr` at the end of step 2).
241
+ //
242
+ // Auth errors (401/403) STILL fail loudly here — no point in
243
+ // trying the next endpoint with the same credentials.
244
+ if (isAuthError(err)) {
245
+ spinner.fail("Authentication failed");
246
+ handleAuthError(err);
247
+ throw err;
248
+ }
249
+ if (process.env.AUI_DEBUG) {
250
+ console.log(`[debug] agentManagement.getAgent(${effectiveAgentId}) → ${status ?? "non-auth error"}; trying legacy networks.get next`);
251
+ }
252
+ }
253
+ } // close: if (!resolved) — wrapping step 1 (added with step 0 above)
254
+ // 2) Fall back: treat the id as a legacy network_id.
255
+ if (!resolved) {
256
+ try {
257
+ const response = await client.networks.get(effectiveAgentId);
258
+ selectedAgent = response.data;
259
+ if (selectedAgent.account || selectedAgent.organization) {
260
+ client.setScope({
261
+ ...(selectedAgent.account ? { accountId: selectedAgent.account } : {}),
262
+ ...(selectedAgent.organization
263
+ ? { organizationId: selectedAgent.organization }
264
+ : {}),
265
+ });
266
+ }
267
+ spinner.succeed(`Found agent (via legacy networks): ${selectedAgent.name}`);
268
+ resolved = true;
269
+ }
270
+ catch (error) {
271
+ // Neither id shape resolved. Surface the FIRST failure
272
+ // (agent-management) since that's the new-world canonical
273
+ // path and is the more informative error in the migration
274
+ // window. Fall back to the second error if the first was
275
+ // suppressed for some reason.
276
+ spinner.fail(isAuthError(error) || isAuthError(lastErr)
277
+ ? "Authentication failed"
278
+ : "Agent not found (tried both agent-management and legacy networks)");
279
+ const errToReport = lastErr ?? error;
280
+ handleAuthError(errToReport);
281
+ throw errToReport;
282
+ }
283
+ }
284
+ }
285
+ else if (options.templates) {
286
+ // ─── Templates picker (NETWORK_CATEGORY-scoped, kind=template) ──
287
+ //
288
+ // Templates don't live under an account — they're category-scoped
289
+ // and visible to every account in the org. The default
290
+ // `listAgents` call only returns NETWORK-scoped regulars, so we
291
+ // need the new `scope_type=NETWORK_CATEGORY` + `kind=template`
292
+ // filters (added to the API client 2026-05-24). The walk becomes:
293
+ //
294
+ // org → category → template → version
295
+ //
296
+ // (vs. the regular org → account → agent → version)
297
+ //
298
+ // We still pass through `selectVersionForAgent` afterwards —
299
+ // template version management uses the same /v1/agents/{id}/versions
300
+ // endpoints regardless of `kind`.
301
+ const orgResult = await selectOrgForImport(client, session);
302
+ if (!orgResult)
303
+ return;
304
+ log(_jsx(ImportSessionInfo, { orgName: orgResult.orgName, accountName: "(templates are category-scoped)", environment: session.environment }));
305
+ const result = await selectTemplateFromList(client, options.category);
306
+ if (!result)
307
+ return;
308
+ selectedAgent = result.network;
309
+ knownAgentInfo = result.agentInfo;
133
310
  }
134
311
  else {
135
312
  // Step 1: Choose organization
@@ -153,15 +330,29 @@ async function _importAgent(parentSpan, agentId, options = {}) {
153
330
  const networkId = selectedAgent._id || selectedAgent.id;
154
331
  parentSpan.setAttribute("import.agent_id", networkId);
155
332
  parentSpan.setAttribute("import.agent_name", selectedAgent.name);
156
- const selectedVersion = await selectVersionForAgent(client, networkId, options.version, knownAgentInfo);
333
+ const { version: selectedVersion, agentInfo: resolvedAgentInfo } = await selectVersionForAgent(client, networkId, options.version, knownAgentInfo);
334
+ // `selectVersionForAgent` may resolve the agent-management record
335
+ // internally (via `listAgents(network_id=…)`) even when the upstream
336
+ // `selectAgentFromList` had to fall back to legacy networks. Prefer
337
+ // the freshly-resolved id so `doImport` can reach the new `/pull`
338
+ // endpoint instead of silently degrading to the legacy export path.
339
+ const effectiveAgentMgmtId = knownAgentInfo?.id ?? resolvedAgentInfo?.id;
157
340
  if (selectedVersion) {
158
341
  parentSpan.setAttribute("import.version_id", selectedVersion.id);
159
342
  parentSpan.setAttribute("import.version", `v${selectedVersion.version_number}`);
160
343
  }
344
+ if (effectiveAgentMgmtId) {
345
+ parentSpan.setAttribute("import.agent_management_id", effectiveAgentMgmtId);
346
+ }
161
347
  const vLabel = selectedVersion
162
348
  ? `v${selectedVersion.version_number}`
163
349
  : undefined;
164
- await doImport(client, selectedAgent, config, options, parentSpan, selectedVersion?.id, vLabel, selectedVersion?.status, knownAgentInfo?.id);
350
+ // Prefer the AgentInfo fetched in step 1 (`knownAgentInfo`); fall
351
+ // back to the version-resolver's lookup (`resolvedAgentInfo`).
352
+ // Either way, downstream `detectAgentBundleMode` reuses it instead
353
+ // of re-fetching.
354
+ const effectiveAgentInfo = knownAgentInfo ?? resolvedAgentInfo;
355
+ await doImport(client, selectedAgent, config, options, parentSpan, selectedVersion?.id, vLabel, selectedVersion?.status, effectiveAgentMgmtId, effectiveAgentInfo);
165
356
  }
166
357
  // ─── Import from another account ───
167
358
  export async function importFromOtherAccount(options = {}) {
@@ -230,11 +421,13 @@ async function _importFromOtherAccount(parentSpan, options = {}) {
230
421
  if (!result)
231
422
  return;
232
423
  const networkId = result.network._id || result.network.id;
233
- const selectedVersion = await selectVersionForAgent(client, networkId, undefined, result.agentInfo);
424
+ const { version: selectedVersion, agentInfo: resolvedAgentInfo } = await selectVersionForAgent(client, networkId, undefined, result.agentInfo);
425
+ const effectiveAgentMgmtId = result.agentInfo?.id ?? resolvedAgentInfo?.id;
426
+ const effectiveAgentInfo = result.agentInfo ?? resolvedAgentInfo;
234
427
  const vLabel = selectedVersion
235
428
  ? `v${selectedVersion.version_number}`
236
429
  : undefined;
237
- await doImport(client, result.network, config, options, parentSpan, selectedVersion?.id, vLabel, selectedVersion?.status, result.agentInfo?.id);
430
+ await doImport(client, result.network, config, options, parentSpan, selectedVersion?.id, vLabel, selectedVersion?.status, effectiveAgentMgmtId, effectiveAgentInfo);
238
431
  }
239
432
  catch (error) {
240
433
  spinner.fail("Failed to fetch accounts");
@@ -330,11 +523,13 @@ async function _importFromOtherOrg(parentSpan, options = {}) {
330
523
  if (!result)
331
524
  return;
332
525
  const networkId = result.network._id || result.network.id;
333
- const selectedVersion = await selectVersionForAgent(client, networkId, undefined, result.agentInfo);
526
+ const { version: selectedVersion, agentInfo: resolvedAgentInfo } = await selectVersionForAgent(client, networkId, undefined, result.agentInfo);
527
+ const effectiveAgentMgmtId = result.agentInfo?.id ?? resolvedAgentInfo?.id;
528
+ const effectiveAgentInfo = result.agentInfo ?? resolvedAgentInfo;
334
529
  const vLabel = selectedVersion
335
530
  ? `v${selectedVersion.version_number}`
336
531
  : undefined;
337
- await doImport(client, result.network, config, options, parentSpan, selectedVersion?.id, vLabel, selectedVersion?.status, result.agentInfo?.id);
532
+ await doImport(client, result.network, config, options, parentSpan, selectedVersion?.id, vLabel, selectedVersion?.status, effectiveAgentMgmtId, effectiveAgentInfo);
338
533
  }
339
534
  catch (error) {
340
535
  orgSpinner.fail("Failed to fetch organizations");
@@ -444,6 +639,97 @@ function agentInfoToNetwork(a) {
444
639
  updatedAt: a.updated_at,
445
640
  };
446
641
  }
642
+ /**
643
+ * Picker for template (kind=template, NETWORK_CATEGORY-scoped) agents.
644
+ *
645
+ * Mirrors `selectAgentFromList` but calls `listAgents` with the new
646
+ * `scope_type=NETWORK_CATEGORY` + `kind=template` filters so only
647
+ * templates show up. No legacy `networks.list()` fallback — there's no
648
+ * equivalent legacy concept for templates (they only exist in the
649
+ * agent-management world).
650
+ *
651
+ * If `categoryFilter` is provided, it's resolved to a category id (via
652
+ * the same id/key/name lookup `agents --create --category` uses) and
653
+ * forwarded as `network_category_id` so the listing only returns
654
+ * templates in that category. Invalid filter ⇒ helpful error + bail.
655
+ */
656
+ async function selectTemplateFromList(client, categoryFilter) {
657
+ let resolvedCategoryId;
658
+ if (categoryFilter) {
659
+ const catSpinner = startSpinner(`Resolving category "${categoryFilter}"...`);
660
+ try {
661
+ const catResp = await client.categories.list();
662
+ const catUpper = categoryFilter.toUpperCase();
663
+ const match = catResp.data.find((c) => c._id === categoryFilter ||
664
+ c.key?.toUpperCase() === catUpper ||
665
+ c.name?.toUpperCase() === catUpper);
666
+ if (match) {
667
+ resolvedCategoryId = match._id;
668
+ catSpinner.succeed(`Category: ${match.name} (${match.key})`);
669
+ }
670
+ else {
671
+ catSpinner.fail(`Category "${categoryFilter}" not found.`);
672
+ const available = catResp.data
673
+ .map((c) => `${c.name} (${c.key})`)
674
+ .slice(0, 10)
675
+ .join(", ");
676
+ log(_jsx(StatusLine, { kind: "muted", label: `Available: ${available}${catResp.data.length > 10 ? `, … (${catResp.data.length} total)` : ""}` }));
677
+ return null;
678
+ }
679
+ }
680
+ catch (err) {
681
+ catSpinner.warn(`Could not resolve category — listing all templates instead. (${err instanceof Error ? err.message : err})`);
682
+ }
683
+ }
684
+ const spinner = startSpinner("Fetching templates...");
685
+ try {
686
+ const allTemplates = [];
687
+ let page = 1;
688
+ let hasMore = true;
689
+ while (hasMore) {
690
+ const response = await client.agentManagement.listAgents(client.getOrganizationId(), page, 50, {
691
+ scope_type: "NETWORK_CATEGORY",
692
+ kind: "template",
693
+ ...(resolvedCategoryId ? { network_category_id: resolvedCategoryId } : {}),
694
+ });
695
+ allTemplates.push(...response.items);
696
+ hasMore = page < response.pages;
697
+ page++;
698
+ }
699
+ if (allTemplates.length === 0) {
700
+ spinner.succeed("No templates found");
701
+ log(_jsx(StatusLine, { kind: "warning", label: resolvedCategoryId
702
+ ? `No templates in this category. Use \`aui agents --create --template --category <id> --name <name>\` to create one.`
703
+ : "No templates found in this organization. Use `aui agents --create --template --category <id> --name <name>` to create one." }));
704
+ return null;
705
+ }
706
+ spinner.succeed(`Found ${allTemplates.length} template(s)`);
707
+ const { chosen } = await inquirer.prompt([
708
+ {
709
+ type: "list",
710
+ name: "chosen",
711
+ message: "Select template to import:",
712
+ choices: allTemplates.map((a) => {
713
+ // Surface the category id when it's the differentiator —
714
+ // templates with the same name across categories is a real
715
+ // case worth disambiguating in the picker.
716
+ const cat = a.scope?.network_category_id
717
+ ? ` (${a.scope.network_category_id.slice(0, 8)}…)`
718
+ : "";
719
+ return { name: `${a.name}${cat}`, value: a };
720
+ }),
721
+ pageSize: 15,
722
+ },
723
+ ]);
724
+ const selected = chosen;
725
+ return { network: agentInfoToNetwork(selected), agentInfo: selected };
726
+ }
727
+ catch (error) {
728
+ spinner.fail("Failed to fetch templates");
729
+ handleAuthError(error);
730
+ throw error;
731
+ }
732
+ }
447
733
  async function selectAgentFromList(client) {
448
734
  const spinner = startSpinner("Fetching agents...");
449
735
  try {
@@ -519,6 +805,21 @@ async function selectAgentFromList(client) {
519
805
  }
520
806
  }
521
807
  // ─── Shared: select version for an agent ───
808
+ /**
809
+ * Resolve a version (and the underlying agent-management record) for an
810
+ * agent identified by `networkId`. Returns BOTH so the caller can
811
+ * forward the agent-management UUID into `doImport` — without it the
812
+ * new `/pull` blob endpoint can never be reached (it's keyed on
813
+ * `agent_id` = agent-management UUID, not network_id).
814
+ *
815
+ * Previously this returned just `AgentVersion | null`. When the
816
+ * upstream `selectAgentFromList` fell back to the legacy
817
+ * `networks.list()` (because agentManagement `listAgents` returned 0),
818
+ * the agent-management UUID was resolved here locally but lost on the
819
+ * way back, so `doImport` saw `agentManagementId === undefined` and
820
+ * silently took the legacy export path. Returning `agentInfo` plugs
821
+ * that hole.
822
+ */
522
823
  async function selectVersionForAgent(client, networkId, versionIdOverride, knownAgentInfo) {
523
824
  const spinner = startSpinner("Fetching agent versions...");
524
825
  try {
@@ -558,7 +859,7 @@ async function selectVersionForAgent(client, networkId, versionIdOverride, known
558
859
  }
559
860
  if (!agentInfo) {
560
861
  spinner.succeed("No version management found — importing latest");
561
- return null;
862
+ return { version: null, agentInfo: undefined };
562
863
  }
563
864
  if (process.env.AUI_DEBUG) {
564
865
  console.log(`[debug] resolved agent: id=${agentInfo.id} name=${agentInfo.name} network=${agentInfo.scope.network_id} active_version=${agentInfo.active_version_id}`);
@@ -574,19 +875,74 @@ async function selectVersionForAgent(client, networkId, versionIdOverride, known
574
875
  }
575
876
  if (allVersions.length === 0) {
576
877
  spinner.succeed("No versions found — importing latest");
577
- return null;
878
+ return { version: null, agentInfo };
578
879
  }
579
880
  spinner.succeed(`Found ${allVersions.length} version(s)`);
580
881
  if (versionIdOverride) {
581
882
  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}`);
883
+ if (match) {
884
+ const vLabel = `v${match.version_number}`;
885
+ log(_jsx(StatusLine, { kind: "info", label: `Version: ${vLabel} [${match.status}]` }));
886
+ return { version: match, agentInfo };
586
887
  }
587
- const vLabel = `v${match.version_number}`;
588
- log(_jsx(StatusLine, { kind: "info", label: `Version: ${vLabel} [${match.status}]` }));
589
- return match;
888
+ // ─── Multi-agent fallback for shared network_ids ──────────────
889
+ // It's legitimate for one `network_id` to map to MULTIPLE
890
+ // agent-management records (e.g. duplicates from data migration,
891
+ // or one record per account scope under the same network). We
892
+ // picked the first match from `listAgents(network_id=…)` above,
893
+ // but the user's `--version <id>` may belong to a SIBLING agent
894
+ // record. Before throwing "version not found", paginate every
895
+ // agent for this network_id and try `getVersion(candidate.id,
896
+ // versionIdOverride)` on each — return the first one that
897
+ // succeeds (and switch `agentInfo` to that candidate so
898
+ // downstream `/pull` uses the right `agent_id`).
899
+ const probeSpinner = startSpinner(`Version not on agent ${agentInfo.id.slice(0, 10)}…; searching sibling agents for the same network...`);
900
+ try {
901
+ const allCandidates = [];
902
+ let probePage = 1;
903
+ let probeHasMore = true;
904
+ while (probeHasMore) {
905
+ const resp = await client.agentManagement.listAgents(client.getOrganizationId(), probePage, 50, { network_id: networkId });
906
+ allCandidates.push(...resp.items);
907
+ probeHasMore = probePage < resp.pages;
908
+ probePage++;
909
+ }
910
+ const siblings = allCandidates.filter((c) => c.id !== agentInfo.id);
911
+ if (process.env.AUI_DEBUG) {
912
+ console.log(`[debug] multi-agent fallback: found ${siblings.length} sibling agent(s) for network_id=${networkId}; probing each for version ${versionIdOverride}`);
913
+ }
914
+ for (const sibling of siblings) {
915
+ try {
916
+ const ver = await client.agentManagement.getVersion(sibling.id, versionIdOverride);
917
+ probeSpinner.succeed(`Found version on sibling agent ${sibling.id.slice(0, 10)}… (${sibling.name})`);
918
+ const vLabel = `v${ver.version_number}`;
919
+ log(_jsx(StatusLine, { kind: "info", label: `Version: ${vLabel} [${ver.status}] (agent: ${sibling.name})` }));
920
+ return { version: ver, agentInfo: sibling };
921
+ }
922
+ catch (err) {
923
+ const status = err.statusCode ??
924
+ err.status;
925
+ if (status !== 404) {
926
+ // Non-404 (auth / 5xx) — bubble immediately so we
927
+ // don't silently skip a real failure.
928
+ probeSpinner.fail("Sibling-agent probe failed");
929
+ throw err;
930
+ }
931
+ if (process.env.AUI_DEBUG) {
932
+ console.log(`[debug] multi-agent fallback: getVersion(${sibling.id}, ${versionIdOverride}) → 404; trying next sibling`);
933
+ }
934
+ }
935
+ }
936
+ probeSpinner.fail(`Version ${versionIdOverride} not found on any of ${allCandidates.length} agent record(s) for network ${networkId}`);
937
+ }
938
+ catch (probeErr) {
939
+ if (probeSpinner.stop)
940
+ probeSpinner.stop();
941
+ throw probeErr;
942
+ }
943
+ log(_jsx(StatusLine, { kind: "error", label: `Version not found: ${versionIdOverride}` }));
944
+ 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." }));
945
+ throw new Error(`Version not found: ${versionIdOverride}`);
590
946
  }
591
947
  // Auto-select active version if one exists
592
948
  if (agentInfo.active_version_id) {
@@ -594,7 +950,7 @@ async function selectVersionForAgent(client, networkId, versionIdOverride, known
594
950
  if (active) {
595
951
  const vLabel = `v${active.version_number}`;
596
952
  log(_jsx(StatusLine, { kind: "info", label: `Using active version: ${vLabel}` }));
597
- return active;
953
+ return { version: active, agentInfo };
598
954
  }
599
955
  }
600
956
  const { selectedVersion } = await inquirer.prompt([
@@ -624,7 +980,7 @@ async function selectVersionForAgent(client, networkId, versionIdOverride, known
624
980
  const vLabel = `v${selectedVersion.version_number}`;
625
981
  log(_jsx(StatusLine, { kind: "info", label: `Version: ${vLabel}` }));
626
982
  }
627
- return selectedVersion;
983
+ return { version: selectedVersion, agentInfo };
628
984
  }
629
985
  catch (error) {
630
986
  if (error.message?.startsWith("Version not found:"))
@@ -632,11 +988,19 @@ async function selectVersionForAgent(client, networkId, versionIdOverride, known
632
988
  spinner.fail("Failed to fetch versions");
633
989
  handleAuthError(error);
634
990
  log(_jsx(StatusLine, { kind: "muted", label: "Continuing without version selection..." }));
635
- return null;
991
+ return { version: null, agentInfo: undefined };
636
992
  }
637
993
  }
638
994
  // ─── Shared: perform the actual import ───
639
- async function doImport(client, selectedAgent, config, options, span, versionId, versionLabel, versionStatus, agentManagementId) {
995
+ async function doImport(client, selectedAgent, config, options, span, versionId, versionLabel, versionStatus, agentManagementId,
996
+ // The full `AgentInfo` if a previous step already fetched it (e.g.
997
+ // `_importAgent` step 1 or `selectAgentFromList` in the picker
998
+ // flow). Passing it lets `detectAgentBundleMode` skip its own
999
+ // `GET /v1/agents/{id}` round-trip — saving one network call per
1000
+ // import and avoiding the duplicate-fetch the user observed in
1001
+ // the trace (2026-05-24). Undefined is safe; the dispatcher
1002
+ // falls back to its own fetch.
1003
+ knownAgentInfo) {
640
1004
  const networkCategoryId = getCategoryId(selectedAgent);
641
1005
  if (!networkCategoryId) {
642
1006
  log(_jsx(StatusLine, { kind: "warning", label: "No category ID found for this agent. Some exports may fail." }));
@@ -672,111 +1036,403 @@ async function doImport(client, selectedAgent, config, options, span, versionId,
672
1036
  }
673
1037
  cleanAgentFiles(outputDir);
674
1038
  }
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));
1039
+ const networkId = selectedAgent._id || selectedAgent.id;
1040
+ // ─── Filename routing (server bundle → local folder layout) ───
1041
+ // The push/pull endpoint disallows path separators in filenames, so
1042
+ // tool files live at the root of the bundle (e.g. `web_search.aui.json`).
1043
+ // Locally we keep them under `tools/<name>.aui.json` for ergonomics.
1044
+ const CANONICAL_ROOT_FILES = new Set([
1045
+ "agent.aui.json",
1046
+ "parameters.aui.json",
1047
+ "entities.aui.json",
1048
+ "integrations.aui.json",
1049
+ "rules.aui.json",
1050
+ "tools.aui.json",
1051
+ "manifest.json",
1052
+ ]);
1053
+ const targetPathForBundleFile = (bundleDir, filename) => CANONICAL_ROOT_FILES.has(filename)
1054
+ ? path.join(bundleDir, filename)
1055
+ : path.join(bundleDir, "tools", filename);
1056
+ // ─── Default in-memory bundle (used by both new + legacy paths) ───
1057
+ let generalSettings = {
1058
+ general_settings: { name: selectedAgent.name },
1059
+ };
1060
+ let parameters = { parameters: [] };
1061
+ let entities = { entities: [] };
1062
+ let integrations = { integrations: [] };
1063
+ let tools = { tools: [] };
1064
+ let rules = { rules: [] };
1065
+ let writtenFiles = [];
1066
+ let paramCount = 0;
1067
+ let entityCount = 0;
1068
+ let integrationCount = 0;
1069
+ let toolCount = 0;
1070
+ let ruleCount = 0;
1071
+ let resolvedVersionTag;
1072
+ // ─── Last-ditch agent_management_id resolve ───
1073
+ // The new `/pull` endpoint is keyed on the agent-management UUID,
1074
+ // NOT network_id. If neither `_importAgent` nor `selectVersionForAgent`
1075
+ // managed to resolve it (e.g. the user took the legacy `networks.list()`
1076
+ // path because agentManagement `listAgents(org_id=…)` returned 0 items),
1077
+ // try ONE more `listAgents(network_id=…)` before giving up. Without
1078
+ // this, the explicit-`--agent-id` branch silently took the legacy
1079
+ // export path on every import.
1080
+ let effectiveAgentMgmtId = agentManagementId;
1081
+ if (!effectiveAgentMgmtId && versionId && networkId) {
1082
+ try {
1083
+ const resp = await client.agentManagement.listAgents(client.getOrganizationId(), 1, 50, { network_id: networkId });
1084
+ const match = resp.items.find((a) => a.scope.network_id === networkId || a.id === networkId);
1085
+ if (match) {
1086
+ effectiveAgentMgmtId = match.id;
1087
+ if (process.env.AUI_DEBUG) {
1088
+ console.log(`[debug] doImport: last-ditch listAgents(network_id=${networkId}) → ${match.id}`);
724
1089
  }
725
- span.setAttribute("import.failed_endpoints", exportData.errors);
726
1090
  }
727
- spinner.warn(`Fetched agent configuration (${exportData.errors.length} endpoint(s) failed)`);
728
- log(_jsx(ImportWarnings, { errors: exportData.errors }));
729
1091
  }
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}`);
1092
+ catch (err) {
1093
+ if (process.env.AUI_DEBUG) {
1094
+ console.log(`[debug] doImport: last-ditch listAgents failed: ${err instanceof Error ? err.message : err}`);
763
1095
  }
764
1096
  }
765
- else {
766
- writeJson(path.join(outputDir, "tools.aui.json"), tools);
767
- writtenFiles.push("tools.aui.json");
1097
+ }
1098
+ // ─── Mode dispatch (with pure-legacy short-circuit) ─────────────
1099
+ //
1100
+ // Three layers, in order:
1101
+ //
1102
+ // 1. No agent_management_id at all (pure legacy)
1103
+ // → The agent exists in `networks` (step 2 of resolution) but
1104
+ // not in agent-management. It predates the agent-management
1105
+ // rollout entirely, which means it predates `bundle_mode`,
1106
+ // which means it can only be records-mode. Skip the
1107
+ // `detectAgentBundleMode` call (it'd need an
1108
+ // agent_management_id we don't have) and force-route to
1109
+ // records-mode dispatch. `exportAgent(...)` is happy with
1110
+ // scope filters alone — no agent_management_id needed.
1111
+ //
1112
+ // Concrete repro (2026-05-26): `aui import 6a0cb854...` (a
1113
+ // legacy `test-v15` network) found the agent via
1114
+ // `networks.get` but `listAgents(network_id=…)` returned
1115
+ // nothing. Without this branch, the import bailed with
1116
+ // "Could not resolve agent_management_id for this agent"
1117
+ // even though `exportAgent` would have worked.
1118
+ //
1119
+ // 2. Agent_management_id present → call `detectAgentBundleMode`
1120
+ // to read the agent's `bundle_mode` flag. Routes to /pull
1121
+ // (bundle) or per-entity /view (records) accordingly.
1122
+ //
1123
+ // 3. Bundle mode + no version_id → hard error. /pull is keyed
1124
+ // on version_id in the URL and can't fall back to scope
1125
+ // filters. Records-mode + no version_id is fine —
1126
+ // `exportAgent` handles it via scope-level filters.
1127
+ let importModeResolution;
1128
+ if (!effectiveAgentMgmtId) {
1129
+ // (1) Pure legacy — force records mode, skip the dispatcher call
1130
+ // entirely (it'd throw since we have no id to pass it).
1131
+ importModeResolution = { mode: "records", source: "no_agent_management_id" };
1132
+ if (process.env.AUI_DEBUG) {
1133
+ console.log(`[debug] doImport: no agent_management_id for network ${networkId} — forcing records-mode dispatch (pure-legacy agent)`);
768
1134
  }
1135
+ }
1136
+ else {
1137
+ // (2) Normal dispatch path — agent_management_id resolved, ask
1138
+ // the server which mode to use. Reuses `knownAgentInfo` so we
1139
+ // don't pay an extra `GET /v1/agents/{id}` round-trip.
1140
+ importModeResolution = await detectAgentBundleMode(client, effectiveAgentMgmtId, knownAgentInfo);
1141
+ }
1142
+ // (3) Bundle mode requires version_id. Records mode doesn't —
1143
+ // `exportAgent` falls back to scope filters when versionId is
1144
+ // undefined (the legacy contract that's been in the CLI since
1145
+ // before version management existed).
1146
+ if (importModeResolution.mode === "bundle" && !versionId) {
1147
+ throw new ConfigError("No version_id available for the new /pull endpoint.", {
1148
+ suggestion: "Pass --version <versionId>, or re-run `aui import` interactively so a version can be selected. " +
1149
+ "Bundle-mode agents must have at least one push (creates v{N}.1) before they can be imported via /pull. " +
1150
+ "If this is a fresh bundle-mode agent, run `aui push` from the source project first, then re-import.",
1151
+ });
1152
+ }
1153
+ // Bundle mode ALSO requires an agent_management_id (it's in the
1154
+ // /pull URL path). This was implicit before — `detectAgentBundleMode`
1155
+ // would have thrown — but now that we short-circuit to records when
1156
+ // agent_management_id is missing, we should only ever reach this
1157
+ // branch when bundle mode was a real positive detection. Keep the
1158
+ // guard for defensive symmetry with the records path above.
1159
+ if (importModeResolution.mode === "bundle" && !effectiveAgentMgmtId) {
1160
+ throw new ConfigError("Could not resolve agent_management_id for this bundle-mode agent.", {
1161
+ suggestion: "Pass the agent_management_id directly as the positional arg, or verify the agent exists in agent-management (`listAgents(network_id=…)`).",
1162
+ });
1163
+ }
1164
+ if (span) {
1165
+ span.setAttribute("import.dispatch.mode", importModeResolution.mode);
1166
+ span.setAttribute("import.dispatch.mode_source", importModeResolution.source);
1167
+ }
1168
+ const useLegacyImport = importModeResolution.mode === "records";
1169
+ const spinner = startSpinner(useLegacyImport
1170
+ ? "Fetching agent data (legacy /view endpoints)..."
1171
+ : options.tag
1172
+ ? `Pulling bundle at tag ${options.tag} via new /pull endpoint...`
1173
+ : `Pulling agent bundle via new /pull endpoint...`);
1174
+ try {
1175
+ if (useLegacyImport) {
1176
+ // ─── Records-mode legacy export ─────────────────────────────
1177
+ // Same shape as the legacy block restored in `pull-agent.tsx`
1178
+ // (kept in sync). Hits the per-entity /view endpoints once,
1179
+ // optionally retries with an API-key prompt on auth failure,
1180
+ // then writes the canonical .aui.json layout to disk.
1181
+ if (span)
1182
+ span.setAttribute("import.path", "legacy_export");
1183
+ let exportData = await client.exportAgent(networkId, networkCategoryId || "", versionId, options.scopeLevel);
1184
+ const hasAuthErrors = exportData.errors.some((e) => /\b(401|403)\b/.test(e) ||
1185
+ /unauthorized|forbidden|not.*member/i.test(e));
1186
+ if (hasAuthErrors && !loadAgentSettingsApiKey()) {
1187
+ spinner.warn("Authentication failed for some endpoints.");
1188
+ log(_jsx(StatusLine, { kind: "warning", label: "Your access token may not have permission for the agent-settings endpoints." }));
1189
+ log(_jsx(Hint, { message: "You can provide an API key as a fallback. It will be saved to ~/.aui/agent-settings-key" }));
1190
+ const { key } = await inquirer.prompt([
1191
+ {
1192
+ type: "password",
1193
+ name: "key",
1194
+ message: "Paste the Agent Settings API key (or press Enter to skip):",
1195
+ mask: "*",
1196
+ },
1197
+ ]);
1198
+ if (key && key.trim()) {
1199
+ saveAgentSettingsApiKey(key.trim());
1200
+ client.setAgentSettingsApiKey(key.trim());
1201
+ log(_jsx(StatusLine, { kind: "success", label: "Key saved. Retrying..." }));
1202
+ const retrySpinner = startSpinner("Retrying agent configuration fetch...");
1203
+ exportData = await client.exportAgent(networkId, networkCategoryId || "", versionId, options.scopeLevel);
1204
+ retrySpinner.stop();
1205
+ }
1206
+ }
1207
+ generalSettings =
1208
+ normalizeGeneralSettings(exportData.general_settings) ||
1209
+ generalSettings;
1210
+ parameters =
1211
+ normalizeWrapped("parameters", exportData.parameters) || parameters;
1212
+ entities =
1213
+ normalizeWrapped("entities", exportData.entities) || entities;
1214
+ integrations =
1215
+ normalizeWrapped("integrations", exportData.integrations) ||
1216
+ integrations;
1217
+ tools = normalizeWrapped("tools", exportData.tools) || tools;
1218
+ rules = normalizeWrapped("rules", exportData.rules) || rules;
1219
+ if (exportData.errors.length > 0) {
1220
+ if (span) {
1221
+ span.setAttribute("import.failed_endpoints", exportData.errors.join(","));
1222
+ }
1223
+ spinner.warn(`Fetched agent configuration (${exportData.errors.length} endpoint(s) failed)`);
1224
+ log(_jsx(ImportWarnings, { errors: exportData.errors }));
1225
+ }
1226
+ else {
1227
+ spinner.succeed("Agent configuration fetched (all 6 endpoints OK)");
1228
+ }
1229
+ if (!fs.existsSync(outputDir)) {
1230
+ fs.mkdirSync(outputDir, { recursive: true });
1231
+ }
1232
+ const toolsDir = path.join(outputDir, "tools");
1233
+ if (!fs.existsSync(toolsDir)) {
1234
+ fs.mkdirSync(toolsDir, { recursive: true });
1235
+ }
1236
+ writeJson(path.join(outputDir, "agent.aui.json"), generalSettings);
1237
+ writtenFiles.push("agent.aui.json");
1238
+ writeJson(path.join(outputDir, "parameters.aui.json"), parameters);
1239
+ writtenFiles.push("parameters.aui.json");
1240
+ writeJson(path.join(outputDir, "entities.aui.json"), entities);
1241
+ writtenFiles.push("entities.aui.json");
1242
+ writeJson(path.join(outputDir, "integrations.aui.json"), integrations);
1243
+ writtenFiles.push("integrations.aui.json");
1244
+ writeJson(path.join(outputDir, "rules.aui.json"), rules);
1245
+ writtenFiles.push("rules.aui.json");
1246
+ // Per-tool files match the disassembler layout: one
1247
+ // `tools/<tool_code>.aui.json` per tool. Empty tools list
1248
+ // collapses to a single `tools.aui.json` so downstream code
1249
+ // always finds a baseline.
1250
+ const toolsData = tools;
1251
+ if (toolsData.tools &&
1252
+ Array.isArray(toolsData.tools) &&
1253
+ toolsData.tools.length > 0) {
1254
+ for (const tool of toolsData.tools) {
1255
+ const toolCode = (tool.code ||
1256
+ tool.name ||
1257
+ tool.tool_name ||
1258
+ "unknown");
1259
+ const fileName = `${toolCode
1260
+ .toLowerCase()
1261
+ .replace(/\s+/g, "_")}.aui.json`;
1262
+ writeJson(path.join(toolsDir, fileName), { tool });
1263
+ writtenFiles.push(`tools/${fileName}`);
1264
+ }
1265
+ }
1266
+ else {
1267
+ writeJson(path.join(outputDir, "tools.aui.json"), tools);
1268
+ writtenFiles.push("tools.aui.json");
1269
+ }
1270
+ paramCount = (parameters?.parameters || []).length;
1271
+ entityCount = (entities?.entities || []).length;
1272
+ integrationCount =
1273
+ (integrations?.integrations || []).length;
1274
+ toolCount = (toolsData.tools || []).length;
1275
+ ruleCount = (rules?.rules || []).length;
1276
+ // Skip the rest of the new-endpoint branch (the blob disassembler
1277
+ // code below). Drop into the shared finalization block by
1278
+ // simply not entering the `else` block. We DO continue past
1279
+ // this point to write `.auirc` etc. — same as the new path.
1280
+ }
1281
+ else {
1282
+ // ─── Blob-mode (new /pull endpoint) ─────────────────────────
1283
+ //
1284
+ // `versionId!` assertion: we reach this branch only when
1285
+ // `useLegacyImport === false` (bundle mode), and the conditional
1286
+ // throw above ("No version_id available for the new /pull
1287
+ // endpoint.") already guarantees `versionId` is defined in
1288
+ // that case. TS can't narrow across the throw, so the
1289
+ // assertion is documented intent rather than a hand-wave.
1290
+ let pullData = null;
1291
+ try {
1292
+ pullData = await tryPullViaBlobEndpoint(client, effectiveAgentMgmtId, versionId, options.tag);
1293
+ }
1294
+ catch (pullErr) {
1295
+ if (pullErr instanceof PullModeMismatch) {
1296
+ // Server changed the agent's mode between detection and
1297
+ // this call. Re-run with `useLegacyImport=true`. Recursing
1298
+ // into _doImport would duplicate every preflight — instead
1299
+ // do the legacy export inline. We hit the same code path
1300
+ // as the records-mode branch above, so we just bail out
1301
+ // and ask the user to rerun.
1302
+ if (span) {
1303
+ span.setAttribute("import.dispatch.mode_changed_midflight", true);
1304
+ }
1305
+ spinner.warn("Server reports the agent is now in records-mode.");
1306
+ throw new CLIError("Server reports this agent is now in DB-records mode mid-import.", {
1307
+ 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.",
1308
+ });
1309
+ }
1310
+ throw pullErr;
1311
+ }
1312
+ if (!pullData) {
1313
+ // 404 from /pull — agent has never been Pushed via the new
1314
+ // endpoint, or the requested tag doesn't exist on that version.
1315
+ spinner.fail("No blob revision found at this version.");
1316
+ throw new CLIError(options.tag
1317
+ ? `No blob revision found at tag ${options.tag} for version ${versionId}.`
1318
+ : `No blob revision found for version ${versionId}.`, {
1319
+ 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`.",
1320
+ });
1321
+ }
1322
+ // ─── New bundle path: disassemble + write files ───
1323
+ if (!fs.existsSync(outputDir)) {
1324
+ fs.mkdirSync(outputDir, { recursive: true });
1325
+ }
1326
+ const toolsDir = path.join(outputDir, "tools");
1327
+ if (!fs.existsSync(toolsDir)) {
1328
+ fs.mkdirSync(toolsDir, { recursive: true });
1329
+ }
1330
+ const disassembled = disassembleBundleToFiles(pullData.bundle);
1331
+ for (const entry of disassembled) {
1332
+ const targetPath = path.join(outputDir, entry.relativePath);
1333
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
1334
+ // Normalize to canonical on-disk shape before writing. The
1335
+ // disassembler already produces canonical shapes; this is a
1336
+ // belt+suspenders pass for any edge case the disassembler
1337
+ // might mishandle on legacy bundle data.
1338
+ const basename = path.basename(entry.relativePath);
1339
+ const normalized = normalizeBundleFileBody(basename, entry.body);
1340
+ writeJson(targetPath, normalized);
1341
+ writtenFiles.push(entry.relativePath);
1342
+ if (basename === "agent.aui.json") {
1343
+ generalSettings = normalized;
1344
+ }
1345
+ else if (basename === "parameters.aui.json") {
1346
+ parameters = normalized;
1347
+ paramCount = (normalized?.parameters || []).length;
1348
+ }
1349
+ else if (basename === "entities.aui.json") {
1350
+ entities = normalized;
1351
+ entityCount = (normalized?.entities || []).length;
1352
+ }
1353
+ else if (basename === "integrations.aui.json") {
1354
+ integrations = normalized;
1355
+ integrationCount = (normalized?.integrations || []).length;
1356
+ }
1357
+ else if (basename === "rules.aui.json") {
1358
+ rules = normalized;
1359
+ ruleCount = (normalized?.rules || []).length;
1360
+ }
1361
+ else if (entry.relativePath.startsWith("tools/")) {
1362
+ toolCount++;
1363
+ }
1364
+ else if (basename === "tools.aui.json") {
1365
+ tools = normalized;
1366
+ toolCount += (normalized?.tools || []).length;
1367
+ }
1368
+ }
1369
+ resolvedVersionTag = pullData.version_tag;
1370
+ if (span) {
1371
+ span.setAttribute("import.path", "blob_endpoint");
1372
+ if (resolvedVersionTag) {
1373
+ span.setAttribute("import.version_tag", resolvedVersionTag);
1374
+ }
1375
+ }
1376
+ spinner.succeed(`Pulled bundle via /pull (${disassembled.length} file(s)) at ${resolvedVersionTag || `v${versionLabel}`}`);
1377
+ // ─── Empty-domain warning banner ─────────────────────────────────
1378
+ // The server can legitimately return a bundle with an empty
1379
+ // domain (e.g. tools[]=[]) — it's not a CLI error. But it IS
1380
+ // usually a sign the user pulled the wrong version (a v8 line
1381
+ // that has no tools while v9-LIVE has them, etc.). Surface
1382
+ // every empty domain prominently so the user can decide whether
1383
+ // to re-import a different version instead of being surprised
1384
+ // when their `tools/` folder is empty after import.
1385
+ const emptyDomains = [];
1386
+ if (!pullData.bundle.general_settings)
1387
+ emptyDomains.push("general_settings");
1388
+ if (!pullData.bundle.parameters || pullData.bundle.parameters.length === 0)
1389
+ emptyDomains.push("parameters");
1390
+ if (!pullData.bundle.entities || pullData.bundle.entities.length === 0)
1391
+ emptyDomains.push("entities");
1392
+ if (!pullData.bundle.integrations || pullData.bundle.integrations.length === 0)
1393
+ emptyDomains.push("integrations");
1394
+ if (!pullData.bundle.rules || pullData.bundle.rules.length === 0)
1395
+ emptyDomains.push("rules");
1396
+ // Tools live under `agent_tools` on the live `/pull` response and
1397
+ // under `tools` per the OpenAPI schema. Either counts as "present".
1398
+ const bundleTools = readBundleTools(pullData.bundle);
1399
+ if (!bundleTools || bundleTools.length === 0)
1400
+ emptyDomains.push("tools");
1401
+ if (emptyDomains.length > 0) {
1402
+ 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. " +
1403
+ "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>`." })] }));
1404
+ if (span) {
1405
+ span.setAttribute("import.empty_domains", emptyDomains.join(","));
1406
+ }
1407
+ }
1408
+ } // end of bundle-mode `else` branch (matches `if (useLegacyImport)` above)
1409
+ // Persist agent + version + revision context to `.auirc`.
1410
+ // `version_label` = `v{version_number}` (e.g. "v8") — the version
1411
+ // document's display label, used by telemetry / version UX.
1412
+ // `version_tag` = full revision tag from the manifest (e.g.
1413
+ // "v8.14") — used as the default `?version_tag=` on the next
1414
+ // `aui pull` so users re-pull the exact revision they imported
1415
+ // without having to remember it.
1416
+ //
1417
+ // Prefer `effectiveAgentMgmtId` (resolved via the dual-shape
1418
+ // helper above) over the original `agentManagementId` arg so the
1419
+ // saved id reflects whatever actually worked, even when the
1420
+ // upstream `selectAgentFromList` had to fall back to legacy
1421
+ // `networks.list()`.
769
1422
  saveProjectConfig({
770
1423
  agent_code: selectedAgent.niceName ||
771
1424
  selectedAgent.name.toLowerCase().replace(/\s+/g, "-"),
772
1425
  agent_id: networkId,
773
- ...(agentManagementId ? { agent_management_id: agentManagementId } : {}),
1426
+ ...(effectiveAgentMgmtId
1427
+ ? { agent_management_id: effectiveAgentMgmtId }
1428
+ : {}),
774
1429
  environment: config.environment,
775
1430
  account_id: client.getAccountId() || config.accountId,
776
1431
  organization_id: client.getOrganizationId() || config.organizationId,
777
1432
  network_category_id: networkCategoryId,
778
1433
  ...(versionId ? { version_id: versionId } : {}),
779
1434
  ...(versionLabel ? { version_label: versionLabel } : {}),
1435
+ ...(resolvedVersionTag ? { version_tag: resolvedVersionTag } : {}),
780
1436
  ...(options.scopeLevel ? { scope_level: options.scopeLevel } : {}),
781
1437
  }, outputDir);
782
1438
  writtenFiles.push(".auirc");
@@ -793,36 +1449,63 @@ async function doImport(client, selectedAgent, config, options, span, versionId,
793
1449
  catch {
794
1450
  // optional
795
1451
  }
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;
1452
+ // ─── Skip KB export for templates ─────────────────────────────
1453
+ //
1454
+ // Templates are NETWORK_CATEGORY-scoped (no `scope.network_id`,
1455
+ // no `scope.account_id`). KB exports are inherently network-
1456
+ // scoped the `knowledge-base-manager/v1/knowledge-bases/view/
1457
+ // export` endpoint expects valid `network_id` + `account_id`
1458
+ // query params. Calling it for a template results in:
1459
+ //
1460
+ // GET .../export?network_id=<template_agent_mgmt_id>
1461
+ // &account_id= ← empty!
1462
+ // → 422 {"detail":[{"loc":["query","account_id"],
1463
+ // "msg":"Id must be of type PydanticObjectId",
1464
+ // "input":""}]}
1465
+ //
1466
+ // (The bogus `network_id` substitution happens because
1467
+ // `agentInfoToNetwork` falls back to `agent.id` when
1468
+ // `scope.network_id` is null.) The right answer is to not
1469
+ // make the call at all — templates don't have knowledge bases
1470
+ // in this data model.
1471
+ //
1472
+ // Detect templates by either `kind === "template"` (canonical,
1473
+ // present on AgentInfo from the new agent-settings response)
1474
+ // OR `scope.type === "NETWORK_CATEGORY"` (back-compat for any
1475
+ // future agent kinds that also live at category scope and
1476
+ // share the same constraint).
1477
+ const isTemplateForKb = knownAgentInfo?.kind === "template" ||
1478
+ knownAgentInfo?.scope?.type === "NETWORK_CATEGORY";
801
1479
  const kbExportPromise = options.skipKbFiles
802
1480
  ? 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
- })();
1481
+ : isTemplateForKb
1482
+ ? Promise.resolve({
1483
+ kind: "skipped",
1484
+ reason: "templates are NETWORK_CATEGORY-scoped and don't have knowledge bases",
1485
+ })
1486
+ : (async () => {
1487
+ try {
1488
+ const kbClient = new KBViewClient({
1489
+ authToken: config.authToken,
1490
+ apiKey: loadAgentSettingsApiKey() || options.apiKey,
1491
+ organizationId: config.organizationId || "",
1492
+ environment: config.environment || "staging",
1493
+ });
1494
+ const scope = buildScope({
1495
+ networkId,
1496
+ organizationId: client.getOrganizationId() || config.organizationId || "",
1497
+ accountId: client.getAccountId() || config.accountId || "",
1498
+ });
1499
+ const result = await exportToFolder(kbClient, scope, outputDir, options.withKbFiles ?? false);
1500
+ return { kind: "ok", result };
1501
+ }
1502
+ catch (kbError) {
1503
+ return {
1504
+ kind: "error",
1505
+ message: kbError instanceof Error ? kbError.message : String(kbError),
1506
+ };
1507
+ }
1508
+ })();
826
1509
  const schemaPromise = (async () => {
827
1510
  try {
828
1511
  const data = await fetchSchemas({
@@ -851,7 +1534,9 @@ async function doImport(client, selectedAgent, config, options, span, versionId,
851
1534
  parallelSpinner.stop();
852
1535
  // Knowledge-hub status
853
1536
  if (kbOutcome.kind === "skipped") {
854
- log(_jsx(StatusLine, { kind: "muted", label: "Skipping knowledge hub export (--skip-kb-files)" }));
1537
+ log(_jsx(StatusLine, { kind: "muted", label: kbOutcome.reason
1538
+ ? `Skipping knowledge hub export — ${kbOutcome.reason}.`
1539
+ : "Skipping knowledge hub export (--skip-kb-files)" }));
855
1540
  }
856
1541
  else if (kbOutcome.kind === "ok") {
857
1542
  const r = kbOutcome.result;
@@ -902,6 +1587,11 @@ async function doImport(client, selectedAgent, config, options, span, versionId,
902
1587
  gitOk,
903
1588
  writtenFiles,
904
1589
  dirName,
1590
+ // Forward `kind` + `scope.type` from the cached AgentInfo so the
1591
+ // view can show the template banner. Both default-safe (the view
1592
+ // falls back to "regular" / no-scope-row when these are omitted).
1593
+ kind: knownAgentInfo?.kind,
1594
+ scopeType: knownAgentInfo?.scope?.type,
905
1595
  };
906
1596
  log(_jsx(ImportAgentView, { data: summaryData }));
907
1597
  }
@@ -911,6 +1601,51 @@ async function doImport(client, selectedAgent, config, options, span, versionId,
911
1601
  throw error;
912
1602
  }
913
1603
  }
1604
+ // ─── New pull endpoint (with 404 fallback to legacy export) ───
1605
+ //
1606
+ // The new bundle endpoint replaces the per-entity multi-endpoint export
1607
+ // for any agent that has been Pushed via the new endpoint. We try it
1608
+ // first and silently fall through to `exportAgent` on 404 (agent has
1609
+ // no blobs yet — typical for pre-migration agents). Other errors are
1610
+ // propagated so we don't mask auth / network issues.
1611
+ //
1612
+ // `versionTag` is optional; when omitted the server picks the version
1613
+ // row's current revision (latest successful Push on that version).
1614
+ /**
1615
+ * Sentinel thrown by `tryPullViaBlobEndpoint` when the server reports
1616
+ * the target agent is in records-mode. Caller catches this and
1617
+ * dispatches to the legacy `client.exportAgent(...)` path instead.
1618
+ * Distinct from a plain 404 (no blob revision yet) — that's a
1619
+ * non-error, while this requires switching surfaces.
1620
+ */
1621
+ class PullModeMismatch extends Error {
1622
+ constructor() {
1623
+ super("Server reports this agent is in records-mode (422)");
1624
+ this.name = "PullModeMismatch";
1625
+ }
1626
+ }
1627
+ async function tryPullViaBlobEndpoint(client, agentManagementId, versionId, versionTag) {
1628
+ try {
1629
+ return await client.agentManagement.pullVersionBlobs(agentManagementId, versionId, { versionTag });
1630
+ }
1631
+ catch (err) {
1632
+ const status = err instanceof AUIAPIError ? err.status : undefined;
1633
+ if (status === 404) {
1634
+ // No blobs at this version (or no blob at the requested tag) yet —
1635
+ // caller surfaces a "run aui push first" error.
1636
+ return null;
1637
+ }
1638
+ if (isModeMismatchError(err)) {
1639
+ // Server flipped `bundle_mode` since our up-front detect.
1640
+ // Caller swaps to the legacy export branch.
1641
+ throw new PullModeMismatch();
1642
+ }
1643
+ // Anything else (auth, 5xx, network) is a real failure: bubble up so
1644
+ // the import surfaces a clear error rather than silently degrading
1645
+ // and ending up with stale data.
1646
+ throw err;
1647
+ }
1648
+ }
914
1649
  // ─── Response normalizers ───
915
1650
  function normalizeGeneralSettings(raw) {
916
1651
  if (!raw)