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.
- package/dist/api-client/index.d.ts +304 -19
- package/dist/api-client/index.d.ts.map +1 -1
- package/dist/api-client/index.js +337 -69
- package/dist/api-client/index.js.map +1 -1
- package/dist/commands/agents.d.ts +55 -1
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +193 -50
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/import-agent.d.ts +23 -0
- package/dist/commands/import-agent.d.ts.map +1 -1
- package/dist/commands/import-agent.js +802 -151
- package/dist/commands/import-agent.js.map +1 -1
- package/dist/commands/legacy/push-records-mode.d.ts +166 -0
- package/dist/commands/legacy/push-records-mode.d.ts.map +1 -0
- package/dist/commands/legacy/push-records-mode.js +2536 -0
- package/dist/commands/legacy/push-records-mode.js.map +1 -0
- package/dist/commands/pull-agent.d.ts +8 -0
- package/dist/commands/pull-agent.d.ts.map +1 -1
- package/dist/commands/pull-agent.js +567 -126
- package/dist/commands/pull-agent.js.map +1 -1
- package/dist/commands/push.d.ts +78 -107
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +942 -1804
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/util/agent-mode.d.ts +69 -0
- package/dist/commands/util/agent-mode.d.ts.map +1 -0
- package/dist/commands/util/agent-mode.js +101 -0
- package/dist/commands/util/agent-mode.js.map +1 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +23 -7
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/version-snapshot.d.ts.map +1 -1
- package/dist/commands/version-snapshot.js +253 -49
- package/dist/commands/version-snapshot.js.map +1 -1
- package/dist/commands/version.d.ts +15 -1
- package/dist/commands/version.d.ts.map +1 -1
- package/dist/commands/version.js +102 -7
- package/dist/commands/version.js.map +1 -1
- package/dist/config/index.d.ts +16 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/index.js +20 -5
- package/dist/index.js.map +1 -1
- package/dist/ui/views/ImportAgentView.d.ts +15 -0
- package/dist/ui/views/ImportAgentView.d.ts.map +1 -1
- package/dist/ui/views/ImportAgentView.js +8 -3
- package/dist/ui/views/ImportAgentView.js.map +1 -1
- package/dist/utils/index.d.ts +80 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +330 -0
- package/dist/utils/index.js.map +1 -1
- 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 {
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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: ${
|
|
157
|
+
spinner.succeed(`Found agent (via agent-management): ${ami.name}`);
|
|
158
|
+
resolved = true;
|
|
127
159
|
}
|
|
128
|
-
catch (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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 (
|
|
583
|
-
|
|
584
|
-
log(_jsx(
|
|
585
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
731
|
-
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
:
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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:
|
|
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)
|