aui-agent-builder 0.3.103 → 0.3.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/api-client/index.d.ts +304 -19
  2. package/dist/api-client/index.d.ts.map +1 -1
  3. package/dist/api-client/index.js +337 -69
  4. package/dist/api-client/index.js.map +1 -1
  5. package/dist/commands/agents.d.ts +55 -1
  6. package/dist/commands/agents.d.ts.map +1 -1
  7. package/dist/commands/agents.js +193 -50
  8. package/dist/commands/agents.js.map +1 -1
  9. package/dist/commands/import-agent.d.ts +23 -0
  10. package/dist/commands/import-agent.d.ts.map +1 -1
  11. package/dist/commands/import-agent.js +802 -151
  12. package/dist/commands/import-agent.js.map +1 -1
  13. package/dist/commands/legacy/push-records-mode.d.ts +166 -0
  14. package/dist/commands/legacy/push-records-mode.d.ts.map +1 -0
  15. package/dist/commands/legacy/push-records-mode.js +2536 -0
  16. package/dist/commands/legacy/push-records-mode.js.map +1 -0
  17. package/dist/commands/pull-agent.d.ts +8 -0
  18. package/dist/commands/pull-agent.d.ts.map +1 -1
  19. package/dist/commands/pull-agent.js +567 -126
  20. package/dist/commands/pull-agent.js.map +1 -1
  21. package/dist/commands/push.d.ts +78 -107
  22. package/dist/commands/push.d.ts.map +1 -1
  23. package/dist/commands/push.js +942 -1804
  24. package/dist/commands/push.js.map +1 -1
  25. package/dist/commands/util/agent-mode.d.ts +69 -0
  26. package/dist/commands/util/agent-mode.d.ts.map +1 -0
  27. package/dist/commands/util/agent-mode.js +101 -0
  28. package/dist/commands/util/agent-mode.js.map +1 -0
  29. package/dist/commands/validate.d.ts.map +1 -1
  30. package/dist/commands/validate.js +23 -7
  31. package/dist/commands/validate.js.map +1 -1
  32. package/dist/commands/version-snapshot.d.ts.map +1 -1
  33. package/dist/commands/version-snapshot.js +253 -49
  34. package/dist/commands/version-snapshot.js.map +1 -1
  35. package/dist/commands/version.d.ts +15 -1
  36. package/dist/commands/version.d.ts.map +1 -1
  37. package/dist/commands/version.js +102 -7
  38. package/dist/commands/version.js.map +1 -1
  39. package/dist/config/index.d.ts +16 -1
  40. package/dist/config/index.d.ts.map +1 -1
  41. package/dist/config/index.js.map +1 -1
  42. package/dist/index.js +20 -5
  43. package/dist/index.js.map +1 -1
  44. package/dist/ui/views/ImportAgentView.d.ts +15 -0
  45. package/dist/ui/views/ImportAgentView.d.ts.map +1 -1
  46. package/dist/ui/views/ImportAgentView.js +8 -3
  47. package/dist/ui/views/ImportAgentView.js.map +1 -1
  48. package/dist/utils/index.d.ts +80 -0
  49. package/dist/utils/index.d.ts.map +1 -1
  50. package/dist/utils/index.js +330 -0
  51. package/dist/utils/index.js.map +1 -1
  52. package/package.json +1 -1
@@ -1,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,297 @@ 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 → give up loud.
398
+ const pullAgentMgmtId = projectConfig.agent_management_id ||
399
+ resolvedAgentManagementId ||
400
+ (await findAgentMgmtId());
401
+ if (!pullAgentMgmtId) {
402
+ throw new ConfigError("Could not resolve agent-management id for this project.", {
403
+ suggestion: "Re-run `aui import-agent` to back-fill `.auirc.agent_management_id`, then try `aui pull` again.",
404
+ });
405
+ }
406
+ if (!activeVersionId) {
407
+ throw new ConfigError("No version_id available to pull — the new pull endpoint requires one.", {
408
+ suggestion: "Pass `--version <version-id>` explicitly, or re-run `aui import-agent` " +
409
+ "to refresh `.auirc.version_id`. If the project's `.auirc` already " +
410
+ "had a `version_id`, the server returned 404 for that id (the " +
411
+ "version may have been deleted or the agent_id changed) — verify " +
412
+ "with `aui agent --versions`.",
413
+ });
414
+ }
415
+ // ─── Mode dispatch: blob (/pull) vs records (legacy /view) ───
416
+ // Same dispatch as `push.tsx`. Reads `Agent.bundle_mode` on the
417
+ // resolved agent record and decides which API surface to use:
418
+ //
419
+ // - `bundle_mode=true` → call the new `/pull` endpoint below
420
+ // and write the assembled bundle inline.
421
+ // - `bundle_mode=false` → call legacy `client.exportAgent(...)`
422
+ // - `bundle_mode` missing (older backend; per-domain export +
423
+ // local bundle reconstruction).
424
+ //
425
+ // The two surfaces are mutually exclusive: a `/pull` request against
426
+ // a records-mode agent returns 422 mode-mismatch, and a legacy
427
+ // /view export against a blob-mode agent returns empty arrays for
428
+ // every domain (silently wrong — the worst kind of mismatch). The
429
+ // up-front check guarantees we go to the right surface on the
430
+ // first try.
431
+ // Pass the AgentInfo from `findAgentMgmtId` so the dispatcher
432
+ // doesn't re-fetch what we already have. `resolvedAgentInfo` is
433
+ // only populated when `findAgentMgmtId` ran (the common case);
434
+ // if `.auirc.agent_management_id` short-circuited the resolver,
435
+ // `cachedInfo` will be undefined and the dispatcher fetches it
436
+ // itself — same safe fallback as before, just one fewer call in
437
+ // the happy path.
438
+ const pullModeResolution = await detectAgentBundleMode(client, pullAgentMgmtId, resolvedAgentInfo);
439
+ parentSpan.setAttribute("pull.dispatch.mode", pullModeResolution.mode);
440
+ parentSpan.setAttribute("pull.dispatch.mode_source", pullModeResolution.source);
441
+ const useLegacyExport = pullModeResolution.mode === "records";
442
+ const spinner = startSpinner(useLegacyExport
443
+ ? "Fetching agent data (legacy /view endpoints)..."
444
+ : options.tag
445
+ ? `Pulling bundle at tag ${options.tag} (version ${activeVersionLabel || activeVersionId.slice(0, 10) + "…"})...`
446
+ : `Pulling bundle ${activeVersionLabel || activeVersionId.slice(0, 10) + "…"}...`);
274
447
  try {
448
+ // ─── New blob endpoint OR legacy /view, decided by mode above ───
449
+ // `useLegacyExport` was set just before the spinner. We skip the
450
+ // /pull call entirely for records-mode agents — calling it would
451
+ // return 422 mode-mismatch and cost a useless round-trip.
452
+ let pullData = null;
453
+ // `useLegacyExport` is `let`-mutable because a server-side mode
454
+ // flip mid-pull (rare: admin toggles `bundle_mode` while we're
455
+ // dispatching) can force us to switch surfaces after the initial
456
+ // detection. We catch the 422 mode-mismatch below and convert it
457
+ // into a legacy-path fall-through.
458
+ let useLegacyExportLocal = useLegacyExport;
459
+ try {
460
+ if (!useLegacyExportLocal) {
461
+ pullData = await client.agentManagement.pullVersionBlobs(pullAgentMgmtId, activeVersionId, {
462
+ // Forward `--tag` so the user can pull a specific historical
463
+ // revision (e.g. `v3.2`). When omitted, the server picks the
464
+ // version row's current revision.
465
+ versionTag: options.tag,
466
+ });
467
+ }
468
+ }
469
+ catch (err) {
470
+ if (isModeMismatchError(err)) {
471
+ // Agent's mode changed since our detectAgentBundleMode call —
472
+ // server says it's records-mode now. Switch surfaces and
473
+ // continue. Single retry, no recursion.
474
+ parentSpan.addEvent("pull.mode_mismatch_falling_back_to_records", {
475
+ agent_management_id: pullAgentMgmtId,
476
+ version_id: activeVersionId,
477
+ });
478
+ useLegacyExportLocal = true;
479
+ }
480
+ else if (err instanceof AUIAPIError && err.status === 404) {
481
+ // Expected when the agent has never been Pushed via the new
482
+ // endpoint. Falls through to the "no blob revision" error
483
+ // (or the records-mode legacy export, if applicable).
484
+ if (process.env.AUI_DEBUG) {
485
+ console.log("[debug] new pull returned 404 — no blob revision for this version");
486
+ }
487
+ }
488
+ else if (err instanceof AUIAPIError &&
489
+ (err.status === 401 || err.status === 403) &&
490
+ !loadAgentSettingsApiKey()) {
491
+ spinner.warn("Authentication failed.");
492
+ log(_jsx(StatusLine, { kind: "warning", label: "Your access token may not have permission for the agent-settings pull endpoint." }));
493
+ log(_jsx(Hint, { message: "You can provide an API key as a fallback. It will be saved to ~/.aui/agent-settings-key" }));
494
+ const { key } = await inquirer.prompt([
495
+ {
496
+ type: "password",
497
+ name: "key",
498
+ message: "Paste the Agent Settings API key (or press Enter to abort):",
499
+ mask: "*",
500
+ },
501
+ ]);
502
+ if (key && key.trim()) {
503
+ saveAgentSettingsApiKey(key.trim());
504
+ client.setAgentSettingsApiKey(key.trim());
505
+ log(_jsx(StatusLine, { kind: "success", label: "Key saved. Retrying..." }));
506
+ const retrySpinner = startSpinner("Retrying pull with API key...");
507
+ try {
508
+ pullData = await client.agentManagement.pullVersionBlobs(pullAgentMgmtId, activeVersionId, { versionTag: options.tag });
509
+ retrySpinner.stop();
510
+ }
511
+ catch (retryErr) {
512
+ retrySpinner.stop();
513
+ if (retryErr instanceof AUIAPIError &&
514
+ retryErr.status === 404) {
515
+ // Same strict-mode error below.
516
+ }
517
+ else {
518
+ throw retryErr;
519
+ }
520
+ }
521
+ }
522
+ else {
523
+ throw err;
524
+ }
525
+ }
526
+ else {
527
+ throw err;
528
+ }
529
+ }
530
+ // ─── Shared output variables (set by either branch) ───
275
531
  let generalSettings = {
276
532
  general_settings: { name: projectConfig.agent_code },
277
533
  };
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();
534
+ let paramCount = 0;
535
+ let entityCount = 0;
536
+ let integrationCount = 0;
537
+ let toolCount = 0;
538
+ let ruleCount = 0;
539
+ const writtenFiles = [];
540
+ // Revision tag from the response (e.g. "v8.14") — saved to
541
+ // `.auirc.version_tag` after the file write so the next `aui pull`
542
+ // can default to the same tag. NOT the same as `activeVersionLabel`
543
+ // (which stays as the major-only "v8" for telemetry / UX).
544
+ let resolvedVersionTag;
545
+ if (pullData) {
546
+ // ─── New bundle path: write files inline ───
547
+ if (pullData.version_tag) {
548
+ resolvedVersionTag = pullData.version_tag;
549
+ parentSpan.setAttribute("pull.version_tag", pullData.version_tag);
550
+ }
551
+ parentSpan.setAttribute("pull.path", "blob_endpoint");
552
+ const disassembled = disassembleBundleToFiles(pullData.bundle);
553
+ spinner.succeed(`Pulled bundle via /pull (${disassembled.length} file(s)) at ${resolvedVersionTag || activeVersionLabel || "current"}`);
554
+ // ─── Empty-domain warning banner ─────────────────────────────────
555
+ // Same rationale as `import-agent.tsx`: an empty domain in the
556
+ // pulled bundle is a real server-side state (not a CLI bug), but
557
+ // it's usually a sign the user pulled the wrong version. Surface
558
+ // every empty domain prominently so they can decide whether to
559
+ // re-pull a different version instead of being surprised by an
560
+ // empty `tools/` (or empty `entities`, etc.) on disk.
561
+ const emptyDomains = [];
562
+ if (!pullData.bundle.general_settings)
563
+ emptyDomains.push("general_settings");
564
+ if (!pullData.bundle.parameters || pullData.bundle.parameters.length === 0)
565
+ emptyDomains.push("parameters");
566
+ if (!pullData.bundle.entities || pullData.bundle.entities.length === 0)
567
+ emptyDomains.push("entities");
568
+ if (!pullData.bundle.integrations || pullData.bundle.integrations.length === 0)
569
+ emptyDomains.push("integrations");
570
+ if (!pullData.bundle.rules || pullData.bundle.rules.length === 0)
571
+ emptyDomains.push("rules");
572
+ // Tools live under `agent_tools` on the live `/pull` response and
573
+ // under `tools` per the OpenAPI schema. Either counts as "present".
574
+ const bundleTools = readBundleTools(pullData.bundle);
575
+ if (!bundleTools || bundleTools.length === 0)
576
+ emptyDomains.push("tools");
577
+ if (emptyDomains.length > 0) {
578
+ 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. " +
579
+ "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>`." })] }));
580
+ parentSpan.setAttribute("pull.empty_domains", emptyDomains.join(","));
581
+ }
582
+ cleanAgentFiles(projectRoot);
583
+ const toolsDir = path.join(projectRoot, "tools");
584
+ if (!fs.existsSync(toolsDir)) {
585
+ fs.mkdirSync(toolsDir, { recursive: true });
586
+ }
587
+ for (const entry of disassembled) {
588
+ const targetPath = path.join(projectRoot, entry.relativePath);
589
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
590
+ // Normalize before write — disassemble already produces the
591
+ // canonical shape, but normalize handles any edge cases the
592
+ // server might have stored historically (legacy
593
+ // wrap/unwrap drift, alternate domain-key locations) so the
594
+ // on-disk file always matches the spec.
595
+ const normalized = normalizeBundleFileBody(path.basename(entry.relativePath), entry.body);
596
+ writeJson(targetPath, normalized);
597
+ writtenFiles.push(entry.relativePath);
598
+ const basename = path.basename(entry.relativePath);
599
+ if (basename === "agent.aui.json") {
600
+ generalSettings = normalized;
601
+ }
602
+ else if (basename === "parameters.aui.json") {
603
+ paramCount = (normalized?.parameters || []).length;
604
+ }
605
+ else if (basename === "entities.aui.json") {
606
+ entityCount = (normalized?.entities || []).length;
607
+ }
608
+ else if (basename === "integrations.aui.json") {
609
+ integrationCount =
610
+ (normalized?.integrations || []).length;
611
+ }
612
+ else if (basename === "rules.aui.json") {
613
+ ruleCount = (normalized?.rules || []).length;
614
+ }
615
+ else if (entry.relativePath.startsWith("tools/")) {
616
+ toolCount++;
617
+ }
618
+ else if (basename === "tools.aui.json") {
619
+ toolCount += (normalized?.tools || []).length;
620
+ }
305
621
  }
306
622
  }
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) {
623
+ else if (useLegacyExportLocal) {
624
+ // ─── Records-mode legacy export ───────────────────────────────
625
+ // Restored 2026-05-24 (per owner directive: "do the fallback
626
+ // exactly right carefully all the prev features should be
627
+ // available"). For agents with `bundle_mode=false` we use the
628
+ // per-entity /view endpoints (`parameters/view`,
629
+ // `scope-entities/view`, `integrations/view`, `agent-tools`,
630
+ // `agent-tools/rules`, `general-settings/view`) via
631
+ // `client.exportAgent(...)`, then reconstruct the canonical
632
+ // `.aui.json` layout locally. This block mirrors the legacy
633
+ // pull behaviour from before the blob migration — keep in
634
+ // sync with `import-agent.tsx`'s equivalent branch.
635
+ //
636
+ // Auth-failure handling: the legacy /view endpoints often
637
+ // 401/403 against an interactive session token even when the
638
+ // user has access via the agent-settings API key. We surface
639
+ // a one-time prompt for the key, save it, and retry once.
640
+ parentSpan.setAttribute("pull.path", "legacy_export");
641
+ let parameters = { parameters: [] };
642
+ let entities = { entities: [] };
643
+ let integrations = { integrations: [] };
644
+ let tools = { tools: [] };
645
+ let rules = { rules: [] };
646
+ let exportData = await client.exportAgent(networkId, networkCategoryId, activeVersionId, options.scopeLevel);
647
+ const hasAuthErrors = exportData.errors.some((e) => /\b(401|403)\b/.test(e) ||
648
+ /unauthorized|forbidden|not.*member/i.test(e));
649
+ if (hasAuthErrors && !loadAgentSettingsApiKey()) {
650
+ spinner.warn("Authentication failed for some endpoints.");
651
+ log(_jsx(StatusLine, { kind: "warning", label: "Your access token may not have permission for the agent-settings endpoints." }));
652
+ log(_jsx(Hint, { message: "You can provide an API key as a fallback. It will be saved to ~/.aui/agent-settings-key" }));
653
+ const { key } = await inquirer.prompt([
654
+ {
655
+ type: "password",
656
+ name: "key",
657
+ message: "Paste the Agent Settings API key (or press Enter to skip):",
658
+ mask: "*",
659
+ },
660
+ ]);
661
+ if (key && key.trim()) {
662
+ saveAgentSettingsApiKey(key.trim());
663
+ client.setAgentSettingsApiKey(key.trim());
664
+ log(_jsx(StatusLine, { kind: "success", label: "Key saved. Retrying..." }));
665
+ const retrySpinner = startSpinner("Retrying agent configuration fetch...");
666
+ exportData = await client.exportAgent(networkId, networkCategoryId, activeVersionId, options.scopeLevel);
667
+ retrySpinner.stop();
668
+ }
669
+ }
670
+ generalSettings =
671
+ normalizeGeneralSettingsLegacy(exportData.general_settings) ||
672
+ generalSettings;
673
+ parameters =
674
+ normalizeWrappedLegacy("parameters", exportData.parameters) ||
675
+ parameters;
676
+ entities =
677
+ normalizeWrappedLegacy("entities", exportData.entities) || entities;
678
+ integrations =
679
+ normalizeWrappedLegacy("integrations", exportData.integrations) ||
680
+ integrations;
681
+ tools = normalizeWrappedLegacy("tools", exportData.tools) || tools;
682
+ rules = normalizeWrappedLegacy("rules", exportData.rules) || rules;
683
+ if (exportData.errors.length > 0) {
318
684
  parentSpan.setStatus({
319
685
  code: SpanStatusCode.ERROR,
320
686
  message: exportData.errors.join("; "),
@@ -322,80 +688,126 @@ async function _pullAgent(parentSpan, options = {}) {
322
688
  for (const e of exportData.errors) {
323
689
  parentSpan.recordException(new Error(e));
324
690
  }
325
- parentSpan.setAttribute("pull.failed_endpoints", exportData.errors);
691
+ parentSpan.setAttribute("pull.failed_endpoints", exportData.errors.join(","));
692
+ spinner.warn(`Fetched agent configuration (${exportData.errors.length} endpoint(s) failed)`);
693
+ log(_jsx(ImportWarnings, { errors: exportData.errors }));
326
694
  }
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}`);
695
+ else {
696
+ spinner.succeed("Agent configuration fetched (all 6 endpoints OK)");
697
+ }
698
+ cleanAgentFiles(projectRoot);
699
+ const toolsDir = path.join(projectRoot, "tools");
700
+ if (!fs.existsSync(toolsDir)) {
701
+ fs.mkdirSync(toolsDir, { recursive: true });
362
702
  }
703
+ writeJson(path.join(projectRoot, "agent.aui.json"), generalSettings);
704
+ writtenFiles.push("agent.aui.json");
705
+ writeJson(path.join(projectRoot, "parameters.aui.json"), parameters);
706
+ writtenFiles.push("parameters.aui.json");
707
+ writeJson(path.join(projectRoot, "entities.aui.json"), entities);
708
+ writtenFiles.push("entities.aui.json");
709
+ writeJson(path.join(projectRoot, "integrations.aui.json"), integrations);
710
+ writtenFiles.push("integrations.aui.json");
711
+ writeJson(path.join(projectRoot, "rules.aui.json"), rules);
712
+ writtenFiles.push("rules.aui.json");
713
+ // Per-tool file layout matches the blob disassembler: one
714
+ // `tools/<tool_code>.aui.json` per tool, wrapped under `{ tool }`.
715
+ // Empty tool list collapses to a single `tools.aui.json`
716
+ // wrapper so the rest of the toolchain doesn't choke on a
717
+ // missing baseline.
718
+ const toolsData = tools;
719
+ if (toolsData.tools &&
720
+ Array.isArray(toolsData.tools) &&
721
+ toolsData.tools.length > 0) {
722
+ for (const tool of toolsData.tools) {
723
+ const toolCode = (tool.code ||
724
+ tool.name ||
725
+ tool.tool_name ||
726
+ "unknown");
727
+ const fileName = `${toolCode
728
+ .toLowerCase()
729
+ .replace(/\s+/g, "_")}.aui.json`;
730
+ writeJson(path.join(toolsDir, fileName), { tool });
731
+ writtenFiles.push(`tools/${fileName}`);
732
+ }
733
+ }
734
+ else {
735
+ writeJson(path.join(projectRoot, "tools.aui.json"), tools);
736
+ writtenFiles.push("tools.aui.json");
737
+ }
738
+ paramCount = (parameters?.parameters || []).length;
739
+ entityCount = (entities?.entities || []).length;
740
+ integrationCount =
741
+ (integrations?.integrations || []).length;
742
+ toolCount = (toolsData.tools || []).length;
743
+ ruleCount = (rules?.rules || []).length;
363
744
  }
364
745
  else {
365
- writeJson(path.join(projectRoot, "tools.aui.json"), tools);
366
- writtenFiles.push("tools.aui.json");
746
+ // ─── Bundle-mode but no revision found ───
747
+ // The agent IS in blob-mode (we just checked) and `/pull`
748
+ // returned 404, meaning no bundle has been uploaded for this
749
+ // version_id yet. Throw a clear error pointing at `aui push`
750
+ // — the user almost certainly meant to seed the version
751
+ // first. This is a different failure mode from "agent doesn't
752
+ // exist" (which would have been caught earlier in mode
753
+ // detection).
754
+ parentSpan.setAttribute("pull.path", "blob_endpoint_404");
755
+ spinner.fail("No blob revision found at this version.");
756
+ throw new CLIError(options.tag
757
+ ? `No blob revision found at tag ${options.tag} for version ${activeVersionLabel || activeVersionId}.`
758
+ : `No blob revision found for version ${activeVersionLabel || activeVersionId}.`, {
759
+ suggestion: "Run `aui push` first to create the initial blob revision via the new /push endpoint, then re-run `aui pull`.",
760
+ });
367
761
  }
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;
762
+ // ─── Skip KB export for templates ─────────────────────────────
763
+ //
764
+ // Templates are NETWORK_CATEGORY-scoped and have no `network_id`
765
+ // / `account_id` on their scope row, so the KB endpoint
766
+ // (`knowledge-base-manager/v1/knowledge-bases/view/export`)
767
+ // rejects with 422 ("Id must be of type PydanticObjectId") on the
768
+ // empty account_id query param. Mirror the same skip in
769
+ // `import-agent.tsx` — KBs are inherently network-scoped and
770
+ // templates don't have them in this data model. See
771
+ // `import-agent.tsx` for the longer comment + reproduction
772
+ // trace from 2026-05-24.
773
+ //
774
+ // `resolvedAgentInfo` is populated by `findAgentMgmtId` above
775
+ // (branch 1 = .auirc canonical id), so `kind` is reliable for
776
+ // any project that ran `aui import-agent` on a recent CLI
777
+ // version. Falls back to `scope.type` for legacy/edge cases.
778
+ const isTemplateForKb = resolvedAgentInfo?.kind === "template" ||
779
+ resolvedAgentInfo?.scope?.type === "NETWORK_CATEGORY";
373
780
  const kbExportPromise = options.skipKbFiles
374
781
  ? 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
- })();
782
+ : isTemplateForKb
783
+ ? Promise.resolve({
784
+ kind: "skipped",
785
+ reason: "templates are NETWORK_CATEGORY-scoped and don't have knowledge bases",
786
+ })
787
+ : (async () => {
788
+ try {
789
+ const kbClient = new KBViewClient({
790
+ authToken: config.authToken,
791
+ apiKey: loadAgentSettingsApiKey() || options.apiKey,
792
+ organizationId: projectConfig.organization_id || config.organizationId || "",
793
+ environment: config.environment || "staging",
794
+ });
795
+ const scope = buildScope({
796
+ networkId,
797
+ organizationId: projectConfig.organization_id || config.organizationId || "",
798
+ accountId: projectConfig.account_id || config.accountId || "",
799
+ });
800
+ const result = await exportToFolder(kbClient, scope, projectRoot, options.withKbFiles ?? false);
801
+ return { kind: "ok", result };
802
+ }
803
+ catch (kbError) {
804
+ return {
805
+ kind: "error",
806
+ error: kbError,
807
+ message: kbError instanceof Error ? kbError.message : String(kbError),
808
+ };
809
+ }
810
+ })();
399
811
  const schemaPromise = (async () => {
400
812
  try {
401
813
  const data = await fetchSchemas({
@@ -423,7 +835,9 @@ async function _pullAgent(parentSpan, options = {}) {
423
835
  parallelSpinner.stop();
424
836
  // Knowledge-hub status
425
837
  if (kbOutcome.kind === "skipped") {
426
- log(_jsx(StatusLine, { kind: "muted", label: "Skipping knowledge hub export (--skip-kb-files)" }));
838
+ log(_jsx(StatusLine, { kind: "muted", label: kbOutcome.reason
839
+ ? `Skipping knowledge hub export — ${kbOutcome.reason}.`
840
+ : "Skipping knowledge hub export (--skip-kb-files)" }));
427
841
  }
428
842
  else if (kbOutcome.kind === "ok") {
429
843
  const r = kbOutcome.result;
@@ -459,13 +873,36 @@ async function _pullAgent(parentSpan, options = {}) {
459
873
  else {
460
874
  log(_jsx(StatusLine, { kind: "warning", label: "Failed to pull schemas (non-blocking)" }));
461
875
  }
462
- // Preserve original behaviour: a KB export failure aborts `pull`.
876
+ // KB export failure is non-blocking same UX as `aui import-agent`.
877
+ //
878
+ // Rationale (2026-05-24): the main bundle (`agent.aui.json`,
879
+ // `parameters.aui.json`, `entities.aui.json`, `integrations.aui.json`,
880
+ // `rules.aui.json`, `tools/*.aui.json`) has already been written to
881
+ // disk successfully at this point — the only thing missing is the
882
+ // `knowledge-hubs/` folder. Killing the whole `aui pull` and
883
+ // discarding the .auirc / version_tag updates over a transient KB
884
+ // service issue (e.g. the GCS object-rate-limit on staging that
885
+ // surfaces as a 500 wrapping a 429) is a worse UX than the
886
+ // import path's "tell the user, keep going" behaviour. Both paths
887
+ // now treat KB export the same way:
888
+ //
889
+ // 1. Log the failure prominently with the full server message
890
+ // (already done above at line ~1110).
891
+ // 2. Show an actionable hint pointing at `aui rag --export` /
892
+ // retry, instead of crashing.
893
+ // 3. Continue with the .auirc / baseline writes below so the
894
+ // project's tracking state matches what was actually pulled.
895
+ //
896
+ // The KB failure is still visible in `aui curl` and in the
897
+ // pre-existing per-task push-logs, so debugging the underlying
898
+ // GCS or KB-service issue stays straightforward.
463
899
  if (kbOutcome.kind === "error") {
464
- throw kbOutcome.error;
900
+ 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
901
  }
466
902
  // Update .auirc with the pulled version info and any newly resolved
467
903
  // agent-management id (back-fills it for projects that pre-date the field).
468
904
  const shouldRewriteConfig = !!activeVersionId ||
905
+ !!resolvedVersionTag ||
469
906
  (resolvedAgentManagementId &&
470
907
  projectConfig.agent_management_id !== resolvedAgentManagementId);
471
908
  if (shouldRewriteConfig) {
@@ -476,6 +913,10 @@ async function _pullAgent(parentSpan, options = {}) {
476
913
  : {}),
477
914
  ...(activeVersionId ? { version_id: activeVersionId } : {}),
478
915
  ...(activeVersionLabel ? { version_label: activeVersionLabel } : {}),
916
+ // Persist the revision tag from the manifest (e.g. "v8.14")
917
+ // so the next `aui pull` can default `?version_tag=…` to it
918
+ // without an extra round-trip.
919
+ ...(resolvedVersionTag ? { version_tag: resolvedVersionTag } : {}),
479
920
  ...(options.scopeLevel ? { scope_level: options.scopeLevel } : {}),
480
921
  }, projectRoot);
481
922
  }