aui-agent-builder 0.3.103 → 0.3.105

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/api-client/index.d.ts +304 -19
  2. package/dist/api-client/index.d.ts.map +1 -1
  3. package/dist/api-client/index.js +337 -69
  4. package/dist/api-client/index.js.map +1 -1
  5. package/dist/commands/agents.d.ts +55 -1
  6. package/dist/commands/agents.d.ts.map +1 -1
  7. package/dist/commands/agents.js +193 -50
  8. package/dist/commands/agents.js.map +1 -1
  9. package/dist/commands/import-agent.d.ts +23 -0
  10. package/dist/commands/import-agent.d.ts.map +1 -1
  11. package/dist/commands/import-agent.js +886 -151
  12. package/dist/commands/import-agent.js.map +1 -1
  13. package/dist/commands/legacy/push-records-mode.d.ts +166 -0
  14. package/dist/commands/legacy/push-records-mode.d.ts.map +1 -0
  15. package/dist/commands/legacy/push-records-mode.js +2575 -0
  16. package/dist/commands/legacy/push-records-mode.js.map +1 -0
  17. package/dist/commands/pull-agent.d.ts +8 -0
  18. package/dist/commands/pull-agent.d.ts.map +1 -1
  19. package/dist/commands/pull-agent.js +598 -126
  20. package/dist/commands/pull-agent.js.map +1 -1
  21. package/dist/commands/push.d.ts +78 -107
  22. package/dist/commands/push.d.ts.map +1 -1
  23. package/dist/commands/push.js +1037 -1804
  24. package/dist/commands/push.js.map +1 -1
  25. package/dist/commands/util/agent-mode.d.ts +69 -0
  26. package/dist/commands/util/agent-mode.d.ts.map +1 -0
  27. package/dist/commands/util/agent-mode.js +101 -0
  28. package/dist/commands/util/agent-mode.js.map +1 -0
  29. package/dist/commands/validate.d.ts.map +1 -1
  30. package/dist/commands/validate.js +23 -7
  31. package/dist/commands/validate.js.map +1 -1
  32. package/dist/commands/version-snapshot.d.ts.map +1 -1
  33. package/dist/commands/version-snapshot.js +253 -49
  34. package/dist/commands/version-snapshot.js.map +1 -1
  35. package/dist/commands/version.d.ts +15 -1
  36. package/dist/commands/version.d.ts.map +1 -1
  37. package/dist/commands/version.js +102 -7
  38. package/dist/commands/version.js.map +1 -1
  39. package/dist/config/index.d.ts +16 -1
  40. package/dist/config/index.d.ts.map +1 -1
  41. package/dist/config/index.js.map +1 -1
  42. package/dist/index.js +20 -5
  43. package/dist/index.js.map +1 -1
  44. package/dist/ui/views/ImportAgentView.d.ts +15 -0
  45. package/dist/ui/views/ImportAgentView.d.ts.map +1 -1
  46. package/dist/ui/views/ImportAgentView.js +8 -3
  47. package/dist/ui/views/ImportAgentView.js.map +1 -1
  48. package/dist/utils/index.d.ts +80 -0
  49. package/dist/utils/index.d.ts.map +1 -1
  50. package/dist/utils/index.js +330 -0
  51. package/dist/utils/index.js.map +1 -1
  52. package/package.json +1 -1
@@ -1,18 +1,20 @@
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
- import { render } from "ink";
4
+ import { render, Box } from "ink";
5
5
  import inquirer from "inquirer";
6
6
  import { getConfig, loadSession, loadProjectConfig, saveProjectConfig, findProjectRoot, loadAgentSettingsApiKey, saveAgentSettingsApiKey, } from "../config/index.js";
7
- import { AUIClient } from "../api-client/index.js";
7
+ import { AUIClient, AUIAPIError } from "../api-client/index.js";
8
8
  import { getTracer, SpanStatusCode, setUserContext } from "../telemetry.js";
9
- import { AuthenticationError } from "../errors/index.js";
9
+ import { AuthenticationError, CLIError, ConfigError } from "../errors/index.js";
10
10
  import { KBViewClient } from "../api-client/kb-view-client.js";
11
11
  import { exportToFolder, buildScope, } from "../services/kb-view.service.js";
12
12
  import { fetchSchemas } from "../services/pull-schema.service.js";
13
13
  import { initAndCommitBaseline } from "../utils/git.js";
14
+ import { disassembleBundleToFiles, normalizeBundleFileBody, readBundleTools, } from "../utils/index.js";
14
15
  import { Header, StatusLine, Spinner, ErrorDisplay, Hint, } from "../ui/components/index.js";
15
16
  import { ImportWarnings } from "../ui/views/ImportAgentView.js";
17
+ import { detectAgentBundleMode, isModeMismatchError, } from "./util/agent-mode.js";
16
18
  import { PullAgentView } from "../ui/views/PullAgentView.js";
17
19
  // ─── Ink Rendering Helpers ───
18
20
  function log(node) {
@@ -39,8 +41,35 @@ function startSpinner(label) {
39
41
  },
40
42
  };
41
43
  }
42
- // ─── Response normalizers (same as import-agent) ───
43
- function normalizeGeneralSettings(raw) {
44
+ // ─── Filename routing (server bundle → local folder layout) ───
45
+ //
46
+ // The push/pull endpoint disallows path separators in filenames, so tool
47
+ // files live at the root of the bundle (e.g. `web_search.aui.json`).
48
+ // Locally we keep them under `tools/<name>.aui.json` for ergonomics —
49
+ // every file in the response is routed to one of these two locations.
50
+ const CANONICAL_ROOT_FILES = new Set([
51
+ "agent.aui.json",
52
+ "parameters.aui.json",
53
+ "entities.aui.json",
54
+ "integrations.aui.json",
55
+ "rules.aui.json",
56
+ "tools.aui.json",
57
+ "manifest.json",
58
+ ]);
59
+ function targetPathForBundleFile(projectRoot, filename) {
60
+ if (CANONICAL_ROOT_FILES.has(filename)) {
61
+ return path.join(projectRoot, filename);
62
+ }
63
+ // Server filenames have no path separators (enforced by the endpoint),
64
+ // so anything that isn't a canonical root file is a per-tool blob —
65
+ // write under `tools/`.
66
+ return path.join(projectRoot, "tools", filename);
67
+ }
68
+ // ─── Legacy normalizers (only the old export endpoints need these) ───
69
+ // Kept under a `Legacy` suffix to make it obvious from the call site
70
+ // which path you're on. New-pull blob bodies are pre-shaped server-side
71
+ // and don't need normalization.
72
+ function normalizeGeneralSettingsLegacy(raw) {
44
73
  if (!raw)
45
74
  return null;
46
75
  if (typeof raw === "object" && !Array.isArray(raw)) {
@@ -50,7 +79,7 @@ function normalizeGeneralSettings(raw) {
50
79
  }
51
80
  return raw;
52
81
  }
53
- function normalizeWrapped(key, raw) {
82
+ function normalizeWrappedLegacy(key, raw) {
54
83
  if (!raw)
55
84
  return null;
56
85
  if (Array.isArray(raw)) {
@@ -165,24 +194,69 @@ async function _pullAgent(parentSpan, options = {}) {
165
194
  // not have `agent_management_id` recorded — refresh it whenever we resolve
166
195
  // one here, regardless of whether the user passed --version.
167
196
  let resolvedAgentManagementId;
168
- // Resolve the agent-management entry for this project's network_id.
197
+ // Captured AgentInfo from `findAgentMgmtId` re-used by
198
+ // `detectAgentBundleMode` later so we don't pay a second
199
+ // `GET /v1/agents/{id}` round-trip just to read `bundle_mode`.
200
+ // `listAgents` returns full AgentInfo rows including the
201
+ // `bundle_mode` field, so the dispatch decision is already
202
+ // available from this resolver. Saves one network call per
203
+ // `aui pull` run and avoids the duplicate-fetch pattern.
204
+ let resolvedAgentInfo;
205
+ // Resolve the agent-management entry for this project.
206
+ //
207
+ // Priority order (top → first match wins):
208
+ //
209
+ // 1. `.auirc.agent_management_id` — the CANONICAL id `aui
210
+ // import-agent` persisted on the last import. Picks the
211
+ // RIGHT agent when multiple records share a `network_id`
212
+ // (e.g. a bundle-mode + records-mode sibling pair). Lookup
213
+ // is a single `getAgent(...)` — no list scan, no
214
+ // pagination quirks. Aligns with `push.tsx:lookupAgent…`
215
+ // and `version-snapshot.tsx:resolveAgentForSnapshot`.
216
+ //
217
+ // 2. `listAgents(network_id=...)` first match — back-compat
218
+ // path for legacy projects without `agent_management_id`
219
+ // in `.auirc` (imported pre-PR #54). Has the sibling-pick
220
+ // risk; a one-time `aui import-agent` re-import promotes
221
+ // the project to branch 1 on every subsequent call.
169
222
  //
170
- // We use `listAgents` with the `network_id` filter (matches what
171
- // `version list` does in `commands/version.tsx#resolveAgentId`). This is a
172
- // single targeted lookup listing every agent in the org and scanning is
173
- // unreliable at scale (orgs with many accounts can have thousands of
174
- // agents and pagination quirks miss the match) and was the cause of
175
- // spurious "this agent has no version management" errors.
223
+ // 3. Session `agent_management_id` (if it points at this
224
+ // network) last-resort for the rare case where the
225
+ // filtered list returns nothing but the session knows the
226
+ // id from a previous resolve.
176
227
  //
177
- // Fall back to session.agent_management_id (if it points at this network)
178
- // for the rare case where the filtered list returns nothing but the
179
- // session already knows the id from a previous resolve.
228
+ // Each branch caches the resolved AgentInfo into
229
+ // `resolvedAgentInfo` so `detectAgentBundleMode` reuses it
230
+ // and we avoid a duplicate `GET /agents/{id}` round-trip.
180
231
  const findAgentMgmtId = async () => {
232
+ // 1. Canonical: `.auirc.agent_management_id`
233
+ if (projectConfig.agent_management_id) {
234
+ try {
235
+ const agent = await client.agentManagement.getAgent(projectConfig.agent_management_id);
236
+ resolvedAgentManagementId = agent.id;
237
+ resolvedAgentInfo = agent;
238
+ if (!options.version && agent.active_version_id) {
239
+ activeVersionId = agent.active_version_id;
240
+ }
241
+ return agent.id;
242
+ }
243
+ catch (err) {
244
+ // Stale id (agent deleted, scope flipped). Fall through
245
+ // to the network-id resolver below. Debug-log only —
246
+ // silent fall-through matches push.tsx's pattern for the
247
+ // same case.
248
+ if (process.env.AUI_DEBUG) {
249
+ console.log(`[debug] pull.findAgentMgmtId: getAgent(${projectConfig.agent_management_id}) from .auirc failed, falling back to network_id lookup: ${err instanceof Error ? err.message : err}`);
250
+ }
251
+ }
252
+ }
253
+ // 2. Network-id listing (legacy projects + general fallback)
181
254
  try {
182
255
  const resp = await client.agentManagement.listAgents(client.getOrganizationId(), 1, 50, { network_id: networkId });
183
256
  const agentMatch = resp.items.find((a) => a.scope.network_id === networkId || a.id === networkId);
184
257
  if (agentMatch) {
185
258
  resolvedAgentManagementId = agentMatch.id;
259
+ resolvedAgentInfo = agentMatch;
186
260
  if (!options.version && agentMatch.active_version_id) {
187
261
  activeVersionId = agentMatch.active_version_id;
188
262
  }
@@ -192,10 +266,12 @@ async function _pullAgent(parentSpan, options = {}) {
192
266
  catch {
193
267
  // Filtered listing failed — fall through to session hint.
194
268
  }
269
+ // 3. Session hint
195
270
  if (session.agent_management_id && session.network_id === networkId) {
196
271
  try {
197
272
  const agent = await client.agentManagement.getAgent(session.agent_management_id);
198
273
  resolvedAgentManagementId = agent.id;
274
+ resolvedAgentInfo = agent;
199
275
  if (!options.version && agent.active_version_id) {
200
276
  activeVersionId = agent.active_version_id;
201
277
  }
@@ -236,6 +312,11 @@ async function _pullAgent(parentSpan, options = {}) {
236
312
  }
237
313
  else {
238
314
  try {
315
+ // `findAgentMgmtId` already prefers `.auirc.agent_management_id`
316
+ // (its branch 1), so we get the right agent without an extra
317
+ // sibling-disambiguation step here. No-op when the project is
318
+ // legacy (no canonical id) — the function still falls through
319
+ // to the network-id listing.
239
320
  const agentMgmtId = await findAgentMgmtId();
240
321
  if (agentMgmtId && activeVersionId) {
241
322
  try {
@@ -247,6 +328,45 @@ async function _pullAgent(parentSpan, options = {}) {
247
328
  }
248
329
  log(_jsx(StatusLine, { kind: "info", label: `Using active version: ${activeVersionLabel || activeVersionId.slice(0, 10) + "…"}` }));
249
330
  }
331
+ else if (agentMgmtId && projectConfig.version_id) {
332
+ // ─── Fallback: `.auirc.version_id` from the last import/pull
333
+ //
334
+ // `aui import-agent` persists `version_id` on every import
335
+ // (regular OR draft-only agents). Without this fallback, a
336
+ // bare `aui pull` inside a freshly-imported draft-only agent
337
+ // (the common case — most agents under active development
338
+ // have no published/activated version) would error out with
339
+ // "No version_id available to pull" even though the project
340
+ // clearly knows which version it's tracking.
341
+ //
342
+ // Hierarchy:
343
+ // 1. --version <id> (explicit; handled above)
344
+ // 2. agent.active_version_id (published+activated; the
345
+ // default "what's live" the
346
+ // user usually wants)
347
+ // 3. .auirc.version_id (this branch — what they
348
+ // originally imported / last
349
+ // pulled from)
350
+ //
351
+ // We still resolve the version label by calling getVersion so
352
+ // the UX line below matches the active-version branch above.
353
+ try {
354
+ const ver = await client.agentManagement.getVersion(agentMgmtId, projectConfig.version_id);
355
+ activeVersionId = ver.id;
356
+ activeVersionLabel = `v${ver.version_number}`;
357
+ log(_jsx(StatusLine, { kind: "info", label: `Using project version (from .auirc): ${activeVersionLabel}` }));
358
+ }
359
+ catch (err) {
360
+ // The version in .auirc no longer exists on the server
361
+ // (got deleted, agent_id drift, etc.). Don't silently
362
+ // promote `projectConfig.version_id` as the activeVersionId
363
+ // — that would round-trip back as a 404 on /pull. Let the
364
+ // `!activeVersionId` guard below fire its actionable error.
365
+ if (process.env.AUI_DEBUG) {
366
+ console.log(`[debug] pull: getVersion(${projectConfig.version_id}) from .auirc failed: ${err instanceof Error ? err.message : err}`);
367
+ }
368
+ }
369
+ }
250
370
  }
251
371
  catch {
252
372
  // No version management available, pull latest
@@ -270,51 +390,328 @@ async function _pullAgent(parentSpan, options = {}) {
270
390
  return;
271
391
  }
272
392
  }
273
- const spinner = startSpinner("Fetching agent data (7 endpoints)...");
393
+ // ─── Resolve the agent-management UUID for the new pull endpoint ───
394
+ // The new bundle endpoint is keyed on `{agent_id, version_id}` where
395
+ // `agent_id` is the agent-management UUID — NOT the network_id we have
396
+ // in `.auirc`. Re-use the same `findAgentMgmtId` resolver as version
397
+ // discovery: filtered listAgents → session hint → return undefined
398
+ // for pure-legacy agents (no agent-management entry at all).
399
+ const pullAgentMgmtId = projectConfig.agent_management_id ||
400
+ resolvedAgentManagementId ||
401
+ (await findAgentMgmtId());
402
+ // ─── Mode dispatch: blob (/pull) vs records (legacy /view) ───────
403
+ //
404
+ // Three layers, mirrors `commands/import-agent.tsx:doImport`:
405
+ //
406
+ // 1. No agent_management_id at all (pure legacy)
407
+ // → The agent exists in `networks` but not in agent-management.
408
+ // It predates the agent-management rollout entirely → predates
409
+ // `bundle_mode` → can only be records-mode. Skip the
410
+ // `detectAgentBundleMode` call (it'd need an
411
+ // agent_management_id we don't have) and force-route to
412
+ // records-mode dispatch. `client.exportAgent(...)` works on
413
+ // scope filters alone — no agent_management_id needed.
414
+ //
415
+ // Concrete repro (2026-05-26 sibling to the import fix):
416
+ // legacy projects whose `.auirc` only has `agent_id`
417
+ // (network_id) used to fail here with "Could not resolve
418
+ // agent-management id for this project". Now they pull via
419
+ // the legacy `exportAgent` path same as `aui import-agent`
420
+ // for the same agent.
421
+ //
422
+ // 2. Agent_management_id present → call `detectAgentBundleMode`
423
+ // to read the agent's `bundle_mode` flag. Routes to /pull
424
+ // (bundle) or per-entity /view (records) accordingly. Reuses
425
+ // `resolvedAgentInfo` from `findAgentMgmtId` so the
426
+ // dispatcher doesn't re-fetch.
427
+ //
428
+ // 3. Bundle mode + no version_id → hard error. /pull is keyed
429
+ // on version_id in the URL and can't fall back to scope
430
+ // filters. Records mode + no version_id is fine — exportAgent
431
+ // handles it via scope-level filters.
432
+ let pullModeResolution;
433
+ if (!pullAgentMgmtId) {
434
+ // (1) Pure legacy — force records mode, skip the dispatcher
435
+ // call entirely (it'd need an id we don't have).
436
+ pullModeResolution = { mode: "records", source: "no_agent_management_id" };
437
+ if (process.env.AUI_DEBUG) {
438
+ console.log(`[debug] pull: no agent_management_id for network ${networkId} — forcing records-mode dispatch (pure-legacy agent)`);
439
+ }
440
+ }
441
+ else {
442
+ // (2) Normal path — dispatcher reads bundle_mode from the cached
443
+ // AgentInfo when available, no extra round-trip.
444
+ pullModeResolution = await detectAgentBundleMode(client, pullAgentMgmtId, resolvedAgentInfo);
445
+ }
446
+ // (3) Bundle mode requires version_id. Records mode doesn't —
447
+ // `exportAgent` falls back to scope filters when versionId is
448
+ // undefined (the legacy contract that's been in the CLI since
449
+ // before version management existed).
450
+ if (pullModeResolution.mode === "bundle" && !activeVersionId) {
451
+ throw new ConfigError("No version_id available to pull — the new pull endpoint requires one.", {
452
+ suggestion: "Pass `--version <version-id>` explicitly, or re-run `aui import-agent` " +
453
+ "to refresh `.auirc.version_id`. If the project's `.auirc` already " +
454
+ "had a `version_id`, the server returned 404 for that id (the " +
455
+ "version may have been deleted or the agent_id changed) — verify " +
456
+ "with `aui agent --versions`.",
457
+ });
458
+ }
459
+ parentSpan.setAttribute("pull.dispatch.mode", pullModeResolution.mode);
460
+ parentSpan.setAttribute("pull.dispatch.mode_source", pullModeResolution.source);
461
+ const useLegacyExport = pullModeResolution.mode === "records";
462
+ // In the bundle-mode branch `activeVersionId` is guaranteed non-undefined
463
+ // (the conditional throw above bails for bundle + missing version). TS
464
+ // can't narrow across the throw, so the `!` assertion is documented
465
+ // intent. In the records-mode branch the value is unused — the legacy
466
+ // "Fetching agent data (legacy /view endpoints)..." string doesn't
467
+ // reference it.
468
+ const spinner = startSpinner(useLegacyExport
469
+ ? "Fetching agent data (legacy /view endpoints)..."
470
+ : options.tag
471
+ ? `Pulling bundle at tag ${options.tag} (version ${activeVersionLabel || activeVersionId.slice(0, 10) + "…"})...`
472
+ : `Pulling bundle ${activeVersionLabel || activeVersionId.slice(0, 10) + "…"}...`);
274
473
  try {
474
+ // ─── New blob endpoint OR legacy /view, decided by mode above ───
475
+ // `useLegacyExport` was set just before the spinner. We skip the
476
+ // /pull call entirely for records-mode agents — calling it would
477
+ // return 422 mode-mismatch and cost a useless round-trip.
478
+ let pullData = null;
479
+ // `useLegacyExport` is `let`-mutable because a server-side mode
480
+ // flip mid-pull (rare: admin toggles `bundle_mode` while we're
481
+ // dispatching) can force us to switch surfaces after the initial
482
+ // detection. We catch the 422 mode-mismatch below and convert it
483
+ // into a legacy-path fall-through.
484
+ let useLegacyExportLocal = useLegacyExport;
485
+ try {
486
+ if (!useLegacyExportLocal) {
487
+ // Bundle-mode path: `pullAgentMgmtId` and `activeVersionId`
488
+ // are both guaranteed non-undefined here (the conditional
489
+ // throws above bail when mode is bundle and either is
490
+ // missing). `!` assertions are documented intent — TS can't
491
+ // narrow across conditional throws.
492
+ pullData = await client.agentManagement.pullVersionBlobs(pullAgentMgmtId, activeVersionId, {
493
+ // Forward `--tag` so the user can pull a specific historical
494
+ // revision (e.g. `v3.2`). When omitted, the server picks the
495
+ // version row's current revision.
496
+ versionTag: options.tag,
497
+ });
498
+ }
499
+ }
500
+ catch (err) {
501
+ if (isModeMismatchError(err)) {
502
+ // Agent's mode changed since our detectAgentBundleMode call —
503
+ // server says it's records-mode now. Switch surfaces and
504
+ // continue. Single retry, no recursion.
505
+ parentSpan.addEvent("pull.mode_mismatch_falling_back_to_records", {
506
+ agent_management_id: pullAgentMgmtId ?? "(none)",
507
+ version_id: activeVersionId ?? "(none)",
508
+ });
509
+ useLegacyExportLocal = true;
510
+ }
511
+ else if (err instanceof AUIAPIError && err.status === 404) {
512
+ // Expected when the agent has never been Pushed via the new
513
+ // endpoint. Falls through to the "no blob revision" error
514
+ // (or the records-mode legacy export, if applicable).
515
+ if (process.env.AUI_DEBUG) {
516
+ console.log("[debug] new pull returned 404 — no blob revision for this version");
517
+ }
518
+ }
519
+ else if (err instanceof AUIAPIError &&
520
+ (err.status === 401 || err.status === 403) &&
521
+ !loadAgentSettingsApiKey()) {
522
+ spinner.warn("Authentication failed.");
523
+ log(_jsx(StatusLine, { kind: "warning", label: "Your access token may not have permission for the agent-settings pull endpoint." }));
524
+ log(_jsx(Hint, { message: "You can provide an API key as a fallback. It will be saved to ~/.aui/agent-settings-key" }));
525
+ const { key } = await inquirer.prompt([
526
+ {
527
+ type: "password",
528
+ name: "key",
529
+ message: "Paste the Agent Settings API key (or press Enter to abort):",
530
+ mask: "*",
531
+ },
532
+ ]);
533
+ if (key && key.trim()) {
534
+ saveAgentSettingsApiKey(key.trim());
535
+ client.setAgentSettingsApiKey(key.trim());
536
+ log(_jsx(StatusLine, { kind: "success", label: "Key saved. Retrying..." }));
537
+ const retrySpinner = startSpinner("Retrying pull with API key...");
538
+ try {
539
+ pullData = await client.agentManagement.pullVersionBlobs(pullAgentMgmtId, activeVersionId, { versionTag: options.tag });
540
+ retrySpinner.stop();
541
+ }
542
+ catch (retryErr) {
543
+ retrySpinner.stop();
544
+ if (retryErr instanceof AUIAPIError &&
545
+ retryErr.status === 404) {
546
+ // Same strict-mode error below.
547
+ }
548
+ else {
549
+ throw retryErr;
550
+ }
551
+ }
552
+ }
553
+ else {
554
+ throw err;
555
+ }
556
+ }
557
+ else {
558
+ throw err;
559
+ }
560
+ }
561
+ // ─── Shared output variables (set by either branch) ───
275
562
  let generalSettings = {
276
563
  general_settings: { name: projectConfig.agent_code },
277
564
  };
278
- let parameters = { parameters: [] };
279
- let entities = { entities: [] };
280
- let integrations = { integrations: [] };
281
- let tools = { tools: [] };
282
- let rules = { rules: [] };
283
- let exportData = await client.exportAgent(networkId, networkCategoryId, activeVersionId, options.scopeLevel);
284
- const hasAuthErrors = exportData.errors.some((e) => /\b(401|403)\b/.test(e) ||
285
- /unauthorized|forbidden|not.*member/i.test(e));
286
- if (hasAuthErrors && !loadAgentSettingsApiKey()) {
287
- spinner.warn("Authentication failed for some endpoints.");
288
- log(_jsx(StatusLine, { kind: "warning", label: "Your access token may not have permission for the agent-settings endpoints." }));
289
- log(_jsx(Hint, { message: "You can provide an API key as a fallback. It will be saved to ~/.aui/agent-settings-key" }));
290
- const { key } = await inquirer.prompt([
291
- {
292
- type: "password",
293
- name: "key",
294
- message: "Paste the Agent Settings API key (or press Enter to skip):",
295
- mask: "*",
296
- },
297
- ]);
298
- if (key && key.trim()) {
299
- saveAgentSettingsApiKey(key.trim());
300
- client.setAgentSettingsApiKey(key.trim());
301
- log(_jsx(StatusLine, { kind: "success", label: "Key saved. Retrying..." }));
302
- const retrySpinner = startSpinner("Retrying agent configuration fetch...");
303
- exportData = await client.exportAgent(networkId, networkCategoryId, activeVersionId, options.scopeLevel);
304
- retrySpinner.stop();
565
+ let paramCount = 0;
566
+ let entityCount = 0;
567
+ let integrationCount = 0;
568
+ let toolCount = 0;
569
+ let ruleCount = 0;
570
+ const writtenFiles = [];
571
+ // Revision tag from the response (e.g. "v8.14") — saved to
572
+ // `.auirc.version_tag` after the file write so the next `aui pull`
573
+ // can default to the same tag. NOT the same as `activeVersionLabel`
574
+ // (which stays as the major-only "v8" for telemetry / UX).
575
+ let resolvedVersionTag;
576
+ if (pullData) {
577
+ // ─── New bundle path: write files inline ───
578
+ if (pullData.version_tag) {
579
+ resolvedVersionTag = pullData.version_tag;
580
+ parentSpan.setAttribute("pull.version_tag", pullData.version_tag);
581
+ }
582
+ parentSpan.setAttribute("pull.path", "blob_endpoint");
583
+ const disassembled = disassembleBundleToFiles(pullData.bundle);
584
+ spinner.succeed(`Pulled bundle via /pull (${disassembled.length} file(s)) at ${resolvedVersionTag || activeVersionLabel || "current"}`);
585
+ // ─── Empty-domain warning banner ─────────────────────────────────
586
+ // Same rationale as `import-agent.tsx`: an empty domain in the
587
+ // pulled bundle is a real server-side state (not a CLI bug), but
588
+ // it's usually a sign the user pulled the wrong version. Surface
589
+ // every empty domain prominently so they can decide whether to
590
+ // re-pull a different version instead of being surprised by an
591
+ // empty `tools/` (or empty `entities`, etc.) on disk.
592
+ const emptyDomains = [];
593
+ if (!pullData.bundle.general_settings)
594
+ emptyDomains.push("general_settings");
595
+ if (!pullData.bundle.parameters || pullData.bundle.parameters.length === 0)
596
+ emptyDomains.push("parameters");
597
+ if (!pullData.bundle.entities || pullData.bundle.entities.length === 0)
598
+ emptyDomains.push("entities");
599
+ if (!pullData.bundle.integrations || pullData.bundle.integrations.length === 0)
600
+ emptyDomains.push("integrations");
601
+ if (!pullData.bundle.rules || pullData.bundle.rules.length === 0)
602
+ emptyDomains.push("rules");
603
+ // Tools live under `agent_tools` on the live `/pull` response and
604
+ // under `tools` per the OpenAPI schema. Either counts as "present".
605
+ const bundleTools = readBundleTools(pullData.bundle);
606
+ if (!bundleTools || bundleTools.length === 0)
607
+ emptyDomains.push("tools");
608
+ if (emptyDomains.length > 0) {
609
+ log(_jsxs(Box, { flexDirection: "column", paddingX: 1, marginY: 1, children: [_jsx(StatusLine, { kind: "warning", label: `Bundle at ${resolvedVersionTag || activeVersionLabel || "current"} has 0 ${emptyDomains.join(", 0 ")}.` }), _jsx(Hint, { message: "If you expected content in these domains, you may be on the wrong version. " +
610
+ "Check the LIVE version in the Playground or run `aui agent --versions` to see all versions on this agent, then re-pull with `--version <id>`." })] }));
611
+ parentSpan.setAttribute("pull.empty_domains", emptyDomains.join(","));
612
+ }
613
+ cleanAgentFiles(projectRoot);
614
+ const toolsDir = path.join(projectRoot, "tools");
615
+ if (!fs.existsSync(toolsDir)) {
616
+ fs.mkdirSync(toolsDir, { recursive: true });
617
+ }
618
+ for (const entry of disassembled) {
619
+ const targetPath = path.join(projectRoot, entry.relativePath);
620
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
621
+ // Normalize before write — disassemble already produces the
622
+ // canonical shape, but normalize handles any edge cases the
623
+ // server might have stored historically (legacy
624
+ // wrap/unwrap drift, alternate domain-key locations) so the
625
+ // on-disk file always matches the spec.
626
+ const normalized = normalizeBundleFileBody(path.basename(entry.relativePath), entry.body);
627
+ writeJson(targetPath, normalized);
628
+ writtenFiles.push(entry.relativePath);
629
+ const basename = path.basename(entry.relativePath);
630
+ if (basename === "agent.aui.json") {
631
+ generalSettings = normalized;
632
+ }
633
+ else if (basename === "parameters.aui.json") {
634
+ paramCount = (normalized?.parameters || []).length;
635
+ }
636
+ else if (basename === "entities.aui.json") {
637
+ entityCount = (normalized?.entities || []).length;
638
+ }
639
+ else if (basename === "integrations.aui.json") {
640
+ integrationCount =
641
+ (normalized?.integrations || []).length;
642
+ }
643
+ else if (basename === "rules.aui.json") {
644
+ ruleCount = (normalized?.rules || []).length;
645
+ }
646
+ else if (entry.relativePath.startsWith("tools/")) {
647
+ toolCount++;
648
+ }
649
+ else if (basename === "tools.aui.json") {
650
+ toolCount += (normalized?.tools || []).length;
651
+ }
305
652
  }
306
653
  }
307
- generalSettings =
308
- normalizeGeneralSettings(exportData.general_settings) || generalSettings;
309
- parameters =
310
- normalizeWrapped("parameters", exportData.parameters) || parameters;
311
- entities = normalizeWrapped("entities", exportData.entities) || entities;
312
- integrations =
313
- normalizeWrapped("integrations", exportData.integrations) || integrations;
314
- tools = normalizeWrapped("tools", exportData.tools) || tools;
315
- rules = normalizeWrapped("rules", exportData.rules) || rules;
316
- if (exportData.errors.length > 0) {
317
- if (parentSpan) {
654
+ else if (useLegacyExportLocal) {
655
+ // ─── Records-mode legacy export ───────────────────────────────
656
+ // Restored 2026-05-24 (per owner directive: "do the fallback
657
+ // exactly right carefully all the prev features should be
658
+ // available"). For agents with `bundle_mode=false` we use the
659
+ // per-entity /view endpoints (`parameters/view`,
660
+ // `scope-entities/view`, `integrations/view`, `agent-tools`,
661
+ // `agent-tools/rules`, `general-settings/view`) via
662
+ // `client.exportAgent(...)`, then reconstruct the canonical
663
+ // `.aui.json` layout locally. This block mirrors the legacy
664
+ // pull behaviour from before the blob migration — keep in
665
+ // sync with `import-agent.tsx`'s equivalent branch.
666
+ //
667
+ // Auth-failure handling: the legacy /view endpoints often
668
+ // 401/403 against an interactive session token even when the
669
+ // user has access via the agent-settings API key. We surface
670
+ // a one-time prompt for the key, save it, and retry once.
671
+ parentSpan.setAttribute("pull.path", "legacy_export");
672
+ let parameters = { parameters: [] };
673
+ let entities = { entities: [] };
674
+ let integrations = { integrations: [] };
675
+ let tools = { tools: [] };
676
+ let rules = { rules: [] };
677
+ let exportData = await client.exportAgent(networkId, networkCategoryId, activeVersionId, options.scopeLevel);
678
+ const hasAuthErrors = exportData.errors.some((e) => /\b(401|403)\b/.test(e) ||
679
+ /unauthorized|forbidden|not.*member/i.test(e));
680
+ if (hasAuthErrors && !loadAgentSettingsApiKey()) {
681
+ spinner.warn("Authentication failed for some endpoints.");
682
+ log(_jsx(StatusLine, { kind: "warning", label: "Your access token may not have permission for the agent-settings endpoints." }));
683
+ log(_jsx(Hint, { message: "You can provide an API key as a fallback. It will be saved to ~/.aui/agent-settings-key" }));
684
+ const { key } = await inquirer.prompt([
685
+ {
686
+ type: "password",
687
+ name: "key",
688
+ message: "Paste the Agent Settings API key (or press Enter to skip):",
689
+ mask: "*",
690
+ },
691
+ ]);
692
+ if (key && key.trim()) {
693
+ saveAgentSettingsApiKey(key.trim());
694
+ client.setAgentSettingsApiKey(key.trim());
695
+ log(_jsx(StatusLine, { kind: "success", label: "Key saved. Retrying..." }));
696
+ const retrySpinner = startSpinner("Retrying agent configuration fetch...");
697
+ exportData = await client.exportAgent(networkId, networkCategoryId, activeVersionId, options.scopeLevel);
698
+ retrySpinner.stop();
699
+ }
700
+ }
701
+ generalSettings =
702
+ normalizeGeneralSettingsLegacy(exportData.general_settings) ||
703
+ generalSettings;
704
+ parameters =
705
+ normalizeWrappedLegacy("parameters", exportData.parameters) ||
706
+ parameters;
707
+ entities =
708
+ normalizeWrappedLegacy("entities", exportData.entities) || entities;
709
+ integrations =
710
+ normalizeWrappedLegacy("integrations", exportData.integrations) ||
711
+ integrations;
712
+ tools = normalizeWrappedLegacy("tools", exportData.tools) || tools;
713
+ rules = normalizeWrappedLegacy("rules", exportData.rules) || rules;
714
+ if (exportData.errors.length > 0) {
318
715
  parentSpan.setStatus({
319
716
  code: SpanStatusCode.ERROR,
320
717
  message: exportData.errors.join("; "),
@@ -322,80 +719,126 @@ async function _pullAgent(parentSpan, options = {}) {
322
719
  for (const e of exportData.errors) {
323
720
  parentSpan.recordException(new Error(e));
324
721
  }
325
- parentSpan.setAttribute("pull.failed_endpoints", exportData.errors);
722
+ parentSpan.setAttribute("pull.failed_endpoints", exportData.errors.join(","));
723
+ spinner.warn(`Fetched agent configuration (${exportData.errors.length} endpoint(s) failed)`);
724
+ log(_jsx(ImportWarnings, { errors: exportData.errors }));
326
725
  }
327
- spinner.warn(`Fetched agent configuration (${exportData.errors.length} endpoint(s) failed)`);
328
- log(_jsx(ImportWarnings, { errors: exportData.errors }));
329
- }
330
- else {
331
- spinner.succeed("Agent configuration fetched (all 6 endpoints OK)");
332
- }
333
- // Clean existing agent files before writing new ones
334
- cleanAgentFiles(projectRoot);
335
- const toolsDir = path.join(projectRoot, "tools");
336
- if (!fs.existsSync(toolsDir)) {
337
- fs.mkdirSync(toolsDir, { recursive: true });
338
- }
339
- const writtenFiles = [];
340
- writeJson(path.join(projectRoot, "agent.aui.json"), generalSettings);
341
- writtenFiles.push("agent.aui.json");
342
- writeJson(path.join(projectRoot, "parameters.aui.json"), parameters);
343
- writtenFiles.push("parameters.aui.json");
344
- writeJson(path.join(projectRoot, "entities.aui.json"), entities);
345
- writtenFiles.push("entities.aui.json");
346
- writeJson(path.join(projectRoot, "integrations.aui.json"), integrations);
347
- writtenFiles.push("integrations.aui.json");
348
- writeJson(path.join(projectRoot, "rules.aui.json"), rules);
349
- writtenFiles.push("rules.aui.json");
350
- const toolsData = tools;
351
- if (toolsData.tools &&
352
- Array.isArray(toolsData.tools) &&
353
- toolsData.tools.length > 0) {
354
- for (const tool of toolsData.tools) {
355
- const toolCode = (tool.code ||
356
- tool.name ||
357
- tool.tool_name ||
358
- "unknown");
359
- const fileName = `${toolCode.toLowerCase().replace(/\s+/g, "_")}.aui.json`;
360
- writeJson(path.join(toolsDir, fileName), { tool });
361
- writtenFiles.push(`tools/${fileName}`);
726
+ else {
727
+ spinner.succeed("Agent configuration fetched (all 6 endpoints OK)");
362
728
  }
729
+ cleanAgentFiles(projectRoot);
730
+ const toolsDir = path.join(projectRoot, "tools");
731
+ if (!fs.existsSync(toolsDir)) {
732
+ fs.mkdirSync(toolsDir, { recursive: true });
733
+ }
734
+ writeJson(path.join(projectRoot, "agent.aui.json"), generalSettings);
735
+ writtenFiles.push("agent.aui.json");
736
+ writeJson(path.join(projectRoot, "parameters.aui.json"), parameters);
737
+ writtenFiles.push("parameters.aui.json");
738
+ writeJson(path.join(projectRoot, "entities.aui.json"), entities);
739
+ writtenFiles.push("entities.aui.json");
740
+ writeJson(path.join(projectRoot, "integrations.aui.json"), integrations);
741
+ writtenFiles.push("integrations.aui.json");
742
+ writeJson(path.join(projectRoot, "rules.aui.json"), rules);
743
+ writtenFiles.push("rules.aui.json");
744
+ // Per-tool file layout matches the blob disassembler: one
745
+ // `tools/<tool_code>.aui.json` per tool, wrapped under `{ tool }`.
746
+ // Empty tool list collapses to a single `tools.aui.json`
747
+ // wrapper so the rest of the toolchain doesn't choke on a
748
+ // missing baseline.
749
+ const toolsData = tools;
750
+ if (toolsData.tools &&
751
+ Array.isArray(toolsData.tools) &&
752
+ toolsData.tools.length > 0) {
753
+ for (const tool of toolsData.tools) {
754
+ const toolCode = (tool.code ||
755
+ tool.name ||
756
+ tool.tool_name ||
757
+ "unknown");
758
+ const fileName = `${toolCode
759
+ .toLowerCase()
760
+ .replace(/\s+/g, "_")}.aui.json`;
761
+ writeJson(path.join(toolsDir, fileName), { tool });
762
+ writtenFiles.push(`tools/${fileName}`);
763
+ }
764
+ }
765
+ else {
766
+ writeJson(path.join(projectRoot, "tools.aui.json"), tools);
767
+ writtenFiles.push("tools.aui.json");
768
+ }
769
+ paramCount = (parameters?.parameters || []).length;
770
+ entityCount = (entities?.entities || []).length;
771
+ integrationCount =
772
+ (integrations?.integrations || []).length;
773
+ toolCount = (toolsData.tools || []).length;
774
+ ruleCount = (rules?.rules || []).length;
363
775
  }
364
776
  else {
365
- writeJson(path.join(projectRoot, "tools.aui.json"), tools);
366
- writtenFiles.push("tools.aui.json");
777
+ // ─── Bundle-mode but no revision found ───
778
+ // The agent IS in blob-mode (we just checked) and `/pull`
779
+ // returned 404, meaning no bundle has been uploaded for this
780
+ // version_id yet. Throw a clear error pointing at `aui push`
781
+ // — the user almost certainly meant to seed the version
782
+ // first. This is a different failure mode from "agent doesn't
783
+ // exist" (which would have been caught earlier in mode
784
+ // detection).
785
+ parentSpan.setAttribute("pull.path", "blob_endpoint_404");
786
+ spinner.fail("No blob revision found at this version.");
787
+ throw new CLIError(options.tag
788
+ ? `No blob revision found at tag ${options.tag} for version ${activeVersionLabel || activeVersionId}.`
789
+ : `No blob revision found for version ${activeVersionLabel || activeVersionId}.`, {
790
+ suggestion: "Run `aui push` first to create the initial blob revision via the new /push endpoint, then re-run `aui pull`.",
791
+ });
367
792
  }
368
- const paramCount = parameters?.parameters?.length || 0;
369
- const entityCount = entities?.entities?.length || 0;
370
- const integrationCount = integrations?.integrations?.length || 0;
371
- const toolCount = (toolsData.tools || []).length;
372
- const ruleCount = rules?.rules?.length || 0;
793
+ // ─── Skip KB export for templates ─────────────────────────────
794
+ //
795
+ // Templates are NETWORK_CATEGORY-scoped and have no `network_id`
796
+ // / `account_id` on their scope row, so the KB endpoint
797
+ // (`knowledge-base-manager/v1/knowledge-bases/view/export`)
798
+ // rejects with 422 ("Id must be of type PydanticObjectId") on the
799
+ // empty account_id query param. Mirror the same skip in
800
+ // `import-agent.tsx` — KBs are inherently network-scoped and
801
+ // templates don't have them in this data model. See
802
+ // `import-agent.tsx` for the longer comment + reproduction
803
+ // trace from 2026-05-24.
804
+ //
805
+ // `resolvedAgentInfo` is populated by `findAgentMgmtId` above
806
+ // (branch 1 = .auirc canonical id), so `kind` is reliable for
807
+ // any project that ran `aui import-agent` on a recent CLI
808
+ // version. Falls back to `scope.type` for legacy/edge cases.
809
+ const isTemplateForKb = resolvedAgentInfo?.kind === "template" ||
810
+ resolvedAgentInfo?.scope?.type === "NETWORK_CATEGORY";
373
811
  const kbExportPromise = options.skipKbFiles
374
812
  ? Promise.resolve({ kind: "skipped" })
375
- : (async () => {
376
- try {
377
- const kbClient = new KBViewClient({
378
- authToken: config.authToken,
379
- apiKey: loadAgentSettingsApiKey() || options.apiKey,
380
- organizationId: projectConfig.organization_id || config.organizationId || "",
381
- environment: config.environment || "staging",
382
- });
383
- const scope = buildScope({
384
- networkId,
385
- organizationId: projectConfig.organization_id || config.organizationId || "",
386
- accountId: projectConfig.account_id || config.accountId || "",
387
- });
388
- const result = await exportToFolder(kbClient, scope, projectRoot, options.withKbFiles ?? false);
389
- return { kind: "ok", result };
390
- }
391
- catch (kbError) {
392
- return {
393
- kind: "error",
394
- error: kbError,
395
- message: kbError instanceof Error ? kbError.message : String(kbError),
396
- };
397
- }
398
- })();
813
+ : isTemplateForKb
814
+ ? Promise.resolve({
815
+ kind: "skipped",
816
+ reason: "templates are NETWORK_CATEGORY-scoped and don't have knowledge bases",
817
+ })
818
+ : (async () => {
819
+ try {
820
+ const kbClient = new KBViewClient({
821
+ authToken: config.authToken,
822
+ apiKey: loadAgentSettingsApiKey() || options.apiKey,
823
+ organizationId: projectConfig.organization_id || config.organizationId || "",
824
+ environment: config.environment || "staging",
825
+ });
826
+ const scope = buildScope({
827
+ networkId,
828
+ organizationId: projectConfig.organization_id || config.organizationId || "",
829
+ accountId: projectConfig.account_id || config.accountId || "",
830
+ });
831
+ const result = await exportToFolder(kbClient, scope, projectRoot, options.withKbFiles ?? false);
832
+ return { kind: "ok", result };
833
+ }
834
+ catch (kbError) {
835
+ return {
836
+ kind: "error",
837
+ error: kbError,
838
+ message: kbError instanceof Error ? kbError.message : String(kbError),
839
+ };
840
+ }
841
+ })();
399
842
  const schemaPromise = (async () => {
400
843
  try {
401
844
  const data = await fetchSchemas({
@@ -423,7 +866,9 @@ async function _pullAgent(parentSpan, options = {}) {
423
866
  parallelSpinner.stop();
424
867
  // Knowledge-hub status
425
868
  if (kbOutcome.kind === "skipped") {
426
- log(_jsx(StatusLine, { kind: "muted", label: "Skipping knowledge hub export (--skip-kb-files)" }));
869
+ log(_jsx(StatusLine, { kind: "muted", label: kbOutcome.reason
870
+ ? `Skipping knowledge hub export — ${kbOutcome.reason}.`
871
+ : "Skipping knowledge hub export (--skip-kb-files)" }));
427
872
  }
428
873
  else if (kbOutcome.kind === "ok") {
429
874
  const r = kbOutcome.result;
@@ -459,13 +904,36 @@ async function _pullAgent(parentSpan, options = {}) {
459
904
  else {
460
905
  log(_jsx(StatusLine, { kind: "warning", label: "Failed to pull schemas (non-blocking)" }));
461
906
  }
462
- // Preserve original behaviour: a KB export failure aborts `pull`.
907
+ // KB export failure is non-blocking same UX as `aui import-agent`.
908
+ //
909
+ // Rationale (2026-05-24): the main bundle (`agent.aui.json`,
910
+ // `parameters.aui.json`, `entities.aui.json`, `integrations.aui.json`,
911
+ // `rules.aui.json`, `tools/*.aui.json`) has already been written to
912
+ // disk successfully at this point — the only thing missing is the
913
+ // `knowledge-hubs/` folder. Killing the whole `aui pull` and
914
+ // discarding the .auirc / version_tag updates over a transient KB
915
+ // service issue (e.g. the GCS object-rate-limit on staging that
916
+ // surfaces as a 500 wrapping a 429) is a worse UX than the
917
+ // import path's "tell the user, keep going" behaviour. Both paths
918
+ // now treat KB export the same way:
919
+ //
920
+ // 1. Log the failure prominently with the full server message
921
+ // (already done above at line ~1110).
922
+ // 2. Show an actionable hint pointing at `aui rag --export` /
923
+ // retry, instead of crashing.
924
+ // 3. Continue with the .auirc / baseline writes below so the
925
+ // project's tracking state matches what was actually pulled.
926
+ //
927
+ // The KB failure is still visible in `aui curl` and in the
928
+ // pre-existing per-task push-logs, so debugging the underlying
929
+ // GCS or KB-service issue stays straightforward.
463
930
  if (kbOutcome.kind === "error") {
464
- throw kbOutcome.error;
931
+ log(_jsx(StatusLine, { kind: "muted", label: "Knowledge hub export failed (non-blocking). Try again with `aui rag --export` or re-run `aui pull` once the KB service recovers." }));
465
932
  }
466
933
  // Update .auirc with the pulled version info and any newly resolved
467
934
  // agent-management id (back-fills it for projects that pre-date the field).
468
935
  const shouldRewriteConfig = !!activeVersionId ||
936
+ !!resolvedVersionTag ||
469
937
  (resolvedAgentManagementId &&
470
938
  projectConfig.agent_management_id !== resolvedAgentManagementId);
471
939
  if (shouldRewriteConfig) {
@@ -476,6 +944,10 @@ async function _pullAgent(parentSpan, options = {}) {
476
944
  : {}),
477
945
  ...(activeVersionId ? { version_id: activeVersionId } : {}),
478
946
  ...(activeVersionLabel ? { version_label: activeVersionLabel } : {}),
947
+ // Persist the revision tag from the manifest (e.g. "v8.14")
948
+ // so the next `aui pull` can default `?version_tag=…` to it
949
+ // without an extra round-trip.
950
+ ...(resolvedVersionTag ? { version_tag: resolvedVersionTag } : {}),
479
951
  ...(options.scopeLevel ? { scope_level: options.scopeLevel } : {}),
480
952
  }, projectRoot);
481
953
  }