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.
- 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 +886 -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 +2575 -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 +598 -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 +1037 -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,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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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 (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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 (
|
|
583
|
-
|
|
584
|
-
log(_jsx(
|
|
585
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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}`);
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
:
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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:
|
|
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)
|